lsst.meas.base g11fdef2535+0f59bc737c
Loading...
Searching...
No Matches
forcedPhotCcd.py
Go to the documentation of this file.
1# This file is part of meas_base.
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
22import warnings
23
24import pandas as pd
25import numpy as np
26
27import lsst.pex.config
29import lsst.pipe.base
30import lsst.geom
32import lsst.afw.geom
33import lsst.afw.image
34import lsst.afw.table
35import lsst.sphgeom
36
37from lsst.pipe.base import PipelineTaskConnections
38import lsst.pipe.base.connectionTypes as cT
39
40import lsst.pipe.base as pipeBase
41from lsst.skymap import BaseSkyMap
42
43from .forcedMeasurement import ForcedMeasurementTask
44from .applyApCorr import ApplyApCorrTask
45from .catalogCalculation import CatalogCalculationTask
46from ._id_generator import DetectorVisitIdGeneratorConfig
47
48__all__ = ("ForcedPhotCcdConfig", "ForcedPhotCcdTask",
49 "ForcedPhotCcdFromDataFrameTask", "ForcedPhotCcdFromDataFrameConfig")
50
51
52class ForcedPhotCcdConnections(PipelineTaskConnections,
53 dimensions=("instrument", "visit", "detector", "skymap", "tract"),
54 defaultTemplates={"inputCoaddName": "deep",
55 "inputName": "calexp",
56 "skyWcsName": "gbdesAstrometricFit",
57 "photoCalibName": "fgcm"}):
58 inputSchema = cT.InitInput(
59 doc="Schema for the input measurement catalogs.",
60 name="{inputCoaddName}Coadd_ref_schema",
61 storageClass="SourceCatalog",
62 )
63 outputSchema = cT.InitOutput(
64 doc="Schema for the output forced measurement catalogs.",
65 name="forced_src_schema",
66 storageClass="SourceCatalog",
67 )
68 exposure = cT.Input(
69 doc="Input exposure to perform photometry on.",
70 name="{inputName}",
71 storageClass="ExposureF",
72 dimensions=["instrument", "visit", "detector"],
73 )
74 refCat = cT.Input(
75 doc="Catalog of shapes and positions at which to force photometry.",
76 name="{inputCoaddName}Coadd_ref",
77 storageClass="SourceCatalog",
78 dimensions=["skymap", "tract", "patch"],
79 multiple=True,
80 deferLoad=True,
81 )
82 skyMap = cT.Input(
83 doc="SkyMap dataset that defines the coordinate system of the reference catalog.",
84 name=BaseSkyMap.SKYMAP_DATASET_TYPE_NAME,
85 storageClass="SkyMap",
86 dimensions=["skymap"],
87 )
88 skyCorr = cT.Input(
89 doc="Input Sky Correction to be subtracted from the calexp if doApplySkyCorr=True",
90 name="skyCorr",
91 storageClass="Background",
92 dimensions=("instrument", "visit", "detector"),
93 )
94 externalSkyWcsTractCatalog = cT.Input(
95 doc=("Per-tract, per-visit wcs calibrations. These catalogs use the detector "
96 "id for the catalog id, sorted on id for fast lookup."),
97 name="{skyWcsName}SkyWcsCatalog",
98 storageClass="ExposureCatalog",
99 dimensions=["instrument", "visit", "tract"],
100 )
101 externalSkyWcsGlobalCatalog = cT.Input(
102 doc=("Per-visit wcs calibrations computed globally (with no tract information). "
103 "These catalogs use the detector id for the catalog id, sorted on id for "
104 "fast lookup."),
105 name="finalVisitSummary",
106 storageClass="ExposureCatalog",
107 dimensions=["instrument", "visit"],
108 )
109 externalPhotoCalibTractCatalog = cT.Input(
110 doc=("Per-tract, per-visit photometric calibrations. These catalogs use the "
111 "detector id for the catalog id, sorted on id for fast lookup."),
112 name="{photoCalibName}PhotoCalibCatalog",
113 storageClass="ExposureCatalog",
114 dimensions=["instrument", "visit", "tract"],
115 )
116 externalPhotoCalibGlobalCatalog = cT.Input(
117 doc=("Per-visit photometric calibrations computed globally (with no tract "
118 "information). These catalogs use the detector id for the catalog id, "
119 "sorted on id for fast lookup."),
120 name="finalVisitSummary",
121 storageClass="ExposureCatalog",
122 dimensions=["instrument", "visit"],
123 )
124 finalizedPsfApCorrCatalog = cT.Input(
125 doc=("Per-visit finalized psf models and aperture correction maps. "
126 "These catalogs use the detector id for the catalog id, "
127 "sorted on id for fast lookup."),
128 name="finalized_psf_ap_corr_catalog",
129 storageClass="ExposureCatalog",
130 dimensions=["instrument", "visit"],
131 )
132 measCat = cT.Output(
133 doc="Output forced photometry catalog.",
134 name="forced_src",
135 storageClass="SourceCatalog",
136 dimensions=["instrument", "visit", "detector", "skymap", "tract"],
137 )
138
139 def __init__(self, *, config=None):
140 super().__init__(config=config)
141 if not config.doApplySkyCorr:
142 self.inputs.remove("skyCorr")
143 if config.doApplyExternalSkyWcs:
144 if config.useGlobalExternalSkyWcs:
145 self.inputs.remove("externalSkyWcsTractCatalog")
146 else:
147 self.inputs.remove("externalSkyWcsGlobalCatalog")
148 else:
149 self.inputs.remove("externalSkyWcsTractCatalog")
150 self.inputs.remove("externalSkyWcsGlobalCatalog")
151 if config.doApplyExternalPhotoCalib:
152 if config.useGlobalExternalPhotoCalib:
153 self.inputs.remove("externalPhotoCalibTractCatalog")
154 else:
155 self.inputs.remove("externalPhotoCalibGlobalCatalog")
156 else:
157 self.inputs.remove("externalPhotoCalibTractCatalog")
158 self.inputs.remove("externalPhotoCalibGlobalCatalog")
159 if not config.doApplyFinalizedPsf:
160 self.inputs.remove("finalizedPsfApCorrCatalog")
161
162
163class ForcedPhotCcdConfig(pipeBase.PipelineTaskConfig,
164 pipelineConnections=ForcedPhotCcdConnections):
165 """Config class for forced measurement driver task."""
166 measurement = lsst.pex.config.ConfigurableField(
167 target=ForcedMeasurementTask,
168 doc="subtask to do forced measurement"
169 )
170 coaddName = lsst.pex.config.Field(
171 doc="coadd name: typically one of deep or goodSeeing",
172 dtype=str,
173 default="deep",
174 )
175 doApCorr = lsst.pex.config.Field(
176 dtype=bool,
177 default=True,
178 doc="Run subtask to apply aperture corrections"
179 )
180 applyApCorr = lsst.pex.config.ConfigurableField(
181 target=ApplyApCorrTask,
182 doc="Subtask to apply aperture corrections"
183 )
184 catalogCalculation = lsst.pex.config.ConfigurableField(
185 target=CatalogCalculationTask,
186 doc="Subtask to run catalogCalculation plugins on catalog"
187 )
188 doApplyUberCal = lsst.pex.config.Field(
189 dtype=bool,
190 doc="Apply meas_mosaic ubercal results to input calexps?",
191 default=False,
192 deprecated="Deprecated by DM-23352; use doApplyExternalPhotoCalib and doApplyExternalSkyWcs instead",
193 )
194 doApplyExternalPhotoCalib = lsst.pex.config.Field(
195 dtype=bool,
196 default=False,
197 doc=("Whether to apply external photometric calibration via an "
198 "`lsst.afw.image.PhotoCalib` object."),
199 )
200 useGlobalExternalPhotoCalib = lsst.pex.config.Field(
201 dtype=bool,
202 default=True,
203 doc=("When using doApplyExternalPhotoCalib, use 'global' calibrations "
204 "that are not run per-tract. When False, use per-tract photometric "
205 "calibration files.")
206 )
207 doApplyExternalSkyWcs = lsst.pex.config.Field(
208 dtype=bool,
209 default=False,
210 doc=("Whether to apply external astrometric calibration via an "
211 "`lsst.afw.geom.SkyWcs` object."),
212 )
213 useGlobalExternalSkyWcs = lsst.pex.config.Field(
214 dtype=bool,
215 default=True,
216 doc=("When using doApplyExternalSkyWcs, use 'global' calibrations "
217 "that are not run per-tract. When False, use per-tract wcs "
218 "files.")
219 )
220 doApplyFinalizedPsf = lsst.pex.config.Field(
221 dtype=bool,
222 default=False,
223 doc="Whether to apply finalized psf models and aperture correction map.",
224 )
225 doApplySkyCorr = lsst.pex.config.Field(
226 dtype=bool,
227 default=False,
228 doc="Apply sky correction?",
229 )
230 includePhotoCalibVar = lsst.pex.config.Field(
231 dtype=bool,
232 default=False,
233 doc="Add photometric calibration variance to warp variance plane?",
234 )
235 footprintSource = lsst.pex.config.ChoiceField(
236 dtype=str,
237 doc="Where to obtain footprints to install in the measurement catalog, prior to measurement.",
238 allowed={
239 "transformed": "Transform footprints from the reference catalog (downgrades HeavyFootprints).",
240 "psf": ("Use the scaled shape of the PSF at the position of each source (does not generate "
241 "HeavyFootprints)."),
242 },
243 optional=True,
244 default="transformed",
245 )
246 psfFootprintScaling = lsst.pex.config.Field(
247 dtype=float,
248 doc="Scaling factor to apply to the PSF shape when footprintSource='psf' (ignored otherwise).",
249 default=3.0,
250 )
251 idGenerator = DetectorVisitIdGeneratorConfig.make_field()
252
253 def setDefaults(self):
254 # Docstring inherited.
255 # Make catalogCalculation a no-op by default as no modelFlux is setup
256 # by default in ForcedMeasurementTask.
257 super().setDefaults()
258 self.measurement.plugins.names |= ['base_LocalPhotoCalib', 'base_LocalWcs']
259 self.catalogCalculation.plugins.names = []
260
261
262class ForcedPhotCcdTask(pipeBase.PipelineTask):
263 """A pipeline task for performing forced measurement on CCD images.
264
265 Parameters
266 ----------
267 butler : `None`
268 Deprecated and unused. Should always be `None`.
269 refSchema : `lsst.afw.table.Schema`, optional
270 The schema of the reference catalog, passed to the constructor of the
271 references subtask. Optional, but must be specified if ``initInputs``
272 is not; if both are specified, ``initInputs`` takes precedence.
273 initInputs : `dict`
274 Dictionary that can contain a key ``inputSchema`` containing the
275 schema. If present will override the value of ``refSchema``.
276 **kwds
277 Keyword arguments are passed to the supertask constructor.
278 """
279
280 ConfigClass = ForcedPhotCcdConfig
281 _DefaultName = "forcedPhotCcd"
282 dataPrefix = ""
283
284 def __init__(self, butler=None, refSchema=None, initInputs=None, **kwds):
285 super().__init__(**kwds)
286
287 if butler is not None:
288 warnings.warn("The 'butler' parameter is no longer used and can be safely removed.",
289 category=FutureWarning, stacklevel=2)
290 butler = None
291
292 if initInputs is not None:
293 refSchema = initInputs['inputSchema'].schema
294
295 if refSchema is None:
296 raise ValueError("No reference schema provided.")
297
298 self.makeSubtask("measurement", refSchema=refSchema)
299 # It is necessary to get the schema internal to the forced measurement
300 # task until such a time that the schema is not owned by the
301 # measurement task, but is passed in by an external caller.
302 if self.config.doApCorr:
303 self.makeSubtask("applyApCorr", schema=self.measurement.schema)
304 self.makeSubtask('catalogCalculation', schema=self.measurement.schema)
305 self.outputSchema = lsst.afw.table.SourceCatalog(self.measurement.schema)
306
307 def runQuantum(self, butlerQC, inputRefs, outputRefs):
308 inputs = butlerQC.get(inputRefs)
309
310 tract = butlerQC.quantum.dataId['tract']
311 skyMap = inputs.pop('skyMap')
312 inputs['refWcs'] = skyMap[tract].getWcs()
313
314 # Connections only exist if they are configured to be used.
315 skyCorr = inputs.pop('skyCorr', None)
316 if self.config.useGlobalExternalSkyWcs:
317 externalSkyWcsCatalog = inputs.pop('externalSkyWcsGlobalCatalog', None)
318 else:
319 externalSkyWcsCatalog = inputs.pop('externalSkyWcsTractCatalog', None)
320 if self.config.useGlobalExternalPhotoCalib:
321 externalPhotoCalibCatalog = inputs.pop('externalPhotoCalibGlobalCatalog', None)
322 else:
323 externalPhotoCalibCatalog = inputs.pop('externalPhotoCalibTractCatalog', None)
324 finalizedPsfApCorrCatalog = inputs.pop('finalizedPsfApCorrCatalog', None)
325
326 inputs['exposure'] = self.prepareCalibratedExposure(
327 inputs['exposure'],
328 skyCorr=skyCorr,
329 externalSkyWcsCatalog=externalSkyWcsCatalog,
330 externalPhotoCalibCatalog=externalPhotoCalibCatalog,
331 finalizedPsfApCorrCatalog=finalizedPsfApCorrCatalog
332 )
333
334 inputs['refCat'] = self.mergeAndFilterReferences(inputs['exposure'], inputs['refCat'],
335 inputs['refWcs'])
336
337 if inputs['refCat'] is None:
338 self.log.info("No WCS for exposure %s. No %s catalog will be written.",
339 butlerQC.quantum.dataId, outputRefs.measCat.datasetType.name)
340 else:
341 inputs['measCat'], inputs['exposureId'] = self.generateMeasCat(inputRefs.exposure.dataId,
342 inputs['exposure'],
343 inputs['refCat'], inputs['refWcs'])
344 self.attachFootprints(inputs['measCat'], inputs['refCat'], inputs['exposure'], inputs['refWcs'])
345 outputs = self.run(**inputs)
346 butlerQC.put(outputs, outputRefs)
347
348 def prepareCalibratedExposure(self, exposure, skyCorr=None, externalSkyWcsCatalog=None,
349 externalPhotoCalibCatalog=None, finalizedPsfApCorrCatalog=None):
350 """Prepare a calibrated exposure and apply external calibrations
351 and sky corrections if so configured.
352
353 Parameters
354 ----------
355 exposure : `lsst.afw.image.exposure.Exposure`
356 Input exposure to adjust calibrations.
357 skyCorr : `lsst.afw.math.backgroundList`, optional
358 Sky correction frame to apply if doApplySkyCorr=True.
359 externalSkyWcsCatalog : `lsst.afw.table.ExposureCatalog`, optional
360 Exposure catalog with external skyWcs to be applied
361 if config.doApplyExternalSkyWcs=True. Catalog uses the detector id
362 for the catalog id, sorted on id for fast lookup.
363 externalPhotoCalibCatalog : `lsst.afw.table.ExposureCatalog`, optional
364 Exposure catalog with external photoCalib to be applied
365 if config.doApplyExternalPhotoCalib=True. Catalog uses the detector
366 id for the catalog id, sorted on id for fast lookup.
367 finalizedPsfApCorrCatalog : `lsst.afw.table.ExposureCatalog`, optional
368 Exposure catalog with finalized psf models and aperture correction
369 maps to be applied if config.doApplyFinalizedPsf=True. Catalog uses
370 the detector id for the catalog id, sorted on id for fast lookup.
371
372 Returns
373 -------
374 exposure : `lsst.afw.image.exposure.Exposure`
375 Exposure with adjusted calibrations.
376 """
377 detectorId = exposure.getInfo().getDetector().getId()
378
379 if externalPhotoCalibCatalog is not None:
380 row = externalPhotoCalibCatalog.find(detectorId)
381 if row is None:
382 self.log.warning("Detector id %s not found in externalPhotoCalibCatalog; "
383 "Using original photoCalib.", detectorId)
384 else:
385 photoCalib = row.getPhotoCalib()
386 if photoCalib is None:
387 self.log.warning("Detector id %s has None for photoCalib in externalPhotoCalibCatalog; "
388 "Using original photoCalib.", detectorId)
389 else:
390 exposure.setPhotoCalib(photoCalib)
391
392 if externalSkyWcsCatalog is not None:
393 row = externalSkyWcsCatalog.find(detectorId)
394 if row is None:
395 self.log.warning("Detector id %s not found in externalSkyWcsCatalog; "
396 "Using original skyWcs.", detectorId)
397 else:
398 skyWcs = row.getWcs()
399 if skyWcs is None:
400 self.log.warning("Detector id %s has None for skyWcs in externalSkyWcsCatalog; "
401 "Using original skyWcs.", detectorId)
402 else:
403 exposure.setWcs(skyWcs)
404
405 if finalizedPsfApCorrCatalog is not None:
406 row = finalizedPsfApCorrCatalog.find(detectorId)
407 if row is None:
408 self.log.warning("Detector id %s not found in finalizedPsfApCorrCatalog; "
409 "Using original psf.", detectorId)
410 else:
411 psf = row.getPsf()
412 apCorrMap = row.getApCorrMap()
413 if psf is None or apCorrMap is None:
414 self.log.warning("Detector id %s has None for psf/apCorrMap in "
415 "finalizedPsfApCorrCatalog; Using original psf.", detectorId)
416 else:
417 exposure.setPsf(psf)
418 exposure.setApCorrMap(apCorrMap)
419
420 if skyCorr is not None:
421 exposure.maskedImage -= skyCorr.getImage()
422
423 return exposure
424
425 def mergeAndFilterReferences(self, exposure, refCats, refWcs):
426 """Filter reference catalog so that all sources are within the
427 boundaries of the exposure.
428
429 Parameters
430 ----------
431 exposure : `lsst.afw.image.exposure.Exposure`
432 Exposure to generate the catalog for.
433 refCats : sequence of `lsst.daf.butler.DeferredDatasetHandle`
434 Handles for catalogs of shapes and positions at which to force
435 photometry.
436 refWcs : `lsst.afw.image.SkyWcs`
437 Reference world coordinate system.
438
439 Returns
440 -------
441 refSources : `lsst.afw.table.SourceCatalog`
442 Filtered catalog of forced sources to measure.
443
444 Notes
445 -----
446 The majority of this code is based on the methods of
447 lsst.meas.algorithms.loadReferenceObjects.ReferenceObjectLoader
448
449 """
450 mergedRefCat = None
451
452 # Step 1: Determine bounds of the exposure photometry will
453 # be performed on.
454 expWcs = exposure.getWcs()
455 if expWcs is None:
456 self.log.info("Exposure has no WCS. Returning None for mergedRefCat.")
457 else:
458 expRegion = exposure.getBBox(lsst.afw.image.PARENT)
459 expBBox = lsst.geom.Box2D(expRegion)
460 expBoxCorners = expBBox.getCorners()
461 expSkyCorners = [expWcs.pixelToSky(corner).getVector() for
462 corner in expBoxCorners]
463 expPolygon = lsst.sphgeom.ConvexPolygon(expSkyCorners)
464
465 # Step 2: Filter out reference catalog sources that are
466 # not contained within the exposure boundaries, or whose
467 # parents are not within the exposure boundaries. Note
468 # that within a single input refCat, the parents always
469 # appear before the children.
470 for refCat in refCats:
471 refCat = refCat.get()
472 if mergedRefCat is None:
473 mergedRefCat = lsst.afw.table.SourceCatalog(refCat.table)
474 containedIds = {0} # zero as a parent ID means "this is a parent"
475 for record in refCat:
476 if (expPolygon.contains(record.getCoord().getVector()) and record.getParent()
477 in containedIds):
478 record.setFootprint(record.getFootprint())
479 mergedRefCat.append(record)
480 containedIds.add(record.getId())
481 if mergedRefCat is None:
482 raise RuntimeError("No reference objects for forced photometry.")
483 mergedRefCat.sort(lsst.afw.table.SourceTable.getParentKey())
484 return mergedRefCat
485
486 def generateMeasCat(self, dataId, exposure, refCat, refWcs):
487 """Generate a measurement catalog.
488
489 Parameters
490 ----------
491 dataId : `lsst.daf.butler.DataCoordinate`
492 Butler data ID for this image, with ``{visit, detector}`` keys.
493 exposure : `lsst.afw.image.exposure.Exposure`
494 Exposure to generate the catalog for.
496 Catalog of shapes and positions at which to force photometry.
497 refWcs : `lsst.afw.image.SkyWcs`
498 Reference world coordinate system.
499 This parameter is not currently used.
500
501 Returns
502 -------
504 Catalog of forced sources to measure.
505 expId : `int`
506 Unique binary id associated with the input exposure
507 """
508 id_generator = self.config.idGenerator.apply(dataId)
509 measCat = self.measurement.generateMeasCat(exposure, refCat, refWcs,
510 idFactory=id_generator.make_table_id_factory())
511 return measCat, id_generator.catalog_id
512
513 def run(self, measCat, exposure, refCat, refWcs, exposureId=None):
514 """Perform forced measurement on a single exposure.
515
516 Parameters
517 ----------
519 The measurement catalog, based on the sources listed in the
520 reference catalog.
521 exposure : `lsst.afw.image.Exposure`
522 The measurement image upon which to perform forced detection.
524 The reference catalog of sources to measure.
525 refWcs : `lsst.afw.image.SkyWcs`
526 The WCS for the references.
527 exposureId : `int`
528 Optional unique exposureId used for random seed in measurement
529 task.
530
531 Returns
532 -------
533 result : `lsst.pipe.base.Struct`
534 Structure with fields:
535
536 ``measCat``
537 Catalog of forced measurement results
539 """
540 self.measurement.run(measCat, exposure, refCat, refWcs, exposureId=exposureId)
541 if self.config.doApCorr:
542 self.applyApCorr.run(
543 catalog=measCat,
544 apCorrMap=exposure.getInfo().getApCorrMap()
545 )
546 self.catalogCalculation.run(measCat)
547
548 return pipeBase.Struct(measCat=measCat)
549
550 def attachFootprints(self, sources, refCat, exposure, refWcs):
551 """Attach footprints to blank sources prior to measurements.
552
553 Notes
554 -----
555 `~lsst.afw.detection.Footprint` objects for forced photometry must
556 be in the pixel coordinate system of the image being measured, while
557 the actual detections may start out in a different coordinate system.
558
559 Subclasses of this class may implement this method to define how
560 those `~lsst.afw.detection.Footprint` objects should be generated.
561
562 This default implementation transforms depends on the
563 ``footprintSource`` configuration parameter.
564 """
565 if self.config.footprintSource == "transformed":
566 return self.measurement.attachTransformedFootprints(sources, refCat, exposure, refWcs)
567 elif self.config.footprintSource == "psf":
568 return self.measurement.attachPsfShapeFootprints(sources, exposure,
569 scaling=self.config.psfFootprintScaling)
570
571
572class ForcedPhotCcdFromDataFrameConnections(PipelineTaskConnections,
573 dimensions=("instrument", "visit", "detector", "skymap", "tract"),
574 defaultTemplates={"inputCoaddName": "goodSeeing",
575 "inputName": "calexp",
576 "skyWcsName": "gbdesAstrometricFit",
577 "photoCalibName": "fgcm"}):
578 refCat = cT.Input(
579 doc="Catalog of positions at which to force photometry.",
580 name="{inputCoaddName}Diff_fullDiaObjTable",
581 storageClass="DataFrame",
582 dimensions=["skymap", "tract", "patch"],
583 multiple=True,
584 deferLoad=True,
585 )
586 exposure = cT.Input(
587 doc="Input exposure to perform photometry on.",
588 name="{inputName}",
589 storageClass="ExposureF",
590 dimensions=["instrument", "visit", "detector"],
591 )
592 skyCorr = cT.Input(
593 doc="Input Sky Correction to be subtracted from the calexp if doApplySkyCorr=True",
594 name="skyCorr",
595 storageClass="Background",
596 dimensions=("instrument", "visit", "detector"),
597 )
598 externalSkyWcsTractCatalog = cT.Input(
599 doc=("Per-tract, per-visit wcs calibrations. These catalogs use the detector "
600 "id for the catalog id, sorted on id for fast lookup."),
601 name="{skyWcsName}SkyWcsCatalog",
602 storageClass="ExposureCatalog",
603 dimensions=["instrument", "visit", "tract"],
604 )
605 externalSkyWcsGlobalCatalog = cT.Input(
606 doc=("Per-visit wcs calibrations computed globally (with no tract information). "
607 "These catalogs use the detector id for the catalog id, sorted on id for "
608 "fast lookup."),
609 name="{skyWcsName}SkyWcsCatalog",
610 storageClass="ExposureCatalog",
611 dimensions=["instrument", "visit"],
612 )
613 externalPhotoCalibTractCatalog = cT.Input(
614 doc=("Per-tract, per-visit photometric calibrations. These catalogs use the "
615 "detector id for the catalog id, sorted on id for fast lookup."),
616 name="{photoCalibName}PhotoCalibCatalog",
617 storageClass="ExposureCatalog",
618 dimensions=["instrument", "visit", "tract"],
619 )
620 externalPhotoCalibGlobalCatalog = cT.Input(
621 doc=("Per-visit photometric calibrations computed globally (with no tract "
622 "information). These catalogs use the detector id for the catalog id, "
623 "sorted on id for fast lookup."),
624 name="{photoCalibName}PhotoCalibCatalog",
625 storageClass="ExposureCatalog",
626 dimensions=["instrument", "visit"],
627 )
628 finalizedPsfApCorrCatalog = cT.Input(
629 doc=("Per-visit finalized psf models and aperture correction maps. "
630 "These catalogs use the detector id for the catalog id, "
631 "sorted on id for fast lookup."),
632 name="finalized_psf_ap_corr_catalog",
633 storageClass="ExposureCatalog",
634 dimensions=["instrument", "visit"],
635 )
636 measCat = cT.Output(
637 doc="Output forced photometry catalog.",
638 name="forced_src_diaObject",
639 storageClass="SourceCatalog",
640 dimensions=["instrument", "visit", "detector", "skymap", "tract"],
641 )
642 outputSchema = cT.InitOutput(
643 doc="Schema for the output forced measurement catalogs.",
644 name="forced_src_diaObject_schema",
645 storageClass="SourceCatalog",
646 )
647
648 def __init__(self, *, config=None):
649 super().__init__(config=config)
650 if not config.doApplySkyCorr:
651 self.inputs.remove("skyCorr")
652 if config.doApplyExternalSkyWcs:
653 if config.useGlobalExternalSkyWcs:
654 self.inputs.remove("externalSkyWcsTractCatalog")
655 else:
656 self.inputs.remove("externalSkyWcsGlobalCatalog")
657 else:
658 self.inputs.remove("externalSkyWcsTractCatalog")
659 self.inputs.remove("externalSkyWcsGlobalCatalog")
660 if config.doApplyExternalPhotoCalib:
661 if config.useGlobalExternalPhotoCalib:
662 self.inputs.remove("externalPhotoCalibTractCatalog")
663 else:
664 self.inputs.remove("externalPhotoCalibGlobalCatalog")
665 else:
666 self.inputs.remove("externalPhotoCalibTractCatalog")
667 self.inputs.remove("externalPhotoCalibGlobalCatalog")
668 if not config.doApplyFinalizedPsf:
669 self.inputs.remove("finalizedPsfApCorrCatalog")
670
671
672class ForcedPhotCcdFromDataFrameConfig(ForcedPhotCcdConfig,
673 pipelineConnections=ForcedPhotCcdFromDataFrameConnections):
674 def setDefaults(self):
675 super().setDefaults()
676 self.footprintSource = "psf"
677 self.measurement.doReplaceWithNoise = False
678 self.measurement.plugins.names = ["base_LocalPhotoCalib", "base_LocalWcs", "base_LocalBackground",
679 "base_TransformedCentroidFromCoord", "base_PsfFlux",
680 "base_PixelFlags"]
681 self.measurement.copyColumns = {'id': 'diaObjectId', 'coord_ra': 'coord_ra', 'coord_dec': 'coord_dec'}
682 self.measurement.slots.centroid = "base_TransformedCentroidFromCoord"
683 self.measurement.slots.psfFlux = "base_PsfFlux"
684 self.measurement.slots.shape = None
685
686 def validate(self):
687 super().validate()
688 if self.footprintSource == "transformed":
689 raise ValueError("Cannot transform footprints from reference catalog, "
690 "because DataFrames can't hold footprints.")
691
692
693class ForcedPhotCcdFromDataFrameTask(ForcedPhotCcdTask):
694 """Force Photometry on a per-detector exposure with coords from a DataFrame
695
696 Uses input from a DataFrame instead of SourceCatalog
697 like the base class ForcedPhotCcd does.
698 Writes out a SourceCatalog so that the downstream
699 WriteForcedSourceTableTask can be reused with output from this Task.
700 """
701 _DefaultName = "forcedPhotCcdFromDataFrame"
702 ConfigClass = ForcedPhotCcdFromDataFrameConfig
703
704 def __init__(self, butler=None, refSchema=None, initInputs=None, **kwds):
705 # Parent's init assumes that we have a reference schema; Cannot reuse
706 pipeBase.PipelineTask.__init__(self, **kwds)
707
708 if butler is not None:
709 warnings.warn("The 'butler' parameter is no longer used and can be safely removed.",
710 category=FutureWarning, stacklevel=2)
711 butler = None
712
713 self.makeSubtask("measurement", refSchema=lsst.afw.table.SourceTable.makeMinimalSchema())
714
715 if self.config.doApCorr:
716 self.makeSubtask("applyApCorr", schema=self.measurement.schema)
717 self.makeSubtask('catalogCalculation', schema=self.measurement.schema)
718 self.outputSchema = lsst.afw.table.SourceCatalog(self.measurement.schema)
719
720 def runQuantum(self, butlerQC, inputRefs, outputRefs):
721 inputs = butlerQC.get(inputRefs)
722
723 # When run with dataframes, we do not need a reference wcs.
724 inputs['refWcs'] = None
725
726 # Connections only exist if they are configured to be used.
727 skyCorr = inputs.pop('skyCorr', None)
728 if self.config.useGlobalExternalSkyWcs:
729 externalSkyWcsCatalog = inputs.pop('externalSkyWcsGlobalCatalog', None)
730 else:
731 externalSkyWcsCatalog = inputs.pop('externalSkyWcsTractCatalog', None)
732 if self.config.useGlobalExternalPhotoCalib:
733 externalPhotoCalibCatalog = inputs.pop('externalPhotoCalibGlobalCatalog', None)
734 else:
735 externalPhotoCalibCatalog = inputs.pop('externalPhotoCalibTractCatalog', None)
736 finalizedPsfApCorrCatalog = inputs.pop('finalizedPsfApCorrCatalog', None)
737
738 inputs['exposure'] = self.prepareCalibratedExposure(
739 inputs['exposure'],
740 skyCorr=skyCorr,
741 externalSkyWcsCatalog=externalSkyWcsCatalog,
742 externalPhotoCalibCatalog=externalPhotoCalibCatalog,
743 finalizedPsfApCorrCatalog=finalizedPsfApCorrCatalog
744 )
745
746 self.log.info("Filtering ref cats: %s", ','.join([str(i.dataId) for i in inputs['refCat']]))
747 if inputs["exposure"].getWcs() is not None:
748 refCat = self.df2RefCat([i.get(parameters={"columns": ['diaObjectId', 'ra', 'decl']})
749 for i in inputs['refCat']],
750 inputs['exposure'].getBBox(), inputs['exposure'].getWcs())
751 inputs['refCat'] = refCat
752 # generateMeasCat does not use the refWcs.
753 inputs['measCat'], inputs['exposureId'] = self.generateMeasCat(
754 inputRefs.exposure.dataId, inputs['exposure'], inputs['refCat'], inputs['refWcs']
755 )
756 # attachFootprints only uses refWcs in ``transformed`` mode, which is not
757 # supported in the DataFrame-backed task.
758 self.attachFootprints(inputs["measCat"], inputs["refCat"], inputs["exposure"], inputs["refWcs"])
759 outputs = self.run(**inputs)
760
761 butlerQC.put(outputs, outputRefs)
762 else:
763 self.log.info("No WCS for %s. Skipping and no %s catalog will be written.",
764 butlerQC.quantum.dataId, outputRefs.measCat.datasetType.name)
765
766 def df2RefCat(self, dfList, exposureBBox, exposureWcs):
767 """Convert list of DataFrames to reference catalog
768
769 Concatenate list of DataFrames presumably from multiple patches and
770 downselect rows that overlap the exposureBBox using the exposureWcs.
771
772 Parameters
773 ----------
774 dfList : `list` of `pandas.DataFrame`
775 Each element containst diaObjects with ra/decl position in degrees
776 Columns 'diaObjectId', 'ra', 'decl' are expected
777 exposureBBox : `lsst.geom.Box2I`
778 Bounding box on which to select rows that overlap
779 exposureWcs : `lsst.afw.geom.SkyWcs`
780 World coordinate system to convert sky coords in ref cat to
781 pixel coords with which to compare with exposureBBox
782
783 Returns
784 -------
786 Source Catalog with minimal schema that overlaps exposureBBox
787 """
788 df = pd.concat(dfList)
789 # translate ra/decl coords in dataframe to detector pixel coords
790 # to down select rows that overlap the detector bbox
791 mapping = exposureWcs.getTransform().getMapping()
792 x, y = mapping.applyInverse(np.array(df[['ra', 'decl']].values*2*np.pi/360).T)
793 inBBox = lsst.geom.Box2D(exposureBBox).contains(x, y)
794 refCat = self.df2SourceCat(df[inBBox])
795 return refCat
796
797 def df2SourceCat(self, df):
798 """Create minimal schema SourceCatalog from a pandas DataFrame.
799
800 The forced measurement subtask expects this as input.
801
802 Parameters
803 ----------
804 df : `pandas.DataFrame`
805 DiaObjects with locations and ids.
806
807 Returns
808 -------
809 outputCatalog : `lsst.afw.table.SourceTable`
810 Output catalog with minimal schema.
811 """
813 outputCatalog = lsst.afw.table.SourceCatalog(schema)
814 outputCatalog.reserve(len(df))
815
816 for diaObjectId, ra, decl in df[['ra', 'decl']].itertuples():
817 outputRecord = outputCatalog.addNew()
818 outputRecord.setId(diaObjectId)
819 outputRecord.setCoord(lsst.geom.SpherePoint(ra, decl, lsst.geom.degrees))
820 return outputCatalog
bool contains(lsst::geom::Point2I const &pix) const
lsst::geom::Box2I getBBox() const
std::string join(std::string const &a, std::string const &b) const
static Schema makeMinimalSchema()
static Key< RecordId > getParentKey()
def df2RefCat(self, dfList, exposureBBox, exposureWcs)
def runQuantum(self, butlerQC, inputRefs, outputRefs)