LSST Applications g0f08755f38+82efc23009,g12f32b3c4e+e7bdf1200e,g1653933729+a8ce1bb630,g1a0ca8cf93+50eff2b06f,g28da252d5a+52db39f6a5,g2bbee38e9b+37c5a29d61,g2bc492864f+37c5a29d61,g2cdde0e794+c05ff076ad,g3156d2b45e+41e33cbcdc,g347aa1857d+37c5a29d61,g35bb328faa+a8ce1bb630,g3a166c0a6a+37c5a29d61,g3e281a1b8c+fb992f5633,g414038480c+7f03dfc1b0,g41af890bb2+11b950c980,g5fbc88fb19+17cd334064,g6b1c1869cb+12dd639c9a,g781aacb6e4+a8ce1bb630,g80478fca09+72e9651da0,g82479be7b0+04c31367b4,g858d7b2824+82efc23009,g9125e01d80+a8ce1bb630,g9726552aa6+8047e3811d,ga5288a1d22+e532dc0a0b,gae0086650b+a8ce1bb630,gb58c049af0+d64f4d3760,gc28159a63d+37c5a29d61,gcf0d15dbbd+2acd6d4d48,gd7358e8bfb+778a810b6e,gda3e153d99+82efc23009,gda6a2b7d83+2acd6d4d48,gdaeeff99f8+1711a396fd,ge2409df99d+6b12de1076,ge79ae78c31+37c5a29d61,gf0baf85859+d0a5978c5a,gf3967379c6+4954f8c433,gfb92a5be7c+82efc23009,gfec2e1e490+2aaed99252,w.2024.46
LSST Data Management Base Package
Loading...
Searching...
No Matches
calibrate.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__ = ["CalibrateConfig", "CalibrateTask"]
23
24import math
25import numpy as np
26
27from lsstDebug import getDebugFrame
28import lsst.pex.config as pexConfig
29import lsst.pipe.base as pipeBase
30import lsst.pipe.base.connectionTypes as cT
31import lsst.afw.table as afwTable
32from lsst.ip.diffim.utils import evaluateMaskFraction
33from lsst.meas.astrom import AstrometryTask, displayAstrometry, denormalizeMatches, AstrometryError
34from lsst.meas.algorithms import LoadReferenceObjectsConfig, SkyObjectsTask
35import lsst.daf.base as dafBase
36from lsst.afw.math import BackgroundList
37from lsst.afw.table import SourceTable
38from lsst.meas.algorithms import (SourceDetectionTask,
39 ReferenceObjectLoader,
40 SetPrimaryFlagsTask,
41 NormalizedCalibrationFluxTask)
42from lsst.meas.base import (SingleFrameMeasurementTask,
43 ApplyApCorrTask,
44 CatalogCalculationTask,
45 IdGenerator,
46 DetectorVisitIdGeneratorConfig)
47from lsst.meas.deblender import SourceDeblendTask
48from lsst.utils.timer import timeMethod
49from .photoCal import PhotoCalTask
50from .computeExposureSummaryStats import ComputeExposureSummaryStatsTask
51
52
53class _EmptyTargetTask(pipeBase.PipelineTask):
54 """
55 This is a placeholder target for CreateSummaryMetrics and must be retargeted at runtime.
56 CreateSummaryMetrics should target an analysis tool task, but that would, at the time
57 of writing, result in a circular import.
58
59 As a result, this class should not be used for anything else.
60 """
61 ConfigClass = pipeBase.PipelineTaskConfig
62
63 def __init__(self, **kwargs) -> None:
64 raise NotImplementedError(
65 "doCreateSummaryMetrics is set to True, in which case "
66 "createSummaryMetrics must be retargeted."
67 )
68
69
70class CalibrateConnections(pipeBase.PipelineTaskConnections, dimensions=("instrument", "visit", "detector"),
71 defaultTemplates={}):
72
73 icSourceSchema = cT.InitInput(
74 doc="Schema produced by characterize image task, used to initialize this task",
75 name="icSrc_schema",
76 storageClass="SourceCatalog",
77 )
78
79 outputSchema = cT.InitOutput(
80 doc="Schema after CalibrateTask has been initialized",
81 name="src_schema",
82 storageClass="SourceCatalog",
83 )
84
85 exposure = cT.Input(
86 doc="Input image to calibrate",
87 name="icExp",
88 storageClass="ExposureF",
89 dimensions=("instrument", "visit", "detector"),
90 )
91
92 background = cT.Input(
93 doc="Backgrounds determined by characterize task",
94 name="icExpBackground",
95 storageClass="Background",
96 dimensions=("instrument", "visit", "detector"),
97 )
98
99 icSourceCat = cT.Input(
100 doc="Source catalog created by characterize task",
101 name="icSrc",
102 storageClass="SourceCatalog",
103 dimensions=("instrument", "visit", "detector"),
104 )
105
106 astromRefCat = cT.PrerequisiteInput(
107 doc="Reference catalog to use for astrometry",
108 name="gaia_dr3_20230707",
109 storageClass="SimpleCatalog",
110 dimensions=("skypix",),
111 deferLoad=True,
112 multiple=True,
113 )
114
115 photoRefCat = cT.PrerequisiteInput(
116 doc="Reference catalog to use for photometric calibration",
117 name="ps1_pv3_3pi_20170110",
118 storageClass="SimpleCatalog",
119 dimensions=("skypix",),
120 deferLoad=True,
121 multiple=True
122 )
123
124 outputExposure = cT.Output(
125 doc="Exposure after running calibration task",
126 name="calexp",
127 storageClass="ExposureF",
128 dimensions=("instrument", "visit", "detector"),
129 )
130
131 outputCat = cT.Output(
132 doc="Source catalog produced in calibrate task",
133 name="src",
134 storageClass="SourceCatalog",
135 dimensions=("instrument", "visit", "detector"),
136 )
137
138 outputBackground = cT.Output(
139 doc="Background models estimated in calibration task",
140 name="calexpBackground",
141 storageClass="Background",
142 dimensions=("instrument", "visit", "detector"),
143 )
144
145 outputSummaryMetrics = cT.Output(
146 doc="Summary metrics created by the calibration task",
147 name="calexpSummary_metrics",
148 storageClass="MetricMeasurementBundle",
149 dimensions=("instrument", "visit", "detector"),
150 )
151
152 matches = cT.Output(
153 doc="Source/refObj matches from the astrometry solver",
154 name="srcMatch",
155 storageClass="Catalog",
156 dimensions=("instrument", "visit", "detector"),
157 )
158
159 matchesDenormalized = cT.Output(
160 doc="Denormalized matches from astrometry solver",
161 name="srcMatchFull",
162 storageClass="Catalog",
163 dimensions=("instrument", "visit", "detector"),
164 )
165
166 def __init__(self, *, config=None):
167 super().__init__(config=config)
168
169 if config.doAstrometry is False:
170 self.prerequisiteInputs.remove("astromRefCat")
171 if config.doPhotoCal is False:
172 self.prerequisiteInputs.remove("photoRefCat")
173
174 if config.doWriteMatches is False or config.doAstrometry is False:
175 self.outputs.remove("matches")
176 if config.doWriteMatchesDenormalized is False or config.doAstrometry is False:
177 self.outputs.remove("matchesDenormalized")
178
179 if config.doCreateSummaryMetrics is False:
180 self.outputs.remove("outputSummaryMetrics")
181
182
183class CalibrateConfig(pipeBase.PipelineTaskConfig, pipelineConnections=CalibrateConnections):
184 """Config for CalibrateTask."""
185
186 doWrite = pexConfig.Field(
187 dtype=bool,
188 default=True,
189 doc="Save calibration results?",
190 )
191 doWriteHeavyFootprintsInSources = pexConfig.Field(
192 dtype=bool,
193 default=True,
194 doc="Include HeavyFootprint data in source table? If false then heavy "
195 "footprints are saved as normal footprints, which saves some space"
196 )
197 doWriteMatches = pexConfig.Field(
198 dtype=bool,
199 default=True,
200 doc="Write reference matches (ignored if doWrite or doAstrometry false)?",
201 )
202 doWriteMatchesDenormalized = pexConfig.Field(
203 dtype=bool,
204 default=True,
205 doc=("Write reference matches in denormalized format? "
206 "This format uses more disk space, but is more convenient to "
207 "read for debugging. Ignored if doWriteMatches=False or doWrite=False."),
208 )
209 doAstrometry = pexConfig.Field(
210 dtype=bool,
211 default=True,
212 doc="Perform astrometric calibration?",
213 )
214 astromRefObjLoader = pexConfig.ConfigField(
215 dtype=LoadReferenceObjectsConfig,
216 doc="reference object loader for astrometric calibration",
217 )
218 photoRefObjLoader = pexConfig.ConfigField(
219 dtype=LoadReferenceObjectsConfig,
220 doc="reference object loader for photometric calibration",
221 )
222 astrometry = pexConfig.ConfigurableField(
223 target=AstrometryTask,
224 doc="Perform astrometric calibration to refine the WCS",
225 )
226 requireAstrometry = pexConfig.Field(
227 dtype=bool,
228 default=True,
229 doc=("Raise an exception if astrometry fails? Ignored if doAstrometry "
230 "false."),
231 )
232 doPhotoCal = pexConfig.Field(
233 dtype=bool,
234 default=True,
235 doc="Perform phometric calibration?",
236 )
237 requirePhotoCal = pexConfig.Field(
238 dtype=bool,
239 default=True,
240 doc=("Raise an exception if photoCal fails? Ignored if doPhotoCal "
241 "false."),
242 )
243 photoCal = pexConfig.ConfigurableField(
244 target=PhotoCalTask,
245 doc="Perform photometric calibration",
246 )
247 icSourceFieldsToCopy = pexConfig.ListField(
248 dtype=str,
249 default=("calib_psf_candidate", "calib_psf_used", "calib_psf_reserved"),
250 doc=("Fields to copy from the icSource catalog to the output catalog "
251 "for matching sources Any missing fields will trigger a "
252 "RuntimeError exception. Ignored if icSourceCat is not provided.")
253 )
254 matchRadiusPix = pexConfig.Field(
255 dtype=float,
256 default=3,
257 doc=("Match radius for matching icSourceCat objects to sourceCat "
258 "objects (pixels)"),
259 )
260 checkUnitsParseStrict = pexConfig.Field(
261 doc=("Strictness of Astropy unit compatibility check, can be 'raise', "
262 "'warn' or 'silent'"),
263 dtype=str,
264 default="raise",
265 )
266 detection = pexConfig.ConfigurableField(
267 target=SourceDetectionTask,
268 doc="Detect sources"
269 )
270 doDeblend = pexConfig.Field(
271 dtype=bool,
272 default=True,
273 doc="Run deblender input exposure"
274 )
275 deblend = pexConfig.ConfigurableField(
276 target=SourceDeblendTask,
277 doc="Split blended sources into their components"
278 )
279 doSkySources = pexConfig.Field(
280 dtype=bool,
281 default=True,
282 doc="Generate sky sources?",
283 )
284 skySources = pexConfig.ConfigurableField(
285 target=SkyObjectsTask,
286 doc="Generate sky sources",
287 )
288 measurement = pexConfig.ConfigurableField(
289 target=SingleFrameMeasurementTask,
290 doc="Measure sources"
291 )
292 doNormalizedCalibration = pexConfig.Field(
293 dtype=bool,
294 default=True,
295 doc="Use normalized calibration flux (e.g. compensated tophats)?",
296 )
297 normalizedCalibrationFlux = pexConfig.ConfigurableField(
298 target=NormalizedCalibrationFluxTask,
299 doc="Task to normalize the calibration flux (e.g. compensated tophats).",
300 )
301 postCalibrationMeasurement = pexConfig.ConfigurableField(
302 target=SingleFrameMeasurementTask,
303 doc="Second round of measurement for plugins that need to be run after photocal"
304 )
305 setPrimaryFlags = pexConfig.ConfigurableField(
306 target=SetPrimaryFlagsTask,
307 doc=("Set flags for primary source classification in single frame "
308 "processing. True if sources are not sky sources and not a parent.")
309 )
310 doApCorr = pexConfig.Field(
311 dtype=bool,
312 default=True,
313 doc="Run subtask to apply aperture correction"
314 )
315 applyApCorr = pexConfig.ConfigurableField(
316 target=ApplyApCorrTask,
317 doc="Subtask to apply aperture corrections"
318 )
319 # If doApCorr is False, and the exposure does not have apcorrections
320 # already applied, the active plugins in catalogCalculation almost
321 # certainly should not contain the characterization plugin
322 catalogCalculation = pexConfig.ConfigurableField(
323 target=CatalogCalculationTask,
324 doc="Subtask to run catalogCalculation plugins on catalog"
325 )
326 doComputeSummaryStats = pexConfig.Field(
327 dtype=bool,
328 default=True,
329 doc="Run subtask to measure exposure summary statistics?"
330 )
331 computeSummaryStats = pexConfig.ConfigurableField(
332 target=ComputeExposureSummaryStatsTask,
333 doc="Subtask to run computeSummaryStats on exposure"
334 )
335 doCreateSummaryMetrics = pexConfig.Field(
336 dtype=bool,
337 default=False,
338 doc="Run the subtask to create summary metrics, and then write those metrics."
339 )
340 createSummaryMetrics = pexConfig.ConfigurableField(
341 target=_EmptyTargetTask,
342 doc="Subtask to create metrics from the summary stats. This must be retargeted, likely to an"
343 "analysis_tools task such as CalexpSummaryMetrics."
344 )
345 doWriteExposure = pexConfig.Field(
346 dtype=bool,
347 default=True,
348 doc="Write the calexp? If fakes have been added then we do not want to write out the calexp as a "
349 "normal calexp but as a fakes_calexp."
350 )
351 idGenerator = DetectorVisitIdGeneratorConfig.make_field()
352
353 def setDefaults(self):
354 super().setDefaults()
355 self.measurement.plugins.names |= ["base_CompensatedTophatFlux"]
356 self.postCalibrationMeasurement.plugins.names = ["base_LocalPhotoCalib", "base_LocalWcs"]
357 self.postCalibrationMeasurement.doReplaceWithNoise = False
358 for key in self.postCalibrationMeasurement.slots:
359 setattr(self.postCalibrationMeasurement.slots, key, None)
360 self.astromRefObjLoader.anyFilterMapsToThis = "phot_g_mean"
361 # The photoRefCat connection is the name to use for the colorterms.
362 self.photoCal.photoCatName = self.connections.photoRefCat
363
364 self.normalizedCalibrationFlux.do_measure_ap_corr = False
365 self.measurement.algorithms["base_CompensatedTophatFlux"].apertures = [12]
366
367 # TODO: Remove in DM-44658, streak masking to happen only in ip_diffim
368 # Keep track of which footprints contain streaks
369 self.measurement.plugins['base_PixelFlags'].masksFpAnywhere = ['STREAK']
370 self.measurement.plugins['base_PixelFlags'].masksFpCenter = ['STREAK']
371
372
373class CalibrateTask(pipeBase.PipelineTask):
374 """Calibrate an exposure: measure sources and perform astrometric and
375 photometric calibration.
376
377 Given an exposure with a good PSF model and aperture correction map(e.g. as
378 provided by `~lsst.pipe.tasks.characterizeImage.CharacterizeImageTask`),
379 perform the following operations:
380 - Run detection and measurement
381 - Run astrometry subtask to fit an improved WCS
382 - Run photoCal subtask to fit the exposure's photometric zero-point
383
384 Parameters
385 ----------
386 butler : `None`
387 Compatibility parameter. Should always be `None`.
388 astromRefObjLoader : `lsst.meas.algorithms.ReferenceObjectLoader`, optional
389 Unused in gen3: must be `None`.
390 photoRefObjLoader : `lsst.meas.algorithms.ReferenceObjectLoader`, optional
391 Unused in gen3: must be `None`.
392 icSourceSchema : `lsst.afw.table.Schema`, optional
393 Schema for the icSource catalog.
394 initInputs : `dict`, optional
395 Dictionary that can contain a key ``icSourceSchema`` containing the
396 input schema. If present will override the value of ``icSourceSchema``.
397
398 Raises
399 ------
400 RuntimeError
401 Raised if any of the following occur:
402 - isSourceCat is missing fields specified in icSourceFieldsToCopy.
403 - PipelineTask form of this task is initialized with reference object
404 loaders.
405
406 Notes
407 -----
408 Quantities set in exposure Metadata:
409
410 MAGZERO_RMS
411 MAGZERO's RMS == sigma reported by photoCal task
412 MAGZERO_NOBJ
413 Number of stars used == ngood reported by photoCal task
414 COLORTERM1
415 ?? (always 0.0)
416 COLORTERM2
417 ?? (always 0.0)
418 COLORTERM3
419 ?? (always 0.0)
420
421 Debugging:
422 CalibrateTask has a debug dictionary containing one key:
423
424 calibrate
425 frame (an int; <= 0 to not display) in which to display the exposure,
426 sources and matches. See @ref lsst.meas.astrom.displayAstrometry for
427 the meaning of the various symbols.
428 """
429
430 ConfigClass = CalibrateConfig
431 _DefaultName = "calibrate"
432
433 def __init__(self, astromRefObjLoader=None,
434 photoRefObjLoader=None, icSourceSchema=None,
435 initInputs=None, **kwargs):
436 super().__init__(**kwargs)
437
438 if initInputs is not None:
439 icSourceSchema = initInputs['icSourceSchema'].schema
440
441 if icSourceSchema is not None:
442 # use a schema mapper to avoid copying each field separately
443 self.schemaMapper = afwTable.SchemaMapper(icSourceSchema)
444 minimumSchema = afwTable.SourceTable.makeMinimalSchema()
445 self.schemaMapper.addMinimalSchema(minimumSchema, False)
446
447 # Add fields to copy from an icSource catalog
448 # and a field to indicate that the source matched a source in that
449 # catalog. If any fields are missing then raise an exception, but
450 # first find all missing fields in order to make the error message
451 # more useful.
452 self.calibSourceKey = self.schemaMapper.addOutputField(
453 afwTable.Field["Flag"]("calib_detected",
454 "Source was detected as an icSource"))
455 missingFieldNames = []
456 for fieldName in self.config.icSourceFieldsToCopy:
457 try:
458 schemaItem = icSourceSchema.find(fieldName)
459 except Exception:
460 missingFieldNames.append(fieldName)
461 else:
462 # field found; if addMapping fails then raise an exception
463 self.schemaMapper.addMapping(schemaItem.getKey())
464
465 if missingFieldNames:
466 raise RuntimeError("isSourceCat is missing fields {} "
467 "specified in icSourceFieldsToCopy"
468 .format(missingFieldNames))
469
470 # produce a temporary schema to pass to the subtasks; finalize it
471 # later
472 self.schema = self.schemaMapper.editOutputSchema()
473 else:
474 self.schemaMapper = None
475 self.schema = afwTable.SourceTable.makeMinimalSchema()
476 afwTable.CoordKey.addErrorFields(self.schema)
477 self.makeSubtask('detection', schema=self.schema)
478
480
481 if self.config.doDeblend:
482 self.makeSubtask("deblend", schema=self.schema)
483 if self.config.doSkySources:
484 self.makeSubtask("skySources")
485 self.skySourceKey = self.schema.addField("sky_source", type="Flag", doc="Sky objects.")
486 self.makeSubtask('measurement', schema=self.schema,
487 algMetadata=self.algMetadata)
488 if self.config.doNormalizedCalibration:
489 self.makeSubtask('normalizedCalibrationFlux', schema=self.schema)
490 self.makeSubtask('postCalibrationMeasurement', schema=self.schema,
491 algMetadata=self.algMetadata)
492 self.makeSubtask("setPrimaryFlags", schema=self.schema, isSingleFrame=True)
493 if self.config.doApCorr:
494 self.makeSubtask('applyApCorr', schema=self.schema)
495 self.makeSubtask('catalogCalculation', schema=self.schema)
496
497 if self.config.doAstrometry:
498 self.makeSubtask("astrometry", refObjLoader=astromRefObjLoader,
499 schema=self.schema)
500 if self.config.doPhotoCal:
501 self.makeSubtask("photoCal", refObjLoader=photoRefObjLoader,
502 schema=self.schema)
503 if self.config.doComputeSummaryStats:
504 self.makeSubtask('computeSummaryStats')
505 if self.config.doCreateSummaryMetrics:
506 self.makeSubtask('createSummaryMetrics')
507
508 if initInputs is not None and (astromRefObjLoader is not None or photoRefObjLoader is not None):
509 raise RuntimeError("PipelineTask form of this task should not be initialized with "
510 "reference object loaders.")
511
512 if self.schemaMapper is not None:
513 # finalize the schema
514 self.schema = self.schemaMapper.getOutputSchema()
515 self.schema.checkUnits(parse_strict=self.config.checkUnitsParseStrict)
516
517 sourceCatSchema = afwTable.SourceCatalog(self.schema)
518 sourceCatSchema.getTable().setMetadata(self.algMetadata)
519 self.outputSchema = sourceCatSchema
520
521 def runQuantum(self, butlerQC, inputRefs, outputRefs):
522 inputs = butlerQC.get(inputRefs)
523 inputs['idGenerator'] = self.config.idGenerator.apply(butlerQC.quantum.dataId)
524
525 if self.config.doAstrometry:
526 refObjLoader = ReferenceObjectLoader(dataIds=[ref.datasetRef.dataId
527 for ref in inputRefs.astromRefCat],
528 refCats=inputs.pop('astromRefCat'),
529 name=self.config.connections.astromRefCat,
530 config=self.config.astromRefObjLoader, log=self.log)
531 self.astrometry.setRefObjLoader(refObjLoader)
532
533 if self.config.doPhotoCal:
534 photoRefObjLoader = ReferenceObjectLoader(dataIds=[ref.datasetRef.dataId
535 for ref in inputRefs.photoRefCat],
536 refCats=inputs.pop('photoRefCat'),
537 name=self.config.connections.photoRefCat,
538 config=self.config.photoRefObjLoader,
539 log=self.log)
540 self.photoCal.match.setRefObjLoader(photoRefObjLoader)
541
542 outputs = self.run(**inputs)
543
544 if self.config.doWriteMatches and self.config.doAstrometry:
545 if outputs.astromMatches is not None:
546 normalizedMatches = afwTable.packMatches(outputs.astromMatches)
547 normalizedMatches.table.setMetadata(outputs.matchMeta)
548 if self.config.doWriteMatchesDenormalized:
549 denormMatches = denormalizeMatches(outputs.astromMatches, outputs.matchMeta)
550 outputs.matchesDenormalized = denormMatches
551 outputs.matches = normalizedMatches
552 else:
553 del outputRefs.matches
554 if self.config.doWriteMatchesDenormalized:
555 del outputRefs.matchesDenormalized
556 butlerQC.put(outputs, outputRefs)
557
558 @timeMethod
559 def run(self, exposure, background=None,
560 icSourceCat=None, idGenerator=None):
561 """Calibrate an exposure.
562
563 Parameters
564 ----------
565 exposure : `lsst.afw.image.ExposureF`
566 Exposure to calibrate.
567 background : `lsst.afw.math.BackgroundList`, optional
568 Initial model of background already subtracted from exposure.
569 icSourceCat : `lsst.afw.image.SourceCatalog`, optional
570 SourceCatalog from CharacterizeImageTask from which we can copy
571 some fields.
572 idGenerator : `lsst.meas.base.IdGenerator`, optional
573 Object that generates source IDs and provides RNG seeds.
574
575 Returns
576 -------
577 result : `lsst.pipe.base.Struct`
578 Results as a struct with attributes:
579
580 ``exposure``
581 Characterized exposure (`lsst.afw.image.ExposureF`).
582 ``sourceCat``
583 Detected sources (`lsst.afw.table.SourceCatalog`).
584 ``outputBackground``
585 Model of subtracted background (`lsst.afw.math.BackgroundList`).
586 ``astromMatches``
587 List of source/ref matches from astrometry solver.
588 ``matchMeta``
589 Metadata from astrometry matches.
590 ``outputExposure``
591 Another reference to ``exposure`` for compatibility.
592 ``outputCat``
593 Another reference to ``sourceCat`` for compatibility.
594 """
595 # detect, deblend and measure sources
596 if idGenerator is None:
597 idGenerator = IdGenerator()
598
599 if background is None:
600 background = BackgroundList()
601 table = SourceTable.make(self.schema, idGenerator.make_table_id_factory())
602 table.setMetadata(self.algMetadata)
603
604 detRes = self.detection.run(table=table, exposure=exposure,
605 doSmooth=True)
606
607 self.recordMaskedPixelFractions(exposure)
608 self.metadata['positive_footprint_count'] = detRes.numPos
609 self.metadata['negative_footprint_count'] = detRes.numNeg
610
611 sourceCat = detRes.sources
612 if detRes.background:
613 for bg in detRes.background:
614 background.append(bg)
615 if self.config.doSkySources:
616 skySourceFootprints = self.skySources.run(mask=exposure.mask, seed=idGenerator.catalog_id)
617 if skySourceFootprints:
618 self.metadata['sky_footprint_count'] = len(skySourceFootprints)
619 for foot in skySourceFootprints:
620 s = sourceCat.addNew()
621 s.setFootprint(foot)
622 s.set(self.skySourceKey, True)
623 if self.config.doDeblend:
624 self.deblend.run(exposure=exposure, sources=sourceCat)
625 if not sourceCat.isContiguous():
626 sourceCat = sourceCat.copy(deep=True)
627 self.metadata['source_count'] = len(sourceCat)
628 self.measurement.run(
629 measCat=sourceCat,
630 exposure=exposure,
631 exposureId=idGenerator.catalog_id,
632 )
633 self.metadata['saturated_source_count'] = (
634 np.sum(sourceCat['base_PixelFlags_flag_saturated'])
635 )
636 self.metadata['bad_source_count'] = (
637 np.sum(sourceCat['base_PixelFlags_flag_bad'])
638 )
639 if self.config.doNormalizedCalibration:
640 self.normalizedCalibrationFlux.run(
641 exposure=exposure,
642 catalog=sourceCat,
643 )
644 if self.config.doApCorr:
645 apCorrMap = exposure.getInfo().getApCorrMap()
646 if apCorrMap is None:
647 self.log.warning("Image does not have valid aperture correction map for %r; "
648 "skipping aperture correction", idGenerator.catalog_id)
649 else:
650 self.applyApCorr.run(
651 catalog=sourceCat,
652 apCorrMap=apCorrMap,
653 )
654 self.catalogCalculation.run(sourceCat)
655
656 self.setPrimaryFlags.run(sourceCat)
657
658 if icSourceCat is not None and \
659 len(self.config.icSourceFieldsToCopy) > 0:
660 self.copyIcSourceFields(icSourceCat=icSourceCat,
661 sourceCat=sourceCat)
662
663 # TODO DM-11568: this contiguous check-and-copy could go away if we
664 # reserve enough space during SourceDetection and/or SourceDeblend.
665 # NOTE: sourceSelectors require contiguous catalogs, so ensure
666 # contiguity now, so views are preserved from here on.
667 if not sourceCat.isContiguous():
668 sourceCat = sourceCat.copy(deep=True)
669
670 # perform astrometry calibration:
671 # fit an improved WCS and update the exposure's WCS in place
672 astromMatches = None
673 matchMeta = None
674 if self.config.doAstrometry:
675 try:
676 astromRes = self.astrometry.run(exposure=exposure, sourceCat=sourceCat)
677 astromMatches = astromRes.matches
678 matchMeta = astromRes.matchMeta
679 except AstrometryError as e:
680 # Maintain old behavior of not stopping for astrometry errors.
681 self.log.warning(e)
682 if exposure.getWcs() is None:
683 if self.config.requireAstrometry:
684 raise RuntimeError(f"WCS fit failed for {idGenerator} and requireAstrometry "
685 "is True.")
686 else:
687 self.log.warning("Unable to perform astrometric calibration for %r but "
688 "requireAstrometry is False: attempting to proceed...",
689 idGenerator)
690
691 # compute photometric calibration
692 if self.config.doPhotoCal:
693 if np.all(np.isnan(sourceCat["coord_ra"])) or np.all(np.isnan(sourceCat["coord_dec"])):
694 if self.config.requirePhotoCal:
695 raise RuntimeError(f"Astrometry failed for {idGenerator}, so cannot do "
696 "photoCal, but requirePhotoCal is True.")
697 self.log.warning("Astrometry failed for %r, so cannot do photoCal. requirePhotoCal "
698 "is False, so skipping photometric calibration and setting photoCalib "
699 "to None. Attempting to proceed...", idGenerator)
700 exposure.setPhotoCalib(None)
701 self.setMetadata(exposure=exposure, photoRes=None)
702 else:
703 try:
704 photoRes = self.photoCal.run(
705 exposure, sourceCat=sourceCat, expId=idGenerator.catalog_id
706 )
707 exposure.setPhotoCalib(photoRes.photoCalib)
708 # TODO: reword this to phrase it in terms of the
709 # calibration factor?
710 self.log.info("Photometric zero-point: %f",
711 photoRes.photoCalib.instFluxToMagnitude(1.0))
712 self.setMetadata(exposure=exposure, photoRes=photoRes)
713 except Exception as e:
714 if self.config.requirePhotoCal:
715 raise
716 self.log.warning("Unable to perform photometric calibration "
717 "(%s): attempting to proceed", e)
718 self.setMetadata(exposure=exposure, photoRes=None)
719
720 self.postCalibrationMeasurement.run(
721 measCat=sourceCat,
722 exposure=exposure,
723 exposureId=idGenerator.catalog_id,
724 )
725
726 summaryMetrics = None
727 if self.config.doComputeSummaryStats:
728 summary = self.computeSummaryStats.run(exposure=exposure,
729 sources=sourceCat,
730 background=background)
731 exposure.getInfo().setSummaryStats(summary)
732 if self.config.doCreateSummaryMetrics:
733 summaryMetrics = self.createSummaryMetrics.run(data=summary.__dict__).metrics
734
735 frame = getDebugFrame(self._display, "calibrate")
736 if frame:
737 displayAstrometry(
738 sourceCat=sourceCat,
739 exposure=exposure,
740 matches=astromMatches,
741 frame=frame,
742 pause=False,
743 )
744
745 return pipeBase.Struct(
746 sourceCat=sourceCat,
747 astromMatches=astromMatches,
748 matchMeta=matchMeta,
749 outputExposure=exposure,
750 outputCat=sourceCat,
751 outputBackground=background,
752 outputSummaryMetrics=summaryMetrics
753 )
754
755 def setMetadata(self, exposure, photoRes=None):
756 """Set task and exposure metadata.
757
758 Logs a warning continues if needed data is missing.
759
760 Parameters
761 ----------
762 exposure : `lsst.afw.image.ExposureF`
763 Exposure to set metadata on.
764 photoRes : `lsst.pipe.base.Struct`, optional
765 Result of running photoCal task.
766 """
767 if photoRes is None:
768 return
769
770 metadata = exposure.getMetadata()
771
772 # convert zero-point to (mag/sec/adu) for task MAGZERO metadata
773 try:
774 exposureTime = exposure.getInfo().getVisitInfo().getExposureTime()
775 magZero = photoRes.zp - 2.5*math.log10(exposureTime)
776 except Exception:
777 self.log.warning("Could not set normalized MAGZERO in header: no "
778 "exposure time")
779 magZero = math.nan
780
781 try:
782 metadata.set('MAGZERO', magZero)
783 metadata.set('MAGZERO_RMS', photoRes.sigma)
784 metadata.set('MAGZERO_NOBJ', photoRes.ngood)
785 metadata.set('COLORTERM1', 0.0)
786 metadata.set('COLORTERM2', 0.0)
787 metadata.set('COLORTERM3', 0.0)
788 except Exception as e:
789 self.log.warning("Could not set exposure metadata: %s", e)
790
791 def copyIcSourceFields(self, icSourceCat, sourceCat):
792 """Match sources in an icSourceCat and a sourceCat and copy fields.
793
794 The fields copied are those specified by
795 ``config.icSourceFieldsToCopy``.
796
797 Parameters
798 ----------
799 icSourceCat : `lsst.afw.table.SourceCatalog`
800 Catalog from which to copy fields.
801 sourceCat : `lsst.afw.table.SourceCatalog`
802 Catalog to which to copy fields.
803
804 Raises
805 ------
806 RuntimeError
807 Raised if any of the following occur:
808 - icSourceSchema and icSourceKeys are not specified.
809 - icSourceCat and sourceCat are not specified.
810 - icSourceFieldsToCopy is empty.
811 """
812 if self.schemaMapper is None:
813 raise RuntimeError("To copy icSource fields you must specify "
814 "icSourceSchema and icSourceKeys when "
815 "constructing this task")
816 if icSourceCat is None or sourceCat is None:
817 raise RuntimeError("icSourceCat and sourceCat must both be "
818 "specified")
819 if len(self.config.icSourceFieldsToCopy) == 0:
820 self.log.warning("copyIcSourceFields doing nothing because "
821 "icSourceFieldsToCopy is empty")
822 return
823
825 mc.findOnlyClosest = False # return all matched objects
826 matches = afwTable.matchXy(icSourceCat, sourceCat,
827 self.config.matchRadiusPix, mc)
828 if self.config.doDeblend:
829 deblendKey = sourceCat.schema["deblend_nChild"].asKey()
830 # if deblended, keep children
831 matches = [m for m in matches if m[1].get(deblendKey) == 0]
832
833 # Because we had to allow multiple matches to handle parents, we now
834 # need to prune to the best matches
835 # closest matches as a dict of icSourceCat source ID:
836 # (icSourceCat source, sourceCat source, distance in pixels)
837 bestMatches = {}
838 for m0, m1, d in matches:
839 id0 = m0.getId()
840 match = bestMatches.get(id0)
841 if match is None or d <= match[2]:
842 bestMatches[id0] = (m0, m1, d)
843 matches = list(bestMatches.values())
844
845 # Check that no sourceCat sources are listed twice (we already know
846 # that each match has a unique icSourceCat source ID, due to using
847 # that ID as the key in bestMatches)
848 numMatches = len(matches)
849 numUniqueSources = len(set(m[1].getId() for m in matches))
850 if numUniqueSources != numMatches:
851 self.log.warning("%d icSourceCat sources matched only %d sourceCat "
852 "sources", numMatches, numUniqueSources)
853
854 self.log.info("Copying flags from icSourceCat to sourceCat for "
855 "%d sources", numMatches)
856
857 # For each match: set the calibSourceKey flag and copy the desired
858 # fields
859 for icSrc, src, d in matches:
860 src.setFlag(self.calibSourceKey, True)
861 # src.assign copies the footprint from icSrc, which we don't want
862 # (DM-407)
863 # so set icSrc's footprint to src's footprint before src.assign,
864 # then restore it
865 icSrcFootprint = icSrc.getFootprint()
866 try:
867 icSrc.setFootprint(src.getFootprint())
868 src.assign(icSrc, self.schemaMapper)
869 finally:
870 icSrc.setFootprint(icSrcFootprint)
871
872 def recordMaskedPixelFractions(self, exposure):
873 """Record the fraction of all the pixels in an exposure
874 that are masked with a given flag. Each fraction is
875 recorded in the task metadata. One record per flag type.
876
877 Parameters
878 ----------
879 exposure : `lsst.afw.image.ExposureF`
880 The target exposure to calculate masked pixel fractions for.
881 """
882
883 mask = exposure.mask
884 metricsMaskPlanes = list(mask.getMaskPlaneDict().keys())
885 for maskPlane in metricsMaskPlanes:
886 self.metadata[f"{maskPlane.lower()}_mask_fraction"] = (
887 evaluateMaskFraction(mask, maskPlane)
888 )
Pass parameters to algorithms that match list of sources.
Definition Match.h:45
A mapping between the keys of two Schemas, used to copy data between them.
Class for storing ordered metadata with comments.
run(self, exposure, background=None, icSourceCat=None, idGenerator=None)
Definition calibrate.py:560
runQuantum(self, butlerQC, inputRefs, outputRefs)
Definition calibrate.py:521
__init__(self, astromRefObjLoader=None, photoRefObjLoader=None, icSourceSchema=None, initInputs=None, **kwargs)
Definition calibrate.py:435
copyIcSourceFields(self, icSourceCat, sourceCat)
Definition calibrate.py:791
setMetadata(self, exposure, photoRes=None)
Definition calibrate.py:755
BaseCatalog packMatches(std::vector< Match< Record1, Record2 > > const &matches)
Return a table representation of a MatchVector that can be used to persist it.
Definition Match.cc:432
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
A description of a field in a table.
Definition Field.h:24