LSST Applications g044012fb7c+304891ab8a,g04a91732dc+4e1b87f259,g07dc498a13+f701f15b83,g114c6a66ad+c7887c1284,g1409bbee79+f701f15b83,g1a7e361dbc+f701f15b83,g1fd858c14a+6ebd102b59,g35bb328faa+0eb18584fe,g3bd4b5ce2c+e83bf4edc8,g4e0f332c67+976ceb6bc8,g53246c7159+0eb18584fe,g5477a8d5ce+51234355ef,g60b5630c4e+c7887c1284,g623d845a50+c7887c1284,g6f0c2978f1+98123c34b6,g71fabbc107+c7887c1284,g75b6c65c88+ce466f4385,g78460c75b0+85633614c8,g786e29fd12+02b9b86fc9,g8852436030+cfe5cf5b7b,g89139ef638+f701f15b83,g9125e01d80+0eb18584fe,g95236ca021+d4f98599f0,g974caa22f6+0eb18584fe,g989de1cb63+f701f15b83,g9f33ca652e+b4908f5dcd,gaaedd4e678+f701f15b83,gabe3b4be73+543c3c03c9,gace736f484+07e57cea59,gb1101e3267+487fd1b06d,gb58c049af0+492386d360,gc99c83e5f0+a513197d39,gcf25f946ba+cfe5cf5b7b,gd0fa69b896+babbe6e5fe,gd6cbbdb0b4+3fef02d88a,gde0f65d7ad+e8379653a2,ge278dab8ac+ae64226a64,gfba249425e+0eb18584fe,w.2025.07
LSST Data Management Base Package
Loading...
Searching...
No Matches
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 # outputs
93 initial_stars_schema = connectionTypes.InitOutput(
94 doc="Schema of the output initial stars catalog.",
95 name="initial_stars_schema",
96 storageClass="SourceCatalog",
97 )
98
99 # TODO DM-38732: We want some kind of flag on Exposures/Catalogs to make
100 # it obvious which components had failed to be computed/persisted.
101 exposure = connectionTypes.Output(
102 doc="Photometrically calibrated, background-subtracted exposure with fitted calibrations and "
103 "summary statistics. To recover the original exposure, first add the background "
104 "(`initial_pvi_background`), and then uncalibrate (divide by `initial_photoCalib_detector`).",
105 name="initial_pvi",
106 storageClass="ExposureF",
107 dimensions=("instrument", "visit", "detector"),
108 )
109 stars = connectionTypes.Output(
110 doc="Catalog of unresolved sources detected on the calibrated exposure.",
111 name="initial_stars_detector",
112 storageClass="ArrowAstropy",
113 dimensions=["instrument", "visit", "detector"],
114 )
115 stars_footprints = connectionTypes.Output(
116 doc="Catalog of unresolved sources detected on the calibrated exposure; "
117 "includes source footprints.",
118 name="initial_stars_footprints_detector",
119 storageClass="SourceCatalog",
120 dimensions=["instrument", "visit", "detector"],
121 )
122 applied_photo_calib = connectionTypes.Output(
123 doc=(
124 "Photometric calibration that was applied to exposure's pixels. "
125 "This connection is disabled when do_calibrate_pixels=False."
126 ),
127 name="initial_photoCalib_detector",
128 storageClass="PhotoCalib",
129 dimensions=("instrument", "visit", "detector"),
130 )
131 background = connectionTypes.Output(
132 doc="Background models estimated during calibration task; calibrated to be in nJy units.",
133 name="initial_pvi_background",
134 storageClass="Background",
135 dimensions=("instrument", "visit", "detector"),
136 )
137
138 # Optional outputs
139 psf_stars_footprints = connectionTypes.Output(
140 doc="Catalog of bright unresolved sources detected on the exposure used for PSF determination; "
141 "includes source footprints.",
142 name="initial_psf_stars_footprints_detector",
143 storageClass="SourceCatalog",
144 dimensions=["instrument", "visit", "detector"],
145 )
146 psf_stars = connectionTypes.Output(
147 doc="Catalog of bright unresolved sources detected on the exposure used for PSF determination.",
148 name="initial_psf_stars_detector",
149 storageClass="ArrowAstropy",
150 dimensions=["instrument", "visit", "detector"],
151 )
152 astrometry_matches = connectionTypes.Output(
153 doc="Source to reference catalog matches from the astrometry solver.",
154 name="initial_astrometry_match_detector",
155 storageClass="Catalog",
156 dimensions=("instrument", "visit", "detector"),
157 )
158 photometry_matches = connectionTypes.Output(
159 doc="Source to reference catalog matches from the photometry solver.",
160 name="initial_photometry_match_detector",
161 storageClass="Catalog",
162 dimensions=("instrument", "visit", "detector"),
163 )
164
165 def __init__(self, *, config=None):
166 super().__init__(config=config)
167 if "psf_stars" not in config.optional_outputs:
168 del self.psf_stars
169 if "psf_stars_footprints" not in config.optional_outputs:
170 del self.psf_stars_footprints
171 if "astrometry_matches" not in config.optional_outputs:
172 del self.astrometry_matches
173 if "photometry_matches" not in config.optional_outputs:
174 del self.photometry_matches
175 if not config.do_calibrate_pixels:
176 del self.applied_photo_calib
177
178
179class CalibrateImageConfig(pipeBase.PipelineTaskConfig, pipelineConnections=CalibrateImageConnections):
180 optional_outputs = pexConfig.ListField(
181 doc="Which optional outputs to save (as their connection name)?",
182 dtype=str,
183 # TODO: note somewhere to disable this for benchmarking, but should
184 # we always have it on for production runs?
185 default=["psf_stars", "psf_stars_footprints", "astrometry_matches", "photometry_matches"],
186 optional=False
187 )
188
189 # To generate catalog ids consistently across subtasks.
190 id_generator = lsst.meas.base.DetectorVisitIdGeneratorConfig.make_field()
191
192 snap_combine = pexConfig.ConfigurableField(
194 doc="Task to combine two snaps to make one exposure.",
195 )
196
197 # subtasks used during psf characterization
198 install_simple_psf = pexConfig.ConfigurableField(
200 doc="Task to install a simple PSF model into the input exposure to use "
201 "when detecting bright sources for PSF estimation.",
202 )
203 psf_repair = pexConfig.ConfigurableField(
204 target=repair.RepairTask,
205 doc="Task to repair cosmic rays on the exposure before PSF determination.",
206 )
207 psf_subtract_background = pexConfig.ConfigurableField(
209 doc="Task to perform intial background subtraction, before first detection pass.",
210 )
211 psf_detection = pexConfig.ConfigurableField(
213 doc="Task to detect sources for PSF determination."
214 )
215 psf_source_measurement = pexConfig.ConfigurableField(
217 doc="Task to measure sources to be used for psf estimation."
218 )
219 psf_measure_psf = pexConfig.ConfigurableField(
221 doc="Task to measure the psf on bright sources."
222 )
223 psf_normalized_calibration_flux = pexConfig.ConfigurableField(
225 doc="Task to normalize the calibration flux (e.g. compensated tophats) "
226 "for the bright stars used for psf estimation.",
227 )
228
229 # TODO DM-39203: we can remove aperture correction from this task once we are
230 # using the shape-based star/galaxy code.
231 measure_aperture_correction = pexConfig.ConfigurableField(
233 doc="Task to compute the aperture correction from the bright stars."
234 )
235
236 # subtasks used during star measurement
237 star_detection = pexConfig.ConfigurableField(
239 doc="Task to detect stars to return in the output catalog."
240 )
241 star_sky_sources = pexConfig.ConfigurableField(
243 doc="Task to generate sky sources ('empty' regions where there are no detections).",
244 )
245 star_deblend = pexConfig.ConfigurableField(
247 doc="Split blended sources into their components."
248 )
249 star_measurement = pexConfig.ConfigurableField(
251 doc="Task to measure stars to return in the output catalog."
252 )
253 star_normalized_calibration_flux = pexConfig.ConfigurableField(
255 doc="Task to apply the normalization for calibration fluxes (e.g. compensated tophats) "
256 "for the final output star catalog.",
257 )
258 star_apply_aperture_correction = pexConfig.ConfigurableField(
260 doc="Task to apply aperture corrections to the selected stars."
261 )
262 star_catalog_calculation = pexConfig.ConfigurableField(
264 doc="Task to compute extendedness values on the star catalog, "
265 "for the star selector to remove extended sources."
266 )
267 star_set_primary_flags = pexConfig.ConfigurableField(
269 doc="Task to add isPrimary to the catalog."
270 )
271 star_selector = lsst.meas.algorithms.sourceSelectorRegistry.makeField(
272 default="science",
273 doc="Task to select reliable stars to use for calibration."
274 )
275
276 # final calibrations and statistics
277 astrometry = pexConfig.ConfigurableField(
279 doc="Task to perform astrometric calibration to fit a WCS.",
280 )
281 astrometry_ref_loader = pexConfig.ConfigField(
283 doc="Configuration of reference object loader for astrometric fit.",
284 )
285 photometry = pexConfig.ConfigurableField(
287 doc="Task to perform photometric calibration to fit a PhotoCalib.",
288 )
289 photometry_ref_loader = pexConfig.ConfigField(
291 doc="Configuration of reference object loader for photometric fit.",
292 )
293
294 compute_summary_stats = pexConfig.ConfigurableField(
296 doc="Task to to compute summary statistics on the calibrated exposure."
297 )
298
299 do_calibrate_pixels = pexConfig.Field(
300 dtype=bool,
301 default=True,
302 doc=(
303 "If True, apply the photometric calibration to the image pixels "
304 "and background model, and attach an identity PhotoCalib to "
305 "the output image to reflect this. If False`, leave the image "
306 "and background uncalibrated and attach the PhotoCalib that maps "
307 "them to physical units."
308 )
309 )
310
311 def setDefaults(self):
312 super().setDefaults()
313
314 # Use a very broad PSF here, to throughly reject CRs.
315 # TODO investigation: a large initial psf guess may make stars look
316 # like CRs for very good seeing images.
317 self.install_simple_psf.fwhm = 4
318
319 # S/N>=50 sources for PSF determination, but detection to S/N=10.
320 # The thresholdValue sets the minimum flux in a pixel to be included in the
321 # footprint, while peaks are only detected when they are above
322 # thresholdValue * includeThresholdMultiplier. The low thresholdValue
323 # ensures that the footprints are large enough for the noise replacer
324 # to mask out faint undetected neighbors that are not to be measured.
325 self.psf_detection.thresholdValue = 10.0
326 self.psf_detection.includeThresholdMultiplier = 5.0
327 # TODO investigation: Probably want False here, but that may require
328 # tweaking the background spatial scale, to make it small enough to
329 # prevent extra peaks in the wings of bright objects.
330 self.psf_detection.doTempLocalBackground = False
331 # NOTE: we do want reEstimateBackground=True in psf_detection, so that
332 # each measurement step is done with the best background available.
333
334 # Minimal measurement plugins for PSF determination.
335 # TODO DM-39203: We can drop GaussianFlux and PsfFlux, if we use
336 # shapeHSM/moments for star/galaxy separation.
337 # TODO DM-39203: we can remove aperture correction from this task once
338 # we are using the shape-based star/galaxy code.
339 self.psf_source_measurement.plugins = ["base_PixelFlags",
340 "base_SdssCentroid",
341 "ext_shapeHSM_HsmSourceMoments",
342 "base_CircularApertureFlux",
343 "base_GaussianFlux",
344 "base_PsfFlux",
345 "base_CompensatedTophatFlux",
346 ]
347 self.psf_source_measurement.slots.shape = "ext_shapeHSM_HsmSourceMoments"
348 # Only measure apertures we need for PSF measurement.
349 self.psf_source_measurement.plugins["base_CircularApertureFlux"].radii = [12.0]
350 self.psf_source_measurement.plugins["base_CompensatedTophatFlux"].apertures = [12]
351 # TODO DM-40843: Remove this line once this is the psfex default.
352 self.psf_measure_psf.psfDeterminer["psfex"].photometricFluxField = \
353 "base_CircularApertureFlux_12_0_instFlux"
354
355 # No extendeness information available: we need the aperture
356 # corrections to determine that.
357 self.measure_aperture_correction.sourceSelector["science"].doUnresolved = False
358 self.measure_aperture_correction.sourceSelector["science"].flags.good = ["calib_psf_used"]
359 self.measure_aperture_correction.sourceSelector["science"].flags.bad = []
360
361 # Detection for good S/N for astrometry/photometry and other
362 # downstream tasks; detection mask to S/N>=5, but S/N>=10 peaks.
363 self.star_detection.thresholdValue = 5.0
364 self.star_detection.includeThresholdMultiplier = 2.0
365 self.star_measurement.plugins = ["base_PixelFlags",
366 "base_SdssCentroid",
367 "ext_shapeHSM_HsmSourceMoments",
368 "ext_shapeHSM_HsmPsfMoments",
369 "base_GaussianFlux",
370 "base_PsfFlux",
371 "base_CircularApertureFlux",
372 "base_ClassificationSizeExtendedness",
373 "base_CompensatedTophatFlux",
374 ]
375 self.star_measurement.slots.psfShape = "ext_shapeHSM_HsmPsfMoments"
376 self.star_measurement.slots.shape = "ext_shapeHSM_HsmSourceMoments"
377 # Only measure the apertures we need for star selection.
378 self.star_measurement.plugins["base_CircularApertureFlux"].radii = [12.0]
379 self.star_measurement.plugins["base_CompensatedTophatFlux"].apertures = [12]
380
381 # We measure and apply the normalization aperture correction with the
382 # psf_normalized_calibration_flux task, and we only apply the normalization
383 # aperture correction for the full list of stars.
384 self.star_normalized_calibration_flux.do_measure_ap_corr = False
385
386 # Select stars with reliable measurements and no bad flags.
387 self.star_selector["science"].doFlags = True
388 self.star_selector["science"].doUnresolved = True
389 self.star_selector["science"].doSignalToNoise = True
390 self.star_selector["science"].signalToNoise.minimum = 10.0
391 # Keep sky sources in the output catalog, even though they aren't
392 # wanted for calibration.
393 self.star_selector["science"].doSkySources = True
394 # Set the flux and error fields
395 self.star_selector["science"].signalToNoise.fluxField = "slot_CalibFlux_instFlux"
396 self.star_selector["science"].signalToNoise.errField = "slot_CalibFlux_instFluxErr"
397
398 # Use the affine WCS fitter (assumes we have a good camera geometry).
399 self.astrometry.wcsFitter.retarget(lsst.meas.astrom.FitAffineWcsTask)
400 # phot_g_mean is the primary Gaia band for all input bands.
401 self.astrometry_ref_loader.anyFilterMapsToThis = "phot_g_mean"
402
403 # Only reject sky sources; we already selected good stars.
404 self.astrometry.sourceSelector["science"].doFlags = True
405 self.astrometry.sourceSelector["science"].flags.good = ["calib_psf_candidate"]
406 self.astrometry.sourceSelector["science"].flags.bad = []
407 self.astrometry.sourceSelector["science"].doUnresolved = False
408 self.astrometry.sourceSelector["science"].doIsolated = False
409 self.astrometry.sourceSelector["science"].doRequirePrimary = False
410 self.photometry.match.sourceSelection.doFlags = True
411 self.photometry.match.sourceSelection.flags.bad = ["sky_source"]
412 # Unset the (otherwise reasonable, but we've already made the
413 # selections we want above) selection settings in PhotoCalTask.
414 self.photometry.match.sourceSelection.doRequirePrimary = False
415 self.photometry.match.sourceSelection.doUnresolved = False
416
417 def validate(self):
418 super().validate()
419
420 # Ensure that the normalization calibration flux tasks
421 # are configured correctly.
422 if not self.psf_normalized_calibration_flux.do_measure_ap_corr:
423 msg = ("psf_normalized_calibration_flux task must be configured with do_measure_ap_corr=True "
424 "or else the normalization and calibration flux will not be properly measured.")
425 raise pexConfig.FieldValidationError(
426 CalibrateImageConfig.psf_normalized_calibration_flux, self, msg,
427 )
428 if self.star_normalized_calibration_flux.do_measure_ap_corr:
429 msg = ("star_normalized_calibration_flux task must be configured with do_measure_ap_corr=False "
430 "to apply the previously measured normalization to the full catalog of calibration "
431 "fluxes.")
432 raise pexConfig.FieldValidationError(
433 CalibrateImageConfig.star_normalized_calibration_flux, self, msg,
434 )
435
436 # Ensure base_LocalPhotoCalib and base_LocalWcs plugins are not run,
437 # because they'd be running too early to pick up the fitted PhotoCalib
438 # and WCS.
439 if "base_LocalWcs" in self.psf_source_measurement.plugins.names:
440 raise pexConfig.FieldValidationError(
441 CalibrateImageConfig.psf_source_measurement,
442 self,
443 "base_LocalWcs cannot run CalibrateImageTask, as it would be run before the astrometry fit."
444 )
445 if "base_LocalWcs" in self.star_measurement.plugins.names:
446 raise pexConfig.FieldValidationError(
447 CalibrateImageConfig.star_measurement,
448 self,
449 "base_LocalWcs cannot run CalibrateImageTask, as it would be run before the astrometry fit."
450 )
451 if "base_LocalPhotoCalib" in self.psf_source_measurement.plugins.names:
452 raise pexConfig.FieldValidationError(
453 CalibrateImageConfig.psf_source_measurement,
454 self,
455 "base_LocalPhotoCalib cannot run CalibrateImageTask, "
456 "as it would be run before the photometry fit."
457 )
458 if "base_LocalPhotoCalib" in self.star_measurement.plugins.names:
459 raise pexConfig.FieldValidationError(
460 CalibrateImageConfig.star_measurement,
461 self,
462 "base_LocalPhotoCalib cannot run CalibrateImageTask, "
463 "as it would be run before the photometry fit."
464 )
465
466
467class CalibrateImageTask(pipeBase.PipelineTask):
468 """Compute the PSF, aperture corrections, astrometric and photometric
469 calibrations, and summary statistics for a single science exposure, and
470 produce a catalog of brighter stars that were used to calibrate it.
471
472 Parameters
473 ----------
474 initial_stars_schema : `lsst.afw.table.Schema`
475 Schema of the initial_stars output catalog.
476 """
477 _DefaultName = "calibrateImage"
478 ConfigClass = CalibrateImageConfig
479
480 def __init__(self, initial_stars_schema=None, **kwargs):
481 super().__init__(**kwargs)
482 self.makeSubtask("snap_combine")
483
484 # PSF determination subtasks
485 self.makeSubtask("install_simple_psf")
486 self.makeSubtask("psf_repair")
487 self.makeSubtask("psf_subtract_background")
488 self.psf_schema = afwTable.SourceTable.makeMinimalSchema()
489 afwTable.CoordKey.addErrorFields(self.psf_schema)
490 self.makeSubtask("psf_detection", schema=self.psf_schema)
491 self.makeSubtask("psf_source_measurement", schema=self.psf_schema)
492 self.makeSubtask("psf_measure_psf", schema=self.psf_schema)
493 self.makeSubtask("psf_normalized_calibration_flux", schema=self.psf_schema)
494
495 self.makeSubtask("measure_aperture_correction", schema=self.psf_schema)
496 self.makeSubtask("astrometry", schema=self.psf_schema)
497
498 # star measurement subtasks
499 if initial_stars_schema is None:
500 initial_stars_schema = afwTable.SourceTable.makeMinimalSchema()
501
502 # These fields let us track which sources were used for psf modeling,
503 # astrometric fitting, and aperture correction calculations.
504 self.psf_fields = ("calib_psf_candidate", "calib_psf_used", "calib_psf_reserved",
505 "calib_astrometry_used",
506 # TODO DM-39203: these can be removed once apcorr is gone.
507 "apcorr_slot_CalibFlux_used", "apcorr_base_GaussianFlux_used",
508 "apcorr_base_PsfFlux_used")
509 for field in self.psf_fields:
510 item = self.psf_schema.find(field)
511 initial_stars_schema.addField(item.getField())
512
513 afwTable.CoordKey.addErrorFields(initial_stars_schema)
514 self.makeSubtask("star_detection", schema=initial_stars_schema)
515 self.makeSubtask("star_sky_sources", schema=initial_stars_schema)
516 self.makeSubtask("star_deblend", schema=initial_stars_schema)
517 self.makeSubtask("star_measurement", schema=initial_stars_schema)
518 self.makeSubtask("star_normalized_calibration_flux", schema=initial_stars_schema)
519
520 self.makeSubtask("star_apply_aperture_correction", schema=initial_stars_schema)
521 self.makeSubtask("star_catalog_calculation", schema=initial_stars_schema)
522 self.makeSubtask("star_set_primary_flags", schema=initial_stars_schema, isSingleFrame=True)
523 self.makeSubtask("star_selector")
524 self.makeSubtask("photometry", schema=initial_stars_schema)
525 self.makeSubtask("compute_summary_stats")
526
527 # The final catalog will have calibrated flux columns, which we add to
528 # the init-output schema by calibrating our zero-length catalog with an
529 # arbitrary dummy PhotoCalib. We also use this schema to initialze
530 # the stars catalog in order to ensure it's the same even when we hit
531 # an error (and write partial outputs) before calibrating the catalog
532 # - note that calibrateCatalog will happily reuse existing output
533 # columns.
534 dummy_photo_calib = afwImage.PhotoCalib(1.0, 0, bbox=lsst.geom.Box2I())
535 self.initial_stars_schema = dummy_photo_calib.calibrateCatalog(
536 afwTable.SourceCatalog(initial_stars_schema)
537 )
538
539 def runQuantum(self, butlerQC, inputRefs, outputRefs):
540 inputs = butlerQC.get(inputRefs)
541 exposures = inputs.pop("exposures")
542
543 id_generator = self.config.id_generator.apply(butlerQC.quantum.dataId)
544
546 dataIds=[ref.datasetRef.dataId for ref in inputRefs.astrometry_ref_cat],
547 refCats=inputs.pop("astrometry_ref_cat"),
548 name=self.config.connections.astrometry_ref_cat,
549 config=self.config.astrometry_ref_loader, log=self.log)
550 self.astrometry.setRefObjLoader(astrometry_loader)
551
553 dataIds=[ref.datasetRef.dataId for ref in inputRefs.photometry_ref_cat],
554 refCats=inputs.pop("photometry_ref_cat"),
555 name=self.config.connections.photometry_ref_cat,
556 config=self.config.photometry_ref_loader, log=self.log)
557 self.photometry.match.setRefObjLoader(photometry_loader)
558
559 # This should not happen with a properly configured execution context.
560 assert not inputs, "runQuantum got more inputs than expected"
561
562 # Specify the fields that `annotate` needs below, to ensure they
563 # exist, even as None.
564 result = pipeBase.Struct(exposure=None,
565 stars_footprints=None,
566 psf_stars_footprints=None,
567 )
568 try:
569 self.run(exposures=exposures, result=result, id_generator=id_generator)
570 except pipeBase.AlgorithmError as e:
571 error = pipeBase.AnnotatedPartialOutputsError.annotate(
572 e,
573 self,
574 result.exposure,
575 result.psf_stars_footprints,
576 result.stars_footprints,
577 log=self.log
578 )
579 butlerQC.put(result, outputRefs)
580 raise error from e
581
582 butlerQC.put(result, outputRefs)
583
584 @timeMethod
585 def run(self, *, exposures, id_generator=None, result=None):
586 """Find stars and perform psf measurement, then do a deeper detection
587 and measurement and calibrate astrometry and photometry from that.
588
589 Parameters
590 ----------
591 exposures : `lsst.afw.image.Exposure` or `list` [`lsst.afw.image.Exposure`]
592 Post-ISR exposure(s), with an initial WCS, VisitInfo, and Filter.
593 Modified in-place during processing if only one is passed.
594 If two exposures are passed, treat them as snaps and combine
595 before doing further processing.
596 id_generator : `lsst.meas.base.IdGenerator`, optional
597 Object that generates source IDs and provides random seeds.
598 result : `lsst.pipe.base.Struct`, optional
599 Result struct that is modified to allow saving of partial outputs
600 for some failure conditions. If the task completes successfully,
601 this is also returned.
602
603 Returns
604 -------
605 result : `lsst.pipe.base.Struct`
606 Results as a struct with attributes:
607
608 ``exposure``
609 Calibrated exposure, with pixels in nJy units.
610 (`lsst.afw.image.Exposure`)
611 ``stars``
612 Stars that were used to calibrate the exposure, with
613 calibrated fluxes and magnitudes.
614 (`astropy.table.Table`)
615 ``stars_footprints``
616 Footprints of stars that were used to calibrate the exposure.
617 (`lsst.afw.table.SourceCatalog`)
618 ``psf_stars``
619 Stars that were used to determine the image PSF.
620 (`astropy.table.Table`)
621 ``psf_stars_footprints``
622 Footprints of stars that were used to determine the image PSF.
623 (`lsst.afw.table.SourceCatalog`)
624 ``background``
625 Background that was fit to the exposure when detecting
626 ``stars``. (`lsst.afw.math.BackgroundList`)
627 ``applied_photo_calib``
628 Photometric calibration that was fit to the star catalog and
629 applied to the exposure. (`lsst.afw.image.PhotoCalib`)
630 This is `None` if ``config.do_calibrate_pixels`` is `False`.
631 ``astrometry_matches``
632 Reference catalog stars matches used in the astrometric fit.
633 (`list` [`lsst.afw.table.ReferenceMatch`] or `lsst.afw.table.BaseCatalog`)
634 ``photometry_matches``
635 Reference catalog stars matches used in the photometric fit.
636 (`list` [`lsst.afw.table.ReferenceMatch`] or `lsst.afw.table.BaseCatalog`)
637 """
638 if result is None:
639 result = pipeBase.Struct()
640 if id_generator is None:
641 id_generator = lsst.meas.base.IdGenerator()
642
643 result.exposure = self.snap_combine.run(exposures).exposure
644 self._recordMaskedPixelFractions(result.exposure)
645 self.log.info("Initial PhotoCalib: %s", result.exposure.getPhotoCalib())
646
647 result.background = None
648 summary_stat_catalog = None
649 # Some exposure components are set to initial placeholder objects
650 # while we try to bootstrap them. If we fail before we fit for them,
651 # we want to reset those components to None so the placeholders don't
652 # masquerade as the real thing.
653 have_fit_psf = False
654 have_fit_astrometry = False
655 have_fit_photometry = False
656 try:
657 result.psf_stars_footprints, result.background, _ = self._compute_psf(result.exposure,
658 id_generator)
659 have_fit_psf = True
660 self._measure_aperture_correction(result.exposure, result.psf_stars_footprints)
661 result.psf_stars = result.psf_stars_footprints.asAstropy()
662 # Run astrometry using PSF candidate stars
663 astrometry_matches, astrometry_meta = self._fit_astrometry(
664 result.exposure, result.psf_stars_footprints
665 )
666 self.metadata["astrometry_matches_count"] = len(astrometry_matches)
667 if "astrometry_matches" in self.config.optional_outputs:
668 result.astrometry_matches = lsst.meas.astrom.denormalizeMatches(astrometry_matches,
669 astrometry_meta)
670 result.psf_stars = result.psf_stars_footprints.asAstropy()
671
672 # Run the stars_detection subtask for the photometric calibration.
673 result.stars_footprints = self._find_stars(result.exposure, result.background, id_generator)
674 self._match_psf_stars(result.psf_stars_footprints, result.stars_footprints)
675
676 # Update the source cooordinates with the current wcs.
677 afwTable.updateSourceCoords(result.exposure.wcs, sourceList=result.stars_footprints)
678
679 summary_stat_catalog = result.stars_footprints
680 result.stars = result.stars_footprints.asAstropy()
681 self.metadata["star_count"] = np.sum(~result.stars["sky_source"])
682
683 # Validate the astrometric fit. Send in the stars_footprints
684 # catalog so that its coords get set to NaN if the fit is deemed
685 # a failure.
686 self.astrometry.check(result.exposure, result.stars_footprints, len(astrometry_matches))
687 result.stars = result.stars_footprints.asAstropy()
688 have_fit_astrometry = True
689
690 result.stars_footprints, photometry_matches, \
691 photometry_meta, photo_calib = self._fit_photometry(result.exposure, result.stars_footprints)
692 have_fit_photometry = True
693 self.metadata["photometry_matches_count"] = len(photometry_matches)
694 # fit_photometry returns a new catalog, so we need a new astropy table view.
695 result.stars = result.stars_footprints.asAstropy()
696 # summary stats don't make use of the calibrated fluxes, but we
697 # might as well use the best catalog we've got in case that
698 # changes, and help the old one get garbage-collected.
699 summary_stat_catalog = result.stars_footprints
700 if "photometry_matches" in self.config.optional_outputs:
701 result.photometry_matches = lsst.meas.astrom.denormalizeMatches(photometry_matches,
702 photometry_meta)
703 except pipeBase.AlgorithmError:
704 if not have_fit_psf:
705 result.exposure.setPsf(None)
706 if not have_fit_astrometry:
707 result.exposure.setWcs(None)
708 if not have_fit_photometry:
709 result.exposure.setPhotoCalib(None)
710 # Summary stat calculations can handle missing components gracefully,
711 # but we want to run them as late as possible (but still before we
712 # calibrate pixels, if we do that at all).
713 # So we run them after we succeed or if we get an AlgorithmError. We
714 # intentionally don't use 'finally' here because we don't want to run
715 # them if we get some other kind of error.
716 self._summarize(result.exposure, summary_stat_catalog, result.background)
717 raise
718 else:
719 self._summarize(result.exposure, summary_stat_catalog, result.background)
720
721 if self.config.do_calibrate_pixels:
722 self._apply_photometry(result.exposure, result.background)
723 result.applied_photo_calib = photo_calib
724 else:
725 result.applied_photo_calib = None
726 return result
727
728 def _compute_psf(self, exposure, id_generator):
729 """Find bright sources detected on an exposure and fit a PSF model to
730 them, repairing likely cosmic rays before detection.
731
732 Repair, detect, measure, and compute PSF twice, to ensure the PSF
733 model does not include contributions from cosmic rays.
734
735 Parameters
736 ----------
737 exposure : `lsst.afw.image.Exposure`
738 Exposure to detect and measure bright stars on.
739 id_generator : `lsst.meas.base.IdGenerator`, optional
740 Object that generates source IDs and provides random seeds.
741
742 Returns
743 -------
744 sources : `lsst.afw.table.SourceCatalog`
745 Catalog of detected bright sources.
746 background : `lsst.afw.math.BackgroundList`
747 Background that was fit to the exposure during detection.
748 cell_set : `lsst.afw.math.SpatialCellSet`
749 PSF candidates returned by the psf determiner.
750 """
751 def log_psf(msg, addToMetadata=False):
752 """Log the parameters of the psf and background, with a prepended
753 message. There is also the option to add the PSF sigma to the task
754 metadata.
755
756 Parameters
757 ----------
758 msg : `str`
759 Message to prepend the log info with.
760 addToMetadata : `bool`, optional
761 Whether to add the final psf sigma value to the task metadata
762 (the default is False).
763 """
764 position = exposure.psf.getAveragePosition()
765 sigma = exposure.psf.computeShape(position).getDeterminantRadius()
766 dimensions = exposure.psf.computeImage(position).getDimensions()
767 median_background = np.median(background.getImage().array)
768 self.log.info("%s sigma=%0.4f, dimensions=%s; median background=%0.2f",
769 msg, sigma, dimensions, median_background)
770 if addToMetadata:
771 self.metadata["final_psf_sigma"] = sigma
772
773 self.log.info("First pass detection with Guassian PSF FWHM=%s pixels",
774 self.config.install_simple_psf.fwhm)
775 self.install_simple_psf.run(exposure=exposure)
776
777 background = self.psf_subtract_background.run(exposure=exposure).background
778 log_psf("Initial PSF:")
779 self.psf_repair.run(exposure=exposure, keepCRs=True)
780
781 table = afwTable.SourceTable.make(self.psf_schema, id_generator.make_table_id_factory())
782 # Re-estimate the background during this detection step, so that
783 # measurement uses the most accurate background-subtraction.
784 detections = self.psf_detection.run(table=table, exposure=exposure, background=background)
785 self.metadata["initial_psf_positive_footprint_count"] = detections.numPos
786 self.metadata["initial_psf_negative_footprint_count"] = detections.numNeg
787 self.metadata["initial_psf_positive_peak_count"] = detections.numPosPeaks
788 self.metadata["initial_psf_negative_peak_count"] = detections.numNegPeaks
789 self.psf_source_measurement.run(detections.sources, exposure)
790 psf_result = self.psf_measure_psf.run(exposure=exposure, sources=detections.sources)
791 # Replace the initial PSF with something simpler for the second
792 # repair/detect/measure/measure_psf step: this can help it converge.
793 self.install_simple_psf.run(exposure=exposure)
794
795 log_psf("Rerunning with simple PSF:")
796 # TODO investigation: Should we only re-run repair here, to use the
797 # new PSF? Maybe we *do* need to re-run measurement with PsfFlux, to
798 # use the fitted PSF?
799 # TODO investigation: do we need a separate measurement task here
800 # for the post-psf_measure_psf step, since we only want to do PsfFlux
801 # and GaussianFlux *after* we have a PSF? Maybe that's not relevant
802 # once DM-39203 is merged?
803 self.psf_repair.run(exposure=exposure, keepCRs=True)
804 # Re-estimate the background during this detection step, so that
805 # measurement uses the most accurate background-subtraction.
806 detections = self.psf_detection.run(table=table, exposure=exposure, background=background)
807 self.metadata["simple_psf_positive_footprint_count"] = detections.numPos
808 self.metadata["simple_psf_negative_footprint_count"] = detections.numNeg
809 self.metadata["simple_psf_positive_peak_count"] = detections.numPosPeaks
810 self.metadata["simple_psf_negative_peak_count"] = detections.numNegPeaks
811 self.psf_source_measurement.run(detections.sources, exposure)
812 psf_result = self.psf_measure_psf.run(exposure=exposure, sources=detections.sources)
813
814 log_psf("Final PSF:", addToMetadata=True)
815
816 # Final repair with final PSF, removing cosmic rays this time.
817 self.psf_repair.run(exposure=exposure)
818 # Final measurement with the CRs removed.
819 self.psf_source_measurement.run(detections.sources, exposure)
820
821 # PSF is set on exposure; candidates are returned to use for
822 # calibration flux normalization and aperture corrections.
823 return detections.sources, background, psf_result.cellSet
824
825 def _measure_aperture_correction(self, exposure, bright_sources):
826 """Measure and set the ApCorrMap on the Exposure, using
827 previously-measured bright sources.
828
829 This function first normalizes the calibration flux and then
830 the full set of aperture corrections are measured relative
831 to this normalized calibration flux.
832
833 Parameters
834 ----------
835 exposure : `lsst.afw.image.Exposure`
836 Exposure to set the ApCorrMap on.
837 bright_sources : `lsst.afw.table.SourceCatalog`
838 Catalog of detected bright sources; modified to include columns
839 necessary for point source determination for the aperture correction
840 calculation.
841 """
842 norm_ap_corr_map = self.psf_normalized_calibration_flux.run(
843 exposure=exposure,
844 catalog=bright_sources,
845 ).ap_corr_map
846
847 ap_corr_map = self.measure_aperture_correction.run(exposure, bright_sources).apCorrMap
848
849 # Need to merge the aperture correction map from the normalization.
850 for key in norm_ap_corr_map:
851 ap_corr_map[key] = norm_ap_corr_map[key]
852
853 exposure.info.setApCorrMap(ap_corr_map)
854
855 def _find_stars(self, exposure, background, id_generator):
856 """Detect stars on an exposure that has a PSF model, and measure their
857 PSF, circular aperture, compensated gaussian fluxes.
858
859 Parameters
860 ----------
861 exposure : `lsst.afw.image.Exposure`
862 Exposure to detect and measure stars on.
863 background : `lsst.afw.math.BackgroundList`
864 Background that was fit to the exposure during detection;
865 modified in-place during subsequent detection.
866 id_generator : `lsst.meas.base.IdGenerator`
867 Object that generates source IDs and provides random seeds.
868
869 Returns
870 -------
871 stars : `SourceCatalog`
872 Sources that are very likely to be stars, with a limited set of
873 measurements performed on them.
874 """
875 table = afwTable.SourceTable.make(self.initial_stars_schema.schema,
876 id_generator.make_table_id_factory())
877 # Re-estimate the background during this detection step, so that
878 # measurement uses the most accurate background-subtraction.
879 detections = self.star_detection.run(table=table, exposure=exposure, background=background)
880 sources = detections.sources
881 self.star_sky_sources.run(exposure.mask, id_generator.catalog_id, sources)
882
883 # TODO investigation: Could this deblender throw away blends of non-PSF sources?
884 self.star_deblend.run(exposure=exposure, sources=sources)
885 # The deblender may not produce a contiguous catalog; ensure
886 # contiguity for subsequent tasks.
887 if not sources.isContiguous():
888 sources = sources.copy(deep=True)
889
890 # Measure everything, and use those results to select only stars.
891 self.star_measurement.run(sources, exposure)
892 self.metadata["post_deblend_source_count"] = np.sum(~sources["sky_source"])
893 self.metadata["saturated_source_count"] = np.sum(sources["base_PixelFlags_flag_saturated"])
894 self.metadata["bad_source_count"] = np.sum(sources["base_PixelFlags_flag_bad"])
895
896 # Run the normalization calibration flux task to apply the
897 # normalization correction to create normalized
898 # calibration fluxes.
899 self.star_normalized_calibration_flux.run(exposure=exposure, catalog=sources)
900 self.star_apply_aperture_correction.run(sources, exposure.apCorrMap)
901 self.star_catalog_calculation.run(sources)
902 self.star_set_primary_flags.run(sources)
903
904 result = self.star_selector.run(sources)
905 # The star selector may not produce a contiguous catalog.
906 if not result.sourceCat.isContiguous():
907 return result.sourceCat.copy(deep=True)
908 else:
909 return result.sourceCat
910
911 def _match_psf_stars(self, psf_stars, stars):
912 """Match calibration stars to psf stars, to identify which were psf
913 candidates, and which were used or reserved during psf measurement
914 and the astrometric fit.
915
916 Parameters
917 ----------
918 psf_stars : `lsst.afw.table.SourceCatalog`
919 PSF candidate stars that were sent to the psf determiner and
920 used in the astrometric fit. Used to populate psf and astrometry
921 related flag fields.
922 stars : `lsst.afw.table.SourceCatalog`
923 Stars that will be used for calibration; psf-related fields will
924 be updated in-place.
925
926 Notes
927 -----
928 This code was adapted from CalibrateTask.copyIcSourceFields().
929 """
930 control = afwTable.MatchControl()
931 # Return all matched objects, to separate blends.
932 control.findOnlyClosest = False
933 matches = afwTable.matchXy(psf_stars, stars, 3.0, control)
934 deblend_key = stars.schema["deblend_nChild"].asKey()
935 matches = [m for m in matches if m[1].get(deblend_key) == 0]
936
937 # Because we had to allow multiple matches to handle parents, we now
938 # need to prune to the best (closest) matches.
939 # Closest matches is a dict of psf_stars source ID to Match record
940 # (psf_stars source, sourceCat source, distance in pixels).
941 best = {}
942 for match_psf, match_stars, d in matches:
943 match = best.get(match_psf.getId())
944 if match is None or d <= match[2]:
945 best[match_psf.getId()] = (match_psf, match_stars, d)
946 matches = list(best.values())
947 # We'll use this to construct index arrays into each catalog.
948 ids = np.array([(match_psf.getId(), match_stars.getId()) for match_psf, match_stars, d in matches]).T
949
950 if (n_matches := len(matches)) == 0:
951 raise NoPsfStarsToStarsMatchError(n_psf_stars=len(psf_stars), n_stars=len(stars))
952
953 self.log.info("%d psf/astrometry stars out of %d matched %d calib stars",
954 n_matches, len(psf_stars), len(stars))
955 self.metadata["matched_psf_star_count"] = n_matches
956
957 # Check that no stars sources are listed twice; we already know
958 # that each match has a unique psf_stars id, due to using as the key
959 # in best above.
960 n_unique = len(set(m[1].getId() for m in matches))
961 if n_unique != n_matches:
962 self.log.warning("%d psf_stars matched only %d stars", n_matches, n_unique)
963
964 # The indices of the IDs, so we can update the flag fields as arrays.
965 idx_psf_stars = np.searchsorted(psf_stars["id"], ids[0])
966 idx_stars = np.searchsorted(stars["id"], ids[1])
967 for field in self.psf_fields:
968 result = np.zeros(len(stars), dtype=bool)
969 result[idx_stars] = psf_stars[field][idx_psf_stars]
970 stars[field] = result
971
972 def _fit_astrometry(self, exposure, stars):
973 """Fit an astrometric model to the data and return the reference
974 matches used in the fit, and the fitted WCS.
975
976 Parameters
977 ----------
978 exposure : `lsst.afw.image.Exposure`
979 Exposure that is being fit, to get PSF and other metadata from.
980 Modified to add the fitted skyWcs.
981 stars : `SourceCatalog`
982 Good stars selected for use in calibration, with RA/Dec coordinates
983 computed from the pixel positions and fitted WCS.
984
985 Returns
986 -------
987 matches : `list` [`lsst.afw.table.ReferenceMatch`]
988 Reference/stars matches used in the fit.
989 """
990 result = self.astrometry.run(stars, exposure)
991 return result.matches, result.matchMeta
992
993 def _fit_photometry(self, exposure, stars):
994 """Fit a photometric model to the data and return the reference
995 matches used in the fit, and the fitted PhotoCalib.
996
997 Parameters
998 ----------
999 exposure : `lsst.afw.image.Exposure`
1000 Exposure that is being fit, to get PSF and other metadata from.
1001 Has the fit `lsst.afw.image.PhotoCalib` attached, with pixel values
1002 unchanged.
1003 stars : `lsst.afw.table.SourceCatalog`
1004 Good stars selected for use in calibration.
1005 background : `lsst.afw.math.BackgroundList`
1006 Background that was fit to the exposure during detection of the
1007 above stars.
1008
1009 Returns
1010 -------
1011 calibrated_stars : `lsst.afw.table.SourceCatalog`
1012 Star catalog with flux/magnitude columns computed from the fitted
1013 photoCalib (instFlux columns are retained as well).
1014 matches : `list` [`lsst.afw.table.ReferenceMatch`]
1015 Reference/stars matches used in the fit.
1016 matchMeta : `lsst.daf.base.PropertyList`
1017 Metadata needed to unpersist matches, as returned by the matcher.
1018 photo_calib : `lsst.afw.image.PhotoCalib`
1019 Photometric calibration that was fit to the star catalog.
1020 """
1021 result = self.photometry.run(exposure, stars)
1022 calibrated_stars = result.photoCalib.calibrateCatalog(stars)
1023 exposure.setPhotoCalib(result.photoCalib)
1024 return calibrated_stars, result.matches, result.matchMeta, result.photoCalib
1025
1026 def _apply_photometry(self, exposure, background):
1027 """Apply the photometric model attached to the exposure to the
1028 exposure's pixels and an associated background model.
1029
1030 Parameters
1031 ----------
1032 exposure : `lsst.afw.image.Exposure`
1033 Exposure with the target `lsst.afw.image.PhotoCalib` attached.
1034 On return, pixel values will be calibrated and an identity
1035 photometric transform will be attached.
1036 background : `lsst.afw.math.BackgroundList`
1037 Background model to convert to nanojansky units in place.
1038 """
1039 photo_calib = exposure.getPhotoCalib()
1040 exposure.maskedImage = photo_calib.calibrateImage(exposure.maskedImage)
1041 identity = afwImage.PhotoCalib(1.0,
1042 photo_calib.getCalibrationErr(),
1043 bbox=exposure.getBBox())
1044 exposure.setPhotoCalib(identity)
1045 exposure.metadata["BUNIT"] = "nJy"
1046
1047 assert photo_calib._isConstant, \
1048 "Background calibration assumes a constant PhotoCalib; PhotoCalTask should always return that."
1049 for bg in background:
1050 # The statsImage is a view, but we can't assign to a function call in python.
1051 binned_image = bg[0].getStatsImage()
1052 binned_image *= photo_calib.getCalibrationMean()
1053
1054 def _summarize(self, exposure, stars, background):
1055 """Compute summary statistics on the exposure and update in-place the
1056 calibrations attached to it.
1057
1058 Parameters
1059 ----------
1060 exposure : `lsst.afw.image.Exposure`
1061 Exposure that was calibrated, to get PSF and other metadata from.
1062 Should be in instrumental units with the photometric calibration
1063 attached.
1064 Modified to contain the computed summary statistics.
1065 stars : `SourceCatalog`
1066 Good stars selected used in calibration.
1067 background : `lsst.afw.math.BackgroundList`
1068 Background that was fit to the exposure during detection of the
1069 above stars. Should be in instrumental units.
1070 """
1071 summary = self.compute_summary_stats.run(exposure, stars, background)
1072 exposure.info.setSummaryStats(summary)
1073
1074 def _recordMaskedPixelFractions(self, exposure):
1075 """Record the fraction of all the pixels in an exposure
1076 that are masked with a given flag. Each fraction is
1077 recorded in the task metadata. One record per flag type.
1078
1079 Parameters
1080 ----------
1081 exposure : `lsst.afw.image.ExposureF`
1082 The target exposure to calculate masked pixel fractions for.
1083 """
1084
1085 mask = exposure.mask
1086 maskPlanes = list(mask.getMaskPlaneDict().keys())
1087 for maskPlane in maskPlanes:
1088 self.metadata[f"{maskPlane.lower()}_mask_fraction"] = (
1089 evaluateMaskFraction(mask, maskPlane)
1090 )
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)
_measure_aperture_correction(self, exposure, bright_sources)
_find_stars(self, exposure, background, id_generator)
__init__(self, initial_stars_schema=None, **kwargs)
_summarize(self, exposure, stars, background)
run(self, *, exposures, id_generator=None, result=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