Loading [MathJax]/extensions/tex2jax.js
LSST Applications g0fba68d861+83433b07ee,g16d25e1f1b+23bc9e47ac,g1ec0fe41b4+3ea9d11450,g1fd858c14a+9be2b0f3b9,g2440f9efcc+8c5ae1fdc5,g35bb328faa+8c5ae1fdc5,g4a4af6cd76+d25431c27e,g4d2262a081+c74e83464e,g53246c7159+8c5ae1fdc5,g55585698de+1e04e59700,g56a49b3a55+92a7603e7a,g60b5630c4e+1e04e59700,g67b6fd64d1+3fc8cb0b9e,g78460c75b0+7e33a9eb6d,g786e29fd12+668abc6043,g8352419a5c+8c5ae1fdc5,g8852436030+60e38ee5ff,g89139ef638+3fc8cb0b9e,g94187f82dc+1e04e59700,g989de1cb63+3fc8cb0b9e,g9d31334357+1e04e59700,g9f33ca652e+0a83e03614,gabe3b4be73+8856018cbb,gabf8522325+977d9fabaf,gb1101e3267+8b4b9c8ed7,gb89ab40317+3fc8cb0b9e,gc0af124501+57ccba3ad1,gcf25f946ba+60e38ee5ff,gd6cbbdb0b4+1cc2750d2e,gd794735e4e+7be992507c,gdb1c4ca869+be65c9c1d7,gde0f65d7ad+c7f52e58fe,ge278dab8ac+6b863515ed,ge410e46f29+3fc8cb0b9e,gf35d7ec915+97dd712d81,gf5e32f922b+8c5ae1fdc5,gf618743f1b+747388abfa,gf67bdafdda+3fc8cb0b9e,w.2025.18
LSST Data Management Base Package
All Classes Namespaces Files Functions Variables Typedefs Enumerations Enumerator Properties Friends Macros Modules Pages
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 # TODO[DM-49400]: remove this config option; it already does nothing.
200 includePhotoCalibVar = lsst.pex.config.Field(
201 dtype=bool,
202 default=False,
203 doc="Add photometric calibration variance to warp variance plane?",
204 deprecated="Deprecated and unused; will be removed after v29.",
205 )
206 footprintSource = lsst.pex.config.ChoiceField(
207 dtype=str,
208 doc="Where to obtain footprints to install in the measurement catalog, prior to measurement.",
209 allowed={
210 "transformed": "Transform footprints from the reference catalog (downgrades HeavyFootprints).",
211 "psf": ("Use the scaled shape of the PSF at the position of each source (does not generate "
212 "HeavyFootprints)."),
213 },
214 optional=True,
215 default="transformed",
216 )
217 psfFootprintScaling = lsst.pex.config.Field(
218 dtype=float,
219 doc="Scaling factor to apply to the PSF shape when footprintSource='psf' (ignored otherwise).",
220 default=3.0,
221 )
222 idGenerator = DetectorVisitIdGeneratorConfig.make_field()
223
224 def setDefaults(self):
225 # Docstring inherited.
226 super().setDefaults()
227 # Footprints here will not be entirely correct, so don't try to make
228 # a biased correction for blended neighbors.
229 self.measurement.doReplaceWithNoise = False
230 # Only run a minimal set of plugins, as these measurements are only
231 # needed for PSF-like sources.
232 self.measurement.plugins.names = ["base_PixelFlags",
233 "base_TransformedCentroid",
234 "base_PsfFlux",
235 "base_LocalBackground",
236 "base_LocalPhotoCalib",
237 "base_LocalWcs",
238 ]
239 self.measurement.slots.psfFlux = "base_PsfFlux"
240 self.measurement.slots.shape = None
241 # Make catalogCalculation a no-op by default as no modelFlux is setup
242 # by default in ForcedMeasurementTask.
243 self.catalogCalculation.plugins.names = []
244
245 def validate(self):
246 super().validate()
247 if self.refCatStorageClass != "SourceCatalog":
248 if self.footprintSource == "transformed":
249 raise ValueError("Cannot transform footprints from reference catalog, because "
250 f"{self.config.refCatStorageClass} datasets can't hold footprints.")
251 if self.measurement.copyColumns["id"] != self.refCatIdColumn:
252 raise ValueError(
253 f"measurement.copyColumns['id'] should be set to {self.refCatIdColumn} "
254 f"(refCatIdColumn) when refCatStorageClass={self.refCatStorageClass}."
255 )
256
257 def configureParquetRefCat(self, refCatStorageClass: str = "ArrowAstropy"):
258 """Set the refCatStorageClass option to a Parquet-based type, and
259 reconfigure the measurement subtask and footprintSources accordingly.
260 """
261 self.refCatStorageClass = refCatStorageClass
262 self.footprintSource = "psf"
263 self.measurement.doReplaceWithNoise = False
264 self.measurement.plugins.names -= {"base_TransformedCentroid"}
265 self.measurement.plugins.names |= {"base_TransformedCentroidFromCoord"}
266 self.measurement.copyColumns["id"] = self.refCatIdColumn
267 self.measurement.copyColumns.pop("deblend_nChild", None)
268 self.measurement.slots.centroid = "base_TransformedCentroidFromCoord"
269
270
271class ForcedPhotCcdTask(pipeBase.PipelineTask):
272 """A pipeline task for performing forced measurement on CCD images.
273
274 Parameters
275 ----------
276 refSchema : `lsst.afw.table.Schema`, optional
277 The schema of the reference catalog, passed to the constructor of the
278 references subtask. Optional, but must be specified if ``initInputs``
279 is not; if both are specified, ``initInputs`` takes precedence.
280 initInputs : `dict`
281 Dictionary that can contain a key ``inputSchema`` containing the
282 schema. If present will override the value of ``refSchema``.
283 **kwargs
284 Keyword arguments are passed to the supertask constructor.
285 """
286
287 ConfigClass = ForcedPhotCcdConfig
288 _DefaultName = "forcedPhotCcd"
289 dataPrefix = ""
290
291 def __init__(self, refSchema=None, initInputs=None, **kwargs):
292 super().__init__(**kwargs)
293
294 if initInputs:
295 refSchema = initInputs['inputSchema'].schema
296
297 if refSchema is None:
299
300 self.makeSubtask("measurement", refSchema=refSchema)
301 # It is necessary to get the schema internal to the forced measurement
302 # task until such a time that the schema is not owned by the
303 # measurement task, but is passed in by an external caller.
304 if self.config.doApCorr:
305 self.makeSubtask("applyApCorr", schema=self.measurement.schema)
306 self.makeSubtask('catalogCalculation', schema=self.measurement.schema)
307 self.outputSchema = lsst.afw.table.SourceCatalog(self.measurement.schema)
308
309 def runQuantum(self, butlerQC, inputRefs, outputRefs):
310 inputs = butlerQC.get(inputRefs)
311
312 tract = butlerQC.quantum.dataId['tract']
313 skyMap = inputs.pop('skyMap')
314 inputs['refWcs'] = skyMap[tract].getWcs()
315
316 # Connections only exist if they are configured to be used.
317 skyCorr = inputs.pop('skyCorr', None)
318
319 inputs['exposure'] = self.prepareCalibratedExposure(
320 inputs['exposure'],
321 skyCorr=skyCorr,
322 visitSummary=inputs.pop("visitSummary", None),
323 )
324
325 if inputs["exposure"].getWcs() is None:
326 raise NoWorkFound("Exposure has no WCS.")
327
328 match self.config.refCatStorageClass:
329 case "SourceCatalog":
330 prepFunc = self._prepSourceCatalogRefCat
331 case "DataFrame":
332 prepFunc = self._prepDataFrameRefCat
333 case "ArrowAstropy":
334 prepFunc = self._prepArrowAstropyRefCat
335 case _:
336 raise AssertionError("Configuration should not have passed validation.")
337 self.log.info("Filtering ref cats: %s", ','.join([str(i.dataId) for i in inputs['refCat']]))
338 inputs['refCat'] = prepFunc(
339 inputs['refCat'],
340 inputs['exposure'].getBBox(),
341 inputs['exposure'].getWcs(),
342 )
343 # generateMeasCat does not actually use the refWcs; parameter is
344 # passed for signature backwards compatibility.
345 inputs['measCat'], inputs['exposureId'] = self.generateMeasCat(
346 inputRefs.exposure.dataId, inputs['exposure'], inputs['refCat'], inputs['refWcs']
347 )
348 # attachFootprints only uses refWcs in ``transformed`` mode, which is
349 # not supported unless refCatStorageClass='SourceCatalog'.
350 self.attachFootprints(inputs["measCat"], inputs["refCat"], inputs["exposure"], inputs["refWcs"])
351 outputs = self.run(**inputs)
352 butlerQC.put(outputs, outputRefs)
353
354 def prepareCalibratedExposure(self, exposure, skyCorr=None, visitSummary=None):
355 """Prepare a calibrated exposure and apply external calibrations
356 and sky corrections if so configured.
357
358 Parameters
359 ----------
360 exposure : `lsst.afw.image.exposure.Exposure`
361 Input exposure to adjust calibrations.
362 skyCorr : `lsst.afw.math.backgroundList`, optional
363 Sky correction frame to apply if doApplySkyCorr=True.
364 visitSummary : `lsst.afw.table.ExposureCatalog`, optional
365 Exposure catalog with update calibrations; any not-None calibration
366 objects attached will be used. These are applied first and may be
367 overridden by other arguments.
368
369 Returns
370 -------
371 exposure : `lsst.afw.image.exposure.Exposure`
372 Exposure with adjusted calibrations.
373 """
374 detectorId = exposure.getInfo().getDetector().getId()
375
376 if visitSummary is not None:
377 row = visitSummary.find(detectorId)
378 if row is None:
379 raise RuntimeError(f"Detector id {detectorId} not found in visitSummary.")
380 if (photoCalib := row.getPhotoCalib()) is not None:
381 exposure.setPhotoCalib(photoCalib)
382 if (skyWcs := row.getWcs()) is not None:
383 exposure.setWcs(skyWcs)
384 if (psf := row.getPsf()) is not None:
385 exposure.setPsf(psf)
386 if (apCorrMap := row.getApCorrMap()) is not None:
387 exposure.info.setApCorrMap(apCorrMap)
388
389 if skyCorr is not None:
390 exposure.maskedImage -= skyCorr.getImage()
391
392 return exposure
393
394 def generateMeasCat(self, dataId, exposure, refCat, refWcs):
395 """Generate a measurement catalog.
396
397 Parameters
398 ----------
399 dataId : `lsst.daf.butler.DataCoordinate`
400 Butler data ID for this image, with ``{visit, detector}`` keys.
401 exposure : `lsst.afw.image.exposure.Exposure`
402 Exposure to generate the catalog for.
403 refCat : `lsst.afw.table.SourceCatalog`
404 Catalog of shapes and positions at which to force photometry.
405 refWcs : `lsst.afw.image.SkyWcs`
406 Reference world coordinate system.
407 This parameter is not currently used.
408
409 Returns
410 -------
411 measCat : `lsst.afw.table.SourceCatalog`
412 Catalog of forced sources to measure.
413 expId : `int`
414 Unique binary id associated with the input exposure
415 """
416 id_generator = self.config.idGenerator.apply(dataId)
417 measCat = self.measurement.generateMeasCat(exposure, refCat, refWcs,
418 idFactory=id_generator.make_table_id_factory())
419 return measCat, id_generator.catalog_id
420
421 def run(self, measCat, exposure, refCat, refWcs, exposureId=None):
422 """Perform forced measurement on a single exposure.
423
424 Parameters
425 ----------
426 measCat : `lsst.afw.table.SourceCatalog`
427 The measurement catalog, based on the sources listed in the
428 reference catalog.
429 exposure : `lsst.afw.image.Exposure`
430 The measurement image upon which to perform forced detection.
431 refCat : `lsst.afw.table.SourceCatalog`
432 The reference catalog of sources to measure.
433 refWcs : `lsst.afw.image.SkyWcs`
434 The WCS for the references.
435 exposureId : `int`
436 Optional unique exposureId used for random seed in measurement
437 task.
438
439 Returns
440 -------
441 result : `lsst.pipe.base.Struct`
442 Structure with fields:
443
444 ``measCat``
445 Catalog of forced measurement results
446 (`lsst.afw.table.SourceCatalog`).
447 """
448 self.measurement.run(measCat, exposure, refCat, refWcs, exposureId=exposureId)
449 if self.config.doApCorr:
450 apCorrMap = exposure.getInfo().getApCorrMap()
451 if apCorrMap is None:
452 self.log.warning("Forced exposure image does not have valid aperture correction; skipping.")
453 else:
454 self.applyApCorr.run(
455 catalog=measCat,
456 apCorrMap=apCorrMap,
457 )
458 self.catalogCalculation.run(measCat)
459
460 return pipeBase.Struct(measCat=measCat)
461
462 def attachFootprints(self, sources, refCat, exposure, refWcs):
463 """Attach footprints to blank sources prior to measurements.
464
465 Notes
466 -----
467 `~lsst.afw.detection.Footprint` objects for forced photometry must
468 be in the pixel coordinate system of the image being measured, while
469 the actual detections may start out in a different coordinate system.
470
471 Subclasses of this class may implement this method to define how
472 those `~lsst.afw.detection.Footprint` objects should be generated.
473
474 This default implementation transforms depends on the
475 ``footprintSource`` configuration parameter.
476 """
477 if self.config.footprintSource == "transformed":
478 return self.measurement.attachTransformedFootprints(sources, refCat, exposure, refWcs)
479 elif self.config.footprintSource == "psf":
480 return self.measurement.attachPsfShapeFootprints(sources, exposure,
481 scaling=self.config.psfFootprintScaling)
482
483 def _prepSourceCatalogRefCat(self, refCatHandles, exposureBBox, exposureWcs):
484 """Prepare a merged, filtered reference catalog from SourceCatalog
485 inputs.
486
487 Parameters
488 ----------
489 refCatHandles : sequence of `lsst.daf.butler.DeferredDatasetHandle`
490 Handles for catalogs of shapes and positions at which to force
491 photometry.
492 exposureBBox : `lsst.geom.Box2I`
493 Bounding box on which to select rows that overlap
494 exposureWcs : `lsst.afw.geom.SkyWcs`
495 World coordinate system to convert sky coords in ref cat to
496 pixel coords with which to compare with exposureBBox
497
498 Returns
499 -------
500 refSources : `lsst.afw.table.SourceCatalog`
501 Filtered catalog of forced sources to measure.
502
503 Notes
504 -----
505 The majority of this code is based on the methods of
506 lsst.meas.algorithms.loadReferenceObjects.ReferenceObjectLoader
507
508 """
509 mergedRefCat = None
510
511 # Step 1: Determine bounds of the exposure photometry will
512 # be performed on.
513 expBBox = lsst.geom.Box2D(exposureBBox)
514 expBoxCorners = expBBox.getCorners()
515 expSkyCorners = [exposureWcs.pixelToSky(corner).getVector() for corner in expBoxCorners]
516 expPolygon = lsst.sphgeom.ConvexPolygon(expSkyCorners)
517
518 # Step 2: Filter out reference catalog sources that are
519 # not contained within the exposure boundaries, or whose
520 # parents are not within the exposure boundaries. Note
521 # that within a single input refCat, the parents always
522 # appear before the children.
523 for refCat in refCatHandles:
524 refCat = refCat.get()
525 if mergedRefCat is None:
526 mergedRefCat = lsst.afw.table.SourceCatalog(refCat.table)
527 containedIds = {0} # zero as a parent ID means "this is a parent"
528 for record in refCat:
529 if (
530 expPolygon.contains(record.getCoord().getVector()) and record.getParent()
531 in containedIds
532 ):
533 record.setFootprint(record.getFootprint())
534 mergedRefCat.append(record)
535 containedIds.add(record.getId())
536 if mergedRefCat is None:
537 raise RuntimeError("No reference objects for forced photometry.")
538 mergedRefCat.sort(lsst.afw.table.SourceTable.getParentKey())
539 return mergedRefCat
540
541 def _prepDataFrameRefCat(self, refCatHandles, exposureBBox, exposureWcs):
542 """Prepare a merged, filtered reference catalog from DataFrame
543 inputs.
544
545 Parameters
546 ----------
547 refCatHandles : sequence of `lsst.daf.butler.DeferredDatasetHandle`
548 Handles for catalogs of shapes and positions at which to force
549 photometry.
550 exposureBBox : `lsst.geom.Box2I`
551 Bounding box on which to select rows that overlap
552 exposureWcs : `lsst.afw.geom.SkyWcs`
553 World coordinate system to convert sky coords in ref cat to
554 pixel coords with which to compare with exposureBBox
555
556 Returns
557 -------
558 refCat : `lsst.afw.table.SourceTable`
559 Source Catalog with minimal schema that overlaps exposureBBox
560 """
561 dfList = [
562 i.get(
563 parameters={
564 "columns": [
565 self.config.refCatIdColumn,
566 self.config.refCatRaColumn,
567 self.config.refCatDecColumn,
568 ]
569 }
570 )
571 for i in refCatHandles
572 ]
573 df = pd.concat(dfList)
574 # translate ra/dec coords in dataframe to detector pixel coords
575 # to down select rows that overlap the detector bbox
576 mapping = exposureWcs.getTransform().getMapping()
577 x, y = mapping.applyInverse(
578 np.array(df[[self.config.refCatRaColumn, self.config.refCatDecColumn]].values*2*np.pi/360).T
579 )
580 inBBox = np.atleast_1d(lsst.geom.Box2D(exposureBBox).contains(x, y))
581 refCat = self._makeMinimalSourceCatalogFromDataFrame(df[inBBox])
582 return refCat
583
584 def _prepArrowAstropyRefCat(self, refCatHandles, exposureBBox, exposureWcs):
585 """Prepare a merged, filtered reference catalog from ArrowAstropy
586 inputs.
587
588 Parameters
589 ----------
590 refCatHandles : sequence of `lsst.daf.butler.DeferredDatasetHandle`
591 Handles for catalogs of shapes and positions at which to force
592 photometry.
593 exposureBBox : `lsst.geom.Box2I`
594 Bounding box on which to select rows that overlap
595 exposureWcs : `lsst.afw.geom.SkyWcs`
596 World coordinate system to convert sky coords in ref cat to
597 pixel coords with which to compare with exposureBBox
598
599 Returns
600 -------
601 refCat : `lsst.afw.table.SourceTable`
602 Source Catalog with minimal schema that overlaps exposureBBox
603 """
604 table_list = [
605 i.get(
606 parameters={
607 "columns": [
608 self.config.refCatIdColumn,
609 self.config.refCatRaColumn,
610 self.config.refCatDecColumn,
611 ]
612 }
613 )
614 for i in refCatHandles
615 ]
616 full_table = astropy.table.vstack(table_list)
617 # translate ra/dec coords in table to detector pixel coords
618 # to down-select rows that overlap the detector bbox
619 mapping = exposureWcs.getTransform().getMapping()
620 ra_dec_rad = np.zeros((2, len(full_table)), dtype=float)
621 ra_dec_rad[0, :] = full_table[self.config.refCatRaColumn]
622 ra_dec_rad[1, :] = full_table[self.config.refCatDecColumn]
623 ra_dec_rad *= np.pi/180.0
624 x, y = mapping.applyInverse(ra_dec_rad)
625 inBBox = lsst.geom.Box2D(exposureBBox).contains(x, y)
626 refCat = self._makeMinimalSourceCatalogFromAstropy(full_table[inBBox])
627 return refCat
628
629 def _makeMinimalSourceCatalogFromDataFrame(self, df):
630 """Create minimal schema SourceCatalog from a pandas DataFrame.
631
632 The forced measurement subtask expects this as input.
633
634 Parameters
635 ----------
636 df : `pandas.DataFrame`
637 Table with locations and ids.
638
639 Returns
640 -------
641 outputCatalog : `lsst.afw.table.SourceTable`
642 Output catalog with minimal schema.
643 """
645 outputCatalog = lsst.afw.table.SourceCatalog(schema)
646 outputCatalog.reserve(len(df))
647
648 for objectId, ra, dec in df[['ra', 'dec']].itertuples():
649 outputRecord = outputCatalog.addNew()
650 outputRecord.setId(objectId)
651 outputRecord.setCoord(lsst.geom.SpherePoint(ra, dec, lsst.geom.degrees))
652 return outputCatalog
653
654 def _makeMinimalSourceCatalogFromAstropy(self, table):
655 """Create minimal schema SourceCatalog from an Astropy Table.
656
657 The forced measurement subtask expects this as input.
658
659 Parameters
660 ----------
661 table : `astropy.table.Table`
662 Table with locations and ids.
663
664 Returns
665 -------
666 outputCatalog : `lsst.afw.table.SourceTable`
667 Output catalog with minimal schema.
668 """
670 outputCatalog = lsst.afw.table.SourceCatalog(schema)
671 outputCatalog.reserve(len(table))
672
673 for objectId, ra, dec in table.iterrows():
674 outputRecord = outputCatalog.addNew()
675 outputRecord.setId(objectId)
676 outputRecord.setCoord(lsst.geom.SpherePoint(ra, dec, lsst.geom.degrees))
677 return outputCatalog
678
679
680class ForcedPhotCcdFromDataFrameConnections(ForcedPhotCcdConnections,
681 dimensions=("instrument", "visit", "detector", "skymap", "tract"),
682 defaultTemplates={"inputCoaddName": "goodSeeing",
683 "inputName": "calexp",
684 }):
685 pass
686
687
688class ForcedPhotCcdFromDataFrameConfig(ForcedPhotCcdConfig,
689 pipelineConnections=ForcedPhotCcdFromDataFrameConnections):
690 def setDefaults(self):
691 super().setDefaults()
692 self.configureParquetRefCat("DataFrame")
693 self.connections.refCat = "{inputCoaddName}Diff_fullDiaObjTable"
694 self.connections.outputSchema = "forced_src_diaObject_schema"
695 self.connections.measCat = "forced_src_diaObject"
696
697
698class ForcedPhotCcdFromDataFrameTask(ForcedPhotCcdTask):
699 """Force Photometry on a per-detector exposure with coords from a DataFrame
700
701 Uses input from a DataFrame instead of SourceCatalog
702 like the base class ForcedPhotCcd does.
703 Writes out a SourceCatalog so that the downstream
704 WriteForcedSourceTableTask can be reused with output from this Task.
705 """
706 _DefaultName = "forcedPhotCcdFromDataFrame"
707 ConfigClass = ForcedPhotCcdFromDataFrameConfig
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.