Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

DM-32285: Fix yy <-> xy bug in SdssShape uncertainties #199

Merged
merged 3 commits into from
Oct 21, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
16 changes: 7 additions & 9 deletions src/SdssShape.cc
Original file line number Diff line number Diff line change
Expand Up @@ -597,18 +597,16 @@ bool getAdaptiveMoments(ImageT const &mimage, double bkgd, double xcen, double y
if (!(shape->flags[SdssShapeAlgorithm::UNWEIGHTED.number])) {
Matrix4d fisher = calc_fisher(*shape, bkgd_var); // Fisher matrix
Matrix4d cov = fisher.inverse();
// convention in afw::geom::ellipses is to order moments (xx, yy, xy),
// but the older algorithmic code uses (xx, xy, yy) - the order of
// indices here is not a bug.
// convention is to order moments (xx, yy, xy)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks good, but can you smash the first two commits so there isn't a commit with the code change but not the doc change?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fair point

shape->instFluxErr = std::sqrt(cov(0, 0));
shape->xxErr = std::sqrt(cov(1, 1));
shape->xyErr = std::sqrt(cov(2, 2));
shape->yyErr = std::sqrt(cov(3, 3));
shape->yyErr = std::sqrt(cov(2, 2));
shape->xyErr = std::sqrt(cov(3, 3));
shape->instFlux_xx_Cov = cov(0, 1);
shape->instFlux_xy_Cov = cov(0, 2);
shape->instFlux_yy_Cov = cov(0, 3);
shape->xx_yy_Cov = cov(1, 3);
shape->xx_xy_Cov = cov(1, 2);
shape->instFlux_yy_Cov = cov(0, 2);
shape->instFlux_xy_Cov = cov(0, 3);
shape->xx_yy_Cov = cov(1, 2);
shape->xx_xy_Cov = cov(1, 3);
shape->yy_xy_Cov = cov(2, 3);
}
}
Expand Down
53 changes: 52 additions & 1 deletion tests/test_SdssShape.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@

import lsst.geom
import lsst.afw.geom
import lsst.afw.table
import lsst.meas.base
import lsst.meas.base.tests
import lsst.utils.tests
Expand All @@ -48,6 +49,15 @@ def tearDown(self):
del self.dataset
del self.config

def makeAlgorithm(self, ctrl=None):
"""Construct an algorithm and return both it and its schema.
"""
if ctrl is None:
ctrl = lsst.meas.base.SdssShapeControl()
schema = lsst.meas.base.tests.TestDataset.makeMinimalSchema()
algorithm = lsst.meas.base.SdssShapeAlgorithm(ctrl, "base_SdssShape", schema)
return algorithm, schema

def assertFinite(self, value):
self.assertTrue(np.isfinite(value), msg="%s is not finite" % (value,))

Expand Down Expand Up @@ -96,7 +106,7 @@ def testMeasureGoodPsf(self):
"""
exposure, catalog = self._runMeasurementTask()
key = lsst.meas.base.SdssShapeResultKey(catalog.schema["base_SdssShape"])
psfTruth = exposure.getPsf().computeShape()
psfTruth = exposure.getPsf().computeShape(catalog[0].getCentroid())
for record in catalog:
result = record.get(key)
self._checkShape(result, record)
Expand Down Expand Up @@ -129,6 +139,47 @@ def testMeasureBadPsf(self):
self._checkShape(result, record)
self.assertTrue(result.getFlag(lsst.meas.base.SdssShapeAlgorithm.PSF_SHAPE_BAD.number))

def testMonteCarlo(self):
"""Test an ideal simulation, with deterministic noise.

Demonstrate that:

- We get the right answer, and
- The reported uncertainty agrees with a Monte Carlo test of the noise.
"""
algorithm, schema = self.makeAlgorithm()
# Results are RNG dependent; we choose a seed that is known to pass.
exposure, cat = self.dataset.realize(0.0, schema, randomSeed=3)
record = cat[1]
instFlux = record["truth_instFlux"]
algorithm.measure(record, exposure)
for suffix in ["xx", "yy", "xy"]:
self.assertFloatsAlmostEqual(record.get("truth_"+suffix),
record.get("base_SdssShape_"+suffix), rtol=1E-4)

for noise in (0.0001, 0.001,):
nSamples = 1000
catalog = lsst.afw.table.SourceCatalog(cat.schema)
for i in range(nSamples):
# By using ``i`` to seed the RNG, we get results which
# fall within the tolerances defined below. If we allow this
# test to be truly random, passing becomes RNG-dependent.
exposure, cat = self.dataset.realize(noise*instFlux, schema, randomSeed=i)
record = cat[1]
algorithm.measure(record, exposure)
catalog.append(record)

catalog = catalog.copy(deep=True)
for suffix in ["xx", "yy", "xy"]:
shapeMean = np.mean(catalog["base_SdssShape_"+suffix])
shapeErrMean = np.nanmean(catalog["base_SdssShape_"+suffix+"Err"])
shapeInterval68 = 0.5*(np.nanpercentile(catalog["base_SdssShape_"+suffix], 84)
- np.nanpercentile(catalog["base_SdssShape_"+suffix], 16))
self.assertFloatsAlmostEqual(np.nanstd(catalog["base_SdssShape_"+suffix]),
shapeInterval68, rtol=0.03)
self.assertFloatsAlmostEqual(shapeErrMean, shapeInterval68, rtol=0.03)
self.assertLess(abs(shapeMean - record.get("truth_"+suffix)), 2.0*shapeErrMean/nSamples**0.5)


class SdssShapeTransformTestCase(lsst.meas.base.tests.FluxTransformTestCase,
lsst.meas.base.tests.CentroidTransformTestCase,
Expand Down