LSST Applications g070148d5b3+33e5256705,g0d53e28543+25c8b88941,g0da5cf3356+2dd1178308,g1081da9e2a+62d12e78cb,g17e5ecfddb+7e422d6136,g1c76d35bf8+ede3a706f7,g295839609d+225697d880,g2e2c1a68ba+cc1f6f037e,g2ffcdf413f+853cd4dcde,g38293774b4+62d12e78cb,g3b44f30a73+d953f1ac34,g48ccf36440+885b902d19,g4b2f1765b6+7dedbde6d2,g5320a0a9f6+0c5d6105b6,g56b687f8c9+ede3a706f7,g5c4744a4d9+ef6ac23297,g5ffd174ac0+0c5d6105b6,g6075d09f38+66af417445,g667d525e37+2ced63db88,g670421136f+2ced63db88,g71f27ac40c+2ced63db88,g774830318a+463cbe8d1f,g7876bc68e5+1d137996f1,g7985c39107+62d12e78cb,g7fdac2220c+0fd8241c05,g96f01af41f+368e6903a7,g9ca82378b8+2ced63db88,g9d27549199+ef6ac23297,gabe93b2c52+e3573e3735,gb065e2a02a+3dfbe639da,gbc3249ced9+0c5d6105b6,gbec6a3398f+0c5d6105b6,gc9534b9d65+35b9f25267,gd01420fc67+0c5d6105b6,geee7ff78d7+a14128c129,gf63283c776+ede3a706f7,gfed783d017+0c5d6105b6,w.2022.47
LSST Data Management Base Package
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.obs.base import ExposureIdInfo
38from lsst.pipe.base import PipelineTaskConnections
39import lsst.pipe.base.connectionTypes as cT
40
41import lsst.pipe.base as pipeBase
42from lsst.skymap import BaseSkyMap
43
44from .forcedMeasurement import ForcedMeasurementTask
45from .applyApCorr import ApplyApCorrTask
46from .catalogCalculation import CatalogCalculationTask
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": "jointcal",
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="{skyWcsName}SkyWcsCatalog",
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="{photoCalibName}PhotoCalibCatalog",
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."""
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 )
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=False,
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
252 def setDefaults(self):
253 # Docstring inherited.
254 # Make catalogCalculation a no-op by default as no modelFlux is setup
255 # by default in ForcedMeasurementTask.
256 super().setDefaults()
257 self.measurement.plugins.names |= ['base_LocalPhotoCalib', 'base_LocalWcs']
258 self.catalogCalculation.plugins.names = []
259
260
261class ForcedPhotCcdTask(pipeBase.PipelineTask):
262 """A pipeline task for performing forced measurement on CCD images.
263
264 Parameters
265 ----------
266 butler : `None`
267 Deprecated and unused. Should always be `None`.
268 refSchema : `lsst.afw.table.Schema`, optional
269 The schema of the reference catalog, passed to the constructor of the
270 references subtask. Optional, but must be specified if ``initInputs``
271 is not; if both are specified, ``initInputs`` takes precedence.
272 initInputs : `dict`
273 Dictionary that can contain a key ``inputSchema`` containing the
274 schema. If present will override the value of ``refSchema``.
275 **kwds
276 Keyword arguments are passed to the supertask constructor.
277 """
278
279 ConfigClass = ForcedPhotCcdConfig
280 _DefaultName = "forcedPhotCcd"
281 dataPrefix = ""
282
283 def __init__(self, butler=None, refSchema=None, initInputs=None, **kwds):
284 super().__init__(**kwds)
285
286 if butler is not None:
287 warnings.warn("The 'butler' parameter is no longer used and can be safely removed.",
288 category=FutureWarning, stacklevel=2)
289 butler = None
290
291 if initInputs is not None:
292 refSchema = initInputs['inputSchema'].schema
293
294 if refSchema is None:
295 raise ValueError("No reference schema provided.")
296
297 self.makeSubtask("measurement", refSchema=refSchema)
298 # It is necessary to get the schema internal to the forced measurement
299 # task until such a time that the schema is not owned by the
300 # measurement task, but is passed in by an external caller.
301 if self.config.doApCorr:
302 self.makeSubtask("applyApCorr", schema=self.measurement.schema)
303 self.makeSubtask('catalogCalculation', schema=self.measurement.schema)
304 self.outputSchema = lsst.afw.table.SourceCatalog(self.measurement.schema)
305
306 def runQuantum(self, butlerQC, inputRefs, outputRefs):
307 inputs = butlerQC.get(inputRefs)
308
309 tract = butlerQC.quantum.dataId['tract']
310 skyMap = inputs.pop('skyMap')
311 inputs['refWcs'] = skyMap[tract].getWcs()
312
313 # Connections only exist if they are configured to be used.
314 skyCorr = inputs.pop('skyCorr', None)
315 if self.config.useGlobalExternalSkyWcs:
316 externalSkyWcsCatalog = inputs.pop('externalSkyWcsGlobalCatalog', None)
317 else:
318 externalSkyWcsCatalog = inputs.pop('externalSkyWcsTractCatalog', None)
319 if self.config.useGlobalExternalPhotoCalib:
320 externalPhotoCalibCatalog = inputs.pop('externalPhotoCalibGlobalCatalog', None)
321 else:
322 externalPhotoCalibCatalog = inputs.pop('externalPhotoCalibTractCatalog', None)
323 finalizedPsfApCorrCatalog = inputs.pop('finalizedPsfApCorrCatalog', None)
324
325 inputs['exposure'] = self.prepareCalibratedExposure(
326 inputs['exposure'],
327 skyCorr=skyCorr,
328 externalSkyWcsCatalog=externalSkyWcsCatalog,
329 externalPhotoCalibCatalog=externalPhotoCalibCatalog,
330 finalizedPsfApCorrCatalog=finalizedPsfApCorrCatalog
331 )
332
333 inputs['refCat'] = self.mergeAndFilterReferences(inputs['exposure'], inputs['refCat'],
334 inputs['refWcs'])
335
336 if inputs['refCat'] is None:
337 self.log.info("No WCS for exposure %s. No %s catalog will be written.",
338 butlerQC.quantum.dataId, outputRefs.measCat.datasetType.name)
339 else:
340 inputs['measCat'], inputs['exposureId'] = self.generateMeasCat(inputRefs.exposure.dataId,
341 inputs['exposure'],
342 inputs['refCat'], inputs['refWcs'],
343 "visit_detector")
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 ----------
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 -------
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 ----------
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
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, exposureDataId, exposure, refCat, refWcs, idPackerName):
487 """Generate a measurement catalog.
488
489 Parameters
490 ----------
491 exposureDataId : `DataId`
492 Butler dataId for this 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 idPackerName : `str`
501 Type of ID packer to construct from the registry.
502
503 Returns
504 -------
506 Catalog of forced sources to measure.
507 expId : `int`
508 Unique binary id associated with the input exposure
509 """
510 exposureIdInfo = ExposureIdInfo.fromDataId(exposureDataId, idPackerName)
511 idFactory = exposureIdInfo.makeSourceIdFactory()
512
513 measCat = self.measurement.generateMeasCat(exposure, refCat, refWcs,
514 idFactory=idFactory)
515 return measCat, exposureIdInfo.expId
516
517 def run(self, measCat, exposure, refCat, refWcs, exposureId=None):
518 """Perform forced measurement on a single exposure.
519
520 Parameters
521 ----------
523 The measurement catalog, based on the sources listed in the
524 reference catalog.
525 exposure : `lsst.afw.image.Exposure`
526 The measurement image upon which to perform forced detection.
528 The reference catalog of sources to measure.
529 refWcs : `lsst.afw.image.SkyWcs`
530 The WCS for the references.
531 exposureId : `int`
532 Optional unique exposureId used for random seed in measurement
533 task.
534
535 Returns
536 -------
537 result : `lsst.pipe.base.Struct`
538 Structure with fields:
539
540 ``measCat``
541 Catalog of forced measurement results
543 """
544 self.measurement.run(measCat, exposure, refCat, refWcs, exposureId=exposureId)
545 if self.config.doApCorr:
546 self.applyApCorr.run(
547 catalog=measCat,
548 apCorrMap=exposure.getInfo().getApCorrMap()
549 )
550 self.catalogCalculation.run(measCat)
551
552 return pipeBase.Struct(measCat=measCat)
553
554 def attachFootprints(self, sources, refCat, exposure, refWcs):
555 """Attach footprints to blank sources prior to measurements.
556
557 Notes
558 -----
559 `~lsst.afw.detection.Footprint` objects for forced photometry must
560 be in the pixel coordinate system of the image being measured, while
561 the actual detections may start out in a different coordinate system.
562
563 Subclasses of this class may implement this method to define how
564 those `~lsst.afw.detection.Footprint` objects should be generated.
565
566 This default implementation transforms depends on the
567 ``footprintSource`` configuration parameter.
568 """
569 if self.config.footprintSource == "transformed":
570 return self.measurement.attachTransformedFootprints(sources, refCat, exposure, refWcs)
571 elif self.config.footprintSource == "psf":
572 return self.measurement.attachPsfShapeFootprints(sources, exposure,
573 scaling=self.config.psfFootprintScaling)
574
575
576class ForcedPhotCcdFromDataFrameConnections(PipelineTaskConnections,
577 dimensions=("instrument", "visit", "detector", "skymap", "tract"),
578 defaultTemplates={"inputCoaddName": "goodSeeing",
579 "inputName": "calexp",
580 "skyWcsName": "jointcal",
581 "photoCalibName": "fgcm"}):
582 refCat = cT.Input(
583 doc="Catalog of positions at which to force photometry.",
584 name="{inputCoaddName}Diff_fullDiaObjTable",
585 storageClass="DataFrame",
586 dimensions=["skymap", "tract", "patch"],
587 multiple=True,
588 deferLoad=True,
589 )
590 exposure = cT.Input(
591 doc="Input exposure to perform photometry on.",
592 name="{inputName}",
593 storageClass="ExposureF",
594 dimensions=["instrument", "visit", "detector"],
595 )
596 skyCorr = cT.Input(
597 doc="Input Sky Correction to be subtracted from the calexp if doApplySkyCorr=True",
598 name="skyCorr",
599 storageClass="Background",
600 dimensions=("instrument", "visit", "detector"),
601 )
602 externalSkyWcsTractCatalog = cT.Input(
603 doc=("Per-tract, per-visit wcs calibrations. These catalogs use the detector "
604 "id for the catalog id, sorted on id for fast lookup."),
605 name="{skyWcsName}SkyWcsCatalog",
606 storageClass="ExposureCatalog",
607 dimensions=["instrument", "visit", "tract"],
608 )
609 externalSkyWcsGlobalCatalog = cT.Input(
610 doc=("Per-visit wcs calibrations computed globally (with no tract information). "
611 "These catalogs use the detector id for the catalog id, sorted on id for "
612 "fast lookup."),
613 name="{skyWcsName}SkyWcsCatalog",
614 storageClass="ExposureCatalog",
615 dimensions=["instrument", "visit"],
616 )
617 externalPhotoCalibTractCatalog = cT.Input(
618 doc=("Per-tract, per-visit photometric calibrations. These catalogs use the "
619 "detector id for the catalog id, sorted on id for fast lookup."),
620 name="{photoCalibName}PhotoCalibCatalog",
621 storageClass="ExposureCatalog",
622 dimensions=["instrument", "visit", "tract"],
623 )
624 externalPhotoCalibGlobalCatalog = cT.Input(
625 doc=("Per-visit photometric calibrations computed globally (with no tract "
626 "information). These catalogs use the detector id for the catalog id, "
627 "sorted on id for fast lookup."),
628 name="{photoCalibName}PhotoCalibCatalog",
629 storageClass="ExposureCatalog",
630 dimensions=["instrument", "visit"],
631 )
632 finalizedPsfApCorrCatalog = cT.Input(
633 doc=("Per-visit finalized psf models and aperture correction maps. "
634 "These catalogs use the detector id for the catalog id, "
635 "sorted on id for fast lookup."),
636 name="finalized_psf_ap_corr_catalog",
637 storageClass="ExposureCatalog",
638 dimensions=["instrument", "visit"],
639 )
640 measCat = cT.Output(
641 doc="Output forced photometry catalog.",
642 name="forced_src_diaObject",
643 storageClass="SourceCatalog",
644 dimensions=["instrument", "visit", "detector", "skymap", "tract"],
645 )
646 outputSchema = cT.InitOutput(
647 doc="Schema for the output forced measurement catalogs.",
648 name="forced_src_diaObject_schema",
649 storageClass="SourceCatalog",
650 )
651
652 def __init__(self, *, config=None):
653 super().__init__(config=config)
654 if not config.doApplySkyCorr:
655 self.inputs.remove("skyCorr")
656 if config.doApplyExternalSkyWcs:
657 if config.useGlobalExternalSkyWcs:
658 self.inputs.remove("externalSkyWcsTractCatalog")
659 else:
660 self.inputs.remove("externalSkyWcsGlobalCatalog")
661 else:
662 self.inputs.remove("externalSkyWcsTractCatalog")
663 self.inputs.remove("externalSkyWcsGlobalCatalog")
664 if config.doApplyExternalPhotoCalib:
665 if config.useGlobalExternalPhotoCalib:
666 self.inputs.remove("externalPhotoCalibTractCatalog")
667 else:
668 self.inputs.remove("externalPhotoCalibGlobalCatalog")
669 else:
670 self.inputs.remove("externalPhotoCalibTractCatalog")
671 self.inputs.remove("externalPhotoCalibGlobalCatalog")
672 if not config.doApplyFinalizedPsf:
673 self.inputs.remove("finalizedPsfApCorrCatalog")
674
675
676class ForcedPhotCcdFromDataFrameConfig(ForcedPhotCcdConfig,
677 pipelineConnections=ForcedPhotCcdFromDataFrameConnections):
678 def setDefaults(self):
679 super().setDefaults()
680 self.footprintSource = "psf"
681 self.measurement.doReplaceWithNoise = False
682 self.measurement.plugins.names = ["base_LocalPhotoCalib", "base_LocalWcs", "base_LocalBackground",
683 "base_TransformedCentroidFromCoord", "base_PsfFlux",
684 "base_PixelFlags"]
685 self.measurement.copyColumns = {'id': 'diaObjectId', 'coord_ra': 'coord_ra', 'coord_dec': 'coord_dec'}
686 self.measurement.slots.centroid = "base_TransformedCentroidFromCoord"
687 self.measurement.slots.psfFlux = "base_PsfFlux"
688 self.measurement.slots.shape = None
689
690 def validate(self):
691 super().validate()
692 if self.footprintSource == "transformed":
693 raise ValueError("Cannot transform footprints from reference catalog, "
694 "because DataFrames can't hold footprints.")
695
696
697class ForcedPhotCcdFromDataFrameTask(ForcedPhotCcdTask):
698 """Force Photometry on a per-detector exposure with coords from a DataFrame
699
700 Uses input from a DataFrame instead of SourceCatalog
701 like the base class ForcedPhotCcd does.
702 Writes out a SourceCatalog so that the downstream
703 WriteForcedSourceTableTask can be reused with output from this Task.
704 """
705 _DefaultName = "forcedPhotCcdFromDataFrame"
706 ConfigClass = ForcedPhotCcdFromDataFrameConfig
707
708 def __init__(self, butler=None, refSchema=None, initInputs=None, **kwds):
709 # Parent's init assumes that we have a reference schema; Cannot reuse
710 pipeBase.PipelineTask.__init__(self, **kwds)
711
712 if butler is not None:
713 warnings.warn("The 'butler' parameter is no longer used and can be safely removed.",
714 category=FutureWarning, stacklevel=2)
715 butler = None
716
717 self.makeSubtask("measurement", refSchema=lsst.afw.table.SourceTable.makeMinimalSchema())
718
719 if self.config.doApCorr:
720 self.makeSubtask("applyApCorr", schema=self.measurement.schema)
721 self.makeSubtask('catalogCalculation', schema=self.measurement.schema)
722 self.outputSchema = lsst.afw.table.SourceCatalog(self.measurement.schema)
723
724 def runQuantum(self, butlerQC, inputRefs, outputRefs):
725 inputs = butlerQC.get(inputRefs)
726
727 # When run with dataframes, we do not need a reference wcs.
728 inputs['refWcs'] = None
729
730 # Connections only exist if they are configured to be used.
731 skyCorr = inputs.pop('skyCorr', None)
732 if self.config.useGlobalExternalSkyWcs:
733 externalSkyWcsCatalog = inputs.pop('externalSkyWcsGlobalCatalog', None)
734 else:
735 externalSkyWcsCatalog = inputs.pop('externalSkyWcsTractCatalog', None)
736 if self.config.useGlobalExternalPhotoCalib:
737 externalPhotoCalibCatalog = inputs.pop('externalPhotoCalibGlobalCatalog', None)
738 else:
739 externalPhotoCalibCatalog = inputs.pop('externalPhotoCalibTractCatalog', None)
740 finalizedPsfApCorrCatalog = inputs.pop('finalizedPsfApCorrCatalog', None)
741
742 inputs['exposure'] = self.prepareCalibratedExposure(
743 inputs['exposure'],
744 skyCorr=skyCorr,
745 externalSkyWcsCatalog=externalSkyWcsCatalog,
746 externalPhotoCalibCatalog=externalPhotoCalibCatalog,
747 finalizedPsfApCorrCatalog=finalizedPsfApCorrCatalog
748 )
749
750 self.log.info("Filtering ref cats: %s", ','.join([str(i.dataId) for i in inputs['refCat']]))
751 if inputs["exposure"].getWcs() is not None:
752 refCat = self.df2RefCat([i.get(parameters={"columns": ['diaObjectId', 'ra', 'decl']})
753 for i in inputs['refCat']],
754 inputs['exposure'].getBBox(), inputs['exposure'].getWcs())
755 inputs['refCat'] = refCat
756 # generateMeasCat does not use the refWcs.
757 inputs['measCat'], inputs['exposureId'] = self.generateMeasCat(
758 inputRefs.exposure.dataId, inputs['exposure'], inputs['refCat'], inputs['refWcs'],
759 "visit_detector")
760 # attachFootprints only uses refWcs in ``transformed`` mode, which is not
761 # supported in the DataFrame-backed task.
762 self.attachFootprints(inputs["measCat"], inputs["refCat"], inputs["exposure"], inputs["refWcs"])
763 outputs = self.run(**inputs)
764
765 butlerQC.put(outputs, outputRefs)
766 else:
767 self.log.info("No WCS for %s. Skipping and no %s catalog will be written.",
768 butlerQC.quantum.dataId, outputRefs.measCat.datasetType.name)
769
770 def df2RefCat(self, dfList, exposureBBox, exposureWcs):
771 """Convert list of DataFrames to reference catalog
772
773 Concatenate list of DataFrames presumably from multiple patches and
774 downselect rows that overlap the exposureBBox using the exposureWcs.
775
776 Parameters
777 ----------
778 dfList : `list` of `pandas.DataFrame`
779 Each element containst diaObjects with ra/decl position in degrees
780 Columns 'diaObjectId', 'ra', 'decl' are expected
781 exposureBBox : `lsst.geom.Box2I`
782 Bounding box on which to select rows that overlap
783 exposureWcs : `lsst.afw.geom.SkyWcs`
784 World coordinate system to convert sky coords in ref cat to
785 pixel coords with which to compare with exposureBBox
786
787 Returns
788 -------
790 Source Catalog with minimal schema that overlaps exposureBBox
791 """
792 df = pd.concat(dfList)
793 # translate ra/decl coords in dataframe to detector pixel coords
794 # to down select rows that overlap the detector bbox
795 mapping = exposureWcs.getTransform().getMapping()
796 x, y = mapping.applyInverse(np.array(df[['ra', 'decl']].values*2*np.pi/360).T)
797 inBBox = lsst.geom.Box2D(exposureBBox).contains(x, y)
798 refCat = self.df2SourceCat(df[inBBox])
799 return refCat
800
801 def df2SourceCat(self, df):
802 """Create minimal schema SourceCatalog from a pandas DataFrame.
803
804 The forced measurement subtask expects this as input.
805
806 Parameters
807 ----------
808 df : `pandas.DataFrame`
809 DiaObjects with locations and ids.
810
811 Returns
812 -------
813 outputCatalog : `lsst.afw.table.SourceTable`
814 Output catalog with minimal schema.
815 """
817 outputCatalog = lsst.afw.table.SourceCatalog(schema)
818 outputCatalog.reserve(len(df))
819
820 for diaObjectId, ra, decl in df[['ra', 'decl']].itertuples():
821 outputRecord = outputCatalog.addNew()
822 outputRecord.setId(diaObjectId)
823 outputRecord.setCoord(lsst.geom.SpherePoint(ra, decl, lsst.geom.degrees))
824 return outputCatalog
table::Key< int > to
Class to describe the properties of a detected object from an image.
Definition: Footprint.h:63
bool contains(lsst::geom::Point2I const &pix) const
Tests if a pixel postion falls inside the Footprint.
Definition: Footprint.cc:79
lsst::geom::Box2I getBBox() const
Return the Footprint's bounding box.
Definition: Footprint.h:208
A 2-dimensional celestial WCS that transform pixels to ICRS RA/Dec, using the LSST standard for pixel...
Definition: SkyWcs.h:117
A class to contain the data, WCS, and other information needed to describe an image of the sky.
Definition: Exposure.h:72
Custom catalog class for ExposureRecord/Table.
Definition: Exposure.h:311
Defines the fields and offsets for a table.
Definition: Schema.h:51
Table class that contains measurements made on a single exposure.
Definition: Source.h:217
static Schema makeMinimalSchema()
Return a minimal schema for Source tables and records.
Definition: Source.h:258
static Key< RecordId > getParentKey()
Key for the parent ID.
Definition: Source.h:273
A floating-point coordinate rectangle geometry.
Definition: Box.h:413
An integer coordinate rectangle.
Definition: Box.h:55
Point in an unspecified spherical coordinate system.
Definition: SpherePoint.h:57
def df2RefCat(self, dfList, exposureBBox, exposureWcs)
def runQuantum(self, butlerQC, inputRefs, outputRefs)
ConvexPolygon is a closed convex polygon on the unit sphere.
Definition: ConvexPolygon.h:57