Coverage for python/lsst/pipe/tasks/calibrateImage.py: 30%
205 statements
« prev ^ index » next coverage.py v7.4.0, created at 2024-01-04 13:31 +0000
« prev ^ index » next coverage.py v7.4.0, created at 2024-01-04 13:31 +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 collections.abc
24import numpy as np
26import lsst.afw.table as afwTable
27import lsst.afw.image as afwImage
28import lsst.meas.algorithms
29import lsst.meas.algorithms.installGaussianPsf
30import lsst.meas.algorithms.measureApCorr
31from lsst.meas.algorithms import sourceSelector
32import lsst.meas.astrom
33import lsst.meas.deblender
34import lsst.meas.extensions.shapeHSM
35import lsst.pex.config as pexConfig
36import lsst.pipe.base as pipeBase
37from lsst.pipe.base import connectionTypes
38from lsst.utils.timer import timeMethod
40from . import measurePsf, repair, setPrimaryFlags, photoCal, \
41 computeExposureSummaryStats, maskStreaks, snapCombine
44class CalibrateImageConnections(pipeBase.PipelineTaskConnections,
45 dimensions=("instrument", "visit", "detector")):
47 astrometry_ref_cat = connectionTypes.PrerequisiteInput(
48 doc="Reference catalog to use for astrometric calibration.",
49 name="gaia_dr3_20230707",
50 storageClass="SimpleCatalog",
51 dimensions=("skypix",),
52 deferLoad=True,
53 multiple=True,
54 )
55 photometry_ref_cat = connectionTypes.PrerequisiteInput(
56 doc="Reference catalog to use for photometric calibration.",
57 name="ps1_pv3_3pi_20170110",
58 storageClass="SimpleCatalog",
59 dimensions=("skypix",),
60 deferLoad=True,
61 multiple=True
62 )
64 exposures = connectionTypes.Input(
65 doc="Exposure (or two snaps) to be calibrated, and detected and measured on.",
66 name="postISRCCD",
67 storageClass="Exposure",
68 multiple=True, # to handle 1 exposure or 2 snaps
69 dimensions=["instrument", "exposure", "detector"],
70 )
72 # outputs
73 initial_stars_schema = connectionTypes.InitOutput(
74 doc="Schema of the output initial stars catalog.",
75 name="initial_stars_schema",
76 storageClass="SourceCatalog",
77 )
79 # TODO: We want some kind of flag on Exposures/Catalogs to make it obvious
80 # which components had failed to be computed/persisted
81 output_exposure = connectionTypes.Output(
82 doc="Photometrically calibrated exposure with fitted calibrations and summary statistics.",
83 name="initial_pvi",
84 storageClass="ExposureF",
85 dimensions=("instrument", "visit", "detector"),
86 )
87 # TODO DM-40061: persist a parquet version of this!
88 stars = connectionTypes.Output(
89 doc="Catalog of unresolved sources detected on the calibrated exposure; "
90 "includes source footprints.",
91 name="initial_stars_footprints_detector",
92 storageClass="SourceCatalog",
93 dimensions=["instrument", "visit", "detector"],
94 )
95 applied_photo_calib = connectionTypes.Output(
96 doc="Photometric calibration that was applied to exposure.",
97 name="initial_photoCalib_detector",
98 storageClass="PhotoCalib",
99 dimensions=("instrument", "visit", "detector"),
100 )
101 background = connectionTypes.Output(
102 doc="Background models estimated during calibration task.",
103 name="initial_pvi_background",
104 storageClass="Background",
105 dimensions=("instrument", "visit", "detector"),
106 )
108 # Optional outputs
110 # TODO: We need to decide on what intermediate outputs we want to save,
111 # and which to save by default.
112 # TODO DM-40061: persist a parquet version of this!
113 psf_stars = connectionTypes.Output(
114 doc="Catalog of bright unresolved sources detected on the exposure used for PSF determination; "
115 "includes source footprints.",
116 name="initial_psf_stars_footprints",
117 storageClass="SourceCatalog",
118 dimensions=["instrument", "visit", "detector"],
119 )
120 astrometry_matches = connectionTypes.Output(
121 doc="Source to reference catalog matches from the astrometry solver.",
122 name="initial_astrometry_match_detector",
123 storageClass="Catalog",
124 dimensions=("instrument", "visit", "detector"),
125 )
126 photometry_matches = connectionTypes.Output(
127 doc="Source to reference catalog matches from the photometry solver.",
128 name="initial_photometry_match_detector",
129 storageClass="Catalog",
130 dimensions=("instrument", "visit", "detector"),
131 )
133 def __init__(self, *, config=None):
134 super().__init__(config=config)
135 if not config.optional_outputs:
136 self.outputs.remove("psf_stars")
137 self.outputs.remove("astrometry_matches")
138 self.outputs.remove("photometry_matches")
141class CalibrateImageConfig(pipeBase.PipelineTaskConfig, pipelineConnections=CalibrateImageConnections):
142 optional_outputs = pexConfig.ListField(
143 doc="Which optional outputs to save (as their connection name)?",
144 dtype=str,
145 # TODO: note somewhere to disable this for benchmarking, but should
146 # we always have it on for production runs?
147 default=["psf_stars", "astrometry_matches", "photometry_matches"],
148 optional=True
149 )
151 snap_combine = pexConfig.ConfigurableField(
152 target=snapCombine.SnapCombineTask,
153 doc="Task to combine two snaps to make one exposure.",
154 )
156 # subtasks used during psf characterization
157 install_simple_psf = pexConfig.ConfigurableField(
158 target=lsst.meas.algorithms.installGaussianPsf.InstallGaussianPsfTask,
159 doc="Task to install a simple PSF model into the input exposure to use "
160 "when detecting bright sources for PSF estimation.",
161 )
162 psf_repair = pexConfig.ConfigurableField(
163 target=repair.RepairTask,
164 doc="Task to repair cosmic rays on the exposure before PSF determination.",
165 )
166 psf_subtract_background = pexConfig.ConfigurableField(
167 target=lsst.meas.algorithms.SubtractBackgroundTask,
168 doc="Task to perform intial background subtraction, before first detection pass.",
169 )
170 psf_detection = pexConfig.ConfigurableField(
171 target=lsst.meas.algorithms.SourceDetectionTask,
172 doc="Task to detect sources for PSF determination."
173 )
174 psf_source_measurement = pexConfig.ConfigurableField(
175 target=lsst.meas.base.SingleFrameMeasurementTask,
176 doc="Task to measure sources to be used for psf estimation."
177 )
178 psf_measure_psf = pexConfig.ConfigurableField(
179 target=measurePsf.MeasurePsfTask,
180 doc="Task to measure the psf on bright sources."
181 )
183 # TODO DM-39203: we can remove aperture correction from this task once we are
184 # using the shape-based star/galaxy code.
185 measure_aperture_correction = pexConfig.ConfigurableField(
186 target=lsst.meas.algorithms.measureApCorr.MeasureApCorrTask,
187 doc="Task to compute the aperture correction from the bright stars."
188 )
190 # subtasks used during star measurement
191 star_detection = pexConfig.ConfigurableField(
192 target=lsst.meas.algorithms.SourceDetectionTask,
193 doc="Task to detect stars to return in the output catalog."
194 )
195 star_mask_streaks = pexConfig.ConfigurableField(
196 target=maskStreaks.MaskStreaksTask,
197 doc="Task for masking streaks. Adds a STREAK mask plane to an exposure.",
198 )
199 star_deblend = pexConfig.ConfigurableField(
200 target=lsst.meas.deblender.SourceDeblendTask,
201 doc="Split blended sources into their components"
202 )
203 star_measurement = pexConfig.ConfigurableField(
204 target=lsst.meas.base.SingleFrameMeasurementTask,
205 doc="Task to measure stars to return in the output catalog."
206 )
207 star_apply_aperture_correction = pexConfig.ConfigurableField(
208 target=lsst.meas.base.ApplyApCorrTask,
209 doc="Task to apply aperture corrections to the selected stars."
210 )
211 star_catalog_calculation = pexConfig.ConfigurableField(
212 target=lsst.meas.base.CatalogCalculationTask,
213 doc="Task to compute extendedness values on the star catalog, "
214 "for the star selector to remove extended sources."
215 )
216 star_set_primary_flags = pexConfig.ConfigurableField(
217 target=setPrimaryFlags.SetPrimaryFlagsTask,
218 doc="Task to add isPrimary to the catalog."
219 )
220 star_selector = lsst.meas.algorithms.sourceSelectorRegistry.makeField(
221 default="science",
222 doc="Task to select isolated stars to use for calibration."
223 )
225 # final calibrations and statistics
226 astrometry = pexConfig.ConfigurableField(
227 target=lsst.meas.astrom.AstrometryTask,
228 doc="Task to perform astrometric calibration to fit a WCS.",
229 )
230 astrometry_ref_loader = pexConfig.ConfigField(
231 dtype=lsst.meas.algorithms.LoadReferenceObjectsConfig,
232 doc="Configuration of reference object loader for astrometric fit.",
233 )
234 photometry = pexConfig.ConfigurableField(
235 target=photoCal.PhotoCalTask,
236 doc="Task to perform photometric calibration to fit a PhotoCalib.",
237 )
238 photometry_ref_loader = pexConfig.ConfigField(
239 dtype=lsst.meas.algorithms.LoadReferenceObjectsConfig,
240 doc="Configuration of reference object loader for photometric fit.",
241 )
243 compute_summary_stats = pexConfig.ConfigurableField(
244 target=computeExposureSummaryStats.ComputeExposureSummaryStatsTask,
245 doc="Task to to compute summary statistics on the calibrated exposure."
246 )
248 def setDefaults(self):
249 super().setDefaults()
251 # Use a very broad PSF here, to throughly reject CRs.
252 # TODO investigation: a large initial psf guess may make stars look
253 # like CRs for very good seeing images.
254 self.install_simple_psf.fwhm = 4
256 # Only use high S/N sources for PSF determination.
257 self.psf_detection.thresholdValue = 50.0
258 # TODO investigation: Probably want False here, but that may require
259 # tweaking the background spatial scale, to make it small enough to
260 # prevent extra peaks in the wings of bright objects.
261 self.psf_detection.doTempLocalBackground = False
262 # NOTE: we do want reEstimateBackground=True in psf_detection, so that
263 # each measurement step is done with the best background available.
265 # Minimal measurement plugins for PSF determination.
266 # TODO DM-39203: We can drop GaussianFlux and PsfFlux, if we use
267 # shapeHSM/moments for star/galaxy separation.
268 # TODO DM-39203: we can remove aperture correction from this task once
269 # we are using the shape-based star/galaxy code.
270 self.psf_source_measurement.plugins = ["base_PixelFlags",
271 "base_SdssCentroid",
272 "ext_shapeHSM_HsmSourceMoments",
273 "base_CircularApertureFlux",
274 "base_GaussianFlux",
275 "base_PsfFlux",
276 ]
277 self.psf_source_measurement.slots.shape = "ext_shapeHSM_HsmSourceMoments"
278 # Only measure apertures we need for PSF measurement.
279 # TODO DM-40064: psfex has a hard-coded value of 9 in a psfex-config
280 # file: make that configurable and/or change it to 12 to be consistent
281 # with our other uses?
282 # https://github.com/lsst/meas_extensions_psfex/blob/main/config/default-lsst.psfex#L14
283 self.psf_source_measurement.plugins["base_CircularApertureFlux"].radii = [9.0, 12.0]
285 # No extendeness information available: we need the aperture
286 # corrections to determine that.
287 self.measure_aperture_correction.sourceSelector["science"].doUnresolved = False
288 self.measure_aperture_correction.sourceSelector["science"].flags.good = ["calib_psf_used"]
289 self.measure_aperture_correction.sourceSelector["science"].flags.bad = []
291 # TODO investigation: how faint do we have to detect, to be able to
292 # deblend, etc? We may need star_selector to have a separate value,
293 # and do initial detection at S/N>5.0?
294 # Detection for good S/N for astrometry/photometry and other
295 # downstream tasks.
296 self.star_detection.thresholdValue = 5.0
297 self.star_measurement.plugins = ["base_PixelFlags",
298 "base_SdssCentroid",
299 "ext_shapeHSM_HsmSourceMoments",
300 'ext_shapeHSM_HsmPsfMoments',
301 "base_GaussianFlux",
302 "base_PsfFlux",
303 "base_CircularApertureFlux",
304 ]
305 self.star_measurement.slots.psfShape = "ext_shapeHSM_HsmPsfMoments"
306 self.star_measurement.slots.shape = "ext_shapeHSM_HsmSourceMoments"
307 # Only measure the apertures we need for star selection.
308 self.star_measurement.plugins["base_CircularApertureFlux"].radii = [12.0]
310 # Keep track of which footprints contain streaks
311 self.star_measurement.plugins['base_PixelFlags'].masksFpAnywhere = ['STREAK']
312 self.star_measurement.plugins['base_PixelFlags'].masksFpCenter = ['STREAK']
314 # Select isolated stars with reliable measurements and no bad flags.
315 self.star_selector["science"].doFlags = True
316 self.star_selector["science"].doUnresolved = True
317 self.star_selector["science"].doSignalToNoise = True
318 self.star_selector["science"].doIsolated = True
319 self.star_selector["science"].signalToNoise.minimum = 10.0
321 # Use the affine WCS fitter (assumes we have a good camera geometry).
322 self.astrometry.wcsFitter.retarget(lsst.meas.astrom.FitAffineWcsTask)
323 # phot_g_mean is the primary Gaia band for all input bands.
324 self.astrometry_ref_loader.anyFilterMapsToThis = "phot_g_mean"
326 # Do not subselect during fitting; we already selected good stars.
327 self.astrometry.sourceSelector = "null"
328 self.photometry.match.sourceSelection.retarget(sourceSelector.NullSourceSelectorTask)
330 # All sources should be good for PSF summary statistics.
331 # TODO: These should both be changed to calib_psf_used with DM-41640.
332 self.compute_summary_stats.starSelection = "calib_photometry_used"
333 self.compute_summary_stats.starSelector.flags.good = ["calib_photometry_used"]
336class CalibrateImageTask(pipeBase.PipelineTask):
337 """Compute the PSF, aperture corrections, astrometric and photometric
338 calibrations, and summary statistics for a single science exposure, and
339 produce a catalog of brighter stars that were used to calibrate it.
341 Parameters
342 ----------
343 initial_stars_schema : `lsst.afw.table.Schema`
344 Schema of the initial_stars output catalog.
345 """
346 _DefaultName = "calibrateImage"
347 ConfigClass = CalibrateImageConfig
349 def __init__(self, initial_stars_schema=None, **kwargs):
350 super().__init__(**kwargs)
352 self.makeSubtask("snap_combine")
354 # PSF determination subtasks
355 self.makeSubtask("install_simple_psf")
356 self.makeSubtask("psf_repair")
357 self.makeSubtask("psf_subtract_background")
358 self.psf_schema = afwTable.SourceTable.makeMinimalSchema()
359 self.makeSubtask("psf_detection", schema=self.psf_schema)
360 self.makeSubtask("psf_source_measurement", schema=self.psf_schema)
361 self.makeSubtask("psf_measure_psf", schema=self.psf_schema)
363 self.makeSubtask("measure_aperture_correction", schema=self.psf_schema)
365 # star measurement subtasks
366 if initial_stars_schema is None:
367 initial_stars_schema = afwTable.SourceTable.makeMinimalSchema()
368 afwTable.CoordKey.addErrorFields(initial_stars_schema)
369 self.makeSubtask("star_detection", schema=initial_stars_schema)
370 self.makeSubtask("star_mask_streaks")
371 self.makeSubtask("star_deblend", schema=initial_stars_schema)
372 self.makeSubtask("star_measurement", schema=initial_stars_schema)
373 self.makeSubtask("star_apply_aperture_correction", schema=initial_stars_schema)
374 self.makeSubtask("star_catalog_calculation", schema=initial_stars_schema)
375 self.makeSubtask("star_set_primary_flags", schema=initial_stars_schema, isSingleFrame=True)
376 self.makeSubtask("star_selector")
378 self.makeSubtask("astrometry", schema=initial_stars_schema)
379 self.makeSubtask("photometry", schema=initial_stars_schema)
381 self.makeSubtask("compute_summary_stats")
383 # For the butler to persist it.
384 self.initial_stars_schema = afwTable.SourceCatalog(initial_stars_schema)
386 def runQuantum(self, butlerQC, inputRefs, outputRefs):
387 inputs = butlerQC.get(inputRefs)
389 astrometry_loader = lsst.meas.algorithms.ReferenceObjectLoader(
390 dataIds=[ref.datasetRef.dataId for ref in inputRefs.astrometry_ref_cat],
391 refCats=inputs.pop("astrometry_ref_cat"),
392 name=self.config.connections.astrometry_ref_cat,
393 config=self.config.astrometry_ref_loader, log=self.log)
394 self.astrometry.setRefObjLoader(astrometry_loader)
396 photometry_loader = lsst.meas.algorithms.ReferenceObjectLoader(
397 dataIds=[ref.datasetRef.dataId for ref in inputRefs.photometry_ref_cat],
398 refCats=inputs.pop("photometry_ref_cat"),
399 name=self.config.connections.photometry_ref_cat,
400 config=self.config.photometry_ref_loader, log=self.log)
401 self.photometry.match.setRefObjLoader(photometry_loader)
403 outputs = self.run(**inputs)
405 butlerQC.put(outputs, outputRefs)
407 @timeMethod
408 def run(self, *, exposures):
409 """Find stars and perform psf measurement, then do a deeper detection
410 and measurement and calibrate astrometry and photometry from that.
412 Parameters
413 ----------
414 exposures : `lsst.afw.image.Exposure` or `list` [`lsst.afw.image.Exposure`]
415 Post-ISR exposure(s), with an initial WCS, VisitInfo, and Filter.
416 Modified in-place during processing if only one is passed.
417 If two exposures are passed, treat them as snaps and combine
418 before doing further processing.
420 Returns
421 -------
422 result : `lsst.pipe.base.Struct`
423 Results as a struct with attributes:
425 ``output_exposure``
426 Calibrated exposure, with pixels in nJy units.
427 (`lsst.afw.image.Exposure`)
428 ``stars``
429 Stars that were used to calibrate the exposure, with
430 calibrated fluxes and magnitudes.
431 (`lsst.afw.table.SourceCatalog`)
432 ``psf_stars``
433 Stars that were used to determine the image PSF.
434 (`lsst.afw.table.SourceCatalog`)
435 ``background``
436 Background that was fit to the exposure when detecting
437 ``stars``. (`lsst.afw.math.BackgroundList`)
438 ``applied_photo_calib``
439 Photometric calibration that was fit to the star catalog and
440 applied to the exposure. (`lsst.afw.image.PhotoCalib`)
441 ``astrometry_matches``
442 Reference catalog stars matches used in the astrometric fit.
443 (`list` [`lsst.afw.table.ReferenceMatch`] or `lsst.afw.table.BaseCatalog`)
444 ``photometry_matches``
445 Reference catalog stars matches used in the photometric fit.
446 (`list` [`lsst.afw.table.ReferenceMatch`] or `lsst.afw.table.BaseCatalog`)
447 """
448 exposure = self._handle_snaps(exposures)
450 psf_stars, background, candidates = self._compute_psf(exposure)
452 self._measure_aperture_correction(exposure, psf_stars)
454 stars = self._find_stars(exposure, background)
456 astrometry_matches, astrometry_meta = self._fit_astrometry(exposure, stars)
457 stars, photometry_matches, photometry_meta, photo_calib = self._fit_photometry(exposure, stars)
459 self._summarize(exposure, stars, background)
461 if self.config.optional_outputs:
462 astrometry_matches = lsst.meas.astrom.denormalizeMatches(astrometry_matches, astrometry_meta)
463 photometry_matches = lsst.meas.astrom.denormalizeMatches(photometry_matches, photometry_meta)
465 return pipeBase.Struct(output_exposure=exposure,
466 stars=stars,
467 psf_stars=psf_stars,
468 background=background,
469 applied_photo_calib=photo_calib,
470 astrometry_matches=astrometry_matches,
471 photometry_matches=photometry_matches)
473 def _handle_snaps(self, exposure):
474 """Combine two snaps into one exposure, or return a single exposure.
476 Parameters
477 ----------
478 exposure : `lsst.afw.image.Exposure` or `list` [`lsst.afw.image.Exposure]`
479 One or two exposures to combine as snaps.
481 Returns
482 -------
483 exposure : `lsst.afw.image.Exposure`
484 A single exposure to continue processing.
486 Raises
487 ------
488 RuntimeError
489 Raised if input does not contain either 1 or 2 exposures.
490 """
491 if isinstance(exposure, lsst.afw.image.Exposure):
492 return exposure
494 if isinstance(exposure, collections.abc.Sequence):
495 match len(exposure):
496 case 1:
497 return exposure[0]
498 case 2:
499 return self.snap_combine.run(exposure[0], exposure[1]).exposure
500 case n:
501 raise RuntimeError(f"Can only process 1 or 2 snaps, not {n}.")
503 def _compute_psf(self, exposure, guess_psf=True):
504 """Find bright sources detected on an exposure and fit a PSF model to
505 them, repairing likely cosmic rays before detection.
507 Repair, detect, measure, and compute PSF twice, to ensure the PSF
508 model does not include contributions from cosmic rays.
510 Parameters
511 ----------
512 exposure : `lsst.afw.image.Exposure`
513 Exposure to detect and measure bright stars on.
515 Returns
516 -------
517 sources : `lsst.afw.table.SourceCatalog`
518 Catalog of detected bright sources.
519 background : `lsst.afw.math.BackgroundList`
520 Background that was fit to the exposure during detection.
521 cell_set : `lsst.afw.math.SpatialCellSet`
522 PSF candidates returned by the psf determiner.
523 """
524 def log_psf(msg):
525 """Log the parameters of the psf and background, with a prepended
526 message.
527 """
528 position = exposure.psf.getAveragePosition()
529 sigma = exposure.psf.computeShape(position).getDeterminantRadius()
530 dimensions = exposure.psf.computeImage(position).getDimensions()
531 median_background = np.median(background.getImage().array)
532 self.log.info("%s sigma=%0.4f, dimensions=%s; median background=%0.2f",
533 msg, sigma, dimensions, median_background)
535 self.log.info("First pass detection with Guassian PSF FWHM=%s pixels",
536 self.config.install_simple_psf.fwhm)
537 self.install_simple_psf.run(exposure=exposure)
539 background = self.psf_subtract_background.run(exposure=exposure).background
540 log_psf("Initial PSF:")
541 self.psf_repair.run(exposure=exposure, keepCRs=True)
543 table = afwTable.SourceTable.make(self.psf_schema)
544 # Re-estimate the background during this detection step, so that
545 # measurement uses the most accurate background-subtraction.
546 detections = self.psf_detection.run(table=table, exposure=exposure, background=background)
547 self.psf_source_measurement.run(detections.sources, exposure)
548 psf_result = self.psf_measure_psf.run(exposure=exposure, sources=detections.sources)
549 # Replace the initial PSF with something simpler for the second
550 # repair/detect/measure/measure_psf step: this can help it converge.
551 self.install_simple_psf.run(exposure=exposure)
553 log_psf("Rerunning with simple PSF:")
554 # TODO investigation: Should we only re-run repair here, to use the
555 # new PSF? Maybe we *do* need to re-run measurement with PsfFlux, to
556 # use the fitted PSF?
557 # TODO investigation: do we need a separate measurement task here
558 # for the post-psf_measure_psf step, since we only want to do PsfFlux
559 # and GaussianFlux *after* we have a PSF? Maybe that's not relevant
560 # once DM-39203 is merged?
561 self.psf_repair.run(exposure=exposure, keepCRs=True)
562 # Re-estimate the background during this detection step, so that
563 # measurement uses the most accurate background-subtraction.
564 detections = self.psf_detection.run(table=table, exposure=exposure, background=background)
565 self.psf_source_measurement.run(detections.sources, exposure)
566 psf_result = self.psf_measure_psf.run(exposure=exposure, sources=detections.sources)
568 log_psf("Final PSF:")
570 # Final repair with final PSF, removing cosmic rays this time.
571 self.psf_repair.run(exposure=exposure)
572 # Final measurement with the CRs removed.
573 self.psf_source_measurement.run(detections.sources, exposure)
575 # PSF is set on exposure; only return candidates for optional saving.
576 return detections.sources, background, psf_result.cellSet
578 def _measure_aperture_correction(self, exposure, bright_sources):
579 """Measure and set the ApCorrMap on the Exposure, using
580 previously-measured bright sources.
582 Parameters
583 ----------
584 exposure : `lsst.afw.image.Exposure`
585 Exposure to set the ApCorrMap on.
586 bright_sources : `lsst.afw.table.SourceCatalog`
587 Catalog of detected bright sources; modified to include columns
588 necessary for point source determination for the aperture correction
589 calculation.
590 """
591 result = self.measure_aperture_correction.run(exposure, bright_sources)
592 exposure.setApCorrMap(result.apCorrMap)
594 def _find_stars(self, exposure, background):
595 """Detect stars on an exposure that has a PSF model, and measure their
596 PSF, circular aperture, compensated gaussian fluxes.
598 Parameters
599 ----------
600 exposure : `lsst.afw.image.Exposure`
601 Exposure to set the ApCorrMap on.
602 background : `lsst.afw.math.BackgroundList`
603 Background that was fit to the exposure during detection;
604 modified in-place during subsequent detection.
606 Returns
607 -------
608 stars : `SourceCatalog`
609 Sources that are very likely to be stars, with a limited set of
610 measurements performed on them.
611 """
612 table = afwTable.SourceTable.make(self.initial_stars_schema.schema)
613 # Re-estimate the background during this detection step, so that
614 # measurement uses the most accurate background-subtraction.
615 detections = self.star_detection.run(table=table, exposure=exposure, background=background)
616 sources = detections.sources
618 # Mask streaks
619 self.star_mask_streaks.run(exposure)
621 # TODO investigation: Could this deblender throw away blends of non-PSF sources?
622 self.star_deblend.run(exposure=exposure, sources=sources)
623 # The deblender may not produce a contiguous catalog; ensure
624 # contiguity for subsequent tasks.
625 if not sources.isContiguous():
626 sources = sources.copy(deep=True)
628 # Measure everything, and use those results to select only stars.
629 self.star_measurement.run(sources, exposure)
630 self.star_apply_aperture_correction.run(sources, exposure.info.getApCorrMap())
631 self.star_catalog_calculation.run(sources)
632 self.star_set_primary_flags.run(sources)
634 result = self.star_selector.run(sources)
635 # The star selector may not produce a contiguous catalog.
636 if not result.sourceCat.isContiguous():
637 return result.sourceCat.copy(deep=True)
638 else:
639 return result.sourceCat
641 def _fit_astrometry(self, exposure, stars):
642 """Fit an astrometric model to the data and return the reference
643 matches used in the fit, and the fitted WCS.
645 Parameters
646 ----------
647 exposure : `lsst.afw.image.Exposure`
648 Exposure that is being fit, to get PSF and other metadata from.
649 Modified to add the fitted skyWcs.
650 stars : `SourceCatalog`
651 Good stars selected for use in calibration, with RA/Dec coordinates
652 computed from the pixel positions and fitted WCS.
654 Returns
655 -------
656 matches : `list` [`lsst.afw.table.ReferenceMatch`]
657 Reference/stars matches used in the fit.
658 """
659 result = self.astrometry.run(stars, exposure)
660 return result.matches, result.matchMeta
662 def _fit_photometry(self, exposure, stars):
663 """Fit a photometric model to the data and return the reference
664 matches used in the fit, and the fitted PhotoCalib.
666 Parameters
667 ----------
668 exposure : `lsst.afw.image.Exposure`
669 Exposure that is being fit, to get PSF and other metadata from.
670 Modified to be in nanojanksy units, with an assigned photoCalib
671 identically 1.
672 stars : `lsst.afw.table.SourceCatalog`
673 Good stars selected for use in calibration.
675 Returns
676 -------
677 calibrated_stars : `lsst.afw.table.SourceCatalog`
678 Star catalog with flux/magnitude columns computed from the fitted
679 photoCalib.
680 matches : `list` [`lsst.afw.table.ReferenceMatch`]
681 Reference/stars matches used in the fit.
682 photoCalib : `lsst.afw.image.PhotoCalib`
683 Photometric calibration that was fit to the star catalog.
684 """
685 result = self.photometry.run(exposure, stars)
686 calibrated_stars = result.photoCalib.calibrateCatalog(stars)
687 exposure.maskedImage = result.photoCalib.calibrateImage(exposure.maskedImage)
688 identity = afwImage.PhotoCalib(1.0,
689 result.photoCalib.getCalibrationErr(),
690 bbox=exposure.getBBox())
691 exposure.setPhotoCalib(identity)
693 return calibrated_stars, result.matches, result.matchMeta, result.photoCalib
695 def _summarize(self, exposure, stars, background):
696 """Compute summary statistics on the exposure and update in-place the
697 calibrations attached to it.
699 Parameters
700 ----------
701 exposure : `lsst.afw.image.Exposure`
702 Exposure that was calibrated, to get PSF and other metadata from.
703 Modified to contain the computed summary statistics.
704 stars : `SourceCatalog`
705 Good stars selected used in calibration.
706 background : `lsst.afw.math.BackgroundList`
707 Background that was fit to the exposure during detection of the
708 above stars.
709 """
710 # TODO investigation: because this takes the photoCalib from the
711 # exposure, photometric summary values may be "incorrect" (i.e. they
712 # will reflect the ==1 nJy calibration on the exposure, not the
713 # applied calibration). This needs to be checked.
714 summary = self.compute_summary_stats.run(exposure, stars, background)
715 exposure.info.setSummaryStats(summary)