Coverage for tests/test_calibrateImage.py: 17%
225 statements
« prev ^ index » next coverage.py v7.4.1, created at 2024-02-13 12:19 +0000
« prev ^ index » next coverage.py v7.4.1, created at 2024-02-13 12:19 +0000
1# This file is part of pipe_tasks.
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
23import tempfile
25import astropy.units as u
26from astropy.coordinates import SkyCoord
27import numpy as np
29import lsst.afw.image as afwImage
30import lsst.afw.table as afwTable
31import lsst.daf.base
32import lsst.daf.butler.tests as butlerTests
33import lsst.geom
34import lsst.meas.algorithms
35from lsst.meas.algorithms import testUtils
36import lsst.meas.extensions.psfex
37import lsst.meas.base.tests
38import lsst.pipe.base.testUtils
39from lsst.pipe.tasks.calibrateImage import CalibrateImageTask
40import lsst.utils.tests
43class CalibrateImageTaskTests(lsst.utils.tests.TestCase):
45 def setUp(self):
46 # Different x/y dimensions so they're easy to distinguish in a plot,
47 # and non-zero minimum, to help catch xy0 errors.
48 bbox = lsst.geom.Box2I(lsst.geom.Point2I(5, 4), lsst.geom.Point2I(205, 184))
49 self.sky_center = lsst.geom.SpherePoint(245.0, -45.0, lsst.geom.degrees)
50 self.photo_calib = 12.3
51 dataset = lsst.meas.base.tests.TestDataset(bbox, crval=self.sky_center, calibration=self.photo_calib)
52 # sqrt of area of a normalized 2d gaussian
53 psf_scale = np.sqrt(4*np.pi*(dataset.psfShape.getDeterminantRadius())**2)
54 noise = 10.0 # stddev of noise per pixel
55 # Sources ordered from faintest to brightest.
56 self.fluxes = np.array((6*noise*psf_scale,
57 12*noise*psf_scale,
58 45*noise*psf_scale,
59 150*noise*psf_scale,
60 400*noise*psf_scale,
61 1000*noise*psf_scale))
62 self.centroids = np.array(((162, 22),
63 (40, 70),
64 (100, 160),
65 (50, 120),
66 (92, 35),
67 (175, 154)), dtype=np.float32)
68 for flux, centroid in zip(self.fluxes, self.centroids):
69 dataset.addSource(instFlux=flux, centroid=lsst.geom.Point2D(centroid[0], centroid[1]))
71 # Bright extended source in the center of the image: should not appear
72 # in any of the output catalogs.
73 center = lsst.geom.Point2D(100, 100)
74 shape = lsst.afw.geom.Quadrupole(8, 9, 3)
75 dataset.addSource(instFlux=500*noise*psf_scale, centroid=center, shape=shape)
77 schema = dataset.makeMinimalSchema()
78 self.truth_exposure, self.truth_cat = dataset.realize(noise=noise, schema=schema)
79 # To make it look like a version=1 (nJy fluxes) refcat
80 self.truth_cat = self.truth_exposure.photoCalib.calibrateCatalog(self.truth_cat)
81 self.ref_loader = testUtils.MockReferenceObjectLoaderFromMemory([self.truth_cat])
82 metadata = lsst.daf.base.PropertyList()
83 metadata.set("REFCAT_FORMAT_VERSION", 1)
84 self.truth_cat.setMetadata(metadata)
86 # TODO: a cosmic ray (need to figure out how to insert a fake-CR)
87 # self.truth_exposure.image.array[10, 10] = 100000
88 # self.truth_exposure.variance.array[10, 10] = 100000/noise
90 # Copy the truth exposure, because CalibrateImage modifies the input.
91 # Post-ISR ccds only contain: initial WCS, VisitInfo, filter
92 self.exposure = afwImage.ExposureF(self.truth_exposure.maskedImage)
93 self.exposure.setWcs(self.truth_exposure.wcs)
94 self.exposure.info.setVisitInfo(self.truth_exposure.visitInfo)
95 # "truth" filter, to match the "truth" refcat.
96 self.exposure.setFilter(lsst.afw.image.FilterLabel(physical='truth', band="truth"))
98 # Test-specific configuration:
99 self.config = CalibrateImageTask.ConfigClass()
100 # We don't have many sources, so have to fit simpler models.
101 self.config.psf_detection.background.approxOrderX = 1
102 self.config.star_detection.background.approxOrderX = 1
103 # Use PCA psf fitter, as psfex fails if there are only 4 stars.
104 self.config.psf_measure_psf.psfDeterminer = 'pca'
105 # We don't have many test points, so can't match on complicated shapes.
106 self.config.astrometry.matcher.numPointsForShape = 3
107 # ApFlux has more noise than PsfFlux (the latter unrealistically small
108 # in this test data), so we need to do magnitude rejection at higher
109 # sigma, otherwise we can lose otherwise good sources.
110 # TODO DM-39203: Once we are using Compensated Gaussian Fluxes, we
111 # will use those fluxes here, and hopefully can remove this.
112 self.config.astrometry.magnitudeOutlierRejectionNSigma = 9.0
114 # Something about this test dataset prefers the older fluxRatio here.
115 self.config.star_catalog_calculation.plugins['base_ClassificationExtendedness'].fluxRatio = 0.925
117 def _check_run(self, calibrate, result, *, photo_calib):
118 """Test the result of CalibrateImage.run().
120 Parameters
121 ----------
122 calibrate : `lsst.pipe.tasks.calibrateImage.CalibrateImageTask`
123 Configured task that had `run` called on it.
124 result : `lsst.pipe.base.Struct`
125 Result of calling calibrate.run().
126 photo_calib : `float`
127 Expected value of the PhotoCalib mean.
128 """
129 # Background should have 4 elements: 3 from compute_psf and one from
130 # re-estimation during source detection.
131 self.assertEqual(len(result.background), 4)
133 # Check that the summary statistics are reasonable.
134 summary = result.output_exposure.info.getSummaryStats()
135 self.assertFloatsAlmostEqual(summary.psfSigma, 2.0, rtol=1e-2)
136 self.assertFloatsAlmostEqual(summary.ra, self.sky_center.getRa().asDegrees(), rtol=1e-7)
137 self.assertFloatsAlmostEqual(summary.dec, self.sky_center.getDec().asDegrees(), rtol=1e-7)
139 # Returned photoCalib should be the applied value, not the ==1 one on the exposure.
140 self.assertFloatsAlmostEqual(result.applied_photo_calib.getCalibrationMean(),
141 photo_calib, rtol=2e-3)
142 # Should have flux/magnitudes in the catalog.
143 self.assertIn("slot_PsfFlux_flux", result.stars.schema)
144 self.assertIn("slot_PsfFlux_mag", result.stars.schema)
146 # Check that all necessary fields are in the output.
147 lsst.pipe.base.testUtils.assertValidOutput(calibrate, result)
149 def test_run(self):
150 """Test that run() returns reasonable values to be butler put.
151 """
152 calibrate = CalibrateImageTask(config=self.config)
153 calibrate.astrometry.setRefObjLoader(self.ref_loader)
154 calibrate.photometry.match.setRefObjLoader(self.ref_loader)
155 result = calibrate.run(exposures=self.exposure)
157 self._check_run(calibrate, result, photo_calib=self.photo_calib)
159 def test_run_2_snaps(self):
160 """Test that run() returns reasonable values to be butler put, when
161 passed two exposures to combine as snaps.
162 """
163 calibrate = CalibrateImageTask(config=self.config)
164 calibrate.astrometry.setRefObjLoader(self.ref_loader)
165 calibrate.photometry.match.setRefObjLoader(self.ref_loader)
166 result = calibrate.run(exposures=[self.exposure, self.exposure])
168 self._check_run(calibrate, result, photo_calib=self.photo_calib/2)
170 def test_handle_snaps(self):
171 calibrate = CalibrateImageTask(config=self.config)
172 self.assertEqual(calibrate._handle_snaps(self.exposure), self.exposure)
173 self.assertEqual(calibrate._handle_snaps((self.exposure, )), self.exposure)
174 self.assertEqual(calibrate._handle_snaps(self.exposure), self.exposure)
175 with self.assertRaisesRegex(RuntimeError, "Can only process 1 or 2 snaps, not 0."):
176 calibrate._handle_snaps([])
177 with self.assertRaisesRegex(RuntimeError, "Can only process 1 or 2 snaps, not 3."):
178 calibrate._handle_snaps(3*[self.exposure])
180 def test_compute_psf(self):
181 """Test that our brightest sources are found by _compute_psf(),
182 that a PSF is assigned to the expopsure.
183 """
184 calibrate = CalibrateImageTask(config=self.config)
185 psf_stars, background, candidates = calibrate._compute_psf(self.exposure)
187 # Background should have 3 elements: initial subtraction, and two from
188 # re-estimation during the two detection passes.
189 self.assertEqual(len(background), 3)
191 # Only the point-sources with S/N > 50 should be in this output.
192 self.assertEqual(psf_stars["calib_psf_used"].sum(), 3)
193 # Sort in order of brightness, to easily compare with expected positions.
194 psf_stars.sort(psf_stars.getPsfFluxSlot().getMeasKey())
195 for record, flux, center in zip(psf_stars[::-1], self.fluxes, self.centroids[self.fluxes > 50]):
196 self.assertFloatsAlmostEqual(record.getX(), center[0], rtol=0.01)
197 self.assertFloatsAlmostEqual(record.getY(), center[1], rtol=0.01)
198 # PsfFlux should match the values inserted.
199 self.assertFloatsAlmostEqual(record["slot_PsfFlux_instFlux"], flux, rtol=0.01)
201 # TODO: While debugging DM-32701, we're using PCA instead of psfex.
202 # Check that we got a useable PSF.
203 # self.assertIsInstance(self.exposure.psf, lsst.meas.extensions.psfex.PsfexPsf)
204 self.assertIsInstance(self.exposure.psf, lsst.meas.algorithms.PcaPsf)
205 # TestDataset sources have PSF radius=2 pixels.
206 radius = self.exposure.psf.computeShape(self.exposure.psf.getAveragePosition()).getDeterminantRadius()
207 self.assertFloatsAlmostEqual(radius, 2.0, rtol=1e-2)
209 # To look at images for debugging (`setup display_ds9` and run ds9):
210 # import lsst.afw.display
211 # display = lsst.afw.display.getDisplay()
212 # display.mtv(self.exposure)
214 def test_measure_aperture_correction(self):
215 """Test that _measure_aperture_correction() assigns an ApCorrMap to the
216 exposure.
217 """
218 calibrate = CalibrateImageTask(config=self.config)
219 psf_stars, background, candidates = calibrate._compute_psf(self.exposure)
221 # First check that the exposure doesn't have an ApCorrMap.
222 self.assertIsNone(self.exposure.apCorrMap)
223 calibrate._measure_aperture_correction(self.exposure, psf_stars)
224 self.assertIsInstance(self.exposure.apCorrMap, afwImage.ApCorrMap)
226 def test_find_stars(self):
227 """Test that _find_stars() correctly identifies the S/N>10 stars
228 in the image and returns them in the output catalog.
229 """
230 calibrate = CalibrateImageTask(config=self.config)
231 psf_stars, background, candidates = calibrate._compute_psf(self.exposure)
232 calibrate._measure_aperture_correction(self.exposure, psf_stars)
234 stars = calibrate._find_stars(self.exposure, background)
236 # Background should have 4 elements: 3 from compute_psf and one from
237 # re-estimation during source detection.
238 self.assertEqual(len(background), 4)
240 # Only psf-like sources with S/N>10 should be in the output catalog.
241 self.assertEqual(len(stars), 5)
242 self.assertTrue(psf_stars.isContiguous())
243 # Sort in order of brightness, to easily compare with expected positions.
244 psf_stars.sort(psf_stars.getPsfFluxSlot().getMeasKey())
245 for record, flux, center in zip(psf_stars[::-1], self.fluxes, self.centroids[self.fluxes > 50]):
246 self.assertFloatsAlmostEqual(record.getX(), center[0], rtol=0.01)
247 self.assertFloatsAlmostEqual(record.getY(), center[1], rtol=0.01)
248 self.assertFloatsAlmostEqual(record["slot_PsfFlux_instFlux"], flux, rtol=0.01)
250 def test_astrometry(self):
251 """Test that the fitted WCS gives good catalog coordinates.
252 """
253 calibrate = CalibrateImageTask(config=self.config)
254 calibrate.astrometry.setRefObjLoader(self.ref_loader)
255 psf_stars, background, candidates = calibrate._compute_psf(self.exposure)
256 calibrate._measure_aperture_correction(self.exposure, psf_stars)
257 stars = calibrate._find_stars(self.exposure, background)
259 calibrate._fit_astrometry(self.exposure, stars)
261 # Check that we got reliable matches with the truth coordinates.
262 fitted = SkyCoord(stars['coord_ra'], stars['coord_dec'], unit="radian")
263 truth = SkyCoord(self.truth_cat['coord_ra'], self.truth_cat['coord_dec'], unit="radian")
264 idx, d2d, _ = fitted.match_to_catalog_sky(truth)
265 np.testing.assert_array_less(d2d.to_value(u.milliarcsecond), 35.0)
267 def test_photometry(self):
268 """Test that the fitted photoCalib matches the one we generated,
269 and that the exposure is calibrated.
270 """
271 calibrate = CalibrateImageTask(config=self.config)
272 calibrate.astrometry.setRefObjLoader(self.ref_loader)
273 calibrate.photometry.match.setRefObjLoader(self.ref_loader)
274 psf_stars, background, candidates = calibrate._compute_psf(self.exposure)
275 calibrate._measure_aperture_correction(self.exposure, psf_stars)
276 stars = calibrate._find_stars(self.exposure, background)
277 calibrate._fit_astrometry(self.exposure, stars)
279 stars, matches, meta, photoCalib = calibrate._fit_photometry(self.exposure, stars)
281 # NOTE: With this test data, PhotoCalTask returns calibrationErr==0,
282 # so we can't check that the photoCal error has been set.
283 self.assertFloatsAlmostEqual(photoCalib.getCalibrationMean(), self.photo_calib, rtol=2e-3)
284 # The exposure should be calibrated by the applied photoCalib.
285 self.assertFloatsAlmostEqual(self.exposure.image.array/self.truth_exposure.image.array,
286 self.photo_calib, rtol=2e-3)
287 # PhotoCalib on the exposure must be identically 1.
288 self.assertEqual(self.exposure.photoCalib.getCalibrationMean(), 1.0)
290 # Check that we got reliable magnitudes and fluxes vs. truth.
291 fitted = SkyCoord(stars['coord_ra'], stars['coord_dec'], unit="radian")
292 truth = SkyCoord(self.truth_cat['coord_ra'], self.truth_cat['coord_dec'], unit="radian")
293 idx, _, _ = fitted.match_to_catalog_sky(truth)
294 # Because the input variance image does not include contributions from
295 # the sources, we can't use fluxErr as a bound on the measurement
296 # quality here.
297 self.assertFloatsAlmostEqual(stars['slot_PsfFlux_flux'], self.truth_cat['truth_flux'][idx], rtol=0.1)
298 self.assertFloatsAlmostEqual(stars['slot_PsfFlux_mag'], self.truth_cat['truth_mag'][idx], rtol=0.01)
300 def test_match_psf_stars(self):
301 """Test that _match_psf_stars() flags the correct stars as psf stars
302 and candidates.
303 """
304 calibrate = CalibrateImageTask(config=self.config)
305 psf_stars, background, candidates = calibrate._compute_psf(self.exposure)
306 calibrate._measure_aperture_correction(self.exposure, psf_stars)
307 stars = calibrate._find_stars(self.exposure, background)
309 # There should be no psf-related flags set at first.
310 self.assertEqual(stars["calib_psf_candidate"].sum(), 0)
311 self.assertEqual(stars["calib_psf_used"].sum(), 0)
312 self.assertEqual(stars["calib_psf_reserved"].sum(), 0)
314 calibrate._match_psf_stars(psf_stars, stars)
316 # Sort in order of brightness; the psf stars are the 3 brightest.
317 stars.sort(stars.getPsfFluxSlot().getMeasKey())
318 # sort() above leaves the catalog non-contiguous.
319 stars = stars.copy(deep=True)
320 np.testing.assert_array_equal(stars["calib_psf_candidate"], [False, False, True, True, True])
321 np.testing.assert_array_equal(stars["calib_psf_used"], [False, False, True, True, True])
322 # Too few sources to reserve any in these tests.
323 self.assertEqual(stars["calib_psf_reserved"].sum(), 0)
326class CalibrateImageTaskRunQuantumTests(lsst.utils.tests.TestCase):
327 """Tests of ``CalibrateImageTask.runQuantum``, which need a test butler,
328 but do not need real images.
329 """
330 def setUp(self):
331 instrument = "testCam"
332 exposure0 = 101
333 exposure1 = 101
334 visit = 100101
335 detector = 42
337 # Create a and populate a test butler for runQuantum tests.
338 self.repo_path = tempfile.TemporaryDirectory(ignore_cleanup_errors=True)
339 self.repo = butlerTests.makeTestRepo(self.repo_path.name)
341 # dataIds for fake data
342 butlerTests.addDataIdValue(self.repo, "instrument", instrument)
343 butlerTests.addDataIdValue(self.repo, "exposure", exposure0)
344 butlerTests.addDataIdValue(self.repo, "exposure", exposure1)
345 butlerTests.addDataIdValue(self.repo, "visit", visit)
346 butlerTests.addDataIdValue(self.repo, "detector", detector)
348 # inputs
349 butlerTests.addDatasetType(self.repo, "postISRCCD", {"instrument", "exposure", "detector"},
350 "ExposureF")
351 butlerTests.addDatasetType(self.repo, "gaia_dr3_20230707", {"htm7"}, "SimpleCatalog")
352 butlerTests.addDatasetType(self.repo, "ps1_pv3_3pi_20170110", {"htm7"}, "SimpleCatalog")
354 # outputs
355 butlerTests.addDatasetType(self.repo, "initial_pvi", {"instrument", "visit", "detector"},
356 "ExposureF")
357 butlerTests.addDatasetType(self.repo, "initial_stars_footprints_detector",
358 {"instrument", "visit", "detector"},
359 "SourceCatalog")
360 butlerTests.addDatasetType(self.repo, "initial_photoCalib_detector",
361 {"instrument", "visit", "detector"},
362 "PhotoCalib")
363 # optional outputs
364 butlerTests.addDatasetType(self.repo, "initial_pvi_background", {"instrument", "visit", "detector"},
365 "Background")
366 butlerTests.addDatasetType(self.repo, "initial_psf_stars_footprints",
367 {"instrument", "visit", "detector"},
368 "SourceCatalog")
369 butlerTests.addDatasetType(self.repo,
370 "initial_astrometry_match_detector",
371 {"instrument", "visit", "detector"},
372 "Catalog")
373 butlerTests.addDatasetType(self.repo,
374 "initial_photometry_match_detector",
375 {"instrument", "visit", "detector"},
376 "Catalog")
378 # dataIds
379 self.exposure0_id = self.repo.registry.expandDataId(
380 {"instrument": instrument, "exposure": exposure0, "detector": detector})
381 self.exposure1_id = self.repo.registry.expandDataId(
382 {"instrument": instrument, "exposure": exposure1, "detector": detector})
383 self.visit_id = self.repo.registry.expandDataId(
384 {"instrument": instrument, "visit": visit, "detector": detector})
385 self.htm_id = self.repo.registry.expandDataId({"htm7": 42})
387 # put empty data
388 self.butler = butlerTests.makeTestCollection(self.repo)
389 self.butler.put(afwImage.ExposureF(), "postISRCCD", self.exposure0_id)
390 self.butler.put(afwTable.SimpleCatalog(), "gaia_dr3_20230707", self.htm_id)
391 self.butler.put(afwTable.SimpleCatalog(), "ps1_pv3_3pi_20170110", self.htm_id)
393 def tearDown(self):
394 self.repo_path.cleanup()
396 def test_runQuantum(self):
397 task = CalibrateImageTask()
398 lsst.pipe.base.testUtils.assertValidInitOutput(task)
400 quantum = lsst.pipe.base.testUtils.makeQuantum(
401 task, self.butler, self.visit_id,
402 {"exposures": [self.exposure0_id],
403 "astrometry_ref_cat": [self.htm_id],
404 "photometry_ref_cat": [self.htm_id],
405 # outputs
406 "output_exposure": self.visit_id,
407 "stars": self.visit_id,
408 "background": self.visit_id,
409 "psf_stars": self.visit_id,
410 "applied_photo_calib": self.visit_id,
411 "initial_pvi_background": self.visit_id,
412 "astrometry_matches": self.visit_id,
413 "photometry_matches": self.visit_id,
414 })
415 mock_run = lsst.pipe.base.testUtils.runTestQuantum(task, self.butler, quantum)
417 # Ensure the reference loaders have been configured.
418 self.assertEqual(task.astrometry.refObjLoader.name, "gaia_dr3_20230707")
419 self.assertEqual(task.photometry.match.refObjLoader.name, "ps1_pv3_3pi_20170110")
420 # Check that the proper kwargs are passed to run().
421 self.assertEqual(mock_run.call_args.kwargs.keys(), {"exposures"})
423 def test_runQuantum_2_snaps(self):
424 task = CalibrateImageTask()
425 lsst.pipe.base.testUtils.assertValidInitOutput(task)
427 quantum = lsst.pipe.base.testUtils.makeQuantum(
428 task, self.butler, self.visit_id,
429 {"exposures": [self.exposure0_id, self.exposure1_id],
430 "astrometry_ref_cat": [self.htm_id],
431 "photometry_ref_cat": [self.htm_id],
432 # outputs
433 "output_exposure": self.visit_id,
434 "stars": self.visit_id,
435 "background": self.visit_id,
436 "psf_stars": self.visit_id,
437 "applied_photo_calib": self.visit_id,
438 "initial_pvi_background": self.visit_id,
439 "astrometry_matches": self.visit_id,
440 "photometry_matches": self.visit_id,
441 })
442 mock_run = lsst.pipe.base.testUtils.runTestQuantum(task, self.butler, quantum)
444 # Ensure the reference loaders have been configured.
445 self.assertEqual(task.astrometry.refObjLoader.name, "gaia_dr3_20230707")
446 self.assertEqual(task.photometry.match.refObjLoader.name, "ps1_pv3_3pi_20170110")
447 # Check that the proper kwargs are passed to run().
448 self.assertEqual(mock_run.call_args.kwargs.keys(), {"exposures"})
450 def test_runQuantum_no_optional_outputs(self):
451 config = CalibrateImageTask.ConfigClass()
452 config.optional_outputs = None
453 task = CalibrateImageTask(config=config)
454 lsst.pipe.base.testUtils.assertValidInitOutput(task)
456 quantum = lsst.pipe.base.testUtils.makeQuantum(
457 task, self.butler, self.visit_id,
458 {"exposures": [self.exposure0_id],
459 "astrometry_ref_cat": [self.htm_id],
460 "photometry_ref_cat": [self.htm_id],
461 # outputs
462 "output_exposure": self.visit_id,
463 "stars": self.visit_id,
464 "applied_photo_calib": self.visit_id,
465 "background": self.visit_id,
466 })
467 mock_run = lsst.pipe.base.testUtils.runTestQuantum(task, self.butler, quantum)
469 # Ensure the reference loaders have been configured.
470 self.assertEqual(task.astrometry.refObjLoader.name, "gaia_dr3_20230707")
471 self.assertEqual(task.photometry.match.refObjLoader.name, "ps1_pv3_3pi_20170110")
472 # Check that the proper kwargs are passed to run().
473 self.assertEqual(mock_run.call_args.kwargs.keys(), {"exposures"})
475 def test_lintConnections(self):
476 """Check that the connections are self-consistent.
477 """
478 Connections = CalibrateImageTask.ConfigClass.ConnectionsClass
479 lsst.pipe.base.testUtils.lintConnections(Connections)
482def setup_module(module):
483 lsst.utils.tests.init()
486class MemoryTestCase(lsst.utils.tests.MemoryTestCase):
487 pass
490if __name__ == "__main__": 490 ↛ 491line 490 didn't jump to line 491, because the condition on line 490 was never true
491 lsst.utils.tests.init()
492 unittest.main()