Nori  24
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, Point2f());
125  bool belowSurface = false;
126  for (int i=0; i<m_sampleCount; ++i) {
127  Point2f sample(random.nextFloat(), random.nextFloat());
128  Color3f result = bsdf->sample(bRec, sample);
129 
130  if ((result.array() == 0).all())
131  continue;
132 
133  // Raise a warning if the sample is below the surface, i.e. wo.z < 0
134  if (bRec.wo.z() < 0)
135  belowSurface = true;
136 
137  int cosThetaBin = std::min(std::max(0, (int) std::floor((bRec.wo.z()*0.5f+0.5f)
138  * m_cosThetaResolution)), m_cosThetaResolution-1);
139 
140  float scaledPhi = std::atan2(bRec.wo.y(), bRec.wo.x()) * INV_TWOPI;
141  if (scaledPhi < 0)
142  scaledPhi += 1;
143 
144  int phiBin = std::min(std::max(0,
145  (int) std::floor(scaledPhi * m_phiResolution)), m_phiResolution-1);
146  obsFrequencies[cosThetaBin * m_phiResolution + phiBin] += 1;
147  }
148  if (belowSurface)
149  cerr << "Warning: some samples are below the surface (wo.z < 0). "
150  << "Make sure the `sample` method returns Color3f(0) in such cases to reject these bad samples!" << endl;
151  cout << "done." << endl;
152 
153  /* Numerically integrate the probability density
154  function over rectangles in spherical coordinates. */
155  double *ptr = expFrequencies.get();
156  cout << "Integrating expected frequencies .. ";
157  cout.flush();
158  for (int i=0; i<m_cosThetaResolution; ++i) {
159  double cosThetaStart = -1.0 + i * 2.0 / m_cosThetaResolution;
160  double cosThetaEnd = -1.0 + (i+1) * 2.0 / m_cosThetaResolution;
161  for (int j=0; j<m_phiResolution; ++j) {
162  double phiStart = j * 2*M_PI / m_phiResolution;
163  double phiEnd = (j+1) * 2*M_PI / m_phiResolution;
164 
165  auto integrand = [&](double cosTheta, double phi) -> double {
166  double sinTheta = std::sqrt(1 - cosTheta * cosTheta);
167  double sinPhi = std::sin(phi), cosPhi = std::cos(phi);
168 
169  Vector3f wo((float) (sinTheta * cosPhi),
170  (float) (sinTheta * sinPhi),
171  (float) cosTheta);
172 
173  BSDFQueryRecord bRec(wi, wo, ESolidAngle, Point2f());
174  return bsdf->pdf(bRec);
175  };
176 
177  double integral = hypothesis::adaptiveSimpson2D(
178  integrand, cosThetaStart, phiStart, cosThetaEnd,
179  phiEnd);
180 
181  *ptr++ = integral * m_sampleCount;
182  }
183  }
184  cout << "done." << endl;
185 
186  /* Write the test input data to disk for debugging */
187  hypothesis::chi2_dump(m_cosThetaResolution, m_phiResolution, obsFrequencies.get(), expFrequencies.get(),
188  tfm::format("chi2test_%i.py", total));
189 
190  /* Perform the Chi^2 test */
191  std::pair<bool, std::string> result =
192  hypothesis::chi2_test(m_cosThetaResolution*m_phiResolution, obsFrequencies.get(), expFrequencies.get(),
193  m_sampleCount, m_minExpFrequency, m_significanceLevel, m_testCount * (int) m_bsdfs.size());
194 
195  if (result.first)
196  ++passed;
197 
198  cout << result.second << endl;
199  }
200  }
201 
202  cout << "Passed " << passed << "/" << total << " tests." << endl;
203 
204  if (passed < total) {
205  throw std::runtime_error("Failed some of the tests");
206  }
207  }
208 
209  virtual std::string toString() const override {
210  return tfm::format("ChiSquareTest[\n"
211  " thetaResolution = %i,\n"
212  " phiResolution = %i,\n"
213  " minExpFrequency = %i,\n"
214  " sampleCount = %i,\n"
215  " testCount = %i,\n"
216  " significanceLevel = %f\n"
217  "]",
218  m_cosThetaResolution,
219  m_phiResolution,
220  m_minExpFrequency,
221  m_sampleCount,
222  m_testCount,
223  m_significanceLevel
224  );
225  }
226 
227  virtual EClassType getClassType() const override { return ETest; }
228 private:
229  int m_cosThetaResolution;
230  int m_phiResolution;
231  int m_minExpFrequency;
232  int m_sampleCount;
233  int m_testCount;
234  float m_significanceLevel;
235  std::vector<BSDF *> m_bsdfs;
236 };
237 
238 NORI_REGISTER_CLASS(ChiSquareTest, "chi2test");
239 NORI_NAMESPACE_END
Superclass of all bidirectional scattering distribution functions.
Definition: bsdf.h:60
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:227
virtual std::string toString() const override
Return a brief string summary of the instance (for debugging purposes)
Definition: chi2test.cpp:209
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