Nori  23
chi2test.cpp
1 /*
2  This file is part of Nori, a simple educational ray tracer
3 
4  Copyright (c) 2015 by Wenzel Jakob
5 
6  Nori is free software; you can redistribute it and/or modify
7  it under the terms of the GNU General Public License Version 3
8  as published by the Free Software Foundation.
9 
10  Nori is distributed in the hope that it will be useful,
11  but WITHOUT ANY WARRANTY; without even the implied warranty of
12  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13  GNU General Public License for more details.
14 
15  You should have received a copy of the GNU General Public License
16  along with this program. If not, see <http://www.gnu.org/licenses/>.
17 */
18 
19 #include <nori/bsdf.h>
20 #include <nori/warp.h>
21 #include <pcg32.h>
22 #include <hypothesis.h>
23 #include <fstream>
24 #include <memory>
25 
26 /*
27  * =======================================================================
28  * WARNING WARNING WARNING WARNING WARNING WARNING
29  * =======================================================================
30  * Remember to put on SAFETY GOGGLES before looking at this file. You
31  * are most certainly not expected to read or understand any of it.
32  * =======================================================================
33  */
34 
35 NORI_NAMESPACE_BEGIN
36 
42 class ChiSquareTest : public NoriObject {
43 public:
44  ChiSquareTest(const PropertyList &propList) {
45  /* The null hypothesis will be rejected when the associated
46  p-value is below the significance level specified here. */
47  m_significanceLevel = propList.getFloat("significanceLevel", 0.01f);
48 
49  /* Number of cells along the latitudinal axis. The azimuthal
50  resolution is twice this value. */
51  m_cosThetaResolution = propList.getInteger("resolution", 10);
52 
53  /* Minimum expected bin frequency. The chi^2 test does not
54  work reliably when the expected frequency in a cell is
55  low (e.g. less than 5), because normality assumptions
56  break down in this case. Therefore, the implementation
57  will merge such low-frequency cells when they fall below
58  the threshold specified here. */
59  m_minExpFrequency = propList.getInteger("minExpFrequency", 5);
60 
61  /* Number of samples that should be taken (-1: automatic) */
62  m_sampleCount = propList.getInteger("sampleCount", -1);
63 
64  /* Each provided BSDF will be tested for a few different
65  incident directions. The value specified here determines
66  how many tests will be executed per BSDF */
67  m_testCount = propList.getInteger("testCount", 5);
68 
69  m_phiResolution = 2 * m_cosThetaResolution;
70 
71  if (m_sampleCount < 0) // ~5K samples per bin
72  m_sampleCount = m_cosThetaResolution * m_phiResolution * 5000;
73  }
74 
75  virtual ~ChiSquareTest() {
76  for (auto bsdf : m_bsdfs)
77  delete bsdf;
78  }
79 
80  virtual void addChild(NoriObject *obj) override {
81  switch (obj->getClassType()) {
82  case EBSDF:
83  m_bsdfs.push_back(static_cast<BSDF *>(obj));
84  break;
85 
86  default:
87  throw NoriException("ChiSquareTest::addChild(<%s>) is not supported!",
88  classTypeName(obj->getClassType()));
89  }
90  }
91 
93  virtual void activate() override {
94  int passed = 0, total = 0, res = m_cosThetaResolution*m_phiResolution;
95  pcg32 random; /* Pseudorandom number generator */
96 
97  std::unique_ptr<double[]> obsFrequencies(new double[res]);
98  std::unique_ptr<double[]> expFrequencies(new double[res]);
99 
100 
101  /* Test each registered BSDF */
102  for (auto bsdf : m_bsdfs) {
103  /* Run several tests per BSDF to be on the safe side */
104  for (int l = 0; l<m_testCount; ++l) {
105  memset(obsFrequencies.get(), 0, res*sizeof(double));
106  memset(expFrequencies.get(), 0, res*sizeof(double));
107 
108  cout << "------------------------------------------------------" << endl;
109  cout << "Testing: " << bsdf->toString() << endl;
110  ++total;
111 
112  float cosTheta = random.nextFloat();
113  float sinTheta = std::sqrt(std::max((float) 0, 1-cosTheta*cosTheta));
114  float sinPhi, cosPhi;
115  sincosf(2.0f * M_PI * random.nextFloat(), &sinPhi, &cosPhi);
116  Vector3f wi(cosPhi * sinTheta, sinPhi * sinTheta, cosTheta);
117 
118  cout << "Accumulating " << m_sampleCount << " samples into a " << m_cosThetaResolution
119  << "x" << m_phiResolution << " contingency table .. ";
120  cout.flush();
121 
122  /* Generate many samples from the BSDF and create
123  a histogram / contingency table */
124  BSDFQueryRecord bRec(wi);
125  for (int i=0; i<m_sampleCount; ++i) {
126  Point2f sample(random.nextFloat(), random.nextFloat());
127  Color3f result = bsdf->sample(bRec, sample);
128 
129  if ((result.array() == 0).all())
130  continue;
131 
132  int cosThetaBin = std::min(std::max(0, (int) std::floor((bRec.wo.z()*0.5f+0.5f)
133  * m_cosThetaResolution)), m_cosThetaResolution-1);
134 
135  float scaledPhi = std::atan2(bRec.wo.y(), bRec.wo.x()) * INV_TWOPI;
136  if (scaledPhi < 0)
137  scaledPhi += 1;
138 
139  int phiBin = std::min(std::max(0,
140  (int) std::floor(scaledPhi * m_phiResolution)), m_phiResolution-1);
141  obsFrequencies[cosThetaBin * m_phiResolution + phiBin] += 1;
142  }
143  cout << "done." << endl;
144 
145  /* Numerically integrate the probability density
146  function over rectangles in spherical coordinates. */
147  double *ptr = expFrequencies.get();
148  cout << "Integrating expected frequencies .. ";
149  cout.flush();
150  for (int i=0; i<m_cosThetaResolution; ++i) {
151  double cosThetaStart = -1.0 + i * 2.0 / m_cosThetaResolution;
152  double cosThetaEnd = -1.0 + (i+1) * 2.0 / m_cosThetaResolution;
153  for (int j=0; j<m_phiResolution; ++j) {
154  double phiStart = j * 2*M_PI / m_phiResolution;
155  double phiEnd = (j+1) * 2*M_PI / m_phiResolution;
156 
157  auto integrand = [&](double cosTheta, double phi) -> double {
158  double sinTheta = std::sqrt(1 - cosTheta * cosTheta);
159  double sinPhi = std::sin(phi), cosPhi = std::cos(phi);
160 
161  Vector3f wo((float) (sinTheta * cosPhi),
162  (float) (sinTheta * sinPhi),
163  (float) cosTheta);
164 
165  BSDFQueryRecord bRec(wi, wo, ESolidAngle);
166  return bsdf->pdf(bRec);
167  };
168 
169  double integral = hypothesis::adaptiveSimpson2D(
170  integrand, cosThetaStart, phiStart, cosThetaEnd,
171  phiEnd);
172 
173  *ptr++ = integral * m_sampleCount;
174  }
175  }
176  cout << "done." << endl;
177 
178  /* Write the test input data to disk for debugging */
179  hypothesis::chi2_dump(m_cosThetaResolution, m_phiResolution, obsFrequencies.get(), expFrequencies.get(),
180  tfm::format("chi2test_%i.m", total));
181 
182  /* Perform the Chi^2 test */
183  std::pair<bool, std::string> result =
184  hypothesis::chi2_test(m_cosThetaResolution*m_phiResolution, obsFrequencies.get(), expFrequencies.get(),
185  m_sampleCount, m_minExpFrequency, m_significanceLevel, m_testCount * (int) m_bsdfs.size());
186 
187  if (result.first)
188  ++passed;
189 
190  cout << result.second << endl;
191  }
192  }
193 
194  cout << "Passed " << passed << "/" << total << " tests." << endl;
195 
196  if (passed < total) {
197  throw std::runtime_error("Failed some of the tests");
198  }
199  }
200 
201  virtual std::string toString() const override {
202  return tfm::format("ChiSquareTest[\n"
203  " thetaResolution = %i,\n"
204  " phiResolution = %i,\n"
205  " minExpFrequency = %i,\n"
206  " sampleCount = %i,\n"
207  " testCount = %i,\n"
208  " significanceLevel = %f\n"
209  "]",
210  m_cosThetaResolution,
211  m_phiResolution,
212  m_minExpFrequency,
213  m_sampleCount,
214  m_testCount,
215  m_significanceLevel
216  );
217  }
218 
219  virtual EClassType getClassType() const override { return ETest; }
220 private:
221  int m_cosThetaResolution;
222  int m_phiResolution;
223  int m_minExpFrequency;
224  int m_sampleCount;
225  int m_testCount;
226  float m_significanceLevel;
227  std::vector<BSDF *> m_bsdfs;
228 };
229 
230 NORI_REGISTER_CLASS(ChiSquareTest, "chi2test");
231 NORI_NAMESPACE_END
Superclass of all bidirectional scattering distribution functions.
Definition: bsdf.h:63
Statistical test for validating that an importance sampling routine (e.g. from a BSDF) produces a dis...
Definition: chi2test.cpp:42
virtual EClassType getClassType() const override
Return the type of object (i.e. Mesh/BSDF/etc.) provided by this instance.
Definition: chi2test.cpp:219
virtual std::string toString() const override
Return a brief string summary of the instance (for debugging purposes)
Definition: chi2test.cpp:201
virtual void addChild(NoriObject *obj) override
Add a child object to the current instance.
Definition: chi2test.cpp:80
virtual void activate() override
Execute the chi-square test.
Definition: chi2test.cpp:93
Simple exception class, which stores a human-readable error description.
Definition: common.h:148
Base class of all objects.
Definition: object.h:32
static std::string classTypeName(EClassType type)
Turn a class type into a human-readable string.
Definition: object.h:51
virtual EClassType getClassType() const =0
Return the type of object (i.e. Mesh/BSDF/etc.) provided by this instance.
This is an associative container used to supply the constructors of NoriObject subclasses with parame...
Definition: proplist.h:32
float getFloat(const std::string &name) const
Get a float property, and throw an exception if it does not exist.
int getInteger(const std::string &name) const
Get an integer property, and throw an exception if it does not exist.
Convenience data structure used to pass multiple parameters to the evaluation and sampling routines i...
Definition: bsdf.h:30
Vector3f wo
Outgoing direction (in the local frame)
Definition: bsdf.h:35
Represents a linear RGB color value.
Definition: color.h:29