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