Coverage for tests/test_jointcal.py : 21%

Hot-keys on this page
r m x p toggle line displays
j k next/prev highlighted chunk
0 (zero) top of page
1 (one) first highlighted chunk
1# This file is part of jointcal.
2#
3# Developed for the LSST Data Management System.
4# This product includes software developed by the LSST Project
5# (https://www.lsst.org).
6# See the COPYRIGHT file at the top-level directory of this distribution
7# for details of code ownership.
8#
9# This program is free software: you can redistribute it and/or modify
10# it under the terms of the GNU General Public License as published by
11# the Free Software Foundation, either version 3 of the License, or
12# (at your option) any later version.
13#
14# This program is distributed in the hope that it will be useful,
15# but WITHOUT ANY WARRANTY; without even the implied warranty of
16# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
17# GNU General Public License for more details.
18#
19# You should have received a copy of the GNU General Public License
20# along with this program. If not, see <https://www.gnu.org/licenses/>.
22import unittest
23from unittest import mock
25import numpy as np
27import lsst.log
28import lsst.utils
30import lsst.afw.table
31import lsst.daf.persistence
32from lsst.daf.base import DateTime
33import lsst.geom
34from lsst.meas.algorithms import getRefFluxField, LoadIndexedReferenceObjectsTask, DatasetConfig
35import lsst.pipe.base
36import lsst.jointcal
37from lsst.jointcal import MinimizeResult
38import lsst.jointcal.chi2
39import lsst.jointcal.testUtils
42# for MemoryTestCase
43def setup_module(module):
44 lsst.utils.tests.init()
47def make_fake_refcat(center, flux, filterName):
48 """Make a fake reference catalog."""
49 schema = LoadIndexedReferenceObjectsTask.makeMinimalSchema([filterName])
50 catalog = lsst.afw.table.SimpleCatalog(schema)
51 record = catalog.addNew()
52 record.setCoord(center)
53 record[filterName + '_flux'] = flux
54 record[filterName + '_fluxErr'] = flux*0.1
55 return catalog
58class JointcalTestBase:
59 def setUp(self):
60 struct = lsst.jointcal.testUtils.createTwoFakeCcdImages(100, 100)
61 self.ccdImageList = struct.ccdImageList
62 # so that countStars() returns nonzero results
63 for ccdImage in self.ccdImageList:
64 ccdImage.resetCatalogForFit()
66 self.goodChi2 = lsst.jointcal.chi2.Chi2Statistic()
67 # chi2/ndof == 2.0 should be non-bad
68 self.goodChi2.chi2 = 200.0
69 self.goodChi2.ndof = 100
71 self.badChi2 = lsst.jointcal.chi2.Chi2Statistic()
72 self.badChi2.chi2 = 600.0
73 self.badChi2.ndof = 100
75 self.nanChi2 = lsst.jointcal.chi2.Chi2Statistic()
76 self.nanChi2.chi2 = np.nan
77 self.nanChi2.ndof = 100
79 self.maxSteps = 20
80 self.name = "testing"
81 self.dataName = "fake"
82 self.whatToFit = "" # unneeded, since we're mocking the fitter
84 # Mock a Butler so the refObjLoaders have something to call `get()` on.
85 self.butler = unittest.mock.Mock(spec=lsst.daf.persistence.Butler)
86 self.butler.get.return_value.indexer = DatasetConfig().indexer
88 # Mock the association manager and give it access to the ccd list above.
89 self.associations = mock.Mock(spec=lsst.jointcal.Associations)
90 self.associations.getCcdImageList.return_value = self.ccdImageList
92 # a default config to be modified by individual tests
93 self.config = lsst.jointcal.jointcal.JointcalConfig()
96class TestJointcalIterateFit(JointcalTestBase, lsst.utils.tests.TestCase):
97 def setUp(self):
98 super().setUp()
99 # Mock the fitter and model, so we can force particular
100 # return values/exceptions. Default to "good" return values.
101 self.fitter = mock.Mock(spec=lsst.jointcal.PhotometryFit)
102 self.fitter.computeChi2.return_value = self.goodChi2
103 self.fitter.minimize.return_value = MinimizeResult.Converged
104 self.model = mock.Mock(spec=lsst.jointcal.SimpleFluxModel)
106 self.jointcal = lsst.jointcal.JointcalTask(config=self.config, butler=self.butler)
108 def test_iterateFit_success(self):
109 chi2 = self.jointcal._iterate_fit(self.associations, self.fitter,
110 self.maxSteps, self.name, self.whatToFit)
111 self.assertEqual(chi2, self.goodChi2)
112 # Once for the for loop, the second time for the rank update.
113 self.assertEqual(self.fitter.minimize.call_count, 2)
115 def test_iterateFit_writeChi2Outer(self):
116 chi2 = self.jointcal._iterate_fit(self.associations, self.fitter,
117 self.maxSteps, self.name, self.whatToFit,
118 dataName=self.dataName)
119 self.assertEqual(chi2, self.goodChi2)
120 # Once for the for loop, the second time for the rank update.
121 self.assertEqual(self.fitter.minimize.call_count, 2)
122 # Default config should not call saveChi2Contributions
123 self.fitter.saveChi2Contributions.assert_not_called()
125 def test_iterateFit_failed(self):
126 self.fitter.minimize.return_value = MinimizeResult.Failed
128 with self.assertRaises(RuntimeError):
129 self.jointcal._iterate_fit(self.associations, self.fitter,
130 self.maxSteps, self.name, self.whatToFit)
131 self.assertEqual(self.fitter.minimize.call_count, 1)
133 def test_iterateFit_badFinalChi2(self):
134 log = mock.Mock(spec=lsst.log.Log)
135 self.jointcal.log = log
136 self.fitter.computeChi2.return_value = self.badChi2
138 chi2 = self.jointcal._iterate_fit(self.associations, self.fitter,
139 self.maxSteps, self.name, self.whatToFit)
140 self.assertEqual(chi2, self.badChi2)
141 log.info.assert_called_with("%s %s", "Fit completed", self.badChi2)
142 log.error.assert_called_with("Potentially bad fit: High chi-squared/ndof.")
144 def test_iterateFit_exceedMaxSteps(self):
145 log = mock.Mock(spec=lsst.log.Log)
146 self.jointcal.log = log
147 self.fitter.minimize.return_value = MinimizeResult.Chi2Increased
148 maxSteps = 3
150 chi2 = self.jointcal._iterate_fit(self.associations, self.fitter,
151 maxSteps, self.name, self.whatToFit)
152 self.assertEqual(chi2, self.goodChi2)
153 self.assertEqual(self.fitter.minimize.call_count, maxSteps)
154 log.error.assert_called_with("testing failed to converge after %s steps" % maxSteps)
156 def test_invalid_model(self):
157 self.model.validate.return_value = False
158 with(self.assertRaises(ValueError)):
159 self.jointcal._logChi2AndValidate(self.associations, self.fitter, self.model)
161 def test_nonfinite_chi2(self):
162 self.fitter.computeChi2.return_value = self.nanChi2
163 with(self.assertRaises(FloatingPointError)):
164 self.jointcal._logChi2AndValidate(self.associations, self.fitter, self.model)
166 def test_writeChi2(self):
167 filename = "somefile"
168 self.jointcal._logChi2AndValidate(self.associations, self.fitter, self.model,
169 writeChi2Name=filename)
170 # logChi2AndValidate prepends `config.debugOutputPath` to the filename
171 self.fitter.saveChi2Contributions.assert_called_with("./"+filename+"{type}")
174class TestJointcalLoadRefCat(JointcalTestBase, lsst.utils.tests.TestCase):
176 def _make_fake_refcat(self):
177 """Make a fake reference catalog and the bits necessary to use it."""
178 center = lsst.geom.SpherePoint(30, -30, lsst.geom.degrees)
179 flux = 10
180 radius = 1 * lsst.geom.degrees
181 filterName = 'fake'
183 fakeRefCat = make_fake_refcat(center, flux, filterName)
184 fluxField = getRefFluxField(fakeRefCat.schema, filterName)
185 returnStruct = lsst.pipe.base.Struct(refCat=fakeRefCat, fluxField=fluxField)
186 refObjLoader = mock.Mock(spec=LoadIndexedReferenceObjectsTask)
187 refObjLoader.loadSkyCircle.return_value = returnStruct
189 return refObjLoader, center, radius, filterName, fakeRefCat
191 def test_load_reference_catalog(self):
192 refObjLoader, center, radius, filterName, fakeRefCat = self._make_fake_refcat()
194 config = lsst.jointcal.jointcal.JointcalConfig()
195 config.astrometryReferenceErr = 0.1 # our test refcats don't have coord errors
196 jointcal = lsst.jointcal.JointcalTask(config=config, butler=self.butler)
198 refCat, fluxField = jointcal._load_reference_catalog(refObjLoader,
199 jointcal.astrometryReferenceSelector,
200 center,
201 radius,
202 filterName)
203 # operator== isn't implemented for Catalogs, so we have to check like
204 # this, in case the records are copied during load.
205 self.assertEqual(len(refCat), len(fakeRefCat))
206 for r1, r2 in zip(refCat, fakeRefCat):
207 self.assertEqual(r1, r2)
209 def test_load_reference_catalog_subselect(self):
210 """Test that we can select out the one source in the fake refcat
211 with a ridiculous S/N cut.
212 """
213 refObjLoader, center, radius, filterName, fakeRefCat = self._make_fake_refcat()
215 config = lsst.jointcal.jointcal.JointcalConfig()
216 config.astrometryReferenceErr = 0.1 # our test refcats don't have coord errors
217 config.astrometryReferenceSelector.doSignalToNoise = True
218 config.astrometryReferenceSelector.signalToNoise.minimum = 1e10
219 config.astrometryReferenceSelector.signalToNoise.fluxField = "fake_flux"
220 config.astrometryReferenceSelector.signalToNoise.errField = "fake_fluxErr"
221 jointcal = lsst.jointcal.JointcalTask(config=config, butler=self.butler)
223 refCat, fluxField = jointcal._load_reference_catalog(refObjLoader,
224 jointcal.astrometryReferenceSelector,
225 center,
226 radius,
227 filterName)
228 self.assertEqual(len(refCat), 0)
231class TestJointcalFitModel(JointcalTestBase, lsst.utils.tests.TestCase):
232 def test_fit_photometry_writeChi2(self):
233 """Test that we are calling saveChi2 with appropriate file prefixes."""
234 self.config.photometryModel = "constrainedFlux"
235 self.config.writeChi2FilesOuterLoop = True
236 jointcal = lsst.jointcal.JointcalTask(config=self.config, butler=self.butler)
237 jointcal.focalPlaneBBox = lsst.geom.Box2D()
239 # Mock the fitter, so we can pretend it found a good fit
240 with mock.patch("lsst.jointcal.PhotometryFit", autospect=True) as fitPatch:
241 fitPatch.return_value.computeChi2.return_value = self.goodChi2
242 fitPatch.return_value.minimize.return_value = MinimizeResult.Converged
244 # config.debugOutputPath is prepended to the filenames that go into saveChi2Contributions
245 expected = ["./photometry_init-ModelVisit_chi2", "./photometry_init-Model_chi2",
246 "./photometry_init-Fluxes_chi2", "./photometry_init-ModelFluxes_chi2"]
247 expected = [mock.call(x+"-fake{type}") for x in expected]
248 jointcal._fit_photometry(self.associations, dataName=self.dataName)
249 fitPatch.return_value.saveChi2Contributions.assert_has_calls(expected)
251 def test_fit_astrometry_writeChi2(self):
252 """Test that we are calling saveChi2 with appropriate file prefixes."""
253 self.config.astrometryModel = "constrained"
254 self.config.writeChi2FilesOuterLoop = True
255 jointcal = lsst.jointcal.JointcalTask(config=self.config, butler=self.butler)
256 jointcal.focalPlaneBBox = lsst.geom.Box2D()
258 # Mock the fitter, so we can pretend it found a good fit
259 fitPatch = mock.patch("lsst.jointcal.AstrometryFit")
260 # Mock the projection handler so we don't segfault due to not-fully initialized ccdImages
261 projectorPatch = mock.patch("lsst.jointcal.OneTPPerVisitHandler")
262 with fitPatch as fit, projectorPatch as projector:
263 fit.return_value.computeChi2.return_value = self.goodChi2
264 fit.return_value.minimize.return_value = MinimizeResult.Converged
265 # return a real ProjectionHandler to keep ConstrainedAstrometryModel() happy
266 projector.return_value = lsst.jointcal.IdentityProjectionHandler()
268 # config.debugOutputPath is prepended to the filenames that go into saveChi2Contributions
269 expected = ["./astrometry_init-DistortionsVisit_chi2", "./astrometry_init-Distortions_chi2",
270 "./astrometry_init-Positions_chi2", "./astrometry_init-DistortionsPositions_chi2"]
271 expected = [mock.call(x+"-fake{type}") for x in expected]
272 jointcal._fit_astrometry(self.associations, dataName=self.dataName)
273 fit.return_value.saveChi2Contributions.assert_has_calls(expected)
276class TestComputeBoundingCircle(lsst.utils.tests.TestCase):
277 """Tests of Associations.computeBoundingCircle()"""
278 def _checkPointsInCircle(self, points, center, radius):
279 """Check that all points are within the (center, radius) circle.
281 The test is whether the max(points - center) separation is equal to
282 (or slightly less than) radius.
283 """
284 maxSeparation = 0*lsst.geom.degrees
285 for point in points:
286 maxSeparation = max(maxSeparation, center.separation(point))
287 self.assertAnglesAlmostEqual(maxSeparation, radius, maxDiff=3*lsst.geom.arcseconds)
288 self.assertLess(maxSeparation, radius)
290 def _testPoints(self, ccdImage1, ccdImage2, skyWcs1, skyWcs2, bbox):
291 """Fill an Associations object and test that it computes the correct
292 bounding circle for the input data.
294 Parameters
295 ----------
296 ccdImage1, ccdImage2 : `lsst.jointcal.CcdImage`
297 The CcdImages to add to the Associations object.
298 skyWcs1, skyWcs2 : `lsst.afw.geom.SkyWcs`
299 The WCS of each of the above images.
300 bbox : `lsst.geom.Box2D`
301 The ccd bounding box of both images.
302 """
303 lsst.log.setLevel('jointcal', lsst.log.DEBUG)
304 associations = lsst.jointcal.Associations()
305 associations.addCcdImage(ccdImage1)
306 associations.addCcdImage(ccdImage2)
307 associations.computeCommonTangentPoint()
309 circle = associations.computeBoundingCircle()
310 center = lsst.geom.SpherePoint(circle.getCenter())
311 radius = lsst.geom.Angle(circle.getOpeningAngle().asRadians(), lsst.geom.radians)
312 points = [lsst.geom.SpherePoint(skyWcs1.pixelToSky(lsst.geom.Point2D(x)))
313 for x in bbox.getCorners()]
314 points.extend([lsst.geom.SpherePoint(skyWcs2.pixelToSky(lsst.geom.Point2D(x)))
315 for x in bbox.getCorners()])
316 self._checkPointsInCircle(points, center, radius)
318 def testPoints(self):
319 """Test for points in an "easy" area, far from RA=0 or the poles."""
320 struct = lsst.jointcal.testUtils.createTwoFakeCcdImages()
321 self._testPoints(struct.ccdImageList[0], struct.ccdImageList[1],
322 struct.skyWcs[0], struct.skyWcs[1], struct.bbox)
324 def testPointsRA0(self):
325 """Test for CcdImages crossing RA=0; this demonstrates a fix for
326 the bug described in DM-19802.
327 """
328 # Use the same pixel origins as the cfht_minimal data, but put the sky origin at RA=0
329 crpix = lsst.geom.Point2D(931.517869, 2438.572109)
330 cd = np.array([[5.19513851e-05, -2.81124812e-07],
331 [-3.25186974e-07, -5.19112119e-05]])
332 crval1 = lsst.geom.SpherePoint(0.01, -0.01, lsst.geom.degrees)
333 crval2 = lsst.geom.SpherePoint(-0.01, 0.01, lsst.geom.degrees)
334 wcs1 = lsst.afw.geom.makeSkyWcs(crpix, crval1, cd)
335 wcs2 = lsst.afw.geom.makeSkyWcs(crpix, crval2, cd)
337 # Put the visit boresights at the WCS origin, for consistency
338 visitInfo1 = lsst.afw.image.VisitInfo(exposureId=30577512,
339 date=DateTime(65321.1),
340 boresightRaDec=wcs1.getSkyOrigin())
341 visitInfo2 = lsst.afw.image.VisitInfo(exposureId=30621144,
342 date=DateTime(65322.1),
343 boresightRaDec=wcs1.getSkyOrigin())
345 struct = lsst.jointcal.testUtils.createTwoFakeCcdImages(fakeWcses=[wcs1, wcs2],
346 fakeVisitInfos=[visitInfo1, visitInfo2])
347 self._testPoints(struct.ccdImageList[0], struct.ccdImageList[1],
348 struct.skyWcs[0], struct.skyWcs[1], struct.bbox)
351class MemoryTester(lsst.utils.tests.MemoryTestCase):
352 pass
355if __name__ == "__main__": 355 ↛ 356line 355 didn't jump to line 356, because the condition on line 355 was never true
356 lsst.utils.tests.init()
357 unittest.main()