LSST Applications g063fba187b+cac8b7c890,g0f08755f38+6aee506743,g1653933729+a8ce1bb630,g168dd56ebc+a8ce1bb630,g1a2382251a+b4475c5878,g1dcb35cd9c+8f9bc1652e,g20f6ffc8e0+6aee506743,g217e2c1bcf+73dee94bd0,g28da252d5a+1f19c529b9,g2bbee38e9b+3f2625acfc,g2bc492864f+3f2625acfc,g3156d2b45e+6e55a43351,g32e5bea42b+1bb94961c2,g347aa1857d+3f2625acfc,g35bb328faa+a8ce1bb630,g3a166c0a6a+3f2625acfc,g3e281a1b8c+c5dd892a6c,g3e8969e208+a8ce1bb630,g414038480c+5927e1bc1e,g41af890bb2+8a9e676b2a,g7af13505b9+809c143d88,g80478fca09+6ef8b1810f,g82479be7b0+f568feb641,g858d7b2824+6aee506743,g89c8672015+f4add4ffd5,g9125e01d80+a8ce1bb630,ga5288a1d22+2903d499ea,gb58c049af0+d64f4d3760,gc28159a63d+3f2625acfc,gcab2d0539d+b12535109e,gcf0d15dbbd+46a3f46ba9,gda6a2b7d83+46a3f46ba9,gdaeeff99f8+1711a396fd,ge79ae78c31+3f2625acfc,gef2f8181fd+0a71e47438,gf0baf85859+c1f95f4921,gfa517265be+6aee506743,gfa999e8aa5+17cd334064,w.2024.51
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 dataclasses
23
24import astropy.table
25import pandas as pd
26import numpy as np
27
28import lsst.pex.config
30import lsst.pipe.base
31import lsst.geom
33import lsst.afw.geom
34import lsst.afw.image
35import lsst.afw.table
36import lsst.sphgeom
37
38from lsst.pipe.base import PipelineTaskConnections, NoWorkFound
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
47from ._id_generator import DetectorVisitIdGeneratorConfig
48
49__all__ = ("ForcedPhotCcdConfig", "ForcedPhotCcdTask",
50 "ForcedPhotCcdFromDataFrameTask", "ForcedPhotCcdFromDataFrameConfig")
51
52
53class ForcedPhotCcdConnections(PipelineTaskConnections,
54 dimensions=("instrument", "visit", "detector", "skymap", "tract"),
55 defaultTemplates={"inputCoaddName": "deep",
56 "inputName": "calexp"}):
57 inputSchema = cT.InitInput(
58 doc="Schema for the input measurement catalogs.",
59 name="{inputCoaddName}Coadd_ref_schema",
60 storageClass="SourceCatalog",
61 )
62 outputSchema = cT.InitOutput(
63 doc="Schema for the output forced measurement catalogs.",
64 name="forced_src_schema",
65 storageClass="SourceCatalog",
66 )
67 exposure = cT.Input(
68 doc="Input exposure to perform photometry on.",
69 name="{inputName}",
70 storageClass="ExposureF",
71 dimensions=["instrument", "visit", "detector"],
72 )
73 refCat = cT.Input(
74 doc="Catalog of shapes and positions at which to force photometry.",
75 name="{inputCoaddName}Coadd_ref",
76 storageClass="SourceCatalog",
77 dimensions=["skymap", "tract", "patch"],
78 multiple=True,
79 deferLoad=True,
80 )
81 skyMap = cT.Input(
82 doc="SkyMap dataset that defines the coordinate system of the reference catalog.",
83 name=BaseSkyMap.SKYMAP_DATASET_TYPE_NAME,
84 storageClass="SkyMap",
85 dimensions=["skymap"],
86 )
87 skyCorr = cT.Input(
88 doc="Input Sky Correction to be subtracted from the calexp if doApplySkyCorr=True",
89 name="skyCorr",
90 storageClass="Background",
91 dimensions=("instrument", "visit", "detector"),
92 )
93 visitSummary = cT.Input(
94 doc="Input visit-summary catalog with updated calibration objects.",
95 name="finalVisitSummary",
96 storageClass="ExposureCatalog",
97 dimensions=("instrument", "visit"),
98 )
99 measCat = cT.Output(
100 doc="Output forced photometry catalog.",
101 name="forced_src",
102 storageClass="SourceCatalog",
103 dimensions=["instrument", "visit", "detector", "skymap", "tract"],
104 )
105
106 def __init__(self, *, config=None):
107 super().__init__(config=config)
108 if not config.doApplySkyCorr:
109 del self.skyCorr
110 if not config.useVisitSummary:
111 del self.visitSummary
112 if config.refCatStorageClass != "SourceCatalog":
113 del self.inputSchema
114 # Connections are immutable, so we have to replace them entirely
115 # rather than edit them in-place.
116 self.refCat = dataclasses.replace(self.refCat, storageClass=config.refCatStorageClass)
117
118
119class ForcedPhotCcdConfig(pipeBase.PipelineTaskConfig,
120 pipelineConnections=ForcedPhotCcdConnections):
121 """Config class for forced measurement driver task."""
123 target=ForcedMeasurementTask,
124 doc="subtask to do forced measurement"
125 )
126 coaddName = lsst.pex.config.Field(
127 doc="coadd name: typically one of deep or goodSeeing",
128 dtype=str,
129 default="deep",
130 )
131 doApCorr = lsst.pex.config.Field(
132 dtype=bool,
133 default=True,
134 doc="Run subtask to apply aperture corrections"
135 )
137 target=ApplyApCorrTask,
138 doc="Subtask to apply aperture corrections"
139 )
140 catalogCalculation = lsst.pex.config.ConfigurableField(
141 target=CatalogCalculationTask,
142 doc="Subtask to run catalogCalculation plugins on catalog"
143 )
144 doApplySkyCorr = lsst.pex.config.Field(
145 dtype=bool,
146 default=False,
147 doc="Apply sky correction?",
148 )
149 useVisitSummary = lsst.pex.config.Field(
150 dtype=bool,
151 default=True,
152 doc=(
153 "Use updated WCS, PhotoCalib, ApCorr, and PSF from visit summary? "
154 "This should be False if and only if the input image already has the best-available calibration "
155 "objects attached."
156 ),
157 )
158 refCatStorageClass = lsst.pex.config.ChoiceField(
159 dtype=str,
160 allowed={
161 "SourceCatalog": "Read an lsst.afw.table.SourceCatalog.",
162 "DataFrame": "Read a pandas.DataFrame.",
163 "ArrowAstropy": "Read an astropy.table.Table saved to Parquet.",
164 },
165 default="SourceCatalog",
166 doc=(
167 "The butler storage class for the refCat connection. "
168 "If set to something other than 'SourceCatalog', the "
169 "'inputSchema' connection will be ignored."
170 )
171 )
172 refCatIdColumn = lsst.pex.config.Field(
173 dtype=str,
174 default="diaObjectId",
175 doc=(
176 "Name of the column that provides the object ID from the refCat connection. "
177 "measurement.copyColumns['id'] must be set to this value as well."
178 "Ignored if refCatStorageClass='SourceCatalog'."
179 )
180 )
181 refCatRaColumn = lsst.pex.config.Field(
182 dtype=str,
183 default="ra",
184 doc=(
185 "Name of the column that provides the right ascension (in floating-point degrees) from the "
186 "refCat connection. "
187 "Ignored if refCatStorageClass='SourceCatalog'."
188 )
189 )
190 refCatDecColumn = lsst.pex.config.Field(
191 dtype=str,
192 default="dec",
193 doc=(
194 "Name of the column that provides the declination (in floating-point degrees) from the "
195 "refCat connection. "
196 "Ignored if refCatStorageClass='SourceCatalog'."
197 )
198 )
199 includePhotoCalibVar = lsst.pex.config.Field(
200 dtype=bool,
201 default=False,
202 doc="Add photometric calibration variance to warp variance plane?",
203 )
204 footprintSource = lsst.pex.config.ChoiceField(
205 dtype=str,
206 doc="Where to obtain footprints to install in the measurement catalog, prior to measurement.",
207 allowed={
208 "transformed": "Transform footprints from the reference catalog (downgrades HeavyFootprints).",
209 "psf": ("Use the scaled shape of the PSF at the position of each source (does not generate "
210 "HeavyFootprints)."),
211 },
212 optional=True,
213 default="transformed",
214 )
215 psfFootprintScaling = lsst.pex.config.Field(
216 dtype=float,
217 doc="Scaling factor to apply to the PSF shape when footprintSource='psf' (ignored otherwise).",
218 default=3.0,
219 )
220 idGenerator = DetectorVisitIdGeneratorConfig.make_field()
221
222 def setDefaults(self):
223 # Docstring inherited.
224 super().setDefaults()
225 # Footprints here will not be entirely correct, so don't try to make
226 # a biased correction for blended neighbors.
227 self.measurement.doReplaceWithNoise = False
228 # Only run a minimal set of plugins, as these measurements are only
229 # needed for PSF-like sources.
230 self.measurement.plugins.names = ["base_PixelFlags",
231 "base_TransformedCentroid",
232 "base_PsfFlux",
233 "base_LocalBackground",
234 "base_LocalPhotoCalib",
235 "base_LocalWcs",
236 ]
237 self.measurement.slots.psfFlux = "base_PsfFlux"
238 self.measurement.slots.shape = None
239 # Make catalogCalculation a no-op by default as no modelFlux is setup
240 # by default in ForcedMeasurementTask.
241 self.catalogCalculation.plugins.names = []
242
243 def validate(self):
244 super().validate()
245 if self.refCatStorageClass != "SourceCatalog":
246 if self.footprintSource == "transformed":
247 raise ValueError("Cannot transform footprints from reference catalog, because "
248 f"{self.config.refCatStorageClass} datasets can't hold footprints.")
249 if self.measurement.copyColumns["id"] != self.refCatIdColumn:
250 raise ValueError(
251 f"measurement.copyColumns['id'] should be set to {self.refCatIdColumn} "
252 f"(refCatIdColumn) when refCatStorageClass={self.refCatStorageClass}."
253 )
254
255 def configureParquetRefCat(self, refCatStorageClass: str = "ArrowAstropy"):
256 """Set the refCatStorageClass option to a Parquet-based type, and
257 reconfigure the measurement subtask and footprintSources accordingly.
258 """
259 self.refCatStorageClass = refCatStorageClass
260 self.footprintSource = "psf"
261 self.measurement.doReplaceWithNoise = False
262 self.measurement.plugins.names -= {"base_TransformedCentroid"}
263 self.measurement.plugins.names |= {"base_TransformedCentroidFromCoord"}
264 self.measurement.copyColumns["id"] = self.refCatIdColumn
265 self.measurement.copyColumns.pop("deblend_nChild", None)
266 self.measurement.slots.centroid = "base_TransformedCentroidFromCoord"
267
268
269class ForcedPhotCcdTask(pipeBase.PipelineTask):
270 """A pipeline task for performing forced measurement on CCD images.
271
272 Parameters
273 ----------
274 refSchema : `lsst.afw.table.Schema`, optional
275 The schema of the reference catalog, passed to the constructor of the
276 references subtask. Optional, but must be specified if ``initInputs``
277 is not; if both are specified, ``initInputs`` takes precedence.
278 initInputs : `dict`
279 Dictionary that can contain a key ``inputSchema`` containing the
280 schema. If present will override the value of ``refSchema``.
281 **kwargs
282 Keyword arguments are passed to the supertask constructor.
283 """
284
285 ConfigClass = ForcedPhotCcdConfig
286 _DefaultName = "forcedPhotCcd"
287 dataPrefix = ""
288
289 def __init__(self, refSchema=None, initInputs=None, **kwargs):
290 super().__init__(**kwargs)
291
292 if initInputs:
293 refSchema = initInputs['inputSchema'].schema
294
295 if refSchema is None:
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
317 inputs['exposure'] = self.prepareCalibratedExposure(
318 inputs['exposure'],
319 skyCorr=skyCorr,
320 visitSummary=inputs.pop("visitSummary", None),
321 )
322
323 if inputs["exposure"].getWcs() is None:
324 raise NoWorkFound("Exposure has no WCS.")
325
326 match self.config.refCatStorageClass:
327 case "SourceCatalog":
328 prepFunc = self._prepSourceCatalogRefCat
329 case "DataFrame":
330 prepFunc = self._prepDataFrameRefCat
331 case "ArrowAstropy":
332 prepFunc = self._prepArrowAstropyRefCat
333 case _:
334 raise AssertionError("Configuration should not have passed validation.")
335 self.log.info("Filtering ref cats: %s", ','.join([str(i.dataId) for i in inputs['refCat']]))
336 inputs['refCat'] = prepFunc(
337 inputs['refCat'],
338 inputs['exposure'].getBBox(),
339 inputs['exposure'].getWcs(),
340 )
341 # generateMeasCat does not actually use the refWcs; parameter is
342 # passed for signature backwards compatibility.
343 inputs['measCat'], inputs['exposureId'] = self.generateMeasCat(
344 inputRefs.exposure.dataId, inputs['exposure'], inputs['refCat'], inputs['refWcs']
345 )
346 # attachFootprints only uses refWcs in ``transformed`` mode, which is
347 # not supported unless refCatStorageClass='SourceCatalog'.
348 self.attachFootprints(inputs["measCat"], inputs["refCat"], inputs["exposure"], inputs["refWcs"])
349 outputs = self.run(**inputs)
350 butlerQC.put(outputs, outputRefs)
351
352 def prepareCalibratedExposure(self, exposure, skyCorr=None, visitSummary=None):
353 """Prepare a calibrated exposure and apply external calibrations
354 and sky corrections if so configured.
355
356 Parameters
357 ----------
358 exposure : `lsst.afw.image.exposure.Exposure`
359 Input exposure to adjust calibrations.
360 skyCorr : `lsst.afw.math.backgroundList`, optional
361 Sky correction frame to apply if doApplySkyCorr=True.
362 visitSummary : `lsst.afw.table.ExposureCatalog`, optional
363 Exposure catalog with update calibrations; any not-None calibration
364 objects attached will be used. These are applied first and may be
365 overridden by other arguments.
366
367 Returns
368 -------
369 exposure : `lsst.afw.image.exposure.Exposure`
370 Exposure with adjusted calibrations.
371 """
372 detectorId = exposure.getInfo().getDetector().getId()
373
374 if visitSummary is not None:
375 row = visitSummary.find(detectorId)
376 if row is None:
377 raise RuntimeError(f"Detector id {detectorId} not found in visitSummary.")
378 if (photoCalib := row.getPhotoCalib()) is not None:
379 exposure.setPhotoCalib(photoCalib)
380 if (skyWcs := row.getWcs()) is not None:
381 exposure.setWcs(skyWcs)
382 if (psf := row.getPsf()) is not None:
383 exposure.setPsf(psf)
384 if (apCorrMap := row.getApCorrMap()) is not None:
385 exposure.info.setApCorrMap(apCorrMap)
386
387 if skyCorr is not None:
388 exposure.maskedImage -= skyCorr.getImage()
389
390 return exposure
391
392 def generateMeasCat(self, dataId, exposure, refCat, refWcs):
393 """Generate a measurement catalog.
394
395 Parameters
396 ----------
397 dataId : `lsst.daf.butler.DataCoordinate`
398 Butler data ID for this image, with ``{visit, detector}`` keys.
399 exposure : `lsst.afw.image.exposure.Exposure`
400 Exposure to generate the catalog for.
401 refCat : `lsst.afw.table.SourceCatalog`
402 Catalog of shapes and positions at which to force photometry.
403 refWcs : `lsst.afw.image.SkyWcs`
404 Reference world coordinate system.
405 This parameter is not currently used.
406
407 Returns
408 -------
409 measCat : `lsst.afw.table.SourceCatalog`
410 Catalog of forced sources to measure.
411 expId : `int`
412 Unique binary id associated with the input exposure
413 """
414 id_generator = self.config.idGenerator.apply(dataId)
415 measCat = self.measurement.generateMeasCat(exposure, refCat, refWcs,
416 idFactory=id_generator.make_table_id_factory())
417 return measCat, id_generator.catalog_id
418
419 def run(self, measCat, exposure, refCat, refWcs, exposureId=None):
420 """Perform forced measurement on a single exposure.
421
422 Parameters
423 ----------
424 measCat : `lsst.afw.table.SourceCatalog`
425 The measurement catalog, based on the sources listed in the
426 reference catalog.
427 exposure : `lsst.afw.image.Exposure`
428 The measurement image upon which to perform forced detection.
429 refCat : `lsst.afw.table.SourceCatalog`
430 The reference catalog of sources to measure.
431 refWcs : `lsst.afw.image.SkyWcs`
432 The WCS for the references.
433 exposureId : `int`
434 Optional unique exposureId used for random seed in measurement
435 task.
436
437 Returns
438 -------
439 result : `lsst.pipe.base.Struct`
440 Structure with fields:
441
442 ``measCat``
443 Catalog of forced measurement results
444 (`lsst.afw.table.SourceCatalog`).
445 """
446 self.measurement.run(measCat, exposure, refCat, refWcs, exposureId=exposureId)
447 if self.config.doApCorr:
448 apCorrMap = exposure.getInfo().getApCorrMap()
449 if apCorrMap is None:
450 self.log.warning("Forced exposure image does not have valid aperture correction; skipping.")
451 else:
452 self.applyApCorr.run(
453 catalog=measCat,
454 apCorrMap=apCorrMap,
455 )
456 self.catalogCalculation.run(measCat)
457
458 return pipeBase.Struct(measCat=measCat)
459
460 def attachFootprints(self, sources, refCat, exposure, refWcs):
461 """Attach footprints to blank sources prior to measurements.
462
463 Notes
464 -----
465 `~lsst.afw.detection.Footprint` objects for forced photometry must
466 be in the pixel coordinate system of the image being measured, while
467 the actual detections may start out in a different coordinate system.
468
469 Subclasses of this class may implement this method to define how
470 those `~lsst.afw.detection.Footprint` objects should be generated.
471
472 This default implementation transforms depends on the
473 ``footprintSource`` configuration parameter.
474 """
475 if self.config.footprintSource == "transformed":
476 return self.measurement.attachTransformedFootprints(sources, refCat, exposure, refWcs)
477 elif self.config.footprintSource == "psf":
478 return self.measurement.attachPsfShapeFootprints(sources, exposure,
479 scaling=self.config.psfFootprintScaling)
480
481 def _prepSourceCatalogRefCat(self, refCatHandles, exposureBBox, exposureWcs):
482 """Prepare a merged, filtered reference catalog from SourceCatalog
483 inputs.
484
485 Parameters
486 ----------
487 refCatHandles : sequence of `lsst.daf.butler.DeferredDatasetHandle`
488 Handles for catalogs of shapes and positions at which to force
489 photometry.
490 exposureBBox : `lsst.geom.Box2I`
491 Bounding box on which to select rows that overlap
492 exposureWcs : `lsst.afw.geom.SkyWcs`
493 World coordinate system to convert sky coords in ref cat to
494 pixel coords with which to compare with exposureBBox
495
496 Returns
497 -------
498 refSources : `lsst.afw.table.SourceCatalog`
499 Filtered catalog of forced sources to measure.
500
501 Notes
502 -----
503 The majority of this code is based on the methods of
504 lsst.meas.algorithms.loadReferenceObjects.ReferenceObjectLoader
505
506 """
507 mergedRefCat = None
508
509 # Step 1: Determine bounds of the exposure photometry will
510 # be performed on.
511 expBBox = lsst.geom.Box2D(exposureBBox)
512 expBoxCorners = expBBox.getCorners()
513 expSkyCorners = [exposureWcs.pixelToSky(corner).getVector() for corner in expBoxCorners]
514 expPolygon = lsst.sphgeom.ConvexPolygon(expSkyCorners)
515
516 # Step 2: Filter out reference catalog sources that are
517 # not contained within the exposure boundaries, or whose
518 # parents are not within the exposure boundaries. Note
519 # that within a single input refCat, the parents always
520 # appear before the children.
521 for refCat in refCatHandles:
522 refCat = refCat.get()
523 if mergedRefCat is None:
524 mergedRefCat = lsst.afw.table.SourceCatalog(refCat.table)
525 containedIds = {0} # zero as a parent ID means "this is a parent"
526 for record in refCat:
527 if (
528 expPolygon.contains(record.getCoord().getVector()) and record.getParent()
529 in containedIds
530 ):
531 record.setFootprint(record.getFootprint())
532 mergedRefCat.append(record)
533 containedIds.add(record.getId())
534 if mergedRefCat is None:
535 raise RuntimeError("No reference objects for forced photometry.")
536 mergedRefCat.sort(lsst.afw.table.SourceTable.getParentKey())
537 return mergedRefCat
538
539 def _prepDataFrameRefCat(self, refCatHandles, exposureBBox, exposureWcs):
540 """Prepare a merged, filtered reference catalog from DataFrame
541 inputs.
542
543 Parameters
544 ----------
545 refCatHandles : sequence of `lsst.daf.butler.DeferredDatasetHandle`
546 Handles for catalogs of shapes and positions at which to force
547 photometry.
548 exposureBBox : `lsst.geom.Box2I`
549 Bounding box on which to select rows that overlap
550 exposureWcs : `lsst.afw.geom.SkyWcs`
551 World coordinate system to convert sky coords in ref cat to
552 pixel coords with which to compare with exposureBBox
553
554 Returns
555 -------
556 refCat : `lsst.afw.table.SourceTable`
557 Source Catalog with minimal schema that overlaps exposureBBox
558 """
559 dfList = [
560 i.get(
561 parameters={
562 "columns": [
563 self.config.refCatIdColumn,
564 self.config.refCatRaColumn,
565 self.config.refCatDecColumn,
566 ]
567 }
568 )
569 for i in refCatHandles
570 ]
571 df = pd.concat(dfList)
572 # translate ra/dec coords in dataframe to detector pixel coords
573 # to down select rows that overlap the detector bbox
574 mapping = exposureWcs.getTransform().getMapping()
575 x, y = mapping.applyInverse(
576 np.array(df[[self.config.refCatRaColumn, self.config.refCatDecColumn]].values*2*np.pi/360).T
577 )
578 inBBox = np.atleast_1d(lsst.geom.Box2D(exposureBBox).contains(x, y))
579 refCat = self._makeMinimalSourceCatalogFromDataFrame(df[inBBox])
580 return refCat
581
582 def _prepArrowAstropyRefCat(self, refCatHandles, exposureBBox, exposureWcs):
583 """Prepare a merged, filtered reference catalog from ArrowAstropy
584 inputs.
585
586 Parameters
587 ----------
588 refCatHandles : sequence of `lsst.daf.butler.DeferredDatasetHandle`
589 Handles for catalogs of shapes and positions at which to force
590 photometry.
591 exposureBBox : `lsst.geom.Box2I`
592 Bounding box on which to select rows that overlap
593 exposureWcs : `lsst.afw.geom.SkyWcs`
594 World coordinate system to convert sky coords in ref cat to
595 pixel coords with which to compare with exposureBBox
596
597 Returns
598 -------
599 refCat : `lsst.afw.table.SourceTable`
600 Source Catalog with minimal schema that overlaps exposureBBox
601 """
602 table_list = [
603 i.get(
604 parameters={
605 "columns": [
606 self.config.refCatIdColumn,
607 self.config.refCatRaColumn,
608 self.config.refCatDecColumn,
609 ]
610 }
611 )
612 for i in refCatHandles
613 ]
614 full_table = astropy.table.vstack(table_list)
615 # translate ra/dec coords in table to detector pixel coords
616 # to down-select rows that overlap the detector bbox
617 mapping = exposureWcs.getTransform().getMapping()
618 ra_dec_rad = np.zeros((2, len(full_table)), dtype=float)
619 ra_dec_rad[0, :] = full_table[self.config.refCatRaColumn]
620 ra_dec_rad[1, :] = full_table[self.config.refCatDecColumn]
621 ra_dec_rad *= np.pi/180.0
622 x, y = mapping.applyInverse(ra_dec_rad)
623 inBBox = lsst.geom.Box2D(exposureBBox).contains(x, y)
624 refCat = self._makeMinimalSourceCatalogFromAstropy(full_table[inBBox])
625 return refCat
626
627 def _makeMinimalSourceCatalogFromDataFrame(self, df):
628 """Create minimal schema SourceCatalog from a pandas DataFrame.
629
630 The forced measurement subtask expects this as input.
631
632 Parameters
633 ----------
634 df : `pandas.DataFrame`
635 Table with locations and ids.
636
637 Returns
638 -------
639 outputCatalog : `lsst.afw.table.SourceTable`
640 Output catalog with minimal schema.
641 """
643 outputCatalog = lsst.afw.table.SourceCatalog(schema)
644 outputCatalog.reserve(len(df))
645
646 for objectId, ra, dec in df[['ra', 'dec']].itertuples():
647 outputRecord = outputCatalog.addNew()
648 outputRecord.setId(objectId)
649 outputRecord.setCoord(lsst.geom.SpherePoint(ra, dec, lsst.geom.degrees))
650 return outputCatalog
651
652 def _makeMinimalSourceCatalogFromAstropy(self, table):
653 """Create minimal schema SourceCatalog from an Astropy Table.
654
655 The forced measurement subtask expects this as input.
656
657 Parameters
658 ----------
659 table : `astropy.table.Table`
660 Table with locations and ids.
661
662 Returns
663 -------
664 outputCatalog : `lsst.afw.table.SourceTable`
665 Output catalog with minimal schema.
666 """
668 outputCatalog = lsst.afw.table.SourceCatalog(schema)
669 outputCatalog.reserve(len(table))
670
671 for objectId, ra, dec in table.iterrows():
672 outputRecord = outputCatalog.addNew()
673 outputRecord.setId(objectId)
674 outputRecord.setCoord(lsst.geom.SpherePoint(ra, dec, lsst.geom.degrees))
675 return outputCatalog
676
677
678class ForcedPhotCcdFromDataFrameConnections(ForcedPhotCcdConnections,
679 dimensions=("instrument", "visit", "detector", "skymap", "tract"),
680 defaultTemplates={"inputCoaddName": "goodSeeing",
681 "inputName": "calexp",
682 }):
683 pass
684
685
686class ForcedPhotCcdFromDataFrameConfig(ForcedPhotCcdConfig,
687 pipelineConnections=ForcedPhotCcdFromDataFrameConnections):
688 def setDefaults(self):
689 super().setDefaults()
690 self.configureParquetRefCat("DataFrame")
691 self.connections.refCat = "{inputCoaddName}Diff_fullDiaObjTable"
692 self.connections.outputSchema = "forced_src_diaObject_schema"
693 self.connections.measCat = "forced_src_diaObject"
694
695
696class ForcedPhotCcdFromDataFrameTask(ForcedPhotCcdTask):
697 """Force Photometry on a per-detector exposure with coords from a DataFrame
698
699 Uses input from a DataFrame instead of SourceCatalog
700 like the base class ForcedPhotCcd does.
701 Writes out a SourceCatalog so that the downstream
702 WriteForcedSourceTableTask can be reused with output from this Task.
703 """
704 _DefaultName = "forcedPhotCcdFromDataFrame"
705 ConfigClass = ForcedPhotCcdFromDataFrameConfig
Tag types used to declare specialized field types.
Definition misc.h:31
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
Point in an unspecified spherical coordinate system.
Definition SpherePoint.h:57
ConvexPolygon is a closed convex polygon on the unit sphere.