Coverage for tests/test_jointcal.py : 20%

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_invalid_model(self):
178 self.model.validate.return_value = False
179 with(self.assertRaises(ValueError)):
180 self.jointcal._logChi2AndValidate(self.associations, self.fitter, self.model, "invalid")
182 def test_nonfinite_chi2(self):
183 self.fitter.computeChi2.return_value = self.nanChi2
184 with(self.assertRaises(FloatingPointError)):
185 self.jointcal._logChi2AndValidate(self.associations, self.fitter, self.model, "nonfinite")
187 def test_writeChi2(self):
188 filename = "somefile"
189 self.jointcal._logChi2AndValidate(self.associations, self.fitter, self.model, "writeCh2",
190 writeChi2Name=filename)
191 # logChi2AndValidate prepends `config.debugOutputPath` to the filename
192 self.fitter.saveChi2Contributions.assert_called_with("./"+filename+"{type}")
195class TestJointcalLoadRefCat(JointcalTestBase, lsst.utils.tests.TestCase):
197 def _make_fake_refcat(self):
198 """Mock a fake reference catalog and the bits necessary to use it."""
199 center = lsst.geom.SpherePoint(30, -30, lsst.geom.degrees)
200 flux = 10
201 radius = 1 * lsst.geom.degrees
202 filterName = 'fake'
204 fakeRefCat = make_fake_refcat(center, flux, filterName)
205 fluxField = getRefFluxField(fakeRefCat.schema, filterName)
206 returnStruct = lsst.pipe.base.Struct(refCat=fakeRefCat, fluxField=fluxField)
207 refObjLoader = mock.Mock(spec=LoadIndexedReferenceObjectsTask)
208 refObjLoader.loadSkyCircle.return_value = returnStruct
210 return refObjLoader, center, radius, filterName, fakeRefCat
212 def test_load_reference_catalog(self):
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 jointcal = lsst.jointcal.JointcalTask(config=config, butler=self.butler)
219 # NOTE: we cannot test application of proper motion here, because we
220 # mock the refObjLoader, so the real loader is never called.
221 refCat, fluxField = jointcal._load_reference_catalog(refObjLoader,
222 jointcal.astrometryReferenceSelector,
223 center,
224 radius,
225 filterName)
226 # operator== isn't implemented for Catalogs, so we have to check like
227 # this, in case the records are copied during load.
228 self.assertEqual(len(refCat), len(fakeRefCat))
229 for r1, r2 in zip(refCat, fakeRefCat):
230 self.assertEqual(r1, r2)
232 def test_load_reference_catalog_subselect(self):
233 """Test that we can select out the one source in the fake refcat
234 with a ridiculous S/N cut.
235 """
236 refObjLoader, center, radius, filterName, fakeRefCat = self._make_fake_refcat()
238 config = lsst.jointcal.jointcal.JointcalConfig()
239 config.astrometryReferenceErr = 0.1 # our test refcats don't have coord errors
240 config.astrometryReferenceSelector.doSignalToNoise = True
241 config.astrometryReferenceSelector.signalToNoise.minimum = 1e10
242 config.astrometryReferenceSelector.signalToNoise.fluxField = "fake_flux"
243 config.astrometryReferenceSelector.signalToNoise.errField = "fake_fluxErr"
244 jointcal = lsst.jointcal.JointcalTask(config=config, butler=self.butler)
246 refCat, fluxField = jointcal._load_reference_catalog(refObjLoader,
247 jointcal.astrometryReferenceSelector,
248 center,
249 radius,
250 filterName)
251 self.assertEqual(len(refCat), 0)
254class TestJointcalFitModel(JointcalTestBase, lsst.utils.tests.TestCase):
255 def test_fit_photometry_writeChi2(self):
256 """Test that we are calling saveChi2 with appropriate file prefixes."""
257 self.config.photometryModel = "constrainedFlux"
258 self.config.writeChi2FilesOuterLoop = True
259 jointcal = lsst.jointcal.JointcalTask(config=self.config, butler=self.butler)
260 jointcal.focalPlaneBBox = lsst.geom.Box2D()
262 # Mock the fitter, so we can pretend it found a good fit
263 with mock.patch("lsst.jointcal.PhotometryFit", autospect=True) as fitPatch:
264 fitPatch.return_value.computeChi2.return_value = self.goodChi2
265 fitPatch.return_value.minimize.return_value = MinimizeResult.Converged
267 # config.debugOutputPath is prepended to the filenames that go into saveChi2Contributions
268 expected = ["./photometry_init-ModelVisit_chi2", "./photometry_init-Model_chi2",
269 "./photometry_init-Fluxes_chi2", "./photometry_init-ModelFluxes_chi2"]
270 expected = [mock.call(x+"-fake{type}") for x in expected]
271 jointcal._fit_photometry(self.associations, dataName=self.dataName)
272 fitPatch.return_value.saveChi2Contributions.assert_has_calls(expected)
274 def test_fit_astrometry_writeChi2(self):
275 """Test that we are calling saveChi2 with appropriate file prefixes."""
276 self.config.astrometryModel = "constrained"
277 self.config.writeChi2FilesOuterLoop = True
278 jointcal = lsst.jointcal.JointcalTask(config=self.config, butler=self.butler)
279 jointcal.focalPlaneBBox = lsst.geom.Box2D()
281 # Mock the fitter, so we can pretend it found a good fit
282 fitPatch = mock.patch("lsst.jointcal.AstrometryFit")
283 # Mock the projection handler so we don't segfault due to not-fully initialized ccdImages
284 projectorPatch = mock.patch("lsst.jointcal.OneTPPerVisitHandler")
285 with fitPatch as fit, projectorPatch as projector:
286 fit.return_value.computeChi2.return_value = self.goodChi2
287 fit.return_value.minimize.return_value = MinimizeResult.Converged
288 # return a real ProjectionHandler to keep ConstrainedAstrometryModel() happy
289 projector.return_value = lsst.jointcal.IdentityProjectionHandler()
291 # config.debugOutputPath is prepended to the filenames that go into saveChi2Contributions
292 expected = ["./astrometry_init-DistortionsVisit_chi2", "./astrometry_init-Distortions_chi2",
293 "./astrometry_init-Positions_chi2", "./astrometry_init-DistortionsPositions_chi2"]
294 expected = [mock.call(x+"-fake{type}") for x in expected]
295 jointcal._fit_astrometry(self.associations, dataName=self.dataName)
296 fit.return_value.saveChi2Contributions.assert_has_calls(expected)
299class TestComputeBoundingCircle(lsst.utils.tests.TestCase):
300 """Tests of Associations.computeBoundingCircle()"""
301 def _checkPointsInCircle(self, points, center, radius):
302 """Check that all points are within the (center, radius) circle.
304 The test is whether the max(points - center) separation is equal to
305 (or slightly less than) radius.
306 """
307 maxSeparation = 0*lsst.geom.degrees
308 for point in points:
309 maxSeparation = max(maxSeparation, center.separation(point))
310 self.assertAnglesAlmostEqual(maxSeparation, radius, maxDiff=3*lsst.geom.arcseconds)
311 self.assertLess(maxSeparation, radius)
313 def _testPoints(self, ccdImage1, ccdImage2, skyWcs1, skyWcs2, bbox):
314 """Fill an Associations object and test that it computes the correct
315 bounding circle for the input data.
317 Parameters
318 ----------
319 ccdImage1, ccdImage2 : `lsst.jointcal.CcdImage`
320 The CcdImages to add to the Associations object.
321 skyWcs1, skyWcs2 : `lsst.afw.geom.SkyWcs`
322 The WCS of each of the above images.
323 bbox : `lsst.geom.Box2D`
324 The ccd bounding box of both images.
325 """
326 lsst.log.setLevel('jointcal', lsst.log.DEBUG)
327 associations = lsst.jointcal.Associations()
328 associations.addCcdImage(ccdImage1)
329 associations.addCcdImage(ccdImage2)
330 associations.computeCommonTangentPoint()
332 circle = associations.computeBoundingCircle()
333 center = lsst.geom.SpherePoint(circle.getCenter())
334 radius = lsst.geom.Angle(circle.getOpeningAngle().asRadians(), lsst.geom.radians)
335 points = [lsst.geom.SpherePoint(skyWcs1.pixelToSky(lsst.geom.Point2D(x)))
336 for x in bbox.getCorners()]
337 points.extend([lsst.geom.SpherePoint(skyWcs2.pixelToSky(lsst.geom.Point2D(x)))
338 for x in bbox.getCorners()])
339 self._checkPointsInCircle(points, center, radius)
341 def testPoints(self):
342 """Test for points in an "easy" area, far from RA=0 or the poles."""
343 struct = lsst.jointcal.testUtils.createTwoFakeCcdImages()
344 self._testPoints(struct.ccdImageList[0], struct.ccdImageList[1],
345 struct.skyWcs[0], struct.skyWcs[1], struct.bbox)
347 def testPointsRA0(self):
348 """Test for CcdImages crossing RA=0; this demonstrates a fix for
349 the bug described in DM-19802.
350 """
351 wcs1, wcs2 = make_fake_wcs()
353 # Put the visit boresights at the WCS origin, for consistency
354 visitInfo1 = lsst.afw.image.VisitInfo(exposureId=30577512,
355 date=DateTime(65321.1),
356 boresightRaDec=wcs1.getSkyOrigin())
357 visitInfo2 = lsst.afw.image.VisitInfo(exposureId=30621144,
358 date=DateTime(65322.1),
359 boresightRaDec=wcs1.getSkyOrigin())
361 struct = lsst.jointcal.testUtils.createTwoFakeCcdImages(fakeWcses=[wcs1, wcs2],
362 fakeVisitInfos=[visitInfo1, visitInfo2])
363 self._testPoints(struct.ccdImageList[0], struct.ccdImageList[1],
364 struct.skyWcs[0], struct.skyWcs[1], struct.bbox)
367class TestJointcalComputePMDate(JointcalTestBase, lsst.utils.tests.TestCase):
368 """Tests of jointcal._compute_proper_motion_epoch()"""
369 def test_compute_proper_motion_epoch(self):
370 mjds = np.array((65432.1, 66666, 65555, 64322.2))
372 wcs1, wcs2 = make_fake_wcs()
373 visitInfo1 = lsst.afw.image.VisitInfo(exposureId=30577512,
374 date=DateTime(mjds[0]),
375 boresightRaDec=wcs1.getSkyOrigin())
376 visitInfo2 = lsst.afw.image.VisitInfo(exposureId=30621144,
377 date=DateTime(mjds[1]),
378 boresightRaDec=wcs2.getSkyOrigin())
379 visitInfo3 = lsst.afw.image.VisitInfo(exposureId=30577513,
380 date=DateTime(mjds[2]),
381 boresightRaDec=wcs1.getSkyOrigin())
382 visitInfo4 = lsst.afw.image.VisitInfo(exposureId=30621145,
383 date=DateTime(mjds[3]),
384 boresightRaDec=wcs2.getSkyOrigin())
386 struct1 = lsst.jointcal.testUtils.createTwoFakeCcdImages(fakeWcses=[wcs1, wcs2],
387 fakeVisitInfos=[visitInfo1, visitInfo2])
388 struct2 = lsst.jointcal.testUtils.createTwoFakeCcdImages(fakeWcses=[wcs1, wcs2],
389 fakeVisitInfos=[visitInfo3, visitInfo4])
390 ccdImageList = list(itertools.chain(struct1.ccdImageList, struct2.ccdImageList))
391 associations = lsst.jointcal.Associations()
392 for ccdImage in ccdImageList:
393 associations.addCcdImage(ccdImage)
394 associations.computeCommonTangentPoint()
396 jointcal = lsst.jointcal.JointcalTask(config=self.config, butler=self.butler)
397 result = jointcal._compute_proper_motion_epoch(ccdImageList)
398 self.assertEqual(result.mjd, mjds.mean())
401class MemoryTester(lsst.utils.tests.MemoryTestCase):
402 pass
405if __name__ == "__main__": 405 ↛ 406line 405 didn't jump to line 406, because the condition on line 405 was never true
406 lsst.utils.tests.init()
407 unittest.main()