Coverage for tests/test_jointcal.py : 19%

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 itertools
23import unittest
24from unittest import mock
26import numpy as np
28import lsst.log
29import lsst.utils
31import lsst.afw.table
32import lsst.daf.persistence
33from lsst.daf.base import DateTime
34import lsst.geom
35from lsst.meas.algorithms import getRefFluxField, LoadIndexedReferenceObjectsTask, DatasetConfig
36import lsst.pipe.base
37import lsst.jointcal
38from lsst.jointcal import MinimizeResult
39import lsst.jointcal.chi2
40import lsst.jointcal.testUtils
43# for MemoryTestCase
44def setup_module(module):
45 lsst.utils.tests.init()
48def make_fake_refcat(center, flux, filterName):
49 """Make a fake reference catalog."""
50 schema = LoadIndexedReferenceObjectsTask.makeMinimalSchema([filterName],
51 addProperMotion=True)
52 catalog = lsst.afw.table.SimpleCatalog(schema)
53 record = catalog.addNew()
54 record.setCoord(center)
55 record[filterName + '_flux'] = flux
56 record[filterName + '_fluxErr'] = flux*0.1
57 record['pm_ra'] = lsst.geom.Angle(1)
58 record['pm_dec'] = lsst.geom.Angle(2)
59 record['epoch'] = 65432.1
60 return catalog
63def make_fake_wcs():
64 """Return two simple SkyWcs objects, with slightly different sky positions.
66 Use the same pixel origins as the cfht_minimal data, but put the sky origin
67 at RA=0
68 """
69 crpix = lsst.geom.Point2D(931.517869, 2438.572109)
70 cd = np.array([[5.19513851e-05, -2.81124812e-07],
71 [-3.25186974e-07, -5.19112119e-05]])
72 crval1 = lsst.geom.SpherePoint(0.01, -0.01, lsst.geom.degrees)
73 crval2 = lsst.geom.SpherePoint(-0.01, 0.01, lsst.geom.degrees)
74 wcs1 = lsst.afw.geom.makeSkyWcs(crpix, crval1, cd)
75 wcs2 = lsst.afw.geom.makeSkyWcs(crpix, crval2, cd)
76 return wcs1, wcs2
79class JointcalTestBase:
80 def setUp(self):
81 struct = lsst.jointcal.testUtils.createTwoFakeCcdImages(100, 100)
82 self.ccdImageList = struct.ccdImageList
83 # so that countStars() returns nonzero results
84 for ccdImage in self.ccdImageList:
85 ccdImage.resetCatalogForFit()
87 self.goodChi2 = lsst.jointcal.chi2.Chi2Statistic()
88 # chi2/ndof == 2.0 should be non-bad
89 self.goodChi2.chi2 = 200.0
90 self.goodChi2.ndof = 100
92 self.badChi2 = lsst.jointcal.chi2.Chi2Statistic()
93 self.badChi2.chi2 = 600.0
94 self.badChi2.ndof = 100
96 self.nanChi2 = lsst.jointcal.chi2.Chi2Statistic()
97 self.nanChi2.chi2 = np.nan
98 self.nanChi2.ndof = 100
100 self.maxSteps = 20
101 self.name = "testing"
102 self.dataName = "fake"
103 self.whatToFit = "" # unneeded, since we're mocking the fitter
105 # Mock a Butler so the refObjLoaders have something to call `get()` on.
106 self.butler = unittest.mock.Mock(spec=lsst.daf.persistence.Butler)
107 self.butler.get.return_value.indexer = DatasetConfig().indexer
109 # Mock the association manager and give it access to the ccd list above.
110 self.associations = mock.Mock(spec=lsst.jointcal.Associations)
111 self.associations.getCcdImageList.return_value = self.ccdImageList
113 # a default config to be modified by individual tests
114 self.config = lsst.jointcal.jointcal.JointcalConfig()
117class TestJointcalIterateFit(JointcalTestBase, lsst.utils.tests.TestCase):
118 def setUp(self):
119 super().setUp()
120 # Mock the fitter and model, so we can force particular
121 # return values/exceptions. Default to "good" return values.
122 self.fitter = mock.Mock(spec=lsst.jointcal.PhotometryFit)
123 self.fitter.computeChi2.return_value = self.goodChi2
124 self.fitter.minimize.return_value = MinimizeResult.Converged
125 self.model = mock.Mock(spec=lsst.jointcal.SimpleFluxModel)
127 self.jointcal = lsst.jointcal.JointcalTask(config=self.config, butler=self.butler)
129 def test_iterateFit_success(self):
130 chi2 = self.jointcal._iterate_fit(self.associations, self.fitter,
131 self.maxSteps, self.name, self.whatToFit)
132 self.assertEqual(chi2, self.goodChi2)
133 # Once for the for loop, the second time for the rank update.
134 self.assertEqual(self.fitter.minimize.call_count, 2)
136 def test_iterateFit_writeChi2Outer(self):
137 chi2 = self.jointcal._iterate_fit(self.associations, self.fitter,
138 self.maxSteps, self.name, self.whatToFit,
139 dataName=self.dataName)
140 self.assertEqual(chi2, self.goodChi2)
141 # Once for the for loop, the second time for the rank update.
142 self.assertEqual(self.fitter.minimize.call_count, 2)
143 # Default config should not call saveChi2Contributions
144 self.fitter.saveChi2Contributions.assert_not_called()
146 def test_iterateFit_failed(self):
147 self.fitter.minimize.return_value = MinimizeResult.Failed
149 with self.assertRaises(RuntimeError):
150 self.jointcal._iterate_fit(self.associations, self.fitter,
151 self.maxSteps, self.name, self.whatToFit)
152 self.assertEqual(self.fitter.minimize.call_count, 1)
154 def test_iterateFit_badFinalChi2(self):
155 log = mock.Mock(spec=lsst.log.Log)
156 self.jointcal.log = log
157 self.fitter.computeChi2.return_value = self.badChi2
159 chi2 = self.jointcal._iterate_fit(self.associations, self.fitter,
160 self.maxSteps, self.name, self.whatToFit)
161 self.assertEqual(chi2, self.badChi2)
162 log.info.assert_called_with("%s %s", "Fit completed", self.badChi2)
163 log.error.assert_called_with("Potentially bad fit: High chi-squared/ndof.")
165 def test_iterateFit_exceedMaxSteps(self):
166 log = mock.Mock(spec=lsst.log.Log)
167 self.jointcal.log = log
168 self.fitter.minimize.return_value = MinimizeResult.Chi2Increased
169 maxSteps = 3
171 chi2 = self.jointcal._iterate_fit(self.associations, self.fitter,
172 maxSteps, self.name, self.whatToFit)
173 self.assertEqual(chi2, self.goodChi2)
174 self.assertEqual(self.fitter.minimize.call_count, maxSteps)
175 log.error.assert_called_with("testing failed to converge after %s steps" % maxSteps)
177 def test_moderate_chi2_increase(self):
178 """DM-25159: warn, but don't fail, on moderate chi2 increases between
179 steps.
180 """
181 chi2_1 = lsst.jointcal.chi2.Chi2Statistic()
182 chi2_1.chi2 = 100.0
183 chi2_1.ndof = 100
184 chi2_2 = lsst.jointcal.chi2.Chi2Statistic()
185 chi2_2.chi2 = 300.0
186 chi2_2.ndof = 100
188 chi2s = [self.goodChi2, chi2_1, chi2_2, self.goodChi2, self.goodChi2]
189 self.fitter.computeChi2.side_effect = chi2s
190 self.fitter.minimize.side_effect = [MinimizeResult.Chi2Increased,
191 MinimizeResult.Chi2Increased,
192 MinimizeResult.Chi2Increased,
193 MinimizeResult.Converged,
194 MinimizeResult.Converged]
195 with lsst.log.UsePythonLogging(): # so that assertLogs works with lsst.log
196 with self.assertLogs("jointcal", level="WARN") as logger:
197 self.jointcal._iterate_fit(self.associations, self.fitter,
198 self.maxSteps, self.name, self.whatToFit)
199 msg = "WARNING:jointcal:Significant chi2 increase by a factor of 300 / 100 = 3"
200 self.assertIn(msg, logger.output)
202 def test_large_chi2_increase_fails(self):
203 """DM-25159: fail on large chi2 increases between steps."""
204 chi2_1 = lsst.jointcal.chi2.Chi2Statistic()
205 chi2_1.chi2 = 1e11
206 chi2_1.ndof = 100
207 chi2_2 = lsst.jointcal.chi2.Chi2Statistic()
208 chi2_2.chi2 = 1.123456e13 # to check floating point formatting
209 chi2_2.ndof = 100
211 chi2s = [chi2_1, chi2_1, chi2_2]
212 self.fitter.computeChi2.side_effect = chi2s
213 self.fitter.minimize.return_value = MinimizeResult.Chi2Increased
214 with lsst.log.UsePythonLogging(): # so that assertLogs works with lsst.log
215 with self.assertLogs("jointcal", level="WARN") as logger:
216 with(self.assertRaisesRegex(RuntimeError, "Large chi2 increase")):
217 self.jointcal._iterate_fit(self.associations, self.fitter,
218 self.maxSteps, self.name, self.whatToFit)
219 msg = "WARNING:jointcal:Significant chi2 increase by a factor of 1.123e+13 / 1e+11 = 112.3"
220 self.assertIn(msg, logger.output)
222 def test_invalid_model(self):
223 self.model.validate.return_value = False
224 with(self.assertRaises(ValueError)):
225 self.jointcal._logChi2AndValidate(self.associations, self.fitter, self.model, "invalid")
227 def test_nonfinite_chi2(self):
228 self.fitter.computeChi2.return_value = self.nanChi2
229 with(self.assertRaises(FloatingPointError)):
230 self.jointcal._logChi2AndValidate(self.associations, self.fitter, self.model, "nonfinite")
232 def test_writeChi2(self):
233 filename = "somefile"
234 self.jointcal._logChi2AndValidate(self.associations, self.fitter, self.model, "writeCh2",
235 writeChi2Name=filename)
236 # logChi2AndValidate prepends `config.debugOutputPath` to the filename
237 self.fitter.saveChi2Contributions.assert_called_with("./"+filename+"{type}")
240class TestJointcalLoadRefCat(JointcalTestBase, lsst.utils.tests.TestCase):
242 def _make_fake_refcat(self):
243 """Mock a fake reference catalog and the bits necessary to use it."""
244 center = lsst.geom.SpherePoint(30, -30, lsst.geom.degrees)
245 flux = 10
246 radius = 1 * lsst.geom.degrees
247 filterName = 'fake'
249 fakeRefCat = make_fake_refcat(center, flux, filterName)
250 fluxField = getRefFluxField(fakeRefCat.schema, filterName)
251 returnStruct = lsst.pipe.base.Struct(refCat=fakeRefCat, fluxField=fluxField)
252 refObjLoader = mock.Mock(spec=LoadIndexedReferenceObjectsTask)
253 refObjLoader.loadSkyCircle.return_value = returnStruct
255 return refObjLoader, center, radius, filterName, fakeRefCat
257 def test_load_reference_catalog(self):
258 refObjLoader, center, radius, filterName, fakeRefCat = self._make_fake_refcat()
260 config = lsst.jointcal.jointcal.JointcalConfig()
261 config.astrometryReferenceErr = 0.1 # our test refcats don't have coord errors
262 jointcal = lsst.jointcal.JointcalTask(config=config, butler=self.butler)
264 # NOTE: we cannot test application of proper motion here, because we
265 # mock the refObjLoader, so the real loader is never called.
266 refCat, fluxField = jointcal._load_reference_catalog(refObjLoader,
267 jointcal.astrometryReferenceSelector,
268 center,
269 radius,
270 filterName)
271 # operator== isn't implemented for Catalogs, so we have to check like
272 # this, in case the records are copied during load.
273 self.assertEqual(len(refCat), len(fakeRefCat))
274 for r1, r2 in zip(refCat, fakeRefCat):
275 self.assertEqual(r1, r2)
277 def test_load_reference_catalog_subselect(self):
278 """Test that we can select out the one source in the fake refcat
279 with a ridiculous S/N cut.
280 """
281 refObjLoader, center, radius, filterName, fakeRefCat = self._make_fake_refcat()
283 config = lsst.jointcal.jointcal.JointcalConfig()
284 config.astrometryReferenceErr = 0.1 # our test refcats don't have coord errors
285 config.astrometryReferenceSelector.doSignalToNoise = True
286 config.astrometryReferenceSelector.signalToNoise.minimum = 1e10
287 config.astrometryReferenceSelector.signalToNoise.fluxField = "fake_flux"
288 config.astrometryReferenceSelector.signalToNoise.errField = "fake_fluxErr"
289 jointcal = lsst.jointcal.JointcalTask(config=config, butler=self.butler)
291 refCat, fluxField = jointcal._load_reference_catalog(refObjLoader,
292 jointcal.astrometryReferenceSelector,
293 center,
294 radius,
295 filterName)
296 self.assertEqual(len(refCat), 0)
299class TestJointcalFitModel(JointcalTestBase, lsst.utils.tests.TestCase):
300 def test_fit_photometry_writeChi2(self):
301 """Test that we are calling saveChi2 with appropriate file prefixes."""
302 self.config.photometryModel = "constrainedFlux"
303 self.config.writeChi2FilesOuterLoop = True
304 jointcal = lsst.jointcal.JointcalTask(config=self.config, butler=self.butler)
305 jointcal.focalPlaneBBox = lsst.geom.Box2D()
307 # Mock the fitter, so we can pretend it found a good fit
308 with mock.patch("lsst.jointcal.PhotometryFit", autospect=True) as fitPatch:
309 fitPatch.return_value.computeChi2.return_value = self.goodChi2
310 fitPatch.return_value.minimize.return_value = MinimizeResult.Converged
312 # config.debugOutputPath is prepended to the filenames that go into saveChi2Contributions
313 expected = ["./photometry_init-ModelVisit_chi2", "./photometry_init-Model_chi2",
314 "./photometry_init-Fluxes_chi2", "./photometry_init-ModelFluxes_chi2"]
315 expected = [mock.call(x+"-fake{type}") for x in expected]
316 jointcal._fit_photometry(self.associations, dataName=self.dataName)
317 fitPatch.return_value.saveChi2Contributions.assert_has_calls(expected)
319 def test_fit_astrometry_writeChi2(self):
320 """Test that we are calling saveChi2 with appropriate file prefixes."""
321 self.config.astrometryModel = "constrained"
322 self.config.writeChi2FilesOuterLoop = True
323 jointcal = lsst.jointcal.JointcalTask(config=self.config, butler=self.butler)
324 jointcal.focalPlaneBBox = lsst.geom.Box2D()
326 # Mock the fitter, so we can pretend it found a good fit
327 fitPatch = mock.patch("lsst.jointcal.AstrometryFit")
328 # Mock the projection handler so we don't segfault due to not-fully initialized ccdImages
329 projectorPatch = mock.patch("lsst.jointcal.OneTPPerVisitHandler")
330 with fitPatch as fit, projectorPatch as projector:
331 fit.return_value.computeChi2.return_value = self.goodChi2
332 fit.return_value.minimize.return_value = MinimizeResult.Converged
333 # return a real ProjectionHandler to keep ConstrainedAstrometryModel() happy
334 projector.return_value = lsst.jointcal.IdentityProjectionHandler()
336 # config.debugOutputPath is prepended to the filenames that go into saveChi2Contributions
337 expected = ["./astrometry_init-DistortionsVisit_chi2", "./astrometry_init-Distortions_chi2",
338 "./astrometry_init-Positions_chi2", "./astrometry_init-DistortionsPositions_chi2"]
339 expected = [mock.call(x+"-fake{type}") for x in expected]
340 jointcal._fit_astrometry(self.associations, dataName=self.dataName)
341 fit.return_value.saveChi2Contributions.assert_has_calls(expected)
344class TestComputeBoundingCircle(lsst.utils.tests.TestCase):
345 """Tests of Associations.computeBoundingCircle()"""
346 def _checkPointsInCircle(self, points, center, radius):
347 """Check that all points are within the (center, radius) circle.
349 The test is whether the max(points - center) separation is equal to
350 (or slightly less than) radius.
351 """
352 maxSeparation = 0*lsst.geom.degrees
353 for point in points:
354 maxSeparation = max(maxSeparation, center.separation(point))
355 self.assertAnglesAlmostEqual(maxSeparation, radius, maxDiff=3*lsst.geom.arcseconds)
356 self.assertLess(maxSeparation, radius)
358 def _testPoints(self, ccdImage1, ccdImage2, skyWcs1, skyWcs2, bbox):
359 """Fill an Associations object and test that it computes the correct
360 bounding circle for the input data.
362 Parameters
363 ----------
364 ccdImage1, ccdImage2 : `lsst.jointcal.CcdImage`
365 The CcdImages to add to the Associations object.
366 skyWcs1, skyWcs2 : `lsst.afw.geom.SkyWcs`
367 The WCS of each of the above images.
368 bbox : `lsst.geom.Box2D`
369 The ccd bounding box of both images.
370 """
371 lsst.log.setLevel('jointcal', lsst.log.DEBUG)
372 associations = lsst.jointcal.Associations()
373 associations.addCcdImage(ccdImage1)
374 associations.addCcdImage(ccdImage2)
375 associations.computeCommonTangentPoint()
377 circle = associations.computeBoundingCircle()
378 center = lsst.geom.SpherePoint(circle.getCenter())
379 radius = lsst.geom.Angle(circle.getOpeningAngle().asRadians(), lsst.geom.radians)
380 points = [lsst.geom.SpherePoint(skyWcs1.pixelToSky(lsst.geom.Point2D(x)))
381 for x in bbox.getCorners()]
382 points.extend([lsst.geom.SpherePoint(skyWcs2.pixelToSky(lsst.geom.Point2D(x)))
383 for x in bbox.getCorners()])
384 self._checkPointsInCircle(points, center, radius)
386 def testPoints(self):
387 """Test for points in an "easy" area, far from RA=0 or the poles."""
388 struct = lsst.jointcal.testUtils.createTwoFakeCcdImages()
389 self._testPoints(struct.ccdImageList[0], struct.ccdImageList[1],
390 struct.skyWcs[0], struct.skyWcs[1], struct.bbox)
392 def testPointsRA0(self):
393 """Test for CcdImages crossing RA=0; this demonstrates a fix for
394 the bug described in DM-19802.
395 """
396 wcs1, wcs2 = make_fake_wcs()
398 # Put the visit boresights at the WCS origin, for consistency
399 visitInfo1 = lsst.afw.image.VisitInfo(exposureId=30577512,
400 date=DateTime(65321.1),
401 boresightRaDec=wcs1.getSkyOrigin())
402 visitInfo2 = lsst.afw.image.VisitInfo(exposureId=30621144,
403 date=DateTime(65322.1),
404 boresightRaDec=wcs1.getSkyOrigin())
406 struct = lsst.jointcal.testUtils.createTwoFakeCcdImages(fakeWcses=[wcs1, wcs2],
407 fakeVisitInfos=[visitInfo1, visitInfo2])
408 self._testPoints(struct.ccdImageList[0], struct.ccdImageList[1],
409 struct.skyWcs[0], struct.skyWcs[1], struct.bbox)
412class TestJointcalComputePMDate(JointcalTestBase, lsst.utils.tests.TestCase):
413 """Tests of jointcal._compute_proper_motion_epoch()"""
414 def test_compute_proper_motion_epoch(self):
415 mjds = np.array((65432.1, 66666, 65555, 64322.2))
417 wcs1, wcs2 = make_fake_wcs()
418 visitInfo1 = lsst.afw.image.VisitInfo(exposureId=30577512,
419 date=DateTime(mjds[0]),
420 boresightRaDec=wcs1.getSkyOrigin())
421 visitInfo2 = lsst.afw.image.VisitInfo(exposureId=30621144,
422 date=DateTime(mjds[1]),
423 boresightRaDec=wcs2.getSkyOrigin())
424 visitInfo3 = lsst.afw.image.VisitInfo(exposureId=30577513,
425 date=DateTime(mjds[2]),
426 boresightRaDec=wcs1.getSkyOrigin())
427 visitInfo4 = lsst.afw.image.VisitInfo(exposureId=30621145,
428 date=DateTime(mjds[3]),
429 boresightRaDec=wcs2.getSkyOrigin())
431 struct1 = lsst.jointcal.testUtils.createTwoFakeCcdImages(fakeWcses=[wcs1, wcs2],
432 fakeVisitInfos=[visitInfo1, visitInfo2])
433 struct2 = lsst.jointcal.testUtils.createTwoFakeCcdImages(fakeWcses=[wcs1, wcs2],
434 fakeVisitInfos=[visitInfo3, visitInfo4])
435 ccdImageList = list(itertools.chain(struct1.ccdImageList, struct2.ccdImageList))
436 associations = lsst.jointcal.Associations()
437 for ccdImage in ccdImageList:
438 associations.addCcdImage(ccdImage)
439 associations.computeCommonTangentPoint()
441 jointcal = lsst.jointcal.JointcalTask(config=self.config, butler=self.butler)
442 result = jointcal._compute_proper_motion_epoch(ccdImageList)
443 self.assertEqual(result.mjd, mjds.mean())
446class MemoryTester(lsst.utils.tests.MemoryTestCase):
447 pass
450if __name__ == "__main__": 450 ↛ 451line 450 didn't jump to line 451, because the condition on line 450 was never true
451 lsst.utils.tests.init()
452 unittest.main()