mitsuba/src/tests/test_chisquare.cpp

589 lines
20 KiB
C++

/*
This file is part of Mitsuba, a physically based rendering system.
Copyright (c) 2007-2011 by Wenzel Jakob and others.
Mitsuba is free software; you can redistribute it and/or modify
it under the terms of the GNU General Public License Version 3
as published by the Free Software Foundation.
Mitsuba is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
#include <mitsuba/core/plugin.h>
#include <mitsuba/core/statistics.h>
#include <mitsuba/core/chisquare.h>
#include <mitsuba/render/testcase.h>
#include <boost/bind.hpp>
/* Statistical significance level of the test. Set to
1/4 percent by default -- we want there to be strong
evidence of an implementaiton error before failing
a test case */
#define SIGNIFICANCE_LEVEL 0.0025f
/* Relative bound on what is still accepted as roundoff
error -- be quite tolerant */
#if defined(SINGLE_PRECISION)
#define ERROR_REQ 1e-2f
#else
#define ERROR_REQ 1e-5
#endif
MTS_NAMESPACE_BEGIN
/**
* This testcase checks if the sampling methods of various BSDF & phase
* function & luminaire implementations really do what they promise in
* their pdf() methods
*/
class TestChiSquare : public TestCase {
public:
MTS_BEGIN_TESTCASE()
MTS_DECLARE_TEST(test01_BSDF)
MTS_DECLARE_TEST(test02_PhaseFunction)
MTS_END_TESTCASE()
/**
* Replayable fake sampler
*/
class FakeSampler : public Sampler {
public:
FakeSampler(Sampler *sampler)
: Sampler(Properties()), m_sampler(sampler) { }
Float next1D() {
while (m_sampleIndex >= m_values.size())
m_values.push_back(m_sampler->next1D());
return m_values[m_sampleIndex++];
}
Point2 next2D() {
return Point2(next1D(), next1D());
}
void clear() {
m_values.clear();
m_sampleIndex = 0;
}
void rewind() {
m_sampleIndex = 0;
}
Float independent1D() { SLog(EError, "Not supported!"); return 0; }
Point2 independent2D() { SLog(EError, "Not supported!"); return Point2(0.0f); }
ref<Sampler> clone() {
SLog(EError, "Not supported!");
return NULL;
}
std::string toString() const { return "FakeSampler[]"; }
private:
ref<Sampler> m_sampler;
std::vector<Float> m_values;
};
/// Adapter to use BSDFs in the chi-square test
class BSDFAdapter {
public:
BSDFAdapter(const BSDF *bsdf, Sampler *sampler, const Vector &wi, int component)
: m_bsdf(bsdf), m_sampler(sampler), m_wi(wi), m_component(component),
m_largestWeight(0) {
m_fakeSampler = new FakeSampler(m_sampler);
m_its.uv = Point2(0.0f);
m_its.dpdu = Vector(1, 0, 0);
m_its.dpdv = Vector(0, 1, 0);
m_its.dudx = m_its.dvdy = 0.01f;
m_its.dudy = m_its.dvdx = 0.00f;
m_its.shFrame = Frame(Normal(0, 0, 1));
}
boost::tuple<Vector, Float, EMeasure> generateSample() {
Point2 sample(m_sampler->next2D());
BSDFQueryRecord bRec(m_its, m_fakeSampler);
bRec.component = m_component;
bRec.wi = m_wi;
#if defined(MTS_DEBUG_FP)
enableFPExceptions();
#endif
Float pdfVal, pdfVal2;
/* Check the various sampling routines for agreement
amongst each other */
m_fakeSampler->clear();
Spectrum f = m_bsdf->sample(bRec, pdfVal, sample);
m_fakeSampler->rewind();
Spectrum sampled = m_bsdf->sample(bRec, sample);
EMeasure measure = ESolidAngle;
if (!f.isZero())
measure = BSDF::getMeasure(bRec.sampledType);
Spectrum f2 = m_bsdf->eval(bRec, measure);
pdfVal2 = m_bsdf->pdf(bRec, measure);
if (f.isZero() || pdfVal == 0 || pdfVal2 == 0) {
if (!sampled.isZero())
Log(EWarn, "Inconsistency (1): f=%s, f2=%s, pdf=%f, pdf2=%f, sampled f/pdf=%s, bRec=%s, measure=%i",
f.toString().c_str(), f2.toString().c_str(), pdfVal, pdfVal2, sampled.toString().c_str(), bRec.toString().c_str(), measure);
#if defined(MTS_DEBUG_FP)
disableFPExceptions();
#endif
return boost::make_tuple(bRec.wo, 0.0f, ESolidAngle);
} else if (sampled.isZero()) {
if ((!f.isZero() && pdfVal != 0) || (!f2.isZero() && pdfVal2 != 0))
Log(EWarn, "Inconsistency (2): f=%s, f2=%s, pdf=%f, pdf2=%f, sampled f/pdf=%s, bRec=%s",
f.toString().c_str(), f2.toString().c_str(), pdfVal, pdfVal2, sampled.toString().c_str(), bRec.toString().c_str());
#if defined(MTS_DEBUG_FP)
disableFPExceptions();
#endif
return boost::make_tuple(bRec.wo, 0.0f, ESolidAngle);
}
Spectrum sampled2 = f/pdfVal, evaluated = f2/pdfVal2;
if (!sampled.isValid() || !sampled2.isValid() || !evaluated.isValid()) {
Log(EWarn, "Ooops: f=%s, f2=%s, pdf=%f, pdf2=%f, sampled f/pdf=%s, bRec=%s",
f.toString().c_str(), f2.toString().c_str(), pdfVal, pdfVal2, sampled.toString().c_str(), bRec.toString().c_str());
return boost::make_tuple(bRec.wo, 0.0f, ESolidAngle);
}
bool mismatch = false;
for (int i=0; i<SPECTRUM_SAMPLES; ++i) {
Float a = sampled[i], b = sampled2[i], c = evaluated[i];
Float min = std::min(std::min(a, b), c);
Float err = std::max(std::max(std::abs(a - b), std::abs(a - c)), std::abs(b - c));
m_largestWeight = std::max(m_largestWeight, a);
if (min < ERROR_REQ && err > ERROR_REQ) // absolute error threshold
mismatch = true;
else if (min > ERROR_REQ && err/min > ERROR_REQ) // relative error threshold
mismatch = true;
}
if (mismatch) {
Log(EWarn, "Potential inconsistency (3): f/pdf=%s (method 1), f/pdf=%s (method 2), sampled f/pdf=%s, measure=%i",
sampled2.toString().c_str(), evaluated.toString().c_str(), sampled.toString().c_str(), measure);
Log(EWarn, " f=%s, f2=%s, pdf=%f, pdf2=%f", f.toString().c_str(), f.toString().c_str(), pdfVal, pdfVal2);
}
#if defined(MTS_DEBUG_FP)
disableFPExceptions();
#endif
return boost::make_tuple(bRec.wo, 1.0f, measure);
}
Float pdf(const Vector &wo, EMeasure measure) {
BSDFQueryRecord bRec(m_its, m_wi, wo);
bRec.component = m_component;
#if defined(MTS_DEBUG_FP)
enableFPExceptions();
#endif
if (m_bsdf->eval(bRec, measure).isZero())
return 0.0f;
Float result = m_bsdf->pdf(bRec, measure);
#if defined(MTS_DEBUG_FP)
disableFPExceptions();
#endif
return result;
}
inline Float getLargestWeight() const { return m_largestWeight; }
private:
Intersection m_its;
ref<const BSDF> m_bsdf;
ref<Sampler> m_sampler;
ref<FakeSampler> m_fakeSampler;
Vector m_wi;
int m_component;
Float m_largestWeight;
};
/// Adapter to use Phase functions in the chi-square test
class PhaseFunctionAdapter {
public:
PhaseFunctionAdapter(const MediumSamplingRecord &mRec,
const PhaseFunction *phase, Sampler *sampler, const Vector &wi)
: m_mRec(mRec), m_phase(phase), m_sampler(sampler), m_wi(wi),
m_largestWeight(0) { }
boost::tuple<Vector, Float, EMeasure> generateSample() {
Point2 sample(m_sampler->next2D());
PhaseFunctionQueryRecord pRec(m_mRec, m_wi);
#if defined(MTS_DEBUG_FP)
enableFPExceptions();
#endif
/* Check the various sampling routines for agreement amongst each other */
Float pdfVal;
Float f = m_phase->sample(pRec, pdfVal, m_sampler);
Float sampled = m_phase->sample(pRec, m_sampler);
if (f == 0 || pdfVal == 0) {
if (sampled != 0)
Log(EWarn, "Inconsistency: f=%f, pdf=%f, sampled f/pdf=%f",
f, pdfVal, sampled);
#if defined(MTS_DEBUG_FP)
disableFPExceptions();
#endif
return boost::make_tuple(pRec.wo, 0.0f, ESolidAngle);
} else if (sampled == 0) {
if (f != 0 && pdfVal != 0)
Log(EWarn, "Inconsistency: f=%f, pdf=%f, sampled f/pdf=%f",
f, pdfVal, sampled);
#if defined(MTS_DEBUG_FP)
disableFPExceptions();
#endif
return boost::make_tuple(pRec.wo, 0.0f, ESolidAngle);
}
Float sampled2 = f/pdfVal;
bool mismatch = false;
SAssert(sampled >= 0 && sampled2 >= 0);
Float min = std::min(sampled, sampled2);
Float err = std::abs(sampled - sampled2);
m_largestWeight = std::max(m_largestWeight, sampled);
if (min < ERROR_REQ && err > ERROR_REQ) // absolute error threshold
mismatch = true;
else if (min > ERROR_REQ && err/min > ERROR_REQ) // relative error threshold
mismatch = true;
if (mismatch)
Log(EWarn, "Inconsistency: f=%f, pdf=%f, sampled f/pdf=%f",
f, pdfVal, sampled);
#if defined(MTS_DEBUG_FP)
disableFPExceptions();
#endif
return boost::make_tuple(pRec.wo, 1.0f, ESolidAngle);
}
Float pdf(const Vector &wo, EMeasure measure) const {
if (measure != ESolidAngle)
return 0.0f;
PhaseFunctionQueryRecord pRec(m_mRec, m_wi, wo);
#if defined(MTS_DEBUG_FP)
enableFPExceptions();
#endif
if (m_phase->eval(pRec) == 0)
return 0.0f;
Float result = m_phase->pdf(pRec);
#if defined(MTS_DEBUG_FP)
disableFPExceptions();
#endif
return result;
}
inline Float getLargestWeight() const { return m_largestWeight; }
private:
const MediumSamplingRecord &m_mRec;
ref<const PhaseFunction> m_phase;
ref<Sampler> m_sampler;
Vector m_wi;
Float m_largestWeight;
};
/// Adapter to use luminaires in the chi-square test
class LuminaireAdapter {
public:
LuminaireAdapter(
const Luminaire *luminaire, Sampler *sampler)
: m_luminaire(luminaire), m_sampler(sampler) { }
boost::tuple<Vector, Float, EMeasure> generateSample() {
Point2 sample(m_sampler->next2D());
EmissionRecord pRec(m_mRec, m_wi);
#if defined(MTS_DEBUG_FP)
enableFPExceptions();
#endif
/* Check the various sampling routines for agreement amongst each other */
Float pdfVal;
Spectrum value = m_luminaire->sampleEmission(eRec, m_sampler->next2D(), m_sampler->next2D());
if (min < ERROR_REQ && err > ERROR_REQ) // absolute error threshold
mismatch = true;
else if (min > ERROR_REQ && err/min > ERROR_REQ) // relative error threshold
mismatch = true;
if (mismatch)
Log(EWarn, "Inconsistency: f=%f, pdf=%f, sampled f/pdf=%f",
f, pdfVal, sampled);
#if defined(MTS_DEBUG_FP)
disableFPExceptions();
#endif
return boost::make_tuple(pRec.wo, 1.0f, ESolidAngle);
}
Float pdf(const Vector &wo, EMeasure measure) const {
if (measure != ESolidAngle)
return 0.0f;
EmissionRecord pRec(m_mRec, m_wi, wo);
#if defined(MTS_DEBUG_FP)
enableFPExceptions();
#endif
if (m_luminaire->eval(pRec) == 0)
return 0.0f;
Float result = m_luminaire->pdf(pRec);
#if defined(MTS_DEBUG_FP)
disableFPExceptions();
#endif
return result;
}
private:
ref<const Luminaire> m_luminaire;
ref<Sampler> m_sampler;
};
void test01_BSDF() {
/* Load a set of BSDF instances to be tested from the following XML file */
ref<Scene> scene = loadScene("data/tests/test_bsdf.xml");
const std::vector<ConfigurableObject *> objects = scene->getReferencedObjects();
size_t thetaBins = 10, wiSamples = 20, failureCount = 0, testCount = 0;
ref<Sampler> sampler = static_cast<Sampler *> (PluginManager::getInstance()->
createObject(MTS_CLASS(Sampler), Properties("independent")));
ProgressReporter *progress = new ProgressReporter("Checking", wiSamples, NULL);
Log(EInfo, "Verifying BSDF sampling routines ..");
for (size_t i=0; i<objects.size(); ++i) {
if (!objects[i]->getClass()->derivesFrom(MTS_CLASS(BSDF)))
continue;
const BSDF *bsdf = static_cast<const BSDF *>(objects[i]);
Float largestWeight = 0;
Log(EInfo, "Processing BSDF model %s", bsdf->toString().c_str());
Log(EInfo, "Checking the model for %i incident directions and 2D sampling", wiSamples);
progress->reset();
/* Test for a number of different incident directions */
for (size_t j=0; j<wiSamples; ++j) {
Vector wi;
if (bsdf->getType() & BSDF::EBackSide)
wi = squareToSphere(sampler->next2D());
else
wi = squareToHemispherePSA(sampler->next2D());
BSDFAdapter adapter(bsdf, sampler, wi, -1);
ref<ChiSquare> chiSqr = new ChiSquare(thetaBins, 2*thetaBins, wiSamples);
chiSqr->setLogLevel(EDebug);
// Initialize the tables used by the chi-square test
chiSqr->fill(
boost::bind(&BSDFAdapter::generateSample, &adapter),
boost::bind(&BSDFAdapter::pdf, &adapter, _1, _2)
);
// (the following assumes that the distribution has 1 parameter, e.g. exponent value)
chiSqr->dumpTables("test.m");
ChiSquare::ETestResult result = chiSqr->runTest(SIGNIFICANCE_LEVEL);
if (result == ChiSquare::EReject) {
std::string filename = formatString("failure_%i.m", failureCount++);
chiSqr->dumpTables(filename);
failAndContinue(formatString("Uh oh, the chi-square test indicates a potential "
"issue for wi=%s. Dumped the contingency tables to '%s' for user analysis",
wi.toString().c_str(), filename.c_str()));
} else {
succeed();
}
largestWeight = std::max(largestWeight, adapter.getLargestWeight());
++testCount;
progress->update(j+1);
}
Log(EInfo, "The largest encountered importance weight was = %.2f", largestWeight);
largestWeight = 0;
if (bsdf->getComponentCount() > 1) {
for (int comp=0; comp<bsdf->getComponentCount(); ++comp) {
progress->reset();
Log(EInfo, "Individually checking BSDF component %i", comp);
/* Test for a number of different incident directions */
for (size_t j=0; j<wiSamples; ++j) {
Vector wi;
if (bsdf->getType(comp) & BSDF::EBackSide)
wi = squareToSphere(sampler->next2D());
else
wi = squareToHemispherePSA(sampler->next2D());
BSDFAdapter adapter(bsdf, sampler, wi, comp);
ref<ChiSquare> chiSqr = new ChiSquare(thetaBins, 2*thetaBins, wiSamples);
chiSqr->setLogLevel(EDebug);
// Initialize the tables used by the chi-square test
chiSqr->fill(
boost::bind(&BSDFAdapter::generateSample, &adapter),
boost::bind(&BSDFAdapter::pdf, &adapter, _1, _2)
);
// (the following assumes that the distribution has 1 parameter, e.g. exponent value)
ChiSquare::ETestResult result = chiSqr->runTest(SIGNIFICANCE_LEVEL);
if (result == ChiSquare::EReject) {
std::string filename = formatString("failure_%i.m", failureCount++);
chiSqr->dumpTables(filename);
failAndContinue(formatString("Uh oh, the chi-square test indicates a potential "
"issue for wi=%s. Dumped the contingency tables to '%s' for user analysis",
wi.toString().c_str(), filename.c_str()));
} else {
succeed();
}
largestWeight = std::max(largestWeight, adapter.getLargestWeight());
++testCount;
progress->update(j+1);
}
Log(EInfo, "The largest encountered importance weight was = %.2f", largestWeight);
largestWeight = 0;
}
}
}
Log(EInfo, "%i/%i BSDF checks succeeded", testCount-failureCount, testCount);
delete progress;
}
void test02_PhaseFunction() {
/* Load a set of phase function instances to be tested from the following XML file */
ref<Scene> scene = loadScene("data/tests/test_phase.xml");
const std::vector<ConfigurableObject *> objects = scene->getReferencedObjects();
size_t thetaBins = 10, wiSamples = 20, failureCount = 0, testCount = 0;
ref<Sampler> sampler = static_cast<Sampler *> (PluginManager::getInstance()->
createObject(MTS_CLASS(Sampler), Properties("independent")));
ProgressReporter *progress = new ProgressReporter("Checking", wiSamples, NULL);
Log(EInfo, "Verifying phase function sampling routines ..");
for (size_t i=0; i<objects.size(); ++i) {
if (!objects[i]->getClass()->derivesFrom(MTS_CLASS(PhaseFunction)))
continue;
const PhaseFunction *phase = static_cast<const PhaseFunction *>(objects[i]);
Float largestWeight = 0;
Log(EInfo, "Processing phase function model %s", phase->toString().c_str());
Log(EInfo, "Checking the model for %i incident directions", wiSamples);
progress->reset();
MediumSamplingRecord mRec;
/* Sampler fiber/particle orientation */
mRec.orientation = squareToSphere(sampler->next2D());
/* Test for a number of different incident directions */
for (size_t j=0; j<wiSamples; ++j) {
Vector wi = squareToSphere(sampler->next2D());
PhaseFunctionAdapter adapter(mRec, phase, sampler, wi);
ref<ChiSquare> chiSqr = new ChiSquare(thetaBins, 2*thetaBins, wiSamples);
chiSqr->setLogLevel(EDebug);
// Initialize the tables used by the chi-square test
chiSqr->fill(
boost::bind(&PhaseFunctionAdapter::generateSample, &adapter),
boost::bind(&PhaseFunctionAdapter::pdf, &adapter, _1, _2)
);
// (the following assumes that the distribution has 1 parameter, e.g. exponent value)
ChiSquare::ETestResult result = chiSqr->runTest(SIGNIFICANCE_LEVEL);
if (result == ChiSquare::EReject) {
std::string filename = formatString("failure_%i.m", failureCount++);
chiSqr->dumpTables(filename);
failAndContinue(formatString("Uh oh, the chi-square test indicates a potential "
"issue for wi=%s. Dumped the contingency tables to '%s' for user analysis",
wi.toString().c_str(), filename.c_str()));
} else {
succeed();
}
largestWeight = std::max(largestWeight, adapter.getLargestWeight());
++testCount;
progress->update(j+1);
}
Log(EInfo, "Done with this phase function. The largest encountered "
"importance weight was = %.2f", largestWeight);
}
Log(EInfo, "%i/%i phase function checks succeeded", testCount-failureCount, testCount);
delete progress;
}
void test03_Luminaire() {
/* Load a set of luminaire instances to be tested from the following XML file */
ref<Scene> scene = loadScene("data/tests/test_luminaire.xml");
const std::vector<ConfigurableObject *> objects = scene->getReferencedObjects();
size_t thetaBins = 10, wiSamples = 20, failureCount = 0, testCount = 0;
ref<Sampler> sampler = static_cast<Sampler *> (PluginManager::getInstance()->
createObject(MTS_CLASS(Sampler), Properties("independent")));
ProgressReporter *progress = new ProgressReporter("Checking", wiSamples, NULL);
Log(EInfo, "Verifying luminaire sampling routines ..");
for (size_t i=0; i<objects.size(); ++i) {
if (!objects[i]->getClass()->derivesFrom(MTS_CLASS(Luminaire)))
continue;
const Luminaire *phase = static_cast<const Luminaire *>(objects[i]);
Log(EInfo, "Processing phase function model %s", phase->toString().c_str());
Log(EInfo, "Checking the model for %i incident directions", wiSamples);
progress->reset();
/* Test for a number of different incident directions */
for (size_t j=0; j<wiSamples; ++j) {
LuminaireAdapter adapter(luminaire, sampler);
ref<ChiSquare> chiSqr = new ChiSquare(thetaBins, 2*thetaBins, wiSamples);
chiSqr->setLogLevel(EDebug);
// Initialize the tables used by the chi-square test
chiSqr->fill(
boost::bind(&LuminaireAdapter::generateSample, &adapter),
boost::bind(&LuminaireAdapter::pdf, &adapter, _1, _2)
);
// (the following assumes that the distribution has 1 parameter, e.g. exponent value)
ChiSquare::ETestResult result = chiSqr->runTest(SIGNIFICANCE_LEVEL);
if (result == ChiSquare::EReject) {
std::string filename = formatString("failure_%i.m", failureCount++);
chiSqr->dumpTables(filename);
failAndContinue(formatString("Uh oh, the chi-square test indicates a potential "
"issue for wi=%s. Dumped the contingency tables to '%s' for user analysis",
wi.toString().c_str(), filename.c_str()));
} else {
succeed();
}
++testCount;
progress->update(j+1);
}
}
Log(EInfo, "%i/%i luminaire checks succeeded", testCount-failureCount, testCount);
delete progress;
}
};
MTS_EXPORT_TESTCASE(TestChiSquare, "Chi-square test for various sampling functions")
MTS_NAMESPACE_END