Coverage for tests/test_jointcal.py: 24%
284 statements
« prev ^ index » next coverage.py v6.4, created at 2022-05-24 03:53 -0700
« prev ^ index » next coverage.py v6.4, created at 2022-05-24 03:53 -0700
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.butler
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 # NOTE: This parquet file is older and uses the earlier
95 # "capitalize first letter" naming convention for these fields.
96 self.config.sourceFluxType = "ApFlux_12_0"
97 # we don't actually need either fitter to run for these tests
98 self.config.doAstrometry = False
99 self.config.doPhotometry = False
100 self.jointcal = lsst.jointcal.JointcalTask(config=self.config)
102 def test_make_catalog_schema(self):
103 """Check that the slot fields required by CcdImage::loadCatalog are in
104 the schema returned by _make_catalog_schema().
105 """
106 table = make_schema_table()
107 self.assertTrue(table.getCentroidSlot().getMeasKey().isValid())
108 self.assertTrue(table.getCentroidSlot().getErrKey().isValid())
109 self.assertTrue(table.getShapeSlot().getMeasKey().isValid())
111 def test_extract_detector_catalog_from_visit_catalog(self):
112 """Spot check a value output by the script that generated the test
113 parquet catalog and check that the size of the returned catalog
114 is correct for each detectior.
115 """
116 detectorId = 56
117 table = make_schema_table()
118 catalog = extract_detector_catalog_from_visit_catalog(table,
119 self.data,
120 detectorId,
121 'ccd',
122 ['Ixx', 'Iyy', 'Ixy'],
123 self.config.sourceFluxType,
124 self.jointcal.log)
126 # The test catalog has a number of elements for each detector equal to the detector id.
127 self.assertEqual(len(catalog), detectorId)
128 self.assertIn(29798723617816629, catalog['id'])
129 matched = catalog[29798723617816629 == catalog['id']]
130 self.assertEqual(1715.734359473175, matched['slot_Centroid_x'])
131 self.assertEqual(89.06076509964362, matched['slot_Centroid_y'])
134class JointcalTestBase:
135 def setUp(self):
136 # Ensure that the filter list is reset for each test so that we avoid
137 # confusion or contamination each time we create a cfht camera below.
138 lsst.obs.base.FilterDefinitionCollection.reset()
140 struct = lsst.jointcal.testUtils.createTwoFakeCcdImages(100, 100)
141 self.ccdImageList = struct.ccdImageList
142 # so that countStars() returns nonzero results
143 for ccdImage in self.ccdImageList:
144 ccdImage.resetCatalogForFit()
146 self.goodChi2 = lsst.jointcal.chi2.Chi2Statistic()
147 # chi2/ndof == 2.0 should be non-bad
148 self.goodChi2.chi2 = 200.0
149 self.goodChi2.ndof = 100
151 self.badChi2 = lsst.jointcal.chi2.Chi2Statistic()
152 self.badChi2.chi2 = 600.0
153 self.badChi2.ndof = 100
155 self.nanChi2 = lsst.jointcal.chi2.Chi2Statistic()
156 self.nanChi2.chi2 = np.nan
157 self.nanChi2.ndof = 100
159 self.maxSteps = 20
160 self.name = "testing"
161 self.dataName = "fake"
162 self.whatToFit = "" # unneeded, since we're mocking the fitter
164 # Mock a Butler so the refObjLoaders have something to call `get()` on.
165 self.butler = unittest.mock.Mock(spec=lsst.daf.butler.Butler)
166 self.butler.get.return_value.indexer = DatasetConfig().indexer
168 # Mock the association manager and give it access to the ccd list above.
169 self.associations = mock.Mock(spec=lsst.jointcal.Associations)
170 self.associations.getCcdImageList.return_value = self.ccdImageList
172 # a default config to be modified by individual tests
173 self.config = lsst.jointcal.jointcal.JointcalConfig()
176class TestJointcalIterateFit(JointcalTestBase, lsst.utils.tests.TestCase):
177 def setUp(self):
178 super().setUp()
179 # Mock the fitter and model, so we can force particular
180 # return values/exceptions. Default to "good" return values.
181 self.fitter = mock.Mock(spec=lsst.jointcal.PhotometryFit)
182 self.fitter.computeChi2.return_value = self.goodChi2
183 self.fitter.minimize.return_value = MinimizeResult.Converged
184 self.model = mock.Mock(spec=lsst.jointcal.SimpleFluxModel)
186 self.jointcal = lsst.jointcal.JointcalTask(config=self.config, butler=self.butler)
188 def test_iterateFit_success(self):
189 chi2 = self.jointcal._iterate_fit(self.associations, self.fitter,
190 self.maxSteps, self.name, self.whatToFit)
191 self.assertEqual(chi2, self.goodChi2)
192 # Once for the for loop, the second time for the rank update.
193 self.assertEqual(self.fitter.minimize.call_count, 2)
195 def test_iterateFit_writeChi2Outer(self):
196 chi2 = self.jointcal._iterate_fit(self.associations, self.fitter,
197 self.maxSteps, self.name, self.whatToFit,
198 dataName=self.dataName)
199 self.assertEqual(chi2, self.goodChi2)
200 # Once for the for loop, the second time for the rank update.
201 self.assertEqual(self.fitter.minimize.call_count, 2)
202 # Default config should not call saveChi2Contributions
203 self.fitter.saveChi2Contributions.assert_not_called()
205 def test_iterateFit_failed(self):
206 self.fitter.minimize.return_value = MinimizeResult.Failed
208 with self.assertRaises(RuntimeError):
209 self.jointcal._iterate_fit(self.associations, self.fitter,
210 self.maxSteps, self.name, self.whatToFit)
211 self.assertEqual(self.fitter.minimize.call_count, 1)
213 def test_iterateFit_badFinalChi2(self):
214 log = mock.Mock(spec=lsst.log.Log)
215 self.jointcal.log = log
216 self.fitter.computeChi2.return_value = self.badChi2
218 chi2 = self.jointcal._iterate_fit(self.associations, self.fitter,
219 self.maxSteps, self.name, self.whatToFit)
220 self.assertEqual(chi2, self.badChi2)
221 log.info.assert_called_with("%s %s", "Fit completed", self.badChi2)
222 log.error.assert_called_with("Potentially bad fit: High chi-squared/ndof.")
224 def test_iterateFit_exceedMaxSteps(self):
225 log = mock.Mock(spec=lsst.log.Log)
226 self.jointcal.log = log
227 self.fitter.minimize.return_value = MinimizeResult.Chi2Increased
228 maxSteps = 3
230 chi2 = self.jointcal._iterate_fit(self.associations, self.fitter,
231 maxSteps, self.name, self.whatToFit)
232 self.assertEqual(chi2, self.goodChi2)
233 self.assertEqual(self.fitter.minimize.call_count, maxSteps)
234 log.error.assert_called_with("testing failed to converge after %s steps" % maxSteps)
236 def test_moderate_chi2_increase(self):
237 """DM-25159: warn, but don't fail, on moderate chi2 increases between
238 steps.
239 """
240 chi2_1 = lsst.jointcal.chi2.Chi2Statistic()
241 chi2_1.chi2 = 100.0
242 chi2_1.ndof = 100
243 chi2_2 = lsst.jointcal.chi2.Chi2Statistic()
244 chi2_2.chi2 = 300.0
245 chi2_2.ndof = 100
247 chi2s = [self.goodChi2, chi2_1, chi2_2, self.goodChi2, self.goodChi2]
248 self.fitter.computeChi2.side_effect = chi2s
249 self.fitter.minimize.side_effect = [MinimizeResult.Chi2Increased,
250 MinimizeResult.Chi2Increased,
251 MinimizeResult.Chi2Increased,
252 MinimizeResult.Converged,
253 MinimizeResult.Converged]
254 with lsst.log.UsePythonLogging(): # so that assertLogs works with lsst.log
255 with self.assertLogs("lsst.jointcal", level="WARNING") as logger:
256 self.jointcal._iterate_fit(self.associations, self.fitter,
257 self.maxSteps, self.name, self.whatToFit)
258 msg = "Significant chi2 increase by a factor of 300 / 100 = 3"
259 self.assertIn(msg, [rec.message for rec in logger.records])
261 def test_large_chi2_increase_fails(self):
262 """DM-25159: fail on large chi2 increases between steps."""
263 chi2_1 = lsst.jointcal.chi2.Chi2Statistic()
264 chi2_1.chi2 = 1e11
265 chi2_1.ndof = 100
266 chi2_2 = lsst.jointcal.chi2.Chi2Statistic()
267 chi2_2.chi2 = 1.123456e13 # to check floating point formatting
268 chi2_2.ndof = 100
270 chi2s = [chi2_1, chi2_1, chi2_2]
271 self.fitter.computeChi2.side_effect = chi2s
272 self.fitter.minimize.return_value = MinimizeResult.Chi2Increased
273 with lsst.log.UsePythonLogging(): # so that assertLogs works with lsst.log
274 with self.assertLogs("lsst.jointcal", level="WARNING") as logger:
275 with(self.assertRaisesRegex(RuntimeError, "Large chi2 increase")):
276 self.jointcal._iterate_fit(self.associations, self.fitter,
277 self.maxSteps, self.name, self.whatToFit)
278 msg = "Significant chi2 increase by a factor of 1.123e+13 / 1e+11 = 112.3"
279 self.assertIn(msg, [rec.message for rec in logger.records])
281 def test_invalid_model(self):
282 self.model.validate.return_value = False
283 with(self.assertRaises(ValueError)):
284 self.jointcal._logChi2AndValidate(self.associations, self.fitter, self.model, "invalid")
286 def test_nonfinite_chi2(self):
287 self.fitter.computeChi2.return_value = self.nanChi2
288 with(self.assertRaises(FloatingPointError)):
289 self.jointcal._logChi2AndValidate(self.associations, self.fitter, self.model, "nonfinite")
291 def test_writeChi2(self):
292 filename = "somefile"
293 self.jointcal._logChi2AndValidate(self.associations, self.fitter, self.model, "writeCh2",
294 writeChi2Name=filename)
295 # logChi2AndValidate prepends `config.debugOutputPath` to the filename
296 self.fitter.saveChi2Contributions.assert_called_with("./"+filename+"{type}")
299class TestJointcalLoadRefCat(JointcalTestBase, lsst.utils.tests.TestCase):
301 def _make_fake_refcat(self):
302 """Mock a fake reference catalog and the bits necessary to use it."""
303 center = lsst.geom.SpherePoint(30, -30, lsst.geom.degrees)
304 flux = 10
305 radius = 1 * lsst.geom.degrees
306 filter = lsst.afw.image.FilterLabel(band='fake', physical="fake-filter")
308 fakeRefCat = make_fake_refcat(center, flux, filter.bandLabel)
309 fluxField = getRefFluxField(fakeRefCat.schema, filter.bandLabel)
310 returnStruct = lsst.pipe.base.Struct(refCat=fakeRefCat, fluxField=fluxField)
311 refObjLoader = mock.Mock(spec=LoadIndexedReferenceObjectsTask)
312 refObjLoader.loadSkyCircle.return_value = returnStruct
314 return refObjLoader, center, radius, filter, fakeRefCat
316 def test_load_reference_catalog(self):
317 refObjLoader, center, radius, filterLabel, fakeRefCat = self._make_fake_refcat()
319 config = lsst.jointcal.jointcal.JointcalConfig()
320 config.astrometryReferenceErr = 0.1 # our test refcats don't have coord errors
321 jointcal = lsst.jointcal.JointcalTask(config=config, butler=self.butler)
323 # NOTE: we cannot test application of proper motion here, because we
324 # mock the refObjLoader, so the real loader is never called.
325 refCat, fluxField = jointcal._load_reference_catalog(refObjLoader,
326 jointcal.astrometryReferenceSelector,
327 center,
328 radius,
329 filterLabel)
330 # operator== isn't implemented for Catalogs, so we have to check like
331 # this, in case the records are copied during load.
332 self.assertEqual(len(refCat), len(fakeRefCat))
333 for r1, r2 in zip(refCat, fakeRefCat):
334 self.assertEqual(r1, r2)
336 def test_load_reference_catalog_subselect(self):
337 """Test that we can select out the one source in the fake refcat
338 with a ridiculous S/N cut.
339 """
340 refObjLoader, center, radius, filterLabel, fakeRefCat = self._make_fake_refcat()
342 config = lsst.jointcal.jointcal.JointcalConfig()
343 config.astrometryReferenceErr = 0.1 # our test refcats don't have coord errors
344 config.astrometryReferenceSelector.doSignalToNoise = True
345 config.astrometryReferenceSelector.signalToNoise.minimum = 1e10
346 config.astrometryReferenceSelector.signalToNoise.fluxField = "fake_flux"
347 config.astrometryReferenceSelector.signalToNoise.errField = "fake_fluxErr"
348 jointcal = lsst.jointcal.JointcalTask(config=config, butler=self.butler)
350 refCat, fluxField = jointcal._load_reference_catalog(refObjLoader,
351 jointcal.astrometryReferenceSelector,
352 center,
353 radius,
354 filterLabel)
355 self.assertEqual(len(refCat), 0)
358class TestJointcalFitModel(JointcalTestBase, lsst.utils.tests.TestCase):
359 def test_fit_photometry_writeChi2(self):
360 """Test that we are calling saveChi2 with appropriate file prefixes."""
361 self.config.photometryModel = "constrainedFlux"
362 self.config.writeChi2FilesOuterLoop = True
363 jointcal = lsst.jointcal.JointcalTask(config=self.config, butler=self.butler)
364 jointcal.focalPlaneBBox = lsst.geom.Box2D()
366 # Mock the fitter, so we can pretend it found a good fit
367 with mock.patch("lsst.jointcal.PhotometryFit", autospec=True) as fitPatch:
368 fitPatch.return_value.computeChi2.return_value = self.goodChi2
369 fitPatch.return_value.minimize.return_value = MinimizeResult.Converged
371 # config.debugOutputPath is prepended to the filenames that go into saveChi2Contributions
372 expected = ["./photometry_init-ModelVisit_chi2", "./photometry_init-Model_chi2",
373 "./photometry_init-Fluxes_chi2", "./photometry_init-ModelFluxes_chi2"]
374 expected = [mock.call(x+"-fake{type}") for x in expected]
375 jointcal._fit_photometry(self.associations, dataName=self.dataName)
376 fitPatch.return_value.saveChi2Contributions.assert_has_calls(expected)
378 def test_fit_astrometry_writeChi2(self):
379 """Test that we are calling saveChi2 with appropriate file prefixes."""
380 self.config.astrometryModel = "constrained"
381 self.config.writeChi2FilesOuterLoop = True
382 jointcal = lsst.jointcal.JointcalTask(config=self.config, butler=self.butler)
383 jointcal.focalPlaneBBox = lsst.geom.Box2D()
385 # Mock the fitter, so we can pretend it found a good fit
386 fitPatch = mock.patch("lsst.jointcal.AstrometryFit")
387 # Mock the projection handler so we don't segfault due to not-fully initialized ccdImages
388 projectorPatch = mock.patch("lsst.jointcal.OneTPPerVisitHandler")
389 with fitPatch as fit, projectorPatch as projector:
390 fit.return_value.computeChi2.return_value = self.goodChi2
391 fit.return_value.minimize.return_value = MinimizeResult.Converged
392 # return a real ProjectionHandler to keep ConstrainedAstrometryModel() happy
393 projector.return_value = lsst.jointcal.IdentityProjectionHandler()
395 # config.debugOutputPath is prepended to the filenames that go into saveChi2Contributions
396 expected = ["./astrometry_init-DistortionsVisit_chi2", "./astrometry_init-Distortions_chi2",
397 "./astrometry_init-Positions_chi2", "./astrometry_init-DistortionsPositions_chi2"]
398 expected = [mock.call(x+"-fake{type}") for x in expected]
399 jointcal._fit_astrometry(self.associations, dataName=self.dataName)
400 fit.return_value.saveChi2Contributions.assert_has_calls(expected)
403class TestComputeBoundingCircle(lsst.utils.tests.TestCase):
404 """Tests of Associations.computeBoundingCircle()"""
405 def _checkPointsInCircle(self, points, center, radius):
406 """Check that all points are within the (center, radius) circle.
408 The test is whether the max(points - center) separation is equal to
409 (or slightly less than) radius.
410 """
411 maxSeparation = 0*lsst.geom.degrees
412 for point in points:
413 maxSeparation = max(maxSeparation, center.separation(point))
414 self.assertAnglesAlmostEqual(maxSeparation, radius, maxDiff=3*lsst.geom.arcseconds)
415 self.assertLess(maxSeparation, radius)
417 def _testPoints(self, ccdImage1, ccdImage2, skyWcs1, skyWcs2, bbox):
418 """Fill an Associations object and test that it computes the correct
419 bounding circle for the input data.
421 Parameters
422 ----------
423 ccdImage1, ccdImage2 : `lsst.jointcal.CcdImage`
424 The CcdImages to add to the Associations object.
425 skyWcs1, skyWcs2 : `lsst.afw.geom.SkyWcs`
426 The WCS of each of the above images.
427 bbox : `lsst.geom.Box2D`
428 The ccd bounding box of both images.
429 """
430 lsst.log.setLevel('jointcal', lsst.log.DEBUG)
431 associations = lsst.jointcal.Associations()
432 associations.addCcdImage(ccdImage1)
433 associations.addCcdImage(ccdImage2)
434 associations.computeCommonTangentPoint()
436 circle = associations.computeBoundingCircle()
437 center = lsst.geom.SpherePoint(circle.getCenter())
438 radius = lsst.geom.Angle(circle.getOpeningAngle().asRadians(), lsst.geom.radians)
439 points = [lsst.geom.SpherePoint(skyWcs1.pixelToSky(lsst.geom.Point2D(x)))
440 for x in bbox.getCorners()]
441 points.extend([lsst.geom.SpherePoint(skyWcs2.pixelToSky(lsst.geom.Point2D(x)))
442 for x in bbox.getCorners()])
443 self._checkPointsInCircle(points, center, radius)
445 def testPoints(self):
446 """Test for points in an "easy" area, far from RA=0 or the poles."""
447 struct = lsst.jointcal.testUtils.createTwoFakeCcdImages()
448 self._testPoints(struct.ccdImageList[0], struct.ccdImageList[1],
449 struct.skyWcs[0], struct.skyWcs[1], struct.bbox)
451 def testPointsRA0(self):
452 """Test for CcdImages crossing RA=0; this demonstrates a fix for
453 the bug described in DM-19802.
454 """
455 wcs1, wcs2 = make_fake_wcs()
457 # Put the visit boresights at the WCS origin, for consistency
458 visitInfo1 = lsst.afw.image.VisitInfo(exposureId=30577512,
459 date=DateTime(date=65321.1),
460 boresightRaDec=wcs1.getSkyOrigin())
461 visitInfo2 = lsst.afw.image.VisitInfo(exposureId=30621144,
462 date=DateTime(date=65322.1),
463 boresightRaDec=wcs1.getSkyOrigin())
465 struct = lsst.jointcal.testUtils.createTwoFakeCcdImages(fakeWcses=[wcs1, wcs2],
466 fakeVisitInfos=[visitInfo1, visitInfo2])
467 self._testPoints(struct.ccdImageList[0], struct.ccdImageList[1],
468 struct.skyWcs[0], struct.skyWcs[1], struct.bbox)
471class TestJointcalComputePMDate(JointcalTestBase, lsst.utils.tests.TestCase):
472 """Tests of jointcal._compute_proper_motion_epoch(), using fake dates."""
473 def test_compute_proper_motion_epoch(self):
474 mjds = np.array((65432.1, 66666.0, 55555.0, 44322.2))
476 wcs1, wcs2 = make_fake_wcs()
477 visitInfo1 = lsst.afw.image.VisitInfo(exposureId=30577512,
478 date=DateTime(date=mjds[0]),
479 boresightRaDec=wcs1.getSkyOrigin())
480 visitInfo2 = lsst.afw.image.VisitInfo(exposureId=30621144,
481 date=DateTime(date=mjds[1]),
482 boresightRaDec=wcs2.getSkyOrigin())
483 visitInfo3 = lsst.afw.image.VisitInfo(exposureId=30577513,
484 date=DateTime(date=mjds[2]),
485 boresightRaDec=wcs1.getSkyOrigin())
486 visitInfo4 = lsst.afw.image.VisitInfo(exposureId=30621145,
487 date=DateTime(date=mjds[3]),
488 boresightRaDec=wcs2.getSkyOrigin())
490 struct1 = lsst.jointcal.testUtils.createTwoFakeCcdImages(fakeWcses=[wcs1, wcs2],
491 fakeVisitInfos=[visitInfo1, visitInfo2])
492 struct2 = lsst.jointcal.testUtils.createTwoFakeCcdImages(fakeWcses=[wcs1, wcs2],
493 fakeVisitInfos=[visitInfo3, visitInfo4])
494 ccdImageList = list(itertools.chain(struct1.ccdImageList, struct2.ccdImageList))
495 associations = lsst.jointcal.Associations()
496 for ccdImage in ccdImageList:
497 associations.addCcdImage(ccdImage)
498 associations.computeCommonTangentPoint()
500 jointcal = lsst.jointcal.JointcalTask(config=self.config, butler=self.butler)
501 result = jointcal._compute_proper_motion_epoch(ccdImageList)
502 self.assertEqual(result.jyear, (astropy.time.Time(mjds, format="mjd", scale="tai").jyear).mean())
505class MemoryTester(lsst.utils.tests.MemoryTestCase):
506 pass
509if __name__ == "__main__": 509 ↛ 510line 509 didn't jump to line 510, because the condition on line 509 was never true
510 lsst.utils.tests.init()
511 unittest.main()