Loading [MathJax]/extensions/tex2jax.js
LSST Applications g0fba68d861+aff6b1d179,g1e78f5e6d3+b2e1eec3e3,g1fd858c14a+eb8d917efb,g35bb328faa+fcb1d3bbc8,g436fd98eb5+de86862952,g4af146b050+2f70285269,g4d2262a081+c17cfe15e3,g4e0f332c67+8616b824a5,g53246c7159+fcb1d3bbc8,g5a012ec0e7+d65fd7031a,g60b5630c4e+de86862952,g67b6fd64d1+c0248a1c13,g78460c75b0+2f9a1b4bcd,g786e29fd12+cf7ec2a62a,g7b71ed6315+fcb1d3bbc8,g87b7deb4dc+1a71b41694,g8852436030+40f6ec51d1,g89139ef638+c0248a1c13,g9125e01d80+fcb1d3bbc8,g94187f82dc+de86862952,g989de1cb63+c0248a1c13,g9f33ca652e+62adb22cd2,g9f7030ddb1+d892b2cb3e,ga2b97cdc51+de86862952,gabe3b4be73+1e0a283bba,gabf8522325+83c19109ce,gb1101e3267+1371da34ff,gb58c049af0+f03b321e39,gb89ab40317+c0248a1c13,gcf25f946ba+40f6ec51d1,gd6cbbdb0b4+d9e8db455e,gd9a9a58781+fcb1d3bbc8,gde0f65d7ad+fc726a16be,gded526ad44+763ef31e97,ge278dab8ac+4ce6343b44,ge410e46f29+c0248a1c13,gf67bdafdda+c0248a1c13,gfe06eef73a+95f9f0e40c,v29.0.0.rc3
LSST Data Management Base Package
All Classes Namespaces Files Functions Variables Typedefs Enumerations Enumerator Properties Friends Macros Modules Pages
calibrateImage.py
Go to the documentation of this file.
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/>.
21
22__all__ = ["CalibrateImageTask", "CalibrateImageConfig", "NoPsfStarsToStarsMatchError"]
23
24import numpy as np
25
26import lsst.afw.table as afwTable
27import lsst.afw.image as afwImage
28from lsst.ip.diffim.utils import evaluateMaskFraction
33import lsst.meas.base
36import lsst.meas.extensions.shapeHSM
37import lsst.pex.config as pexConfig
38import lsst.pipe.base as pipeBase
39from lsst.pipe.base import connectionTypes
40from lsst.utils.timer import timeMethod
41
42from . import measurePsf, repair, photoCal, computeExposureSummaryStats, snapCombine
43
44
45class NoPsfStarsToStarsMatchError(pipeBase.AlgorithmError):
46 """Raised when there are no matches between the psf_stars and stars
47 catalogs.
48 """
49 def __init__(self, *, n_psf_stars, n_stars):
50 msg = (f"No psf stars out of {n_psf_stars} matched {n_stars} calib stars."
51 " Downstream processes probably won't have useful {calib_type} stars in this case."
52 " Is `star_selector` too strict or is this a bad image?")
53 super().__init__(msg)
54 self.n_psf_stars = n_psf_stars
55 self.n_stars = n_stars
56
57 @property
58 def metadata(self):
59 return {"n_psf_stars": self.n_psf_stars,
60 "n_stars": self.n_stars
61 }
62
63
64class CalibrateImageConnections(pipeBase.PipelineTaskConnections,
65 dimensions=("instrument", "visit", "detector")):
66
67 astrometry_ref_cat = connectionTypes.PrerequisiteInput(
68 doc="Reference catalog to use for astrometric calibration.",
69 name="gaia_dr3_20230707",
70 storageClass="SimpleCatalog",
71 dimensions=("skypix",),
72 deferLoad=True,
73 multiple=True,
74 )
75 photometry_ref_cat = connectionTypes.PrerequisiteInput(
76 doc="Reference catalog to use for photometric calibration.",
77 name="ps1_pv3_3pi_20170110",
78 storageClass="SimpleCatalog",
79 dimensions=("skypix",),
80 deferLoad=True,
81 multiple=True
82 )
83
84 exposures = connectionTypes.Input(
85 doc="Exposure (or two snaps) to be calibrated, and detected and measured on.",
86 name="postISRCCD",
87 storageClass="Exposure",
88 multiple=True, # to handle 1 exposure or 2 snaps
89 dimensions=["instrument", "exposure", "detector"],
90 )
91
92 background_flat = connectionTypes.PrerequisiteInput(
93 name="flat",
94 doc="Flat calibration frame used for background correction.",
95 storageClass="ExposureF",
96 dimensions=["instrument", "detector", "physical_filter"],
97 isCalibration=True,
98 )
99 illumination_correction = connectionTypes.PrerequisiteInput(
100 name="illuminationCorrection",
101 doc="Illumination correction frame.",
102 storageClass="Exposure",
103 dimensions=["instrument", "detector", "physical_filter"],
104 isCalibration=True,
105 )
106
107 # outputs
108 initial_stars_schema = connectionTypes.InitOutput(
109 doc="Schema of the output initial stars catalog.",
110 name="initial_stars_schema",
111 storageClass="SourceCatalog",
112 )
113
114 # TODO DM-38732: We want some kind of flag on Exposures/Catalogs to make
115 # it obvious which components had failed to be computed/persisted.
116 exposure = connectionTypes.Output(
117 doc="Photometrically calibrated, background-subtracted exposure with fitted calibrations and "
118 "summary statistics. To recover the original exposure, first add the background "
119 "(`initial_pvi_background`), and then uncalibrate (divide by `initial_photoCalib_detector`).",
120 name="initial_pvi",
121 storageClass="ExposureF",
122 dimensions=("instrument", "visit", "detector"),
123 )
124 stars = connectionTypes.Output(
125 doc="Catalog of unresolved sources detected on the calibrated exposure.",
126 name="initial_stars_detector",
127 storageClass="ArrowAstropy",
128 dimensions=["instrument", "visit", "detector"],
129 )
130 stars_footprints = connectionTypes.Output(
131 doc="Catalog of unresolved sources detected on the calibrated exposure; "
132 "includes source footprints.",
133 name="initial_stars_footprints_detector",
134 storageClass="SourceCatalog",
135 dimensions=["instrument", "visit", "detector"],
136 )
137 applied_photo_calib = connectionTypes.Output(
138 doc=(
139 "Photometric calibration that was applied to exposure's pixels. "
140 "This connection is disabled when do_calibrate_pixels=False."
141 ),
142 name="initial_photoCalib_detector",
143 storageClass="PhotoCalib",
144 dimensions=("instrument", "visit", "detector"),
145 )
146 background = connectionTypes.Output(
147 doc="Background models estimated during calibration task; calibrated to be in nJy units.",
148 name="initial_pvi_background",
149 storageClass="Background",
150 dimensions=("instrument", "visit", "detector"),
151 )
152 background_to_photometric_ratio = connectionTypes.Output(
153 doc="Ratio of a background-flattened image to a photometric-flattened image. Only persisted "
154 "if do_illumination_correction is True.",
155 name="background_to_photometric_ratio",
156 storageClass="Image",
157 dimensions=("instrument", "visit", "detector"),
158 )
159
160 # Optional outputs
161 psf_stars_footprints = connectionTypes.Output(
162 doc="Catalog of bright unresolved sources detected on the exposure used for PSF determination; "
163 "includes source footprints.",
164 name="initial_psf_stars_footprints_detector",
165 storageClass="SourceCatalog",
166 dimensions=["instrument", "visit", "detector"],
167 )
168 psf_stars = connectionTypes.Output(
169 doc="Catalog of bright unresolved sources detected on the exposure used for PSF determination.",
170 name="initial_psf_stars_detector",
171 storageClass="ArrowAstropy",
172 dimensions=["instrument", "visit", "detector"],
173 )
174 astrometry_matches = connectionTypes.Output(
175 doc="Source to reference catalog matches from the astrometry solver.",
176 name="initial_astrometry_match_detector",
177 storageClass="Catalog",
178 dimensions=("instrument", "visit", "detector"),
179 )
180 photometry_matches = connectionTypes.Output(
181 doc="Source to reference catalog matches from the photometry solver.",
182 name="initial_photometry_match_detector",
183 storageClass="Catalog",
184 dimensions=("instrument", "visit", "detector"),
185 )
186
187 def __init__(self, *, config=None):
188 super().__init__(config=config)
189 if "psf_stars" not in config.optional_outputs:
190 del self.psf_stars
191 if "psf_stars_footprints" not in config.optional_outputs:
192 del self.psf_stars_footprints
193 if "astrometry_matches" not in config.optional_outputs:
194 del self.astrometry_matches
195 if "photometry_matches" not in config.optional_outputs:
196 del self.photometry_matches
197 if not config.do_calibrate_pixels:
198 del self.applied_photo_calib
199 if not config.do_illumination_correction:
200 del self.background_flat
203
204
205class CalibrateImageConfig(pipeBase.PipelineTaskConfig, pipelineConnections=CalibrateImageConnections):
206 optional_outputs = pexConfig.ListField(
207 doc="Which optional outputs to save (as their connection name)?",
208 dtype=str,
209 # TODO: note somewhere to disable this for benchmarking, but should
210 # we always have it on for production runs?
211 default=["psf_stars", "psf_stars_footprints", "astrometry_matches", "photometry_matches"],
212 optional=False
213 )
214
215 # To generate catalog ids consistently across subtasks.
216 id_generator = lsst.meas.base.DetectorVisitIdGeneratorConfig.make_field()
217
218 snap_combine = pexConfig.ConfigurableField(
220 doc="Task to combine two snaps to make one exposure.",
221 )
222
223 # subtasks used during psf characterization
224 install_simple_psf = pexConfig.ConfigurableField(
226 doc="Task to install a simple PSF model into the input exposure to use "
227 "when detecting bright sources for PSF estimation.",
228 )
229 psf_repair = pexConfig.ConfigurableField(
230 target=repair.RepairTask,
231 doc="Task to repair cosmic rays on the exposure before PSF determination.",
232 )
233 psf_subtract_background = pexConfig.ConfigurableField(
235 doc="Task to perform intial background subtraction, before first detection pass.",
236 )
237 psf_detection = pexConfig.ConfigurableField(
239 doc="Task to detect sources for PSF determination."
240 )
241 psf_source_measurement = pexConfig.ConfigurableField(
243 doc="Task to measure sources to be used for psf estimation."
244 )
245 psf_measure_psf = pexConfig.ConfigurableField(
247 doc="Task to measure the psf on bright sources."
248 )
249 psf_normalized_calibration_flux = pexConfig.ConfigurableField(
251 doc="Task to normalize the calibration flux (e.g. compensated tophats) "
252 "for the bright stars used for psf estimation.",
253 )
254
255 # TODO DM-39203: we can remove aperture correction from this task once we are
256 # using the shape-based star/galaxy code.
257 measure_aperture_correction = pexConfig.ConfigurableField(
259 doc="Task to compute the aperture correction from the bright stars."
260 )
261
262 # subtasks used during star measurement
263 star_detection = pexConfig.ConfigurableField(
265 doc="Task to detect stars to return in the output catalog."
266 )
267 star_sky_sources = pexConfig.ConfigurableField(
269 doc="Task to generate sky sources ('empty' regions where there are no detections).",
270 )
271 star_deblend = pexConfig.ConfigurableField(
273 doc="Split blended sources into their components."
274 )
275 star_measurement = pexConfig.ConfigurableField(
277 doc="Task to measure stars to return in the output catalog."
278 )
279 star_normalized_calibration_flux = pexConfig.ConfigurableField(
281 doc="Task to apply the normalization for calibration fluxes (e.g. compensated tophats) "
282 "for the final output star catalog.",
283 )
284 star_apply_aperture_correction = pexConfig.ConfigurableField(
286 doc="Task to apply aperture corrections to the selected stars."
287 )
288 star_catalog_calculation = pexConfig.ConfigurableField(
290 doc="Task to compute extendedness values on the star catalog, "
291 "for the star selector to remove extended sources."
292 )
293 star_set_primary_flags = pexConfig.ConfigurableField(
295 doc="Task to add isPrimary to the catalog."
296 )
297 star_selector = lsst.meas.algorithms.sourceSelectorRegistry.makeField(
298 default="science",
299 doc="Task to select reliable stars to use for calibration."
300 )
301
302 # final calibrations and statistics
303 astrometry = pexConfig.ConfigurableField(
305 doc="Task to perform astrometric calibration to fit a WCS.",
306 )
307 astrometry_ref_loader = pexConfig.ConfigField(
309 doc="Configuration of reference object loader for astrometric fit.",
310 )
311 photometry = pexConfig.ConfigurableField(
313 doc="Task to perform photometric calibration to fit a PhotoCalib.",
314 )
315 photometry_ref_loader = pexConfig.ConfigField(
317 doc="Configuration of reference object loader for photometric fit.",
318 )
319
320 compute_summary_stats = pexConfig.ConfigurableField(
322 doc="Task to to compute summary statistics on the calibrated exposure."
323 )
324
325 do_illumination_correction = pexConfig.Field(
326 dtype=bool,
327 default=False,
328 doc="If True, apply the illumination correction. This assumes that the "
329 "input image has already been flat-fielded such that it is suitable "
330 "for background subtraction.",
331 )
332
333 do_calibrate_pixels = pexConfig.Field(
334 dtype=bool,
335 default=True,
336 doc=(
337 "If True, apply the photometric calibration to the image pixels "
338 "and background model, and attach an identity PhotoCalib to "
339 "the output image to reflect this. If False`, leave the image "
340 "and background uncalibrated and attach the PhotoCalib that maps "
341 "them to physical units."
342 )
343 )
344
345 def setDefaults(self):
346 super().setDefaults()
347
348 # Use a very broad PSF here, to throughly reject CRs.
349 # TODO investigation: a large initial psf guess may make stars look
350 # like CRs for very good seeing images.
351 self.install_simple_psf.fwhm = 4
352
353 # S/N>=50 sources for PSF determination, but detection to S/N=10.
354 # The thresholdValue sets the minimum flux in a pixel to be included in the
355 # footprint, while peaks are only detected when they are above
356 # thresholdValue * includeThresholdMultiplier. The low thresholdValue
357 # ensures that the footprints are large enough for the noise replacer
358 # to mask out faint undetected neighbors that are not to be measured.
359 self.psf_detection.thresholdValue = 10.0
360 self.psf_detection.includeThresholdMultiplier = 5.0
361 # TODO investigation: Probably want False here, but that may require
362 # tweaking the background spatial scale, to make it small enough to
363 # prevent extra peaks in the wings of bright objects.
364 self.psf_detection.doTempLocalBackground = False
365 # NOTE: we do want reEstimateBackground=True in psf_detection, so that
366 # each measurement step is done with the best background available.
367
368 # Minimal measurement plugins for PSF determination.
369 # TODO DM-39203: We can drop GaussianFlux and PsfFlux, if we use
370 # shapeHSM/moments for star/galaxy separation.
371 # TODO DM-39203: we can remove aperture correction from this task once
372 # we are using the shape-based star/galaxy code.
373 self.psf_source_measurement.plugins = ["base_PixelFlags",
374 "base_SdssCentroid",
375 "ext_shapeHSM_HsmSourceMoments",
376 "base_CircularApertureFlux",
377 "base_GaussianFlux",
378 "base_PsfFlux",
379 "base_CompensatedTophatFlux",
380 ]
381 self.psf_source_measurement.slots.shape = "ext_shapeHSM_HsmSourceMoments"
382 # Only measure apertures we need for PSF measurement.
383 self.psf_source_measurement.plugins["base_CircularApertureFlux"].radii = [12.0]
384 self.psf_source_measurement.plugins["base_CompensatedTophatFlux"].apertures = [12]
385 # TODO DM-40843: Remove this line once this is the psfex default.
386 self.psf_measure_psf.psfDeterminer["psfex"].photometricFluxField = \
387 "base_CircularApertureFlux_12_0_instFlux"
388
389 # No extendeness information available: we need the aperture
390 # corrections to determine that.
391 self.measure_aperture_correction.sourceSelector["science"].doUnresolved = False
392 self.measure_aperture_correction.sourceSelector["science"].flags.good = ["calib_psf_used"]
393 self.measure_aperture_correction.sourceSelector["science"].flags.bad = []
394
395 # Detection for good S/N for astrometry/photometry and other
396 # downstream tasks; detection mask to S/N>=5, but S/N>=10 peaks.
397 self.star_detection.thresholdValue = 5.0
398 self.star_detection.includeThresholdMultiplier = 2.0
399 self.star_measurement.plugins = ["base_PixelFlags",
400 "base_SdssCentroid",
401 "ext_shapeHSM_HsmSourceMoments",
402 "ext_shapeHSM_HsmPsfMoments",
403 "base_GaussianFlux",
404 "base_PsfFlux",
405 "base_CircularApertureFlux",
406 "base_ClassificationSizeExtendedness",
407 "base_CompensatedTophatFlux",
408 ]
409 self.star_measurement.slots.psfShape = "ext_shapeHSM_HsmPsfMoments"
410 self.star_measurement.slots.shape = "ext_shapeHSM_HsmSourceMoments"
411 # Only measure the apertures we need for star selection.
412 self.star_measurement.plugins["base_CircularApertureFlux"].radii = [12.0]
413 self.star_measurement.plugins["base_CompensatedTophatFlux"].apertures = [12]
414
415 # We measure and apply the normalization aperture correction with the
416 # psf_normalized_calibration_flux task, and we only apply the normalization
417 # aperture correction for the full list of stars.
418 self.star_normalized_calibration_flux.do_measure_ap_corr = False
419
420 # Select stars with reliable measurements and no bad flags.
421 self.star_selector["science"].doFlags = True
422 self.star_selector["science"].doUnresolved = True
423 self.star_selector["science"].doSignalToNoise = True
424 self.star_selector["science"].signalToNoise.minimum = 10.0
425 # Keep sky sources in the output catalog, even though they aren't
426 # wanted for calibration.
427 self.star_selector["science"].doSkySources = True
428 # Set the flux and error fields
429 self.star_selector["science"].signalToNoise.fluxField = "slot_CalibFlux_instFlux"
430 self.star_selector["science"].signalToNoise.errField = "slot_CalibFlux_instFluxErr"
431
432 # Use the affine WCS fitter (assumes we have a good camera geometry).
433 self.astrometry.wcsFitter.retarget(lsst.meas.astrom.FitAffineWcsTask)
434 # phot_g_mean is the primary Gaia band for all input bands.
435 self.astrometry_ref_loader.anyFilterMapsToThis = "phot_g_mean"
436
437 # Only reject sky sources; we already selected good stars.
438 self.astrometry.sourceSelector["science"].doFlags = True
439 self.astrometry.sourceSelector["science"].flags.good = ["calib_psf_candidate"]
440 self.astrometry.sourceSelector["science"].flags.bad = []
441 self.astrometry.sourceSelector["science"].doUnresolved = False
442 self.astrometry.sourceSelector["science"].doIsolated = False
443 self.astrometry.sourceSelector["science"].doRequirePrimary = False
444 self.photometry.match.sourceSelection.doFlags = True
445 self.photometry.match.sourceSelection.flags.bad = ["sky_source"]
446 # Unset the (otherwise reasonable, but we've already made the
447 # selections we want above) selection settings in PhotoCalTask.
448 self.photometry.match.sourceSelection.doRequirePrimary = False
449 self.photometry.match.sourceSelection.doUnresolved = False
450
451 def validate(self):
452 super().validate()
453
454 # Ensure that the normalization calibration flux tasks
455 # are configured correctly.
456 if not self.psf_normalized_calibration_flux.do_measure_ap_corr:
457 msg = ("psf_normalized_calibration_flux task must be configured with do_measure_ap_corr=True "
458 "or else the normalization and calibration flux will not be properly measured.")
459 raise pexConfig.FieldValidationError(
460 CalibrateImageConfig.psf_normalized_calibration_flux, self, msg,
461 )
462 if self.star_normalized_calibration_flux.do_measure_ap_corr:
463 msg = ("star_normalized_calibration_flux task must be configured with do_measure_ap_corr=False "
464 "to apply the previously measured normalization to the full catalog of calibration "
465 "fluxes.")
466 raise pexConfig.FieldValidationError(
467 CalibrateImageConfig.star_normalized_calibration_flux, self, msg,
468 )
469
470 # Ensure base_LocalPhotoCalib and base_LocalWcs plugins are not run,
471 # because they'd be running too early to pick up the fitted PhotoCalib
472 # and WCS.
473 if "base_LocalWcs" in self.psf_source_measurement.plugins.names:
474 raise pexConfig.FieldValidationError(
475 CalibrateImageConfig.psf_source_measurement,
476 self,
477 "base_LocalWcs cannot run CalibrateImageTask, as it would be run before the astrometry fit."
478 )
479 if "base_LocalWcs" in self.star_measurement.plugins.names:
480 raise pexConfig.FieldValidationError(
481 CalibrateImageConfig.star_measurement,
482 self,
483 "base_LocalWcs cannot run CalibrateImageTask, as it would be run before the astrometry fit."
484 )
485 if "base_LocalPhotoCalib" in self.psf_source_measurement.plugins.names:
486 raise pexConfig.FieldValidationError(
487 CalibrateImageConfig.psf_source_measurement,
488 self,
489 "base_LocalPhotoCalib cannot run CalibrateImageTask, "
490 "as it would be run before the photometry fit."
491 )
492 if "base_LocalPhotoCalib" in self.star_measurement.plugins.names:
493 raise pexConfig.FieldValidationError(
494 CalibrateImageConfig.star_measurement,
495 self,
496 "base_LocalPhotoCalib cannot run CalibrateImageTask, "
497 "as it would be run before the photometry fit."
498 )
499
500 # Check for illumination correction and background consistency.
502 if not self.psf_subtract_background.doApplyFlatBackgroundRatio:
503 raise pexConfig.FieldValidationError(
504 CalibrateImageConfig.psf_subtract_background,
505 self,
506 "CalibrateImageTask.psf_subtract_background must be configured with "
507 "doApplyFlatBackgroundRatio=True if do_illumination_correction=True."
508 )
509 if self.psf_detection.reEstimateBackground:
510 if not self.psf_detection.doApplyFlatBackgroundRatio:
511 raise pexConfig.FieldValidationError(
512 CalibrateImageConfig.psf_detection,
513 self,
514 "CalibrateImageTask.psf_detection background must be configured with "
515 "doApplyFlatBackgroundRatio=True if do_illumination_correction=True."
516 )
517 if self.star_detection.reEstimateBackground:
518 if not self.star_detection.doApplyFlatBackgroundRatio:
519 raise pexConfig.FieldValidationError(
520 CalibrateImageConfig.star_detection,
521 self,
522 "CalibrateImageTask.star_detection background must be configured with "
523 "doApplyFlatBackgroundRatio=True if do_illumination_correction=True."
524 )
525
526
527class CalibrateImageTask(pipeBase.PipelineTask):
528 """Compute the PSF, aperture corrections, astrometric and photometric
529 calibrations, and summary statistics for a single science exposure, and
530 produce a catalog of brighter stars that were used to calibrate it.
531
532 Parameters
533 ----------
534 initial_stars_schema : `lsst.afw.table.Schema`
535 Schema of the initial_stars output catalog.
536 """
537 _DefaultName = "calibrateImage"
538 ConfigClass = CalibrateImageConfig
539
540 def __init__(self, initial_stars_schema=None, **kwargs):
541 super().__init__(**kwargs)
542 self.makeSubtask("snap_combine")
543
544 # PSF determination subtasks
545 self.makeSubtask("install_simple_psf")
546 self.makeSubtask("psf_repair")
547 self.makeSubtask("psf_subtract_background")
548 self.psf_schema = afwTable.SourceTable.makeMinimalSchema()
549 afwTable.CoordKey.addErrorFields(self.psf_schema)
550 self.makeSubtask("psf_detection", schema=self.psf_schema)
551 self.makeSubtask("psf_source_measurement", schema=self.psf_schema)
552 self.makeSubtask("psf_measure_psf", schema=self.psf_schema)
553 self.makeSubtask("psf_normalized_calibration_flux", schema=self.psf_schema)
554
555 self.makeSubtask("measure_aperture_correction", schema=self.psf_schema)
556 self.makeSubtask("astrometry", schema=self.psf_schema)
557
558 # star measurement subtasks
559 if initial_stars_schema is None:
560 initial_stars_schema = afwTable.SourceTable.makeMinimalSchema()
561
562 # These fields let us track which sources were used for psf modeling,
563 # astrometric fitting, and aperture correction calculations.
564 self.psf_fields = ("calib_psf_candidate", "calib_psf_used", "calib_psf_reserved",
565 "calib_astrometry_used",
566 # TODO DM-39203: these can be removed once apcorr is gone.
567 "apcorr_slot_CalibFlux_used", "apcorr_base_GaussianFlux_used",
568 "apcorr_base_PsfFlux_used")
569 for field in self.psf_fields:
570 item = self.psf_schema.find(field)
571 initial_stars_schema.addField(item.getField())
572 id_type = self.psf_schema["id"].asField().getTypeString()
573 initial_stars_schema.addField("psf_id",
574 type=id_type,
575 doc="id of this source in psf_stars; 0 if there is no match.")
576
577 afwTable.CoordKey.addErrorFields(initial_stars_schema)
578 self.makeSubtask("star_detection", schema=initial_stars_schema)
579 self.makeSubtask("star_sky_sources", schema=initial_stars_schema)
580 self.makeSubtask("star_deblend", schema=initial_stars_schema)
581 self.makeSubtask("star_measurement", schema=initial_stars_schema)
582 self.makeSubtask("star_normalized_calibration_flux", schema=initial_stars_schema)
583
584 self.makeSubtask("star_apply_aperture_correction", schema=initial_stars_schema)
585 self.makeSubtask("star_catalog_calculation", schema=initial_stars_schema)
586 self.makeSubtask("star_set_primary_flags", schema=initial_stars_schema, isSingleFrame=True)
587 self.makeSubtask("star_selector")
588 self.makeSubtask("photometry", schema=initial_stars_schema)
589 self.makeSubtask("compute_summary_stats")
590
591 # The final catalog will have calibrated flux columns, which we add to
592 # the init-output schema by calibrating our zero-length catalog with an
593 # arbitrary dummy PhotoCalib. We also use this schema to initialze
594 # the stars catalog in order to ensure it's the same even when we hit
595 # an error (and write partial outputs) before calibrating the catalog
596 # - note that calibrateCatalog will happily reuse existing output
597 # columns.
598 dummy_photo_calib = afwImage.PhotoCalib(1.0, 0, bbox=lsst.geom.Box2I())
599 self.initial_stars_schema = dummy_photo_calib.calibrateCatalog(
600 afwTable.SourceCatalog(initial_stars_schema)
601 )
602
603 def runQuantum(self, butlerQC, inputRefs, outputRefs):
604 inputs = butlerQC.get(inputRefs)
605 exposures = inputs.pop("exposures")
606
607 id_generator = self.config.id_generator.apply(butlerQC.quantum.dataId)
608
610 dataIds=[ref.datasetRef.dataId for ref in inputRefs.astrometry_ref_cat],
611 refCats=inputs.pop("astrometry_ref_cat"),
612 name=self.config.connections.astrometry_ref_cat,
613 config=self.config.astrometry_ref_loader, log=self.log)
614 self.astrometry.setRefObjLoader(astrometry_loader)
615
617 dataIds=[ref.datasetRef.dataId for ref in inputRefs.photometry_ref_cat],
618 refCats=inputs.pop("photometry_ref_cat"),
619 name=self.config.connections.photometry_ref_cat,
620 config=self.config.photometry_ref_loader, log=self.log)
621 self.photometry.match.setRefObjLoader(photometry_loader)
622
623 if self.config.do_illumination_correction:
624 background_flat = inputs.pop("background_flat")
625 illumination_correction = inputs.pop("illumination_correction")
626 else:
627 background_flat = None
628 illumination_correction = None
629
630 # This should not happen with a properly configured execution context.
631 assert not inputs, "runQuantum got more inputs than expected"
632
633 # Specify the fields that `annotate` needs below, to ensure they
634 # exist, even as None.
635 result = pipeBase.Struct(
636 exposure=None,
637 stars_footprints=None,
638 psf_stars_footprints=None,
639 background_to_photometric_ratio=None,
640 )
641 try:
642 self.run(
643 exposures=exposures,
644 result=result,
645 id_generator=id_generator,
646 background_flat=background_flat,
647 illumination_correction=illumination_correction,
648 )
649 except pipeBase.AlgorithmError as e:
650 error = pipeBase.AnnotatedPartialOutputsError.annotate(
651 e,
652 self,
653 result.exposure,
654 result.psf_stars_footprints,
655 result.stars_footprints,
656 log=self.log
657 )
658 butlerQC.put(result, outputRefs)
659 raise error from e
660
661 butlerQC.put(result, outputRefs)
662
663 @timeMethod
664 def run(
665 self,
666 *,
667 exposures,
668 id_generator=None,
669 result=None,
670 background_flat=None,
671 illumination_correction=None,
672 ):
673 """Find stars and perform psf measurement, then do a deeper detection
674 and measurement and calibrate astrometry and photometry from that.
675
676 Parameters
677 ----------
678 exposures : `lsst.afw.image.Exposure` or `list` [`lsst.afw.image.Exposure`]
679 Post-ISR exposure(s), with an initial WCS, VisitInfo, and Filter.
680 Modified in-place during processing if only one is passed.
681 If two exposures are passed, treat them as snaps and combine
682 before doing further processing.
683 id_generator : `lsst.meas.base.IdGenerator`, optional
684 Object that generates source IDs and provides random seeds.
685 result : `lsst.pipe.base.Struct`, optional
686 Result struct that is modified to allow saving of partial outputs
687 for some failure conditions. If the task completes successfully,
688 this is also returned.
689 background_flat : `lsst.afw.image.Exposure`, optional
690 Background flat-field image.
691 illumination_correction : `lsst.afw.image.Exposure`, optional
692 Illumination correction image.
693
694 Returns
695 -------
696 result : `lsst.pipe.base.Struct`
697 Results as a struct with attributes:
698
699 ``exposure``
700 Calibrated exposure, with pixels in nJy units.
701 (`lsst.afw.image.Exposure`)
702 ``stars``
703 Stars that were used to calibrate the exposure, with
704 calibrated fluxes and magnitudes.
705 (`astropy.table.Table`)
706 ``stars_footprints``
707 Footprints of stars that were used to calibrate the exposure.
708 (`lsst.afw.table.SourceCatalog`)
709 ``psf_stars``
710 Stars that were used to determine the image PSF.
711 (`astropy.table.Table`)
712 ``psf_stars_footprints``
713 Footprints of stars that were used to determine the image PSF.
714 (`lsst.afw.table.SourceCatalog`)
715 ``background``
716 Background that was fit to the exposure when detecting
717 ``stars``. (`lsst.afw.math.BackgroundList`)
718 ``applied_photo_calib``
719 Photometric calibration that was fit to the star catalog and
720 applied to the exposure. (`lsst.afw.image.PhotoCalib`)
721 This is `None` if ``config.do_calibrate_pixels`` is `False`.
722 ``astrometry_matches``
723 Reference catalog stars matches used in the astrometric fit.
724 (`list` [`lsst.afw.table.ReferenceMatch`] or `lsst.afw.table.BaseCatalog`)
725 ``photometry_matches``
726 Reference catalog stars matches used in the photometric fit.
727 (`list` [`lsst.afw.table.ReferenceMatch`] or `lsst.afw.table.BaseCatalog`)
728 """
729 if result is None:
730 result = pipeBase.Struct()
731 if id_generator is None:
732 id_generator = lsst.meas.base.IdGenerator()
733
734 result.exposure = self.snap_combine.run(exposures).exposure
735 self._recordMaskedPixelFractions(result.exposure)
736 self.log.info("Initial PhotoCalib: %s", result.exposure.getPhotoCalib())
737
738 result.exposure.metadata["LSST CALIB ILLUMCORR APPLIED"] = False
739
740 # Check input image processing.
741 if self.config.do_illumination_correction:
742 if not result.exposure.metadata.get("LSST ISR FLAT APPLIED", False):
743 raise pipeBase.InvalidQuantumError(
744 "Cannot use do_illumination_correction with an image that has not had a flat applied",
745 )
746
747 result.background = None
748 summary_stat_catalog = None
749 # Some exposure components are set to initial placeholder objects
750 # while we try to bootstrap them. If we fail before we fit for them,
751 # we want to reset those components to None so the placeholders don't
752 # masquerade as the real thing.
753 have_fit_psf = False
754 have_fit_astrometry = False
755 have_fit_photometry = False
756 try:
757 result.background_to_photometric_ratio = self._apply_illumination_correction(
758 result.exposure,
759 background_flat,
760 illumination_correction,
761 )
762
763 result.psf_stars_footprints, result.background, _ = self._compute_psf(
764 result.exposure,
765 id_generator,
766 background_to_photometric_ratio=result.background_to_photometric_ratio,
767 )
768 have_fit_psf = True
769 self._measure_aperture_correction(result.exposure, result.psf_stars_footprints)
770 result.psf_stars = result.psf_stars_footprints.asAstropy()
771 # Run astrometry using PSF candidate stars
772 astrometry_matches, astrometry_meta = self._fit_astrometry(
773 result.exposure, result.psf_stars_footprints
774 )
775 self.metadata["astrometry_matches_count"] = len(astrometry_matches)
776 if "astrometry_matches" in self.config.optional_outputs:
777 result.astrometry_matches = lsst.meas.astrom.denormalizeMatches(astrometry_matches,
778 astrometry_meta)
779 result.psf_stars = result.psf_stars_footprints.asAstropy()
780
781 # Run the stars_detection subtask for the photometric calibration.
782 result.stars_footprints = self._find_stars(
783 result.exposure,
784 result.background,
785 id_generator,
786 background_to_photometric_ratio=result.background_to_photometric_ratio,
787 )
788 self._match_psf_stars(result.psf_stars_footprints, result.stars_footprints)
789
790 # Update the source cooordinates with the current wcs.
791 afwTable.updateSourceCoords(result.exposure.wcs, sourceList=result.stars_footprints)
792
793 summary_stat_catalog = result.stars_footprints
794 result.stars = result.stars_footprints.asAstropy()
795 self.metadata["star_count"] = np.sum(~result.stars["sky_source"])
796
797 # Validate the astrometric fit. Send in the stars_footprints
798 # catalog so that its coords get set to NaN if the fit is deemed
799 # a failure.
800 self.astrometry.check(result.exposure, result.stars_footprints, len(astrometry_matches))
801 result.stars = result.stars_footprints.asAstropy()
802 have_fit_astrometry = True
803
804 result.stars_footprints, photometry_matches, \
805 photometry_meta, photo_calib = self._fit_photometry(result.exposure, result.stars_footprints)
806 have_fit_photometry = True
807 self.metadata["photometry_matches_count"] = len(photometry_matches)
808 # fit_photometry returns a new catalog, so we need a new astropy table view.
809 result.stars = result.stars_footprints.asAstropy()
810 # summary stats don't make use of the calibrated fluxes, but we
811 # might as well use the best catalog we've got in case that
812 # changes, and help the old one get garbage-collected.
813 summary_stat_catalog = result.stars_footprints
814 if "photometry_matches" in self.config.optional_outputs:
815 result.photometry_matches = lsst.meas.astrom.denormalizeMatches(photometry_matches,
816 photometry_meta)
817 except pipeBase.AlgorithmError:
818 if not have_fit_psf:
819 result.exposure.setPsf(None)
820 if not have_fit_astrometry:
821 result.exposure.setWcs(None)
822 if not have_fit_photometry:
823 result.exposure.setPhotoCalib(None)
824 # Summary stat calculations can handle missing components gracefully,
825 # but we want to run them as late as possible (but still before we
826 # calibrate pixels, if we do that at all).
827 # So we run them after we succeed or if we get an AlgorithmError. We
828 # intentionally don't use 'finally' here because we don't want to run
829 # them if we get some other kind of error.
830 self._summarize(result.exposure, summary_stat_catalog, result.background)
831 raise
832 else:
833 self._summarize(result.exposure, summary_stat_catalog, result.background)
834
835 if self.config.do_calibrate_pixels:
837 result.exposure,
838 result.background,
839 background_to_photometric_ratio=result.background_to_photometric_ratio,
840 )
841 result.applied_photo_calib = photo_calib
842 else:
843 result.applied_photo_calib = None
844 return result
845
846 def _apply_illumination_correction(self, exposure, background_flat, illumination_correction):
847 """Apply the illumination correction to a background-flattened image.
848
849 Parameters
850 ----------
851 exposure : `lsst.afw.image.Exposure`
852 Exposure to convert to a photometric-flattened image.
853 background_flat : `lsst.afw.image.Exposure`
854 Flat image that had previously been applied to exposure.
855 illumination_correction : `lsst.afw.image.Exposure`
856 Illumination correction image to convert to photometric-flattened image.
857
858 Returns
859 -------
860 background_to_photometric_ratio : `lsst.afw.image.Image`
861 Ratio image to convert a photometric-flattened image to/from
862 a background-flattened image. Will be None if task not
863 configured to use the illumination correction.
864 """
865 if not self.config.do_illumination_correction:
866 return None
867
868 # From a raw image to a background-flattened image, we have:
869 # bfi = image / background_flat
870 # From a raw image to a photometric-flattened image, we have:
871 # pfi = image / reference_flux_flat
872 # pfi = image / (dome_flat * illumination_correction),
873 # where the illumination correction contains the jacobian
874 # of the wcs, converting to fluence units.
875 # Currently background_flat == dome_flat, so we have for the
876 # "background_to_photometric_ratio", the ratio of the background-
877 # flattened image to the photometric-flattened image:
878 # bfi / pfi = illumination_correction.
879
880 background_to_photometric_ratio = illumination_correction.image.clone()
881
882 # Dividing the ratio will convert a background-flattened image to
883 # a photometric-flattened image.
884 exposure.maskedImage /= background_to_photometric_ratio
885
886 exposure.metadata["LSST CALIB ILLUMCORR APPLIED"] = True
887
888 return background_to_photometric_ratio
889
890 def _compute_psf(self, exposure, id_generator, background_to_photometric_ratio=None):
891 """Find bright sources detected on an exposure and fit a PSF model to
892 them, repairing likely cosmic rays before detection.
893
894 Repair, detect, measure, and compute PSF twice, to ensure the PSF
895 model does not include contributions from cosmic rays.
896
897 Parameters
898 ----------
899 exposure : `lsst.afw.image.Exposure`
900 Exposure to detect and measure bright stars on.
901 id_generator : `lsst.meas.base.IdGenerator`
902 Object that generates source IDs and provides random seeds.
903 background_to_photometric_ratio : `lsst.afw.image.Image`, optional
904 Image to convert photometric-flattened image to
905 background-flattened image.
906
907 Returns
908 -------
909 sources : `lsst.afw.table.SourceCatalog`
910 Catalog of detected bright sources.
911 background : `lsst.afw.math.BackgroundList`
912 Background that was fit to the exposure during detection.
913 cell_set : `lsst.afw.math.SpatialCellSet`
914 PSF candidates returned by the psf determiner.
915 """
916 def log_psf(msg, addToMetadata=False):
917 """Log the parameters of the psf and background, with a prepended
918 message. There is also the option to add the PSF sigma to the task
919 metadata.
920
921 Parameters
922 ----------
923 msg : `str`
924 Message to prepend the log info with.
925 addToMetadata : `bool`, optional
926 Whether to add the final psf sigma value to the task metadata
927 (the default is False).
928 """
929 position = exposure.psf.getAveragePosition()
930 sigma = exposure.psf.computeShape(position).getDeterminantRadius()
931 dimensions = exposure.psf.computeImage(position).getDimensions()
932 median_background = np.median(background.getImage().array)
933 self.log.info("%s sigma=%0.4f, dimensions=%s; median background=%0.2f",
934 msg, sigma, dimensions, median_background)
935 if addToMetadata:
936 self.metadata["final_psf_sigma"] = sigma
937
938 self.log.info("First pass detection with Guassian PSF FWHM=%s pixels",
939 self.config.install_simple_psf.fwhm)
940 self.install_simple_psf.run(exposure=exposure)
941
942 background = self.psf_subtract_background.run(
943 exposure=exposure,
944 backgroundToPhotometricRatio=background_to_photometric_ratio,
945 ).background
946 log_psf("Initial PSF:")
947 self.psf_repair.run(exposure=exposure, keepCRs=True)
948
949 table = afwTable.SourceTable.make(self.psf_schema, id_generator.make_table_id_factory())
950 # Re-estimate the background during this detection step, so that
951 # measurement uses the most accurate background-subtraction.
952 detections = self.psf_detection.run(
953 table=table,
954 exposure=exposure,
955 background=background,
956 backgroundToPhotometricRatio=background_to_photometric_ratio,
957 )
958 self.metadata["initial_psf_positive_footprint_count"] = detections.numPos
959 self.metadata["initial_psf_negative_footprint_count"] = detections.numNeg
960 self.metadata["initial_psf_positive_peak_count"] = detections.numPosPeaks
961 self.metadata["initial_psf_negative_peak_count"] = detections.numNegPeaks
962 self.psf_source_measurement.run(detections.sources, exposure)
963 psf_result = self.psf_measure_psf.run(exposure=exposure, sources=detections.sources)
964 # Replace the initial PSF with something simpler for the second
965 # repair/detect/measure/measure_psf step: this can help it converge.
966 self.install_simple_psf.run(exposure=exposure)
967
968 log_psf("Rerunning with simple PSF:")
969 # TODO investigation: Should we only re-run repair here, to use the
970 # new PSF? Maybe we *do* need to re-run measurement with PsfFlux, to
971 # use the fitted PSF?
972 # TODO investigation: do we need a separate measurement task here
973 # for the post-psf_measure_psf step, since we only want to do PsfFlux
974 # and GaussianFlux *after* we have a PSF? Maybe that's not relevant
975 # once DM-39203 is merged?
976 self.psf_repair.run(exposure=exposure, keepCRs=True)
977 # Re-estimate the background during this detection step, so that
978 # measurement uses the most accurate background-subtraction.
979 detections = self.psf_detection.run(
980 table=table,
981 exposure=exposure,
982 background=background,
983 backgroundToPhotometricRatio=background_to_photometric_ratio,
984 )
985 self.metadata["simple_psf_positive_footprint_count"] = detections.numPos
986 self.metadata["simple_psf_negative_footprint_count"] = detections.numNeg
987 self.metadata["simple_psf_positive_peak_count"] = detections.numPosPeaks
988 self.metadata["simple_psf_negative_peak_count"] = detections.numNegPeaks
989 self.psf_source_measurement.run(detections.sources, exposure)
990 psf_result = self.psf_measure_psf.run(exposure=exposure, sources=detections.sources)
991
992 log_psf("Final PSF:", addToMetadata=True)
993
994 # Final repair with final PSF, removing cosmic rays this time.
995 self.psf_repair.run(exposure=exposure)
996 # Final measurement with the CRs removed.
997 self.psf_source_measurement.run(detections.sources, exposure)
998
999 # PSF is set on exposure; candidates are returned to use for
1000 # calibration flux normalization and aperture corrections.
1001 return detections.sources, background, psf_result.cellSet
1002
1003 def _measure_aperture_correction(self, exposure, bright_sources):
1004 """Measure and set the ApCorrMap on the Exposure, using
1005 previously-measured bright sources.
1006
1007 This function first normalizes the calibration flux and then
1008 the full set of aperture corrections are measured relative
1009 to this normalized calibration flux.
1010
1011 Parameters
1012 ----------
1013 exposure : `lsst.afw.image.Exposure`
1014 Exposure to set the ApCorrMap on.
1015 bright_sources : `lsst.afw.table.SourceCatalog`
1016 Catalog of detected bright sources; modified to include columns
1017 necessary for point source determination for the aperture correction
1018 calculation.
1019 """
1020 norm_ap_corr_map = self.psf_normalized_calibration_flux.run(
1021 exposure=exposure,
1022 catalog=bright_sources,
1023 ).ap_corr_map
1024
1025 ap_corr_map = self.measure_aperture_correction.run(exposure, bright_sources).apCorrMap
1026
1027 # Need to merge the aperture correction map from the normalization.
1028 for key in norm_ap_corr_map:
1029 ap_corr_map[key] = norm_ap_corr_map[key]
1030
1031 exposure.info.setApCorrMap(ap_corr_map)
1032
1033 def _find_stars(self, exposure, background, id_generator, background_to_photometric_ratio=None):
1034 """Detect stars on an exposure that has a PSF model, and measure their
1035 PSF, circular aperture, compensated gaussian fluxes.
1036
1037 Parameters
1038 ----------
1039 exposure : `lsst.afw.image.Exposure`
1040 Exposure to detect and measure stars on.
1041 background : `lsst.afw.math.BackgroundList`
1042 Background that was fit to the exposure during detection;
1043 modified in-place during subsequent detection.
1044 id_generator : `lsst.meas.base.IdGenerator`
1045 Object that generates source IDs and provides random seeds.
1046 background_to_photometric_ratio : `lsst.afw.image.Image`, optional
1047 Image to convert photometric-flattened image to
1048 background-flattened image.
1049
1050 Returns
1051 -------
1052 stars : `SourceCatalog`
1053 Sources that are very likely to be stars, with a limited set of
1054 measurements performed on them.
1055 """
1056 table = afwTable.SourceTable.make(self.initial_stars_schema.schema,
1057 id_generator.make_table_id_factory())
1058 # Re-estimate the background during this detection step, so that
1059 # measurement uses the most accurate background-subtraction.
1060 detections = self.star_detection.run(
1061 table=table,
1062 exposure=exposure,
1063 background=background,
1064 backgroundToPhotometricRatio=background_to_photometric_ratio,
1065 )
1066 sources = detections.sources
1067 self.star_sky_sources.run(exposure.mask, id_generator.catalog_id, sources)
1068
1069 # TODO investigation: Could this deblender throw away blends of non-PSF sources?
1070 self.star_deblend.run(exposure=exposure, sources=sources)
1071 # The deblender may not produce a contiguous catalog; ensure
1072 # contiguity for subsequent tasks.
1073 if not sources.isContiguous():
1074 sources = sources.copy(deep=True)
1075
1076 # Measure everything, and use those results to select only stars.
1077 self.star_measurement.run(sources, exposure)
1078 self.metadata["post_deblend_source_count"] = np.sum(~sources["sky_source"])
1079 self.metadata["saturated_source_count"] = np.sum(sources["base_PixelFlags_flag_saturated"])
1080 self.metadata["bad_source_count"] = np.sum(sources["base_PixelFlags_flag_bad"])
1081
1082 # Run the normalization calibration flux task to apply the
1083 # normalization correction to create normalized
1084 # calibration fluxes.
1085 self.star_normalized_calibration_flux.run(exposure=exposure, catalog=sources)
1086 self.star_apply_aperture_correction.run(sources, exposure.apCorrMap)
1087 self.star_catalog_calculation.run(sources)
1088 self.star_set_primary_flags.run(sources)
1089
1090 result = self.star_selector.run(sources)
1091 # The star selector may not produce a contiguous catalog.
1092 if not result.sourceCat.isContiguous():
1093 return result.sourceCat.copy(deep=True)
1094 else:
1095 return result.sourceCat
1096
1097 def _match_psf_stars(self, psf_stars, stars):
1098 """Match calibration stars to psf stars, to identify which were psf
1099 candidates, and which were used or reserved during psf measurement
1100 and the astrometric fit.
1101
1102 Parameters
1103 ----------
1104 psf_stars : `lsst.afw.table.SourceCatalog`
1105 PSF candidate stars that were sent to the psf determiner and
1106 used in the astrometric fit. Used to populate psf and astrometry
1107 related flag fields.
1108 stars : `lsst.afw.table.SourceCatalog`
1109 Stars that will be used for calibration; psf-related fields will
1110 be updated in-place.
1111
1112 Notes
1113 -----
1114 This code was adapted from CalibrateTask.copyIcSourceFields().
1115 """
1116 control = afwTable.MatchControl()
1117 # Return all matched objects, to separate blends.
1118 control.findOnlyClosest = False
1119 matches = afwTable.matchXy(psf_stars, stars, 3.0, control)
1120 deblend_key = stars.schema["deblend_nChild"].asKey()
1121 matches = [m for m in matches if m[1].get(deblend_key) == 0]
1122
1123 # Because we had to allow multiple matches to handle parents, we now
1124 # need to prune to the best (closest) matches.
1125 # Closest matches is a dict of psf_stars source ID to Match record
1126 # (psf_stars source, sourceCat source, distance in pixels).
1127 best = {}
1128 for match_psf, match_stars, d in matches:
1129 match = best.get(match_psf.getId())
1130 if match is None or d <= match[2]:
1131 best[match_psf.getId()] = (match_psf, match_stars, d)
1132 matches = list(best.values())
1133 # We'll use this to construct index arrays into each catalog.
1134 ids = np.array([(match_psf.getId(), match_stars.getId()) for match_psf, match_stars, d in matches]).T
1135
1136 if (n_matches := len(matches)) == 0:
1137 raise NoPsfStarsToStarsMatchError(n_psf_stars=len(psf_stars), n_stars=len(stars))
1138
1139 self.log.info("%d psf/astrometry stars out of %d matched %d calib stars",
1140 n_matches, len(psf_stars), len(stars))
1141 self.metadata["matched_psf_star_count"] = n_matches
1142
1143 # Check that no stars sources are listed twice; we already know
1144 # that each match has a unique psf_stars id, due to using as the key
1145 # in best above.
1146 n_unique = len(set(m[1].getId() for m in matches))
1147 if n_unique != n_matches:
1148 self.log.warning("%d psf_stars matched only %d stars", n_matches, n_unique)
1149
1150 # The indices of the IDs, so we can update the flag fields as arrays.
1151 idx_psf_stars = np.searchsorted(psf_stars["id"], ids[0])
1152 idx_stars = np.searchsorted(stars["id"], ids[1])
1153 for field in self.psf_fields:
1154 result = np.zeros(len(stars), dtype=bool)
1155 result[idx_stars] = psf_stars[field][idx_psf_stars]
1156 stars[field] = result
1157 stars['psf_id'][idx_stars] = psf_stars['id'][idx_psf_stars]
1158
1159 def _fit_astrometry(self, exposure, stars):
1160 """Fit an astrometric model to the data and return the reference
1161 matches used in the fit, and the fitted WCS.
1162
1163 Parameters
1164 ----------
1165 exposure : `lsst.afw.image.Exposure`
1166 Exposure that is being fit, to get PSF and other metadata from.
1167 Modified to add the fitted skyWcs.
1168 stars : `SourceCatalog`
1169 Good stars selected for use in calibration, with RA/Dec coordinates
1170 computed from the pixel positions and fitted WCS.
1171
1172 Returns
1173 -------
1174 matches : `list` [`lsst.afw.table.ReferenceMatch`]
1175 Reference/stars matches used in the fit.
1176 """
1177 result = self.astrometry.run(stars, exposure)
1178 return result.matches, result.matchMeta
1179
1180 def _fit_photometry(self, exposure, stars):
1181 """Fit a photometric model to the data and return the reference
1182 matches used in the fit, and the fitted PhotoCalib.
1183
1184 Parameters
1185 ----------
1186 exposure : `lsst.afw.image.Exposure`
1187 Exposure that is being fit, to get PSF and other metadata from.
1188 Has the fit `lsst.afw.image.PhotoCalib` attached, with pixel values
1189 unchanged.
1190 stars : `lsst.afw.table.SourceCatalog`
1191 Good stars selected for use in calibration.
1192 background : `lsst.afw.math.BackgroundList`
1193 Background that was fit to the exposure during detection of the
1194 above stars.
1195
1196 Returns
1197 -------
1198 calibrated_stars : `lsst.afw.table.SourceCatalog`
1199 Star catalog with flux/magnitude columns computed from the fitted
1200 photoCalib (instFlux columns are retained as well).
1201 matches : `list` [`lsst.afw.table.ReferenceMatch`]
1202 Reference/stars matches used in the fit.
1203 matchMeta : `lsst.daf.base.PropertyList`
1204 Metadata needed to unpersist matches, as returned by the matcher.
1205 photo_calib : `lsst.afw.image.PhotoCalib`
1206 Photometric calibration that was fit to the star catalog.
1207 """
1208 result = self.photometry.run(exposure, stars)
1209 calibrated_stars = result.photoCalib.calibrateCatalog(stars)
1210 exposure.setPhotoCalib(result.photoCalib)
1211 return calibrated_stars, result.matches, result.matchMeta, result.photoCalib
1212
1213 def _apply_photometry(self, exposure, background, background_to_photometric_ratio=None):
1214 """Apply the photometric model attached to the exposure to the
1215 exposure's pixels and an associated background model.
1216
1217 Parameters
1218 ----------
1219 exposure : `lsst.afw.image.Exposure`
1220 Exposure with the target `lsst.afw.image.PhotoCalib` attached.
1221 On return, pixel values will be calibrated and an identity
1222 photometric transform will be attached.
1223 background : `lsst.afw.math.BackgroundList`
1224 Background model to convert to nanojansky units in place.
1225 background_to_photometric_ratio : `lsst.afw.image.Image`, optional
1226 Image to convert photometric-flattened image to
1227 background-flattened image.
1228 """
1229 photo_calib = exposure.getPhotoCalib()
1230 exposure.maskedImage = photo_calib.calibrateImage(exposure.maskedImage)
1231 identity = afwImage.PhotoCalib(1.0,
1232 photo_calib.getCalibrationErr(),
1233 bbox=exposure.getBBox())
1234 exposure.setPhotoCalib(identity)
1235 exposure.metadata["BUNIT"] = "nJy"
1236
1237 assert photo_calib._isConstant, \
1238 "Background calibration assumes a constant PhotoCalib; PhotoCalTask should always return that."
1239
1240 for bg in background:
1241 # The statsImage is a view, but we can't assign to a function call in python.
1242 binned_image = bg[0].getStatsImage()
1243 binned_image *= photo_calib.getCalibrationMean()
1244
1245 def _summarize(self, exposure, stars, background):
1246 """Compute summary statistics on the exposure and update in-place the
1247 calibrations attached to it.
1248
1249 Parameters
1250 ----------
1251 exposure : `lsst.afw.image.Exposure`
1252 Exposure that was calibrated, to get PSF and other metadata from.
1253 Should be in instrumental units with the photometric calibration
1254 attached.
1255 Modified to contain the computed summary statistics.
1256 stars : `SourceCatalog`
1257 Good stars selected used in calibration.
1258 background : `lsst.afw.math.BackgroundList`
1259 Background that was fit to the exposure during detection of the
1260 above stars. Should be in instrumental units.
1261 """
1262 summary = self.compute_summary_stats.run(exposure, stars, background)
1263 exposure.info.setSummaryStats(summary)
1264
1265 def _recordMaskedPixelFractions(self, exposure):
1266 """Record the fraction of all the pixels in an exposure
1267 that are masked with a given flag. Each fraction is
1268 recorded in the task metadata. One record per flag type.
1269
1270 Parameters
1271 ----------
1272 exposure : `lsst.afw.image.ExposureF`
1273 The target exposure to calculate masked pixel fractions for.
1274 """
1275
1276 mask = exposure.mask
1277 maskPlanes = list(mask.getMaskPlaneDict().keys())
1278 for maskPlane in maskPlanes:
1279 self.metadata[f"{maskPlane.lower()}_mask_fraction"] = (
1280 evaluateMaskFraction(mask, maskPlane)
1281 )
The photometric calibration of an exposure.
Definition PhotoCalib.h:114
Pass parameters to algorithms that match list of sources.
Definition Match.h:45
An integer coordinate rectangle.
Definition Box.h:55
runQuantum(self, butlerQC, inputRefs, outputRefs)
_find_stars(self, exposure, background, id_generator, background_to_photometric_ratio=None)
_apply_illumination_correction(self, exposure, background_flat, illumination_correction)
_measure_aperture_correction(self, exposure, bright_sources)
_apply_photometry(self, exposure, background, background_to_photometric_ratio=None)
__init__(self, initial_stars_schema=None, **kwargs)
_compute_psf(self, exposure, id_generator, background_to_photometric_ratio=None)
_summarize(self, exposure, stars, background)
run(self, *, exposures, id_generator=None, result=None, background_flat=None, illumination_correction=None)
SourceMatchVector matchXy(SourceCatalog const &cat1, SourceCatalog const &cat2, double radius, MatchControl const &mc=MatchControl())
Compute all tuples (s1,s2,d) where s1 belings to cat1, s2 belongs to cat2 and d, the distance between...
Definition Match.cc:305
void updateSourceCoords(geom::SkyWcs const &wcs, SourceCollection &sourceList, bool include_covariance=true)
Update sky coordinates in a collection of source objects.
Definition wcsUtils.cc:125