Coverage for tests/test_jointcal.py: 24%
Shortcuts 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
Shortcuts 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 os.path
24import unittest
25from unittest import mock
27import numpy as np
28import pyarrow.parquet
29import astropy.time
31import lsst.log
32import lsst.utils
34import lsst.afw.table
35import lsst.daf.persistence
36from lsst.daf.base import DateTime
37import lsst.geom
38from lsst.meas.algorithms import getRefFluxField, LoadIndexedReferenceObjectsTask, DatasetConfig
39import lsst.obs.base
40import lsst.pipe.base
41import lsst.jointcal
42from lsst.jointcal.jointcal import make_schema_table, extract_detector_catalog_from_visit_catalog
43from lsst.jointcal import MinimizeResult
44import lsst.jointcal.chi2
45import lsst.jointcal.testUtils
48# for MemoryTestCase
49def setup_module(module):
50 lsst.utils.tests.init()
53def make_fake_refcat(center, flux, filterName):
54 """Make a fake reference catalog."""
55 schema = LoadIndexedReferenceObjectsTask.makeMinimalSchema([filterName],
56 addProperMotion=True)
57 catalog = lsst.afw.table.SimpleCatalog(schema)
58 record = catalog.addNew()
59 record.setCoord(center)
60 record[filterName + '_flux'] = flux
61 record[filterName + '_fluxErr'] = flux*0.1
62 record['pm_ra'] = lsst.geom.Angle(1)
63 record['pm_dec'] = lsst.geom.Angle(2)
64 record['epoch'] = 65432.1
65 return catalog
68def make_fake_wcs():
69 """Return two simple SkyWcs objects, with slightly different sky positions.
71 Use the same pixel origins as the cfht_minimal data, but put the sky origin
72 at RA=0
73 """
74 crpix = lsst.geom.Point2D(931.517869, 2438.572109)
75 cd = np.array([[5.19513851e-05, -2.81124812e-07],
76 [-3.25186974e-07, -5.19112119e-05]])
77 crval1 = lsst.geom.SpherePoint(0.01, -0.01, lsst.geom.degrees)
78 crval2 = lsst.geom.SpherePoint(-0.01, 0.01, lsst.geom.degrees)
79 wcs1 = lsst.afw.geom.makeSkyWcs(crpix, crval1, cd)
80 wcs2 = lsst.afw.geom.makeSkyWcs(crpix, crval2, cd)
81 return wcs1, wcs2
84class TestJointcalVisitCatalog(lsst.utils.tests.TestCase):
85 """Tests of jointcal's sourceTable_visit parquet ->single detector afw
86 table catalog unrolling.
87 """
88 def setUp(self):
89 filename = os.path.join(os.path.dirname(__file__),
90 "data/subselected-sourceTable-0034690.parq")
91 file = pyarrow.parquet.ParquetFile(filename)
92 self.data = file.read(use_pandas_metadata=True).to_pandas()
93 self.config = lsst.jointcal.jointcal.JointcalConfig()
94 # TODO DM-29008: Remove this (to use the new gen3 default) before gen2 removal.
95 self.config.sourceFluxType = "ApFlux_12_0"
96 # we don't actually need either fitter to run for these tests
97 self.config.doAstrometry = False
98 self.config.doPhotometry = False
99 self.jointcal = lsst.jointcal.JointcalTask(config=self.config)
101 def test_make_catalog_schema(self):
102 """Check that the slot fields required by CcdImage::loadCatalog are in
103 the schema returned by _make_catalog_schema().
104 """
105 table = make_schema_table()
106 self.assertTrue(table.getCentroidSlot().getMeasKey().isValid())
107 self.assertTrue(table.getCentroidSlot().getErrKey().isValid())
108 self.assertTrue(table.getShapeSlot().getMeasKey().isValid())
110 def test_extract_detector_catalog_from_visit_catalog(self):
111 """Spot check a value output by the script that generated the test
112 parquet catalog and check that the size of the returned catalog
113 is correct for each detectior.
114 """
115 detectorId = 56
116 table = make_schema_table()
117 catalog = extract_detector_catalog_from_visit_catalog(table,
118 self.data,
119 detectorId,
120 'ccd',
121 ['Ixx', 'Iyy', 'Ixy'],
122 self.config.sourceFluxType,
123 self.jointcal.log)
125 # The test catalog has a number of elements for each detector equal to the detector id.
126 self.assertEqual(len(catalog), detectorId)
127 self.assertIn(29798723617816629, catalog['id'])
128 matched = catalog[29798723617816629 == catalog['id']]
129 self.assertEqual(1715.734359473175, matched['slot_Centroid_x'])
130 self.assertEqual(89.06076509964362, matched['slot_Centroid_y'])
133class JointcalTestBase:
134 def setUp(self):
135 # Ensure that the filter list is reset for each test so that we avoid
136 # confusion or contamination each time we create a cfht camera below.
137 lsst.obs.base.FilterDefinitionCollection.reset()
139 struct = lsst.jointcal.testUtils.createTwoFakeCcdImages(100, 100)
140 self.ccdImageList = struct.ccdImageList
141 # so that countStars() returns nonzero results
142 for ccdImage in self.ccdImageList:
143 ccdImage.resetCatalogForFit()
145 self.goodChi2 = lsst.jointcal.chi2.Chi2Statistic()
146 # chi2/ndof == 2.0 should be non-bad
147 self.goodChi2.chi2 = 200.0
148 self.goodChi2.ndof = 100
150 self.badChi2 = lsst.jointcal.chi2.Chi2Statistic()
151 self.badChi2.chi2 = 600.0
152 self.badChi2.ndof = 100
154 self.nanChi2 = lsst.jointcal.chi2.Chi2Statistic()
155 self.nanChi2.chi2 = np.nan
156 self.nanChi2.ndof = 100
158 self.maxSteps = 20
159 self.name = "testing"
160 self.dataName = "fake"
161 self.whatToFit = "" # unneeded, since we're mocking the fitter
163 # Mock a Butler so the refObjLoaders have something to call `get()` on.
164 self.butler = unittest.mock.Mock(spec=lsst.daf.persistence.Butler)
165 self.butler.get.return_value.indexer = DatasetConfig().indexer
167 # Mock the association manager and give it access to the ccd list above.
168 self.associations = mock.Mock(spec=lsst.jointcal.Associations)
169 self.associations.getCcdImageList.return_value = self.ccdImageList
171 # a default config to be modified by individual tests
172 self.config = lsst.jointcal.jointcal.JointcalConfig()
175class TestJointcalIterateFit(JointcalTestBase, lsst.utils.tests.TestCase):
176 def setUp(self):
177 super().setUp()
178 # Mock the fitter and model, so we can force particular
179 # return values/exceptions. Default to "good" return values.
180 self.fitter = mock.Mock(spec=lsst.jointcal.PhotometryFit)
181 self.fitter.computeChi2.return_value = self.goodChi2
182 self.fitter.minimize.return_value = MinimizeResult.Converged
183 self.model = mock.Mock(spec=lsst.jointcal.SimpleFluxModel)
185 self.jointcal = lsst.jointcal.JointcalTask(config=self.config, butler=self.butler)
187 def test_iterateFit_success(self):
188 chi2 = self.jointcal._iterate_fit(self.associations, self.fitter,
189 self.maxSteps, self.name, self.whatToFit)
190 self.assertEqual(chi2, self.goodChi2)
191 # Once for the for loop, the second time for the rank update.
192 self.assertEqual(self.fitter.minimize.call_count, 2)
194 def test_iterateFit_writeChi2Outer(self):
195 chi2 = self.jointcal._iterate_fit(self.associations, self.fitter,
196 self.maxSteps, self.name, self.whatToFit,
197 dataName=self.dataName)
198 self.assertEqual(chi2, self.goodChi2)
199 # Once for the for loop, the second time for the rank update.
200 self.assertEqual(self.fitter.minimize.call_count, 2)
201 # Default config should not call saveChi2Contributions
202 self.fitter.saveChi2Contributions.assert_not_called()
204 def test_iterateFit_failed(self):
205 self.fitter.minimize.return_value = MinimizeResult.Failed
207 with self.assertRaises(RuntimeError):
208 self.jointcal._iterate_fit(self.associations, self.fitter,
209 self.maxSteps, self.name, self.whatToFit)
210 self.assertEqual(self.fitter.minimize.call_count, 1)
212 def test_iterateFit_badFinalChi2(self):
213 log = mock.Mock(spec=lsst.log.Log)
214 self.jointcal.log = log
215 self.fitter.computeChi2.return_value = self.badChi2
217 chi2 = self.jointcal._iterate_fit(self.associations, self.fitter,
218 self.maxSteps, self.name, self.whatToFit)
219 self.assertEqual(chi2, self.badChi2)
220 log.info.assert_called_with("%s %s", "Fit completed", self.badChi2)
221 log.error.assert_called_with("Potentially bad fit: High chi-squared/ndof.")
223 def test_iterateFit_exceedMaxSteps(self):
224 log = mock.Mock(spec=lsst.log.Log)
225 self.jointcal.log = log
226 self.fitter.minimize.return_value = MinimizeResult.Chi2Increased
227 maxSteps = 3
229 chi2 = self.jointcal._iterate_fit(self.associations, self.fitter,
230 maxSteps, self.name, self.whatToFit)
231 self.assertEqual(chi2, self.goodChi2)
232 self.assertEqual(self.fitter.minimize.call_count, maxSteps)
233 log.error.assert_called_with("testing failed to converge after %s steps" % maxSteps)
235 def test_moderate_chi2_increase(self):
236 """DM-25159: warn, but don't fail, on moderate chi2 increases between
237 steps.
238 """
239 chi2_1 = lsst.jointcal.chi2.Chi2Statistic()
240 chi2_1.chi2 = 100.0
241 chi2_1.ndof = 100
242 chi2_2 = lsst.jointcal.chi2.Chi2Statistic()
243 chi2_2.chi2 = 300.0
244 chi2_2.ndof = 100
246 chi2s = [self.goodChi2, chi2_1, chi2_2, self.goodChi2, self.goodChi2]
247 self.fitter.computeChi2.side_effect = chi2s
248 self.fitter.minimize.side_effect = [MinimizeResult.Chi2Increased,
249 MinimizeResult.Chi2Increased,
250 MinimizeResult.Chi2Increased,
251 MinimizeResult.Converged,
252 MinimizeResult.Converged]
253 with lsst.log.UsePythonLogging(): # so that assertLogs works with lsst.log
254 with self.assertLogs("lsst.jointcal", level="WARNING") as logger:
255 self.jointcal._iterate_fit(self.associations, self.fitter,
256 self.maxSteps, self.name, self.whatToFit)
257 msg = "Significant chi2 increase by a factor of 300 / 100 = 3"
258 self.assertIn(msg, [rec.message for rec in logger.records])
260 def test_large_chi2_increase_fails(self):
261 """DM-25159: fail on large chi2 increases between steps."""
262 chi2_1 = lsst.jointcal.chi2.Chi2Statistic()
263 chi2_1.chi2 = 1e11
264 chi2_1.ndof = 100
265 chi2_2 = lsst.jointcal.chi2.Chi2Statistic()
266 chi2_2.chi2 = 1.123456e13 # to check floating point formatting
267 chi2_2.ndof = 100
269 chi2s = [chi2_1, chi2_1, chi2_2]
270 self.fitter.computeChi2.side_effect = chi2s
271 self.fitter.minimize.return_value = MinimizeResult.Chi2Increased
272 with lsst.log.UsePythonLogging(): # so that assertLogs works with lsst.log
273 with self.assertLogs("lsst.jointcal", level="WARNING") as logger:
274 with(self.assertRaisesRegex(RuntimeError, "Large chi2 increase")):
275 self.jointcal._iterate_fit(self.associations, self.fitter,
276 self.maxSteps, self.name, self.whatToFit)
277 msg = "Significant chi2 increase by a factor of 1.123e+13 / 1e+11 = 112.3"
278 self.assertIn(msg, [rec.message for rec in logger.records])
280 def test_invalid_model(self):
281 self.model.validate.return_value = False
282 with(self.assertRaises(ValueError)):
283 self.jointcal._logChi2AndValidate(self.associations, self.fitter, self.model, "invalid")
285 def test_nonfinite_chi2(self):
286 self.fitter.computeChi2.return_value = self.nanChi2
287 with(self.assertRaises(FloatingPointError)):
288 self.jointcal._logChi2AndValidate(self.associations, self.fitter, self.model, "nonfinite")
290 def test_writeChi2(self):
291 filename = "somefile"
292 self.jointcal._logChi2AndValidate(self.associations, self.fitter, self.model, "writeCh2",
293 writeChi2Name=filename)
294 # logChi2AndValidate prepends `config.debugOutputPath` to the filename
295 self.fitter.saveChi2Contributions.assert_called_with("./"+filename+"{type}")
298class TestJointcalLoadRefCat(JointcalTestBase, lsst.utils.tests.TestCase):
300 def _make_fake_refcat(self):
301 """Mock a fake reference catalog and the bits necessary to use it."""
302 center = lsst.geom.SpherePoint(30, -30, lsst.geom.degrees)
303 flux = 10
304 radius = 1 * lsst.geom.degrees
305 filter = lsst.afw.image.FilterLabel(band='fake', physical="fake-filter")
307 fakeRefCat = make_fake_refcat(center, flux, filter.bandLabel)
308 fluxField = getRefFluxField(fakeRefCat.schema, filter.bandLabel)
309 returnStruct = lsst.pipe.base.Struct(refCat=fakeRefCat, fluxField=fluxField)
310 refObjLoader = mock.Mock(spec=LoadIndexedReferenceObjectsTask)
311 refObjLoader.loadSkyCircle.return_value = returnStruct
313 return refObjLoader, center, radius, filter, fakeRefCat
315 def test_load_reference_catalog(self):
316 refObjLoader, center, radius, filterLabel, fakeRefCat = self._make_fake_refcat()
318 config = lsst.jointcal.jointcal.JointcalConfig()
319 config.astrometryReferenceErr = 0.1 # our test refcats don't have coord errors
320 jointcal = lsst.jointcal.JointcalTask(config=config, butler=self.butler)
322 # NOTE: we cannot test application of proper motion here, because we
323 # mock the refObjLoader, so the real loader is never called.
324 refCat, fluxField = jointcal._load_reference_catalog(refObjLoader,
325 jointcal.astrometryReferenceSelector,
326 center,
327 radius,
328 filterLabel)
329 # operator== isn't implemented for Catalogs, so we have to check like
330 # this, in case the records are copied during load.
331 self.assertEqual(len(refCat), len(fakeRefCat))
332 for r1, r2 in zip(refCat, fakeRefCat):
333 self.assertEqual(r1, r2)
335 def test_load_reference_catalog_subselect(self):
336 """Test that we can select out the one source in the fake refcat
337 with a ridiculous S/N cut.
338 """
339 refObjLoader, center, radius, filterLabel, fakeRefCat = self._make_fake_refcat()
341 config = lsst.jointcal.jointcal.JointcalConfig()
342 config.astrometryReferenceErr = 0.1 # our test refcats don't have coord errors
343 config.astrometryReferenceSelector.doSignalToNoise = True
344 config.astrometryReferenceSelector.signalToNoise.minimum = 1e10
345 config.astrometryReferenceSelector.signalToNoise.fluxField = "fake_flux"
346 config.astrometryReferenceSelector.signalToNoise.errField = "fake_fluxErr"
347 jointcal = lsst.jointcal.JointcalTask(config=config, butler=self.butler)
349 refCat, fluxField = jointcal._load_reference_catalog(refObjLoader,
350 jointcal.astrometryReferenceSelector,
351 center,
352 radius,
353 filterLabel)
354 self.assertEqual(len(refCat), 0)
357class TestJointcalFitModel(JointcalTestBase, lsst.utils.tests.TestCase):
358 def test_fit_photometry_writeChi2(self):
359 """Test that we are calling saveChi2 with appropriate file prefixes."""
360 self.config.photometryModel = "constrainedFlux"
361 self.config.writeChi2FilesOuterLoop = True
362 jointcal = lsst.jointcal.JointcalTask(config=self.config, butler=self.butler)
363 jointcal.focalPlaneBBox = lsst.geom.Box2D()
365 # Mock the fitter, so we can pretend it found a good fit
366 with mock.patch("lsst.jointcal.PhotometryFit", autospect=True) as fitPatch:
367 fitPatch.return_value.computeChi2.return_value = self.goodChi2
368 fitPatch.return_value.minimize.return_value = MinimizeResult.Converged
370 # config.debugOutputPath is prepended to the filenames that go into saveChi2Contributions
371 expected = ["./photometry_init-ModelVisit_chi2", "./photometry_init-Model_chi2",
372 "./photometry_init-Fluxes_chi2", "./photometry_init-ModelFluxes_chi2"]
373 expected = [mock.call(x+"-fake{type}") for x in expected]
374 jointcal._fit_photometry(self.associations, dataName=self.dataName)
375 fitPatch.return_value.saveChi2Contributions.assert_has_calls(expected)
377 def test_fit_astrometry_writeChi2(self):
378 """Test that we are calling saveChi2 with appropriate file prefixes."""
379 self.config.astrometryModel = "constrained"
380 self.config.writeChi2FilesOuterLoop = True
381 jointcal = lsst.jointcal.JointcalTask(config=self.config, butler=self.butler)
382 jointcal.focalPlaneBBox = lsst.geom.Box2D()
384 # Mock the fitter, so we can pretend it found a good fit
385 fitPatch = mock.patch("lsst.jointcal.AstrometryFit")
386 # Mock the projection handler so we don't segfault due to not-fully initialized ccdImages
387 projectorPatch = mock.patch("lsst.jointcal.OneTPPerVisitHandler")
388 with fitPatch as fit, projectorPatch as projector:
389 fit.return_value.computeChi2.return_value = self.goodChi2
390 fit.return_value.minimize.return_value = MinimizeResult.Converged
391 # return a real ProjectionHandler to keep ConstrainedAstrometryModel() happy
392 projector.return_value = lsst.jointcal.IdentityProjectionHandler()
394 # config.debugOutputPath is prepended to the filenames that go into saveChi2Contributions
395 expected = ["./astrometry_init-DistortionsVisit_chi2", "./astrometry_init-Distortions_chi2",
396 "./astrometry_init-Positions_chi2", "./astrometry_init-DistortionsPositions_chi2"]
397 expected = [mock.call(x+"-fake{type}") for x in expected]
398 jointcal._fit_astrometry(self.associations, dataName=self.dataName)
399 fit.return_value.saveChi2Contributions.assert_has_calls(expected)
402class TestComputeBoundingCircle(lsst.utils.tests.TestCase):
403 """Tests of Associations.computeBoundingCircle()"""
404 def _checkPointsInCircle(self, points, center, radius):
405 """Check that all points are within the (center, radius) circle.
407 The test is whether the max(points - center) separation is equal to
408 (or slightly less than) radius.
409 """
410 maxSeparation = 0*lsst.geom.degrees
411 for point in points:
412 maxSeparation = max(maxSeparation, center.separation(point))
413 self.assertAnglesAlmostEqual(maxSeparation, radius, maxDiff=3*lsst.geom.arcseconds)
414 self.assertLess(maxSeparation, radius)
416 def _testPoints(self, ccdImage1, ccdImage2, skyWcs1, skyWcs2, bbox):
417 """Fill an Associations object and test that it computes the correct
418 bounding circle for the input data.
420 Parameters
421 ----------
422 ccdImage1, ccdImage2 : `lsst.jointcal.CcdImage`
423 The CcdImages to add to the Associations object.
424 skyWcs1, skyWcs2 : `lsst.afw.geom.SkyWcs`
425 The WCS of each of the above images.
426 bbox : `lsst.geom.Box2D`
427 The ccd bounding box of both images.
428 """
429 lsst.log.setLevel('jointcal', lsst.log.DEBUG)
430 associations = lsst.jointcal.Associations()
431 associations.addCcdImage(ccdImage1)
432 associations.addCcdImage(ccdImage2)
433 associations.computeCommonTangentPoint()
435 circle = associations.computeBoundingCircle()
436 center = lsst.geom.SpherePoint(circle.getCenter())
437 radius = lsst.geom.Angle(circle.getOpeningAngle().asRadians(), lsst.geom.radians)
438 points = [lsst.geom.SpherePoint(skyWcs1.pixelToSky(lsst.geom.Point2D(x)))
439 for x in bbox.getCorners()]
440 points.extend([lsst.geom.SpherePoint(skyWcs2.pixelToSky(lsst.geom.Point2D(x)))
441 for x in bbox.getCorners()])
442 self._checkPointsInCircle(points, center, radius)
444 def testPoints(self):
445 """Test for points in an "easy" area, far from RA=0 or the poles."""
446 struct = lsst.jointcal.testUtils.createTwoFakeCcdImages()
447 self._testPoints(struct.ccdImageList[0], struct.ccdImageList[1],
448 struct.skyWcs[0], struct.skyWcs[1], struct.bbox)
450 def testPointsRA0(self):
451 """Test for CcdImages crossing RA=0; this demonstrates a fix for
452 the bug described in DM-19802.
453 """
454 wcs1, wcs2 = make_fake_wcs()
456 # Put the visit boresights at the WCS origin, for consistency
457 visitInfo1 = lsst.afw.image.VisitInfo(exposureId=30577512,
458 date=DateTime(date=65321.1),
459 boresightRaDec=wcs1.getSkyOrigin())
460 visitInfo2 = lsst.afw.image.VisitInfo(exposureId=30621144,
461 date=DateTime(date=65322.1),
462 boresightRaDec=wcs1.getSkyOrigin())
464 struct = lsst.jointcal.testUtils.createTwoFakeCcdImages(fakeWcses=[wcs1, wcs2],
465 fakeVisitInfos=[visitInfo1, visitInfo2])
466 self._testPoints(struct.ccdImageList[0], struct.ccdImageList[1],
467 struct.skyWcs[0], struct.skyWcs[1], struct.bbox)
470class TestJointcalComputePMDate(JointcalTestBase, lsst.utils.tests.TestCase):
471 """Tests of jointcal._compute_proper_motion_epoch(), using fake dates."""
472 def test_compute_proper_motion_epoch(self):
473 mjds = np.array((65432.1, 66666.0, 55555.0, 44322.2))
475 wcs1, wcs2 = make_fake_wcs()
476 visitInfo1 = lsst.afw.image.VisitInfo(exposureId=30577512,
477 date=DateTime(date=mjds[0]),
478 boresightRaDec=wcs1.getSkyOrigin())
479 visitInfo2 = lsst.afw.image.VisitInfo(exposureId=30621144,
480 date=DateTime(date=mjds[1]),
481 boresightRaDec=wcs2.getSkyOrigin())
482 visitInfo3 = lsst.afw.image.VisitInfo(exposureId=30577513,
483 date=DateTime(date=mjds[2]),
484 boresightRaDec=wcs1.getSkyOrigin())
485 visitInfo4 = lsst.afw.image.VisitInfo(exposureId=30621145,
486 date=DateTime(date=mjds[3]),
487 boresightRaDec=wcs2.getSkyOrigin())
489 struct1 = lsst.jointcal.testUtils.createTwoFakeCcdImages(fakeWcses=[wcs1, wcs2],
490 fakeVisitInfos=[visitInfo1, visitInfo2])
491 struct2 = lsst.jointcal.testUtils.createTwoFakeCcdImages(fakeWcses=[wcs1, wcs2],
492 fakeVisitInfos=[visitInfo3, visitInfo4])
493 ccdImageList = list(itertools.chain(struct1.ccdImageList, struct2.ccdImageList))
494 associations = lsst.jointcal.Associations()
495 for ccdImage in ccdImageList:
496 associations.addCcdImage(ccdImage)
497 associations.computeCommonTangentPoint()
499 jointcal = lsst.jointcal.JointcalTask(config=self.config, butler=self.butler)
500 result = jointcal._compute_proper_motion_epoch(ccdImageList)
501 self.assertEqual(result.jyear, (astropy.time.Time(mjds, format="mjd", scale="tai").jyear).mean())
504class MemoryTester(lsst.utils.tests.MemoryTestCase):
505 pass
508if __name__ == "__main__": 508 ↛ 509line 508 didn't jump to line 509, because the condition on line 508 was never true
509 lsst.utils.tests.init()
510 unittest.main()