Skip to content

Commit 1ba55a2

Browse files
authored
Merge pull request #946 from lsst/tickets/DM-44802
DM-44802: Fix missing test coverage in CalibrateImage
2 parents 1e125bf + b7fd09b commit 1ba55a2

File tree

2 files changed

+70
-36
lines changed

2 files changed

+70
-36
lines changed

python/lsst/pipe/tasks/calibrateImage.py

Lines changed: 19 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -139,16 +139,20 @@ class CalibrateImageConnections(pipeBase.PipelineTaskConnections,
139139

140140
def __init__(self, *, config=None):
141141
super().__init__(config=config)
142-
if not config.optional_outputs:
142+
if config.optional_outputs is None or "psf_stars" not in config.optional_outputs:
143143
del self.psf_stars
144+
if config.optional_outputs is None or "psf_stars_footprints" not in config.optional_outputs:
144145
del self.psf_stars_footprints
146+
if config.optional_outputs is None or "astrometry_matches" not in config.optional_outputs:
145147
del self.astrometry_matches
148+
if config.optional_outputs is None or "photometry_matches" not in config.optional_outputs:
146149
del self.photometry_matches
147150

148151

149152
class CalibrateImageConfig(pipeBase.PipelineTaskConfig, pipelineConnections=CalibrateImageConnections):
150153
optional_outputs = pexConfig.ListField(
151-
doc="Which optional outputs to save (as their connection name)?",
154+
doc="Which optional outputs to save (as their connection name)?"
155+
" If None, do not output any of these datasets.",
152156
dtype=str,
153157
# TODO: note somewhere to disable this for benchmarking, but should
154158
# we always have it on for production runs?
@@ -526,7 +530,7 @@ def run(self, *, exposures, id_generator=None, result=None):
526530
result.stars = result.stars_footprints.asAstropy()
527531

528532
astrometry_matches, astrometry_meta = self._fit_astrometry(result.exposure, result.stars_footprints)
529-
if self.config.optional_outputs:
533+
if self.config.optional_outputs is not None and "astrometry_matches" in self.config.optional_outputs:
530534
result.astrometry_matches = lsst.meas.astrom.denormalizeMatches(astrometry_matches,
531535
astrometry_meta)
532536

@@ -535,20 +539,20 @@ def run(self, *, exposures, id_generator=None, result=None):
535539
result.stars_footprints)
536540
# fit_photometry returns a new catalog, so we need a new astropy table view.
537541
result.stars = result.stars_footprints.asAstropy()
538-
if self.config.optional_outputs:
542+
if self.config.optional_outputs is not None and "photometry_matches" in self.config.optional_outputs:
539543
result.photometry_matches = lsst.meas.astrom.denormalizeMatches(photometry_matches,
540544
photometry_meta)
541545

542546
self._summarize(result.exposure, result.stars_footprints, result.background)
543547

544548
return result
545549

546-
def _handle_snaps(self, exposure):
550+
def _handle_snaps(self, exposures):
547551
"""Combine two snaps into one exposure, or return a single exposure.
548552
549553
Parameters
550554
----------
551-
exposure : `lsst.afw.image.Exposure` or `list` [`lsst.afw.image.Exposure]`
555+
exposures : `lsst.afw.image.Exposure` or `list` [`lsst.afw.image.Exposure]`
552556
One or two exposures to combine as snaps.
553557
554558
Returns
@@ -561,17 +565,20 @@ def _handle_snaps(self, exposure):
561565
RuntimeError
562566
Raised if input does not contain either 1 or 2 exposures.
563567
"""
564-
if isinstance(exposure, lsst.afw.image.Exposure):
565-
return exposure
568+
if isinstance(exposures, lsst.afw.image.Exposure):
569+
return exposures
566570

567-
if isinstance(exposure, collections.abc.Sequence):
568-
match len(exposure):
571+
if isinstance(exposures, collections.abc.Sequence) and not isinstance(exposures, str):
572+
match len(exposures):
569573
case 1:
570-
return exposure[0]
574+
return exposures[0]
571575
case 2:
572-
return self.snap_combine.run(exposure[0], exposure[1]).exposure
576+
return self.snap_combine.run(exposures[0], exposures[1]).exposure
573577
case n:
574578
raise RuntimeError(f"Can only process 1 or 2 snaps, not {n}.")
579+
else:
580+
raise RuntimeError("`exposures` must be either an afw Exposure (single snap visit), or a "
581+
"list/tuple of one or two of them.")
575582

576583
def _compute_psf(self, exposure, id_generator):
577584
"""Find bright sources detected on an exposure and fit a PSF model to

tests/test_calibrateImage.py

Lines changed: 51 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -207,6 +207,22 @@ def test_run_2_snaps(self):
207207

208208
self._check_run(calibrate, result)
209209

210+
def test_run_no_optionals(self):
211+
"""Test that disabling optional outputs removes them from the output
212+
struct, as appropriate.
213+
"""
214+
self.config.optional_outputs = None
215+
calibrate = CalibrateImageTask(config=self.config)
216+
calibrate.astrometry.setRefObjLoader(self.ref_loader)
217+
calibrate.photometry.match.setRefObjLoader(self.ref_loader)
218+
result = calibrate.run(exposures=self.exposure)
219+
220+
self._check_run(calibrate, result)
221+
# These are the only optional outputs that require extra computation,
222+
# the others are included in the output struct regardless.
223+
self.assertNotIn("astrometry_matches", result.getDict())
224+
self.assertNotIn("photometry_matches", result.getDict())
225+
210226
def test_handle_snaps(self):
211227
calibrate = CalibrateImageTask(config=self.config)
212228
self.assertEqual(calibrate._handle_snaps(self.exposure), self.exposure)
@@ -216,6 +232,8 @@ def test_handle_snaps(self):
216232
calibrate._handle_snaps([])
217233
with self.assertRaisesRegex(RuntimeError, "Can only process 1 or 2 snaps, not 3."):
218234
calibrate._handle_snaps(3*[self.exposure])
235+
with self.assertRaisesRegex(RuntimeError, "must be either an afw Exposure"):
236+
calibrate._handle_snaps("")
219237

220238
def test_compute_psf(self):
221239
"""Test that our brightest sources are found by _compute_psf(),
@@ -541,30 +559,39 @@ def test_runQuantum_2_snaps(self):
541559
self.assertEqual(mock_run.call_args.kwargs.keys(), {"exposures", "result", "id_generator"})
542560

543561
def test_runQuantum_no_optional_outputs(self):
544-
config = CalibrateImageTask.ConfigClass()
545-
config.optional_outputs = None
546-
task = CalibrateImageTask(config=config)
547-
lsst.pipe.base.testUtils.assertValidInitOutput(task)
548-
549-
quantum = lsst.pipe.base.testUtils.makeQuantum(
550-
task, self.butler, self.visit_id,
551-
{"exposures": [self.exposure0_id],
552-
"astrometry_ref_cat": [self.htm_id],
553-
"photometry_ref_cat": [self.htm_id],
554-
# outputs
555-
"exposure": self.visit_id,
556-
"stars": self.visit_id,
557-
"stars_footprints": self.visit_id,
558-
"applied_photo_calib": self.visit_id,
559-
"background": self.visit_id,
560-
})
561-
mock_run = lsst.pipe.base.testUtils.runTestQuantum(task, self.butler, quantum)
562-
563-
# Ensure the reference loaders have been configured.
564-
self.assertEqual(task.astrometry.refObjLoader.name, "gaia_dr3_20230707")
565-
self.assertEqual(task.photometry.match.refObjLoader.name, "ps1_pv3_3pi_20170110")
566-
# Check that the proper kwargs are passed to run().
567-
self.assertEqual(mock_run.call_args.kwargs.keys(), {"exposures", "result", "id_generator"})
562+
# All the possible connections: we modify this to test each one by
563+
# popping off the removed connection, then re-setting it.
564+
connections = {"exposures": [self.exposure0_id, self.exposure1_id],
565+
"astrometry_ref_cat": [self.htm_id],
566+
"photometry_ref_cat": [self.htm_id],
567+
# outputs
568+
"exposure": self.visit_id,
569+
"stars": self.visit_id,
570+
"stars_footprints": self.visit_id,
571+
"background": self.visit_id,
572+
"psf_stars": self.visit_id,
573+
"psf_stars_footprints": self.visit_id,
574+
"applied_photo_calib": self.visit_id,
575+
"initial_pvi_background": self.visit_id,
576+
"astrometry_matches": self.visit_id,
577+
"photometry_matches": self.visit_id,
578+
}
579+
580+
# Check that we can turn off one output at a time.
581+
for optional in ["psf_stars", "psf_stars_footprints", "astrometry_matches", "photometry_matches"]:
582+
config = CalibrateImageTask.ConfigClass()
583+
config.optional_outputs.remove(optional)
584+
task = CalibrateImageTask(config=config)
585+
lsst.pipe.base.testUtils.assertValidInitOutput(task)
586+
# Save the removed one for the next test.
587+
temp = connections.pop(optional)
588+
# This will fail with "Error in connection ..." if we don't pop
589+
# the optional item from the connections list just above.
590+
quantum = lsst.pipe.base.testUtils.makeQuantum(task, self.butler, self.visit_id, connections)
591+
# This confirms that the outputs did skip the removed one.
592+
self.assertNotIn(optional, quantum.outputs)
593+
# Restore the one we removed for the next test.
594+
connections[optional] = temp
568595

569596
def test_lintConnections(self):
570597
"""Check that the connections are self-consistent.

0 commit comments

Comments
 (0)