Coverage for tests/test_calibrateImage.py: 14%
481 statements
« prev ^ index » next coverage.py v7.11.0, created at 2025-11-04 02:26 -0800
« prev ^ index » next coverage.py v7.11.0, created at 2025-11-04 02:26 -0800
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
23from unittest import mock
24import tempfile
26import astropy.units as u
27from astropy.coordinates import SkyCoord
28import copy
29import numpy as np
30import esutil
31import os
32import requests
34import lsst.afw.image as afwImage
35import lsst.afw.math as afwMath
36import lsst.afw.table as afwTable
37import lsst.daf.base
38import lsst.daf.butler
39import lsst.daf.butler.tests as butlerTests
40import lsst.geom
41import lsst.meas.algorithms
42from lsst.meas.algorithms import testUtils
43import lsst.meas.extensions.psfex
44import lsst.meas.base
45import lsst.meas.base.tests
46import lsst.pipe.base.testUtils
47from lsst.pipe.tasks.calibrateImage import CalibrateImageTask, \
48 NoPsfStarsToStarsMatchError, AllCentroidsFlaggedError
49import lsst.pex.config as pexConfig
50import lsst.utils.tests
52from utils import makeTestVisitInfo
55class CalibrateImageTaskTests(lsst.utils.tests.TestCase):
57 def setUp(self):
58 # Different x/y dimensions so they're easy to distinguish in a plot,
59 # and non-zero minimum, to help catch xy0 errors.
60 bbox = lsst.geom.Box2I(lsst.geom.Point2I(5, 4), lsst.geom.Point2I(205, 184))
61 self.sky_center = lsst.geom.SpherePoint(245.0, -45.0, lsst.geom.degrees)
62 self.photo_calib = 12.3
63 dataset = lsst.meas.base.tests.TestDataset(bbox, crval=self.sky_center, calibration=self.photo_calib)
64 # sqrt of area of a normalized 2d gaussian
65 psf_scale = np.sqrt(4*np.pi*(dataset.psfShape.getDeterminantRadius())**2)
66 noise = 10.0 # stddev of noise per pixel
67 # Sources ordered from faintest to brightest.
68 self.fluxes = np.array((6*noise*psf_scale,
69 12*noise*psf_scale,
70 45*noise*psf_scale,
71 150*noise*psf_scale,
72 400*noise*psf_scale,
73 1000*noise*psf_scale))
74 self.centroids = np.array(((162, 22),
75 (40, 70),
76 (100, 160),
77 (50, 120),
78 (92, 35),
79 (175, 154)), dtype=np.float32)
80 for flux, centroid in zip(self.fluxes, self.centroids):
81 dataset.addSource(instFlux=flux, centroid=lsst.geom.Point2D(centroid[0], centroid[1]))
83 # Bright extended source in the center of the image: should not appear
84 # in any of the output catalogs.
85 self.extended_source = lsst.geom.Point2D(100, 100)
86 shape = lsst.afw.geom.Quadrupole(8, 9, 3)
87 dataset.addSource(instFlux=500*noise*psf_scale, centroid=self.extended_source, shape=shape)
89 schema = dataset.makeMinimalSchema()
90 self.truth_exposure, self.truth_cat = dataset.realize(noise=noise, schema=schema)
91 # Add in a significant background, so we can test that the output
92 # background is self-consistent with the calibrated exposure.
93 self.background_level = 500.0
94 self.truth_exposure.image += self.background_level
95 # To make it look like a version=1 (nJy fluxes) refcat
96 self.truth_cat = self.truth_exposure.photoCalib.calibrateCatalog(self.truth_cat)
97 self.ref_loader = testUtils.MockReferenceObjectLoaderFromMemory([self.truth_cat])
98 metadata = lsst.daf.base.PropertyList()
99 metadata.set("REFCAT_FORMAT_VERSION", 1)
100 self.truth_cat.setMetadata(metadata)
102 # TODO: a cosmic ray (need to figure out how to insert a fake-CR)
103 # self.truth_exposure.image.array[10, 10] = 100000
104 # self.truth_exposure.variance.array[10, 10] = 100000/noise
106 # Copy the truth exposure, because CalibrateImage modifies the input.
107 # Post-ISR ccds only contain: initial WCS, VisitInfo, filter
108 self.exposure = afwImage.ExposureF(self.truth_exposure, deep=True)
109 self.exposure.setWcs(self.truth_exposure.wcs)
110 self.exposure.info.setVisitInfo(self.truth_exposure.visitInfo)
111 self.exposure.info.id = 12345
112 # "truth" filter, to match the "truth" refcat.
113 self.exposure.setFilter(lsst.afw.image.FilterLabel(physical='truth', band="truth"))
114 self.exposure.metadata["LSST ISR FLAT APPLIED"] = True
116 # Test-specific configuration:
117 self.config = CalibrateImageTask.ConfigClass()
118 # We don't have many sources, so have to fit simpler models.
119 self.config.psf_detection.background.approxOrderX = 1
120 self.config.star_detection.background.approxOrderX = 1
121 # Only insert 2 sky sources, for simplicity.
122 self.config.star_sky_sources.nSources = 2
123 # Use PCA psf fitter, as psfex fails if there are only 4 stars.
124 self.config.psf_measure_psf.psfDeterminer = 'pca'
125 # We don't have many test points, so can't match on complicated shapes.
126 self.config.astrometry.sourceSelector["science"].flags.good = []
127 self.config.astrometry.matcher.numPointsForShape = 3
128 self.config.run_sattle = False
129 # ApFlux has more noise than PsfFlux (the latter unrealistically small
130 # in this test data), so we need to do magnitude rejection at higher
131 # sigma, otherwise we can lose otherwise good sources.
132 # TODO DM-39203: Once we are using Compensated Gaussian Fluxes, we
133 # will use those fluxes here, and hopefully can remove this.
134 self.config.astrometry.magnitudeOutlierRejectionNSigma = 9.0
136 # Make a realistic id generator so that output catalog ids are useful.
137 # NOTE: The id generator is used to seed the noise replacer during
138 # measurement, so changes to values here can have subtle effects on
139 # the centroids and fluxes mesaured on the image, which might cause
140 # tests to fail.
141 data_id = lsst.daf.butler.DataCoordinate.standardize(
142 instrument="I",
143 visit=self.truth_exposure.visitInfo.id,
144 detector=12,
145 universe=lsst.daf.butler.DimensionUniverse(),
146 )
147 self.config.id_generator.packer.name = "observation"
148 self.config.id_generator.packer["observation"].n_observations = 10000
149 self.config.id_generator.packer["observation"].n_detectors = 99
150 self.config.id_generator.n_releases = 8
151 self.config.id_generator.release_id = 2
152 self.id_generator = self.config.id_generator.apply(data_id)
154 # Something about this test dataset prefers a larger threshold here.
155 self.config.star_selector["science"].unresolved.maximum = 0.2
157 def _check_run(self, calibrate, result, expect_calibrated_pixels: bool = True):
158 """Test the result of CalibrateImage.run().
160 Parameters
161 ----------
162 calibrate : `lsst.pipe.tasks.calibrateImage.CalibrateImageTask`
163 Configured task that had `run` called on it.
164 result : `lsst.pipe.base.Struct`
165 Result of calling calibrate.run().
166 expect_calibrated_pixels : `bool`, optional
167 Whether to expect image and background pixels to be calibrated.
168 """
169 # Background should have 4 elements: 3 from compute_psf and one from
170 # re-estimation during source detection.
171 self.assertEqual(len(result.background), 4)
173 # Both afw and astropy psf_stars catalogs should be populated.
174 self.assertEqual(result.psf_stars["calib_psf_used"].sum(), 3)
175 self.assertEqual(result.psf_stars_footprints["calib_psf_used"].sum(), 3)
177 # Check that the summary statistics are reasonable.
178 summary = result.exposure.info.getSummaryStats()
179 self.assertFloatsAlmostEqual(summary.psfSigma, 2.0, rtol=1e-2)
180 self.assertFloatsAlmostEqual(summary.ra, self.sky_center.getRa().asDegrees(), rtol=1e-7)
181 self.assertFloatsAlmostEqual(summary.dec, self.sky_center.getDec().asDegrees(), rtol=1e-7)
183 # Should have finite sky coordinates in the afw and astropy catalogs.
184 self.assertTrue(np.isfinite(result.stars_footprints["coord_ra"]).all())
185 self.assertTrue(np.isfinite(result.stars["coord_ra"]).all())
187 if expect_calibrated_pixels:
188 # Fit photoCalib should be the applied value if we calibrated
189 # pixels, not the ==1 one on the exposure.
190 photo_calib = result.applied_photo_calib
191 self.assertEqual(result.exposure.photoCalib.getCalibrationMean(), 1.0)
192 else:
193 self.assertIsNone(result.applied_photo_calib)
194 photo_calib = result.exposure.photoCalib
195 # PhotoCalib comparison is very approximate because we are basing this
196 # comparison on just 2-3 stars.
197 self.assertFloatsAlmostEqual(photo_calib.getCalibrationMean(), self.photo_calib, rtol=1e-2)
198 # Should have calibrated flux/magnitudes in the afw and astropy catalogs
199 self.assertIn("slot_PsfFlux_flux", result.stars_footprints.schema)
200 self.assertIn("slot_PsfFlux_mag", result.stars_footprints.schema)
201 self.assertEqual(result.stars["slot_PsfFlux_flux"].unit, u.nJy)
202 self.assertEqual(result.stars["slot_PsfFlux_mag"].unit, u.ABmag)
204 # Should have detected all S/N >= 10 sources plus 2 sky sources, whether 1 or 2 snaps.
205 self.assertEqual(len(result.stars), 6)
206 # Did the psf flags get propagated from the psf_stars catalog?
207 self.assertEqual(result.stars["calib_psf_used"].sum(), 3)
209 # Check that all necessary fields are in the output.
210 lsst.pipe.base.testUtils.assertValidOutput(calibrate, result)
212 # Check metadata.
213 key = "LSST CALIB ILLUMCORR APPLIED"
214 self.assertIn(key, result.exposure.metadata)
215 self.assertEqual(result.exposure.metadata[key], False)
217 # Check that the psf_stars cross match worked correctly.
218 matches = esutil.numpy_util.match(result.psf_stars["id"], result.stars["psf_id"])
219 self.assertFloatsAlmostEqual(result.psf_stars["slot_Centroid_x"][matches[0]],
220 result.stars["slot_Centroid_x"][matches[1]], atol=2e-5)
221 if "astrometry_matches" in self.config.optional_outputs:
222 matches = esutil.numpy_util.match(result.astrometry_matches["src_id"],
223 result.photometry_matches["src_psf_id"])
224 self.assertFloatsAlmostEqual(result.astrometry_matches["src_slot_Centroid_x"][matches[0]],
225 result.photometry_matches["src_slot_Centroid_x"][matches[1]],
226 atol=2e-5)
228 def test_run(self):
229 """Test that run() returns reasonable values to be butler put.
230 """
231 calibrate = CalibrateImageTask(config=self.config)
232 calibrate.astrometry.setRefObjLoader(self.ref_loader)
233 calibrate.photometry.match.setRefObjLoader(self.ref_loader)
234 result = calibrate.run(exposures=self.exposure)
236 self._check_run(calibrate, result)
238 def test_run_downsample(self):
239 """Test that run() runs with downsample.
240 """
241 config = copy.copy(self.config)
242 config.do_downsample_footprints = True
243 config.downsample_max_footprints = 5
245 calibrate = CalibrateImageTask(config=config)
246 calibrate.astrometry.setRefObjLoader(self.ref_loader)
247 calibrate.photometry.match.setRefObjLoader(self.ref_loader)
248 with self.assertLogs("lsst.calibrateImage", level="INFO") as cm:
249 result = calibrate.run(exposures=self.exposure)
250 self.assertIn(
251 "INFO:lsst.calibrateImage:Downsampling from 8 to 7 non-sky-source footprints.",
252 cm.output,
253 )
255 self._check_run(calibrate, result)
257 def test_run_2_snaps(self):
258 """Test that run() returns reasonable values to be butler put, when
259 passed two exposures to combine as snaps.
260 """
261 calibrate = CalibrateImageTask(config=self.config)
262 calibrate.astrometry.setRefObjLoader(self.ref_loader)
263 calibrate.photometry.match.setRefObjLoader(self.ref_loader)
264 # Halve the flux in each exposure to get the expected visit sum.
265 self.exposure.image /= 2
266 self.exposure.variance /= 2
267 result = calibrate.run(exposures=[self.exposure, self.exposure])
269 self._check_run(calibrate, result)
271 def test_run_no_optionals(self):
272 """Test that disabling optional outputs removes them from the output
273 struct, as appropriate.
274 """
275 self.config.optional_outputs = []
276 calibrate = CalibrateImageTask(config=self.config)
277 calibrate.astrometry.setRefObjLoader(self.ref_loader)
278 calibrate.photometry.match.setRefObjLoader(self.ref_loader)
279 result = calibrate.run(exposures=self.exposure)
281 self._check_run(calibrate, result)
282 # These are the only optional outputs that require extra computation,
283 # the others are included in the output struct regardless.
284 self.assertNotIn("astrometry_matches", result.getDict())
285 self.assertNotIn("photometry_matches", result.getDict())
287 def test_run_no_calibrate_pixels(self):
288 """Test that run() returns reasonable values when
289 do_calibrate_pixels=False.
290 """
291 self.config.do_calibrate_pixels = False
292 calibrate = CalibrateImageTask(config=self.config)
293 calibrate.astrometry.setRefObjLoader(self.ref_loader)
294 calibrate.photometry.match.setRefObjLoader(self.ref_loader)
295 result = calibrate.run(exposures=self.exposure)
297 self._check_run(calibrate, result, expect_calibrated_pixels=False)
299 def test_run_no_astrom_errors(self):
300 """Test that run() returns reasonable values when
301 do_calibrate_pixels=False.
302 """
303 self.config.do_include_astrometric_errors = False
304 calibrate = CalibrateImageTask(config=self.config)
305 calibrate.astrometry.setRefObjLoader(self.ref_loader)
306 calibrate.photometry.match.setRefObjLoader(self.ref_loader)
307 result = calibrate.run(exposures=self.exposure)
309 self._check_run(calibrate, result)
311 def test_compute_psf(self):
312 """Test that our brightest sources are found by _compute_psf(),
313 that a PSF is assigned to the expopsure.
314 """
315 calibrate = CalibrateImageTask(config=self.config)
316 psf_stars, background, candidates = calibrate._compute_psf(self.exposure, self.id_generator)
318 # Catalog ids should be very large from this id generator.
319 self.assertTrue(all(psf_stars['id'] > 1000000000))
321 # Background should have 3 elements: initial subtraction, and two from
322 # re-estimation during the two detection passes.
323 self.assertEqual(len(background), 3)
325 # Only the point-sources with S/N > 50 should be in this output.
326 self.assertEqual(psf_stars["calib_psf_used"].sum(), 3)
327 # Sort in order of brightness, to easily compare with expected positions.
328 psf_stars.sort(psf_stars.getPsfFluxSlot().getMeasKey())
329 for record, flux, center in zip(psf_stars[::-1], self.fluxes, self.centroids[self.fluxes > 50]):
330 self.assertFloatsAlmostEqual(record.getX(), center[0], rtol=0.01)
331 self.assertFloatsAlmostEqual(record.getY(), center[1], rtol=0.01)
332 # PsfFlux should match the values inserted.
333 self.assertFloatsAlmostEqual(record["slot_PsfFlux_instFlux"], flux, rtol=0.01)
335 # TODO: While debugging DM-32701, we're using PCA instead of psfex.
336 # Check that we got a useable PSF.
337 # self.assertIsInstance(self.exposure.psf, lsst.meas.extensions.psfex.PsfexPsf)
338 self.assertIsInstance(self.exposure.psf, lsst.meas.algorithms.PcaPsf)
339 # TestDataset sources have PSF radius=2 pixels.
340 radius = self.exposure.psf.computeShape(self.exposure.psf.getAveragePosition()).getDeterminantRadius()
341 self.assertFloatsAlmostEqual(radius, 2.0, rtol=1e-2)
343 # To look at images for debugging (`setup display_ds9` and run ds9):
344 # import lsst.afw.display
345 # display = lsst.afw.display.getDisplay()
346 # display.mtv(self.exposure)
348 def test_compute_psf_bad_centroids(self):
349 """Test that we raise an appropriate error if all measured centroids
350 are bad in compute_psf.
351 The root cause of this is likely an interaction between the PSF fit
352 and cosmic ray repair. An ugly true PSF can result in the centers of
353 sources being masked and repaired as CRs, thus removing the center of
354 the sources and causing them all to be flagged.
355 """
356 # make the sources have a (not too deep) hole in the middle
357 for point in self.centroids:
358 for i in (-1, 0, 1):
359 for j in (-1, 0, 1):
360 self.exposure.image[point[0]+i, point[1]+j] -= \
361 (self.exposure.image[point[0]+i, point[1]+j] - self.background_level)/1.1
362 # and the extended central source
363 point = self.extended_source
364 self.exposure.image[point[0], point[1]] -= \
365 (self.exposure.image[point[0], point[1]] - self.background_level)
367 # add a diagonal gradient to each source
368 size = 5
369 for point, flux in zip(self.centroids, self.fluxes):
370 box = lsst.geom.Box2I.makeCenteredBox(lsst.geom.Point2D(point[0], point[1]),
371 lsst.geom.Extent2I(size*2, size*2))
372 X, Y = np.ogrid[point[0] - size:point[0] + size, point[1] - size:point[1] + size]
373 distance = np.sqrt((X - point[0])**2 + (Y - point[1])**2)
374 gradient = (X - point[0] + size)*10 + (Y - point[1] + size)*10
375 gradient[distance > size] = 0
376 cutout = self.exposure.image.subset(box)
377 cutout.array += gradient / flux
379 calibrate = CalibrateImageTask(config=self.config)
380 with self.assertRaisesRegex(AllCentroidsFlaggedError, r"source centroids \(out of 4\) flagged"):
381 calibrate.run(exposures=[self.exposure], id_generator=self.id_generator)
383 def test_measure_aperture_correction(self):
384 """Test that _measure_aperture_correction() assigns an ApCorrMap to the
385 exposure.
386 """
387 calibrate = CalibrateImageTask(config=self.config)
388 psf_stars, background, candidates = calibrate._compute_psf(self.exposure, self.id_generator)
390 # First check that the exposure doesn't have an ApCorrMap.
391 self.assertIsNone(self.exposure.apCorrMap)
392 calibrate._measure_aperture_correction(self.exposure, psf_stars)
393 self.assertIsInstance(self.exposure.apCorrMap, afwImage.ApCorrMap)
394 # We know that there are 2 fields from the normalization, plus more from
395 # other configured plugins.
396 self.assertGreater(len(self.exposure.apCorrMap), 2)
398 def test_find_stars(self):
399 """Test that _find_stars() correctly identifies the S/N>10 stars
400 in the image and returns them in the output catalog.
401 """
402 calibrate = CalibrateImageTask(config=self.config)
403 psf_stars, background, candidates = calibrate._compute_psf(self.exposure, self.id_generator)
404 calibrate._measure_aperture_correction(self.exposure, psf_stars)
406 stars = calibrate._find_stars(self.exposure, background, self.id_generator)
408 # Catalog ids should be very large from this id generator.
409 self.assertTrue(all(stars['id'] > 1000000000))
411 # Background should have 4 elements: 3 from compute_psf and one from
412 # re-estimation during source detection.
413 self.assertEqual(len(background), 4)
415 # Only 5 psf-like sources with S/N>10 should be in the output catalog,
416 # plus two sky sources.
417 self.assertEqual(len(stars), 6)
418 self.assertTrue(stars.isContiguous())
419 # Sort in order of brightness, to easily compare with expected positions.
420 stars.sort(stars.getPsfFluxSlot().getMeasKey())
421 for record, flux, center in zip(stars[::-1], self.fluxes, self.centroids[self.fluxes > 50]):
422 self.assertFloatsAlmostEqual(record.getX(), center[0], rtol=0.01)
423 self.assertFloatsAlmostEqual(record.getY(), center[1], rtol=0.01)
424 self.assertFloatsAlmostEqual(record["slot_PsfFlux_instFlux"], flux, rtol=0.01)
426 def test_astrometry(self):
427 """Test that the fitted WCS gives good catalog coordinates.
428 """
429 calibrate = CalibrateImageTask(config=self.config)
430 calibrate.astrometry.setRefObjLoader(self.ref_loader)
431 psf_stars, background, candidates = calibrate._compute_psf(self.exposure, self.id_generator)
432 calibrate._measure_aperture_correction(self.exposure, psf_stars)
433 stars = calibrate._find_stars(self.exposure, background, self.id_generator)
435 calibrate._fit_astrometry(self.exposure, stars)
437 # Check that we got reliable matches with the truth coordinates.
438 sky = stars["sky_source"]
439 fitted = SkyCoord(stars[~sky]['coord_ra'], stars[~sky]['coord_dec'], unit="radian")
440 truth = SkyCoord(self.truth_cat['coord_ra'], self.truth_cat['coord_dec'], unit="radian")
441 idx, d2d, _ = fitted.match_to_catalog_sky(truth)
442 np.testing.assert_array_less(d2d.to_value(u.milliarcsecond), 35.0)
444 def test_photometry(self):
445 """Test that the fitted photoCalib matches the one we generated,
446 and that the exposure is calibrated.
447 """
448 calibrate = CalibrateImageTask(config=self.config)
449 calibrate.astrometry.setRefObjLoader(self.ref_loader)
450 calibrate.photometry.match.setRefObjLoader(self.ref_loader)
451 psf_stars, background, candidates = calibrate._compute_psf(self.exposure, self.id_generator)
452 calibrate._measure_aperture_correction(self.exposure, psf_stars)
453 stars = calibrate._find_stars(self.exposure, background, self.id_generator)
454 calibrate._fit_astrometry(self.exposure, stars)
456 stars, matches, meta, photoCalib = calibrate._fit_photometry(self.exposure, stars)
457 calibrate._apply_photometry(self.exposure, background)
459 # NOTE: With this test data, PhotoCalTask returns calibrationErr==0,
460 # so we can't check that the photoCal error has been set.
461 self.assertFloatsAlmostEqual(photoCalib.getCalibrationMean(), self.photo_calib, rtol=1e-2)
462 # The exposure should be calibrated by the applied photoCalib,
463 # and the background should be calibrated to match.
464 uncalibrated = self.exposure.image.clone()
465 uncalibrated += background.getImage()
466 uncalibrated /= self.photo_calib
467 self.assertFloatsAlmostEqual(uncalibrated.array, self.truth_exposure.image.array, rtol=1e-2)
468 # PhotoCalib on the exposure must be identically 1.
469 self.assertEqual(self.exposure.photoCalib.getCalibrationMean(), 1.0)
471 # Check that we got reliable magnitudes and fluxes vs. truth, ignoring
472 # sky sources.
473 sky = stars["sky_source"]
474 fitted = SkyCoord(stars[~sky]['coord_ra'], stars[~sky]['coord_dec'], unit="radian")
475 truth = SkyCoord(self.truth_cat['coord_ra'], self.truth_cat['coord_dec'], unit="radian")
476 idx, _, _ = fitted.match_to_catalog_sky(truth)
477 # Because the input variance image does not include contributions from
478 # the sources, we can't use fluxErr as a bound on the measurement
479 # quality here.
480 self.assertFloatsAlmostEqual(stars[~sky]['slot_PsfFlux_flux'],
481 self.truth_cat['truth_flux'][idx],
482 rtol=0.1)
483 self.assertFloatsAlmostEqual(stars[~sky]['slot_PsfFlux_mag'],
484 self.truth_cat['truth_mag'][idx],
485 rtol=0.01)
487 def test_match_psf_stars(self):
488 """Test that _match_psf_stars() flags the correct stars as psf stars
489 and candidates.
490 """
491 calibrate = CalibrateImageTask(config=self.config)
492 psf_stars, background, candidates = calibrate._compute_psf(self.exposure, self.id_generator)
493 calibrate._measure_aperture_correction(self.exposure, psf_stars)
494 stars = calibrate._find_stars(self.exposure, background, self.id_generator)
496 # There should be no psf-related flags set at first.
497 self.assertEqual(stars["calib_psf_candidate"].sum(), 0)
498 self.assertEqual(stars["calib_psf_used"].sum(), 0)
499 self.assertEqual(stars["calib_psf_reserved"].sum(), 0)
501 # Reorder stars to be out of order with psf_stars (putting the sky
502 # sources in front); this tests that I get the indexing right.
503 stars.sort(stars.getCentroidSlot().getMeasKey().getX())
504 stars = stars.copy(deep=True)
505 # Re-number the ids: the matcher requires sorted ids: this is always
506 # true in the code itself, but we've permuted them by sorting on
507 # flux. We don't care what the actual ids themselves are here.
508 stars["id"] = np.arange(len(stars))
510 calibrate._match_psf_stars(psf_stars, stars)
512 # Check that the three brightest stars have the psf flags transfered
513 # from the psf_stars catalog by sorting in order of brightness.
514 stars.sort(stars.getPsfFluxSlot().getMeasKey())
515 # sort() above leaves the catalog non-contiguous.
516 stars = stars.copy(deep=True)
517 np.testing.assert_array_equal(stars["calib_psf_candidate"],
518 [False, False, False, True, True, True])
519 np.testing.assert_array_equal(stars["calib_psf_used"],
520 [False, False, False, True, True, True])
521 # Too few sources to reserve any in these tests.
522 self.assertEqual(stars["calib_psf_reserved"].sum(), 0)
524 def test_match_psf_stars_no_matches(self):
525 """Check that _match_psf_stars handles the case of no cross-matches.
526 """
527 calibrate = CalibrateImageTask(config=self.config)
528 # Make two catalogs that cannot have matches.
529 stars = self.truth_cat[2:].copy(deep=True)
530 psf_stars = self.truth_cat[:2].copy(deep=True)
532 with self.assertRaisesRegex(NoPsfStarsToStarsMatchError,
533 "No psf stars out of 2 matched 5 calib stars") as cm:
534 calibrate._match_psf_stars(psf_stars, stars)
535 self.assertEqual(cm.exception.metadata["n_psf_stars"], 2)
536 self.assertEqual(cm.exception.metadata["n_stars"], 5)
538 def test_calibrate_image_illumcorr(self):
539 """Test running through with an illumination correction."""
540 config = copy.copy(self.config)
541 config.do_illumination_correction = True
542 config.psf_subtract_background.doApplyFlatBackgroundRatio = True
543 config.psf_detection.doApplyFlatBackgroundRatio = True
544 config.star_detection.doApplyFlatBackgroundRatio = True
546 calibrate = CalibrateImageTask(config=config)
547 calibrate.astrometry.setRefObjLoader(self.ref_loader)
548 calibrate.photometry.match.setRefObjLoader(self.ref_loader)
550 # Assume that the exposure has been flattened by a flat-flat.
551 background_flat = self.exposure.clone()
552 background_flat.image.array[:, :] = 1.0
553 background_flat.mask.array[:, :] = 0
554 background_flat.variance.array[:, :] = 0.0
556 # And create an illumination correction of 1.1.
557 illum_corr_value = 1.1
558 illumination_correction = self.exposure.clone()
559 illumination_correction.image.array[:, :] = illum_corr_value
560 illumination_correction.mask.array[:, :] = 0
561 illumination_correction.variance.array[:, :] = 0.0
563 result = calibrate.run(
564 exposures=self.exposure,
565 id_generator=self.id_generator,
566 background_flat=background_flat,
567 illumination_correction=illumination_correction,
568 )
570 # We divide the image by the illumination correction, but the reference
571 # sources stay the same, so the applied photocalib will increase by
572 # the illum_corr_value.
573 # Tolerance is the same as the direct test with no illumination
574 # correction.
575 self.assertFloatsAlmostEqual(
576 result.applied_photo_calib.getCalibrationMean(),
577 self.photo_calib * illum_corr_value,
578 rtol=1e-2,
579 )
581 self.assertEqual(len(result.background), 4)
582 self.assertFloatsAlmostEqual(
583 np.median(result.background.getImage().array),
584 result.applied_photo_calib.getCalibrationMean() * self.background_level,
585 rtol=1e-3,
586 )
588 # Check metadata.
589 key = "LSST CALIB ILLUMCORR APPLIED"
590 self.assertIn(key, result.exposure.metadata)
591 self.assertEqual(result.exposure.metadata[key], True)
593 def test_run_with_diffraction_spike_mask(self):
594 """Test that the diffraction spike mask subtask runs.
595 """
596 config = self.config
597 config.doMaskDiffractionSpikes = True
598 config.diffractionSpikeMask.magnitudeThreshold = 19
599 # Define a fake SATURATED mask plane for use by the diffraction spike
600 # task, so that it does not affect the rest of calibrate
601 config.diffractionSpikeMask.saturatedMaskPlane = "FAKESATURATED"
602 calibrate = CalibrateImageTask(config=config)
603 calibrate.astrometry.setRefObjLoader(self.ref_loader)
604 calibrate.photometry.match.setRefObjLoader(self.ref_loader)
605 calibrate.diffractionSpikeMask.setRefObjLoader(self.ref_loader)
607 exposure = self.exposure.clone()
609 exposure.info.setVisitInfo(makeTestVisitInfo())
610 exposure.mask.addMaskPlane(config.diffractionSpikeMask.saturatedMaskPlane)
612 # Set the saturated mask plane in half of the image
613 saturatedMaskBit = exposure.mask.getPlaneBitMask(config.diffractionSpikeMask.saturatedMaskPlane)
614 bbox = exposure.getBBox()
615 bbox.grow(-lsst.geom.Extent2I(0, bbox.height//4))
616 exposure[bbox].mask.array |= saturatedMaskBit
618 result = calibrate.run(exposures=exposure)
620 self._check_run(calibrate, result)
622 @mock.patch.dict(os.environ, {"SATTLE_URI_BASE": ""})
623 def test_fail_on_sattle_miconfiguration(self):
624 """Test for failure if sattle is requested without appropriate configurations.
625 """
626 self.config.run_sattle = True
627 with self.assertRaises(pexConfig.FieldValidationError):
628 CalibrateImageTask(config=self.config)
630 @mock.patch.dict(os.environ, {"SATTLE_URI_BASE": "fake_host:1234"})
631 def test_continue_on_sattle_failure(self):
632 """Processing should continue when sattle returns status codes other than 200.
633 """
634 response = MockResponse({}, 500, "internal sattle error")
636 self.config.run_sattle = True
637 calibrate = CalibrateImageTask(config=self.config)
638 calibrate.astrometry.setRefObjLoader(self.ref_loader)
639 calibrate.photometry.match.setRefObjLoader(self.ref_loader)
640 with mock.patch('requests.put', return_value=response) as mock_put:
641 calibrate.run(exposures=self.exposure)
642 mock_put.assert_called_once()
644 @mock.patch.dict(os.environ, {"SATTLE_URI_BASE": "fake_host:1234"})
645 def test_sattle(self):
646 """Test for successful completion when sattle call returns successfully.
647 """
648 response = MockResponse({}, 200, "success")
650 self.config.run_sattle = True
651 calibrate = CalibrateImageTask(config=self.config)
652 calibrate.astrometry.setRefObjLoader(self.ref_loader)
653 calibrate.photometry.match.setRefObjLoader(self.ref_loader)
654 with mock.patch('requests.put', return_value=response) as mock_put:
655 calibrate.run(exposures=self.exposure)
656 mock_put.assert_called_once()
659class CalibrateImageTaskRunQuantumTests(lsst.utils.tests.TestCase):
660 """Tests of ``CalibrateImageTask.runQuantum``, which need a test butler,
661 but do not need real images.
662 """
663 def setUp(self):
664 instrument = "testCam"
665 exposure0 = 101
666 exposure1 = 102
667 visit = 100101
668 detector = 42
669 physical_filter = "r"
671 # Create a and populate a test butler for runQuantum tests.
672 self.repo_path = tempfile.TemporaryDirectory(ignore_cleanup_errors=True)
673 self.repo = butlerTests.makeTestRepo(self.repo_path.name)
675 # A complete instrument record is necessary for the id generator.
676 instrumentRecord = self.repo.dimensions["instrument"].RecordClass(
677 name=instrument, visit_max=1e6, exposure_max=1e6, detector_max=128,
678 class_name="lsst.obs.base.instrument_tests.DummyCam",
679 )
680 self.repo.registry.syncDimensionData("instrument", instrumentRecord)
682 # dataIds for fake data
683 butlerTests.addDataIdValue(self.repo, "detector", detector)
684 butlerTests.addDataIdValue(self.repo, "exposure", exposure0)
685 butlerTests.addDataIdValue(self.repo, "exposure", exposure1)
686 butlerTests.addDataIdValue(self.repo, "visit", visit)
687 butlerTests.addDataIdValue(self.repo, "physical_filter", physical_filter)
689 # inputs
690 butlerTests.addDatasetType(self.repo, "postISRCCD", {"instrument", "exposure", "detector"},
691 "ExposureF")
692 butlerTests.addDatasetType(self.repo, "gaia_dr3_20230707", {"htm7"}, "SimpleCatalog")
693 butlerTests.addDatasetType(self.repo, "ps1_pv3_3pi_20170110", {"htm7"}, "SimpleCatalog")
694 butlerTests.addDatasetType(self.repo, "flat", {"instrument", "detector", "physical_filter"},
695 "Exposure")
696 butlerTests.addDatasetType(self.repo,
697 "illuminationCorrection",
698 {"instrument", "detector", "physical_filter"},
699 "Exposure")
701 # outputs
702 butlerTests.addDatasetType(self.repo, "initial_pvi", {"instrument", "visit", "detector"},
703 "ExposureF")
704 butlerTests.addDatasetType(self.repo, "initial_stars_footprints_detector",
705 {"instrument", "visit", "detector"},
706 "SourceCatalog")
707 butlerTests.addDatasetType(self.repo, "initial_stars_detector",
708 {"instrument", "visit", "detector"},
709 "ArrowAstropy")
710 butlerTests.addDatasetType(self.repo, "initial_photoCalib_detector",
711 {"instrument", "visit", "detector"},
712 "PhotoCalib")
713 butlerTests.addDatasetType(self.repo, "background_to_photometric_ratio",
714 {"instrument", "visit", "detector"},
715 "Image")
716 # optional outputs
717 butlerTests.addDatasetType(self.repo, "initial_pvi_background", {"instrument", "visit", "detector"},
718 "Background")
719 butlerTests.addDatasetType(self.repo, "initial_psf_stars_footprints_detector",
720 {"instrument", "visit", "detector"},
721 "SourceCatalog")
722 butlerTests.addDatasetType(self.repo, "initial_psf_stars_detector",
723 {"instrument", "visit", "detector"},
724 "ArrowAstropy")
725 butlerTests.addDatasetType(self.repo,
726 "initial_astrometry_match_detector",
727 {"instrument", "visit", "detector"},
728 "Catalog")
729 butlerTests.addDatasetType(self.repo,
730 "initial_photometry_match_detector",
731 {"instrument", "visit", "detector"},
732 "Catalog")
733 butlerTests.addDatasetType(self.repo,
734 "preliminary_visit_mask",
735 {"instrument", "visit", "detector"},
736 "Mask")
738 # dataIds
739 self.exposure0_id = self.repo.registry.expandDataId(
740 {"instrument": instrument, "exposure": exposure0, "detector": detector})
741 self.exposure1_id = self.repo.registry.expandDataId(
742 {"instrument": instrument, "exposure": exposure1, "detector": detector})
743 self.visit_id = self.repo.registry.expandDataId(
744 {"instrument": instrument, "visit": visit, "detector": detector})
745 self.htm_id = self.repo.registry.expandDataId({"htm7": 42})
746 self.flat_id = self.repo.registry.expandDataId(
747 {"instrument": instrument, "detector": detector, "physical_filter": physical_filter})
749 # put empty data
750 self.butler = butlerTests.makeTestCollection(self.repo)
751 self.butler.put(afwImage.ExposureF(), "postISRCCD", self.exposure0_id)
752 self.butler.put(afwImage.ExposureF(), "postISRCCD", self.exposure1_id)
753 self.butler.put(afwTable.SimpleCatalog(), "gaia_dr3_20230707", self.htm_id)
754 self.butler.put(afwTable.SimpleCatalog(), "ps1_pv3_3pi_20170110", self.htm_id)
755 self.butler.put(afwImage.ExposureF(), "flat", self.flat_id)
756 self.butler.put(afwImage.ExposureF(), "illuminationCorrection", self.flat_id)
758 def tearDown(self):
759 self.repo_path.cleanup()
761 def test_runQuantum(self):
762 task = CalibrateImageTask()
763 lsst.pipe.base.testUtils.assertValidInitOutput(task)
765 quantum = lsst.pipe.base.testUtils.makeQuantum(
766 task, self.butler, self.visit_id,
767 {"exposures": [self.exposure0_id],
768 "astrometry_ref_cat": [self.htm_id],
769 "photometry_ref_cat": [self.htm_id],
770 "background_flat": self.flat_id,
771 "illumination_correction": self.flat_id,
772 # outputs
773 "exposure": self.visit_id,
774 "stars": self.visit_id,
775 "stars_footprints": self.visit_id,
776 "background": self.visit_id,
777 "psf_stars": self.visit_id,
778 "psf_stars_footprints": self.visit_id,
779 "applied_photo_calib": self.visit_id,
780 "initial_pvi_background": self.visit_id,
781 "astrometry_matches": self.visit_id,
782 "photometry_matches": self.visit_id,
783 "mask": self.visit_id,
784 })
785 mock_run = lsst.pipe.base.testUtils.runTestQuantum(task, self.butler, quantum)
787 # Ensure the reference loaders have been configured.
788 self.assertEqual(task.astrometry.refObjLoader.name, "gaia_dr3_20230707")
789 self.assertEqual(task.photometry.match.refObjLoader.name, "ps1_pv3_3pi_20170110")
790 # Check that the proper kwargs are passed to run().
791 self.assertEqual(
792 mock_run.call_args.kwargs.keys(),
793 {"exposures",
794 "result",
795 "id_generator",
796 "background_flat",
797 "illumination_correction",
798 "camera_model"
799 },
800 )
802 def test_runQuantum_illumination_correction(self):
803 config = CalibrateImageTask.ConfigClass()
804 config.do_illumination_correction = True
805 config.psf_subtract_background.doApplyFlatBackgroundRatio = True
806 config.psf_detection.doApplyFlatBackgroundRatio = True
807 config.star_detection.doApplyFlatBackgroundRatio = True
808 task = CalibrateImageTask(config=config)
809 lsst.pipe.base.testUtils.assertValidInitOutput(task)
811 quantum = lsst.pipe.base.testUtils.makeQuantum(
812 task, self.butler, self.visit_id,
813 {"exposures": [self.exposure0_id],
814 "astrometry_ref_cat": [self.htm_id],
815 "photometry_ref_cat": [self.htm_id],
816 "background_flat": self.flat_id,
817 "illumination_correction": self.flat_id,
818 # outputs
819 "exposure": self.visit_id,
820 "stars": self.visit_id,
821 "stars_footprints": self.visit_id,
822 "background": self.visit_id,
823 "background_to_photometric_ratio": self.visit_id,
824 "psf_stars": self.visit_id,
825 "psf_stars_footprints": self.visit_id,
826 "applied_photo_calib": self.visit_id,
827 "initial_pvi_background": self.visit_id,
828 "astrometry_matches": self.visit_id,
829 "photometry_matches": self.visit_id,
830 "mask": self.visit_id,
831 })
832 mock_run = lsst.pipe.base.testUtils.runTestQuantum(task, self.butler, quantum)
834 # Ensure the reference loaders have been configured.
835 self.assertEqual(task.astrometry.refObjLoader.name, "gaia_dr3_20230707")
836 self.assertEqual(task.photometry.match.refObjLoader.name, "ps1_pv3_3pi_20170110")
837 # Check that the proper kwargs are passed to run().
838 self.assertEqual(
839 mock_run.call_args.kwargs.keys(),
840 {"exposures",
841 "result",
842 "id_generator",
843 "background_flat",
844 "illumination_correction",
845 "camera_model"
846 },
847 )
849 def test_runQuantum_2_snaps(self):
850 task = CalibrateImageTask()
851 lsst.pipe.base.testUtils.assertValidInitOutput(task)
853 quantum = lsst.pipe.base.testUtils.makeQuantum(
854 task, self.butler, self.visit_id,
855 {"exposures": [self.exposure0_id, self.exposure1_id],
856 "astrometry_ref_cat": [self.htm_id],
857 "photometry_ref_cat": [self.htm_id],
858 "background_flat": self.flat_id,
859 "illumination_correction": self.flat_id,
860 # outputs
861 "exposure": self.visit_id,
862 "stars": self.visit_id,
863 "stars_footprints": self.visit_id,
864 "background": self.visit_id,
865 "psf_stars": self.visit_id,
866 "psf_stars_footprints": self.visit_id,
867 "applied_photo_calib": self.visit_id,
868 "initial_pvi_background": self.visit_id,
869 "astrometry_matches": self.visit_id,
870 "photometry_matches": self.visit_id,
871 "mask": self.visit_id,
872 })
873 mock_run = lsst.pipe.base.testUtils.runTestQuantum(task, self.butler, quantum)
875 # Ensure the reference loaders have been configured.
876 self.assertEqual(task.astrometry.refObjLoader.name, "gaia_dr3_20230707")
877 self.assertEqual(task.photometry.match.refObjLoader.name, "ps1_pv3_3pi_20170110")
878 # Check that the proper kwargs are passed to run().
879 self.assertEqual(
880 mock_run.call_args.kwargs.keys(),
881 {"exposures",
882 "result",
883 "id_generator",
884 "background_flat",
885 "illumination_correction",
886 "camera_model"
887 },
888 )
890 def test_runQuantum_no_optional_outputs(self):
891 # All the possible connections: we modify this to test each one by
892 # popping off the removed connection, then re-setting it.
893 connections = {"exposures": [self.exposure0_id, self.exposure1_id],
894 "astrometry_ref_cat": [self.htm_id],
895 "photometry_ref_cat": [self.htm_id],
896 "background_flat": self.flat_id,
897 "illumination_correction": self.flat_id,
898 # outputs
899 "exposure": self.visit_id,
900 "stars": self.visit_id,
901 "stars_footprints": self.visit_id,
902 "background": self.visit_id,
903 "psf_stars": self.visit_id,
904 "psf_stars_footprints": self.visit_id,
905 "applied_photo_calib": self.visit_id,
906 "initial_pvi_background": self.visit_id,
907 "astrometry_matches": self.visit_id,
908 "photometry_matches": self.visit_id,
909 "mask": self.visit_id,
910 }
912 # Check that we can turn off one output at a time.
913 for optional in ["psf_stars", "psf_stars_footprints", "astrometry_matches", "photometry_matches",
914 "mask"]:
915 config = CalibrateImageTask.ConfigClass()
916 config.optional_outputs.remove(optional)
917 task = CalibrateImageTask(config=config)
918 lsst.pipe.base.testUtils.assertValidInitOutput(task)
919 # Save the removed one for the next test.
920 temp = connections.pop(optional)
921 # This will fail with "Error in connection ..." if we don't pop
922 # the optional item from the connections list just above.
923 quantum = lsst.pipe.base.testUtils.makeQuantum(task, self.butler, self.visit_id, connections)
924 # This confirms that the outputs did skip the removed one.
925 self.assertNotIn(optional, quantum.outputs)
926 # Restore the one we removed for the next test.
927 connections[optional] = temp
929 def test_runQuantum_no_calibrate_pixels(self):
930 """Test that the the task runs when calibrating pixels is disabled,
931 and that this results in the ``applied_photo_calib`` output being
932 removed.
933 """
934 config = CalibrateImageTask.ConfigClass()
935 config.do_calibrate_pixels = False
936 task = CalibrateImageTask(config=config)
937 lsst.pipe.base.testUtils.assertValidInitOutput(task)
939 quantum = lsst.pipe.base.testUtils.makeQuantum(
940 task, self.butler, self.visit_id,
941 {"exposures": [self.exposure0_id],
942 "astrometry_ref_cat": [self.htm_id],
943 "photometry_ref_cat": [self.htm_id],
944 "background_flat": self.flat_id,
945 "illumination_correction": self.flat_id,
946 # outputs
947 "exposure": self.visit_id,
948 "stars": self.visit_id,
949 "stars_footprints": self.visit_id,
950 "background": self.visit_id,
951 "psf_stars": self.visit_id,
952 "psf_stars_footprints": self.visit_id,
953 "initial_pvi_background": self.visit_id,
954 "astrometry_matches": self.visit_id,
955 "photometry_matches": self.visit_id,
956 "mask": self.visit_id,
957 })
958 mock_run = lsst.pipe.base.testUtils.runTestQuantum(task, self.butler, quantum)
960 # Ensure the reference loaders have been configured.
961 self.assertEqual(task.astrometry.refObjLoader.name, "gaia_dr3_20230707")
962 self.assertEqual(task.photometry.match.refObjLoader.name, "ps1_pv3_3pi_20170110")
963 # Check that the proper kwargs are passed to run().
964 self.assertEqual(
965 mock_run.call_args.kwargs.keys(),
966 {"exposures",
967 "result",
968 "id_generator",
969 "background_flat",
970 "illumination_correction",
971 "camera_model"
972 },
973 )
975 def test_lintConnections(self):
976 """Check that the connections are self-consistent.
977 """
978 Connections = CalibrateImageTask.ConfigClass.ConnectionsClass
979 lsst.pipe.base.testUtils.lintConnections(Connections)
981 def test_runQuantum_exception(self):
982 """Test exception handling in runQuantum.
983 """
984 task = CalibrateImageTask()
985 lsst.pipe.base.testUtils.assertValidInitOutput(task)
987 quantum = lsst.pipe.base.testUtils.makeQuantum(
988 task, self.butler, self.visit_id,
989 {"exposures": [self.exposure0_id],
990 "astrometry_ref_cat": [self.htm_id],
991 "photometry_ref_cat": [self.htm_id],
992 "background_flat": self.flat_id,
993 "illuminationCorrection": self.flat_id,
994 # outputs
995 "exposure": self.visit_id,
996 "stars": self.visit_id,
997 "stars_footprints": self.visit_id,
998 "background": self.visit_id,
999 "psf_stars": self.visit_id,
1000 "psf_stars_footprints": self.visit_id,
1001 "applied_photo_calib": self.visit_id,
1002 "initial_pvi_background": self.visit_id,
1003 "astrometry_matches": self.visit_id,
1004 "photometry_matches": self.visit_id,
1005 "mask": self.visit_id,
1006 })
1008 # A generic exception should raise directly.
1009 msg = "mocked run exception"
1010 with (
1011 mock.patch.object(task, "run", side_effect=ValueError(msg)),
1012 self.assertRaisesRegex(ValueError, "mocked run exception")
1013 ):
1014 lsst.pipe.base.testUtils.runTestQuantum(task, self.butler, quantum, mockRun=False)
1016 # An AlgorithmError should write annotated partial outputs.
1017 error = lsst.meas.algorithms.MeasureApCorrError(name="test", nSources=100, ndof=101)
1019 def mock_run(
1020 exposures,
1021 result=None,
1022 id_generator=None,
1023 background_flat=None,
1024 illumination_correction=None,
1025 camera_model=None,
1026 ):
1027 """Mock success through compute_psf, but failure after.
1028 """
1029 result.exposure = afwImage.ExposureF(10, 10)
1030 result.psf_stars_footprints = afwTable.SourceCatalog()
1031 result.psf_stars = afwTable.SourceCatalog().asAstropy()
1032 result.background = afwMath.BackgroundList()
1033 raise error
1035 with (
1036 mock.patch.object(task, "run", side_effect=mock_run),
1037 self.assertRaises(lsst.pipe.base.AnnotatedPartialOutputsError),
1038 ):
1039 with self.assertLogs("lsst.calibrateImage", level="DEBUG") as cm:
1040 lsst.pipe.base.testUtils.runTestQuantum(task,
1041 self.butler,
1042 quantum,
1043 mockRun=False)
1045 logged = "\n".join(cm.output)
1046 self.assertIn("Task failed with only partial outputs", logged)
1047 self.assertIn("MeasureApCorrError", logged)
1049 # NOTE: This is an integration test of afw Exposure & SourceCatalog
1050 # metadata with the error annotation system in pipe_base.
1051 # Check that we did get the annotated partial outputs...
1052 pvi = self.butler.get("initial_pvi", self.visit_id)
1053 self.assertIn("Unable to measure aperture correction", pvi.metadata["failure.message"])
1054 self.assertIn("MeasureApCorrError", pvi.metadata["failure.type"])
1055 self.assertEqual(pvi.metadata["failure.metadata.ndof"], 101)
1056 stars = self.butler.get("initial_psf_stars_footprints_detector", self.visit_id)
1057 self.assertIn("Unable to measure aperture correction", stars.metadata["failure.message"])
1058 self.assertIn("MeasureApCorrError", stars.metadata["failure.type"])
1059 self.assertEqual(stars.metadata["failure.metadata.ndof"], 101)
1060 # ... but not the un-produced outputs.
1061 with self.assertRaises(FileNotFoundError):
1062 self.butler.get("initial_stars_footprints_detector", self.visit_id)
1065class MockResponse:
1066 """Provide a mock for requests.put calls"""
1067 def __init__(self, json_data, status_code, text):
1068 self.json_data = json_data
1069 self.status_code = status_code
1070 self.text = text
1072 def json(self):
1073 return self.json_data
1075 def raise_for_status(self):
1076 if self.status_code != 200:
1077 raise requests.exceptions.HTTPError
1080def setup_module(module):
1081 lsst.utils.tests.init()
1084class MemoryTestCase(lsst.utils.tests.MemoryTestCase):
1085 pass
1088if __name__ == "__main__": 1088 ↛ 1089line 1088 didn't jump to line 1089 because the condition on line 1088 was never true
1089 lsst.utils.tests.init()
1090 unittest.main()