Coverage for python / lsst / analysis / tools / tasks / astrometricCatalogMatch.py: 33%
99 statements
« prev ^ index » next coverage.py v7.13.5, created at 2026-04-22 09:09 +0000
« prev ^ index » next coverage.py v7.13.5, created at 2026-04-22 09:09 +0000
1# This file is part of analysis_tools.
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/>.
22__all__ = (
23 "AstrometricCatalogMatchConfig",
24 "AstrometricCatalogMatchTask",
25 "AstrometricCatalogMatchVisitConfig",
26 "AstrometricCatalogMatchVisitTask",
27)
29import lsst.geom
30import lsst.pex.config as pexConfig
31import lsst.pipe.base as pipeBase
32import numpy as np
33from astropy.table import Table
34from astropy.time import Time
35from lsst.pex.config.configurableActions import ConfigurableActionStructField
36from lsst.pipe.tasks.loadReferenceCatalog import LoadReferenceCatalogTask
38from ..actions.vector import StarSelector, VisitPlotFlagSelector
39from ..tasks.catalogMatch import CatalogMatchConfig, CatalogMatchConnections, CatalogMatchTask
42class AstrometricCatalogMatchConnections(
43 CatalogMatchConnections,
44 dimensions=("tract", "skymap"),
45 defaultTemplates={"targetCatalog": "objectTable_tract", "refCatalog": "gaia_dr3_20230707"},
46):
47 matchedCatalog = pipeBase.connectionTypes.Output(
48 doc="Catalog with matched target and reference objects with separations",
49 name="{targetCatalog}_{refCatalog}_match_astrom",
50 storageClass="ArrowAstropy",
51 dimensions=("tract", "skymap"),
52 )
55class AstrometricCatalogMatchConfig(
56 CatalogMatchConfig, pipelineConnections=AstrometricCatalogMatchConnections
57):
58 bands = pexConfig.ListField[str](
59 doc="The bands to persist downstream",
60 default=["u", "g", "r", "i", "z", "y"],
61 )
62 extraPerBandColumns = pexConfig.ListField[str](
63 doc="Other columns to load that should be loaded for each band individually.",
64 default=["epoch"],
65 )
66 extraColumns = pexConfig.ListField[str](
67 doc="Other catalog columns to persist to downstream tasks",
68 default=["patch", "ebv", "refBand"],
69 )
71 def setDefaults(self):
72 super().setDefaults()
73 self.matchesRefCat = True
74 self.referenceCatalogLoader.doApplyColorTerms = False
75 self.referenceCatalogLoader.refObjLoader.requireProperMotion = True
76 self.referenceCatalogLoader.refObjLoader.anyFilterMapsToThis = "phot_g_mean"
77 self.connections.refCatalog = "gaia_dr3_20230707"
80class AstrometricCatalogMatchTask(CatalogMatchTask):
81 """Match a tract-level catalog to a reference catalog"""
83 ConfigClass = AstrometricCatalogMatchConfig
84 _DefaultName = "analysisToolsAstrometricCatalogMatch"
86 def runQuantum(self, butlerQC, inputRefs, outputRefs):
87 # Docs inherited from base class
89 inputs = butlerQC.get(inputRefs)
90 columns = self.prepColumns(self.config.bands)
91 table = inputs["catalog"].get(parameters={"columns": columns})
93 tract = butlerQC.quantum.dataId["tract"]
95 loaderTask = LoadReferenceCatalogTask(
96 config=self.config.referenceCatalogLoader,
97 dataIds=[ref.dataId for ref in inputRefs.refCat],
98 name=inputs["refCat"][0].ref.datasetType.name,
99 refCats=inputs["refCat"],
100 )
102 skymap = inputs.pop("skymap")
103 # We cannot load the reference catalog at different epochs for each
104 # object. Instead use the median of the epochs corresponding to the
105 # reference band for each object.
106 refEpochs = np.array(
107 [
108 obj[obj["refBand"] + "_epoch"]
109 for obj in table
110 if not np.ma.is_masked(obj[obj["refBand"] + "_epoch"])
111 ]
112 )
113 epoch = Time(np.median(refEpochs), format="mjd")
114 loadedRefCat = self._loadRefCat(loaderTask, skymap[tract], epoch=epoch)
115 outputs = self.run(targetCatalog=table, refCatalog=loadedRefCat, bands=self.config.bands)
117 butlerQC.put(outputs, outputRefs)
120class AstrometricCatalogMatchVisitConnections(
121 pipeBase.PipelineTaskConnections,
122 dimensions=("visit",),
123 defaultTemplates={"targetCatalog": "sourceTable_visit", "refCatalog": "gaia_dr3_20230707"},
124):
125 catalog = pipeBase.connectionTypes.Input(
126 doc="The visit-wide catalog to make plots from.",
127 storageClass="ArrowAstropy",
128 name="{targetCatalog}",
129 dimensions=("visit",),
130 deferLoad=True,
131 )
133 refCat = pipeBase.connectionTypes.PrerequisiteInput(
134 doc="The astrometry reference catalog to match to loaded input catalog sources.",
135 name="{refCatalog}",
136 storageClass="SimpleCatalog",
137 dimensions=("skypix",),
138 deferLoad=True,
139 multiple=True,
140 )
142 visitSummaryTable = pipeBase.connectionTypes.Input(
143 doc="A summary table of the ccds in the visit",
144 storageClass="ExposureCatalog",
145 name="finalVisitSummary",
146 dimensions=("visit",),
147 )
149 matchedCatalog = pipeBase.connectionTypes.Output(
150 doc="Catalog with matched target and reference objects with separations",
151 name="{targetCatalog}_{refCatalog}_match_astrom",
152 storageClass="ArrowAstropy",
153 dimensions=("visit",),
154 )
157class AstrometricCatalogMatchVisitConfig(
158 AstrometricCatalogMatchConfig, pipelineConnections=AstrometricCatalogMatchVisitConnections
159):
160 selectorActions = ConfigurableActionStructField(
161 doc="Which selectors to use to narrow down the data for QA plotting.",
162 default={"flagSelector": VisitPlotFlagSelector},
163 )
165 extraColumns = pexConfig.ListField[str](
166 doc="Other catalog columns to persist to downstream tasks",
167 default=["psfFlux", "psfFluxErr"],
168 )
170 bands = pexConfig.ListField[str](
171 doc="The bands to persist downstream",
172 default=[],
173 )
175 def setDefaults(self):
176 self.matchesRefCat = True
177 self.idColumn = "sourceId"
178 # sourceSelectorActions.sourceSelector is StarSelector
179 self.sourceSelectorActions.sourceSelector = StarSelector()
180 self.sourceSelectorActions.sourceSelector.vectorKey = "extendedness"
181 # extraColumnSelectors.selector1 is SnSelector
182 self.extraColumnSelectors.selector1.fluxType = "psfFlux"
183 # extraColumnSelectors.selector2 is GalaxySelector
184 self.extraColumnSelectors.selector2.vectorKey = "extendedness"
185 self.extraColumnSelectors.selector3.vectorKey = "extendedness"
186 self.extraColumnSelectors.selector4 = VisitPlotFlagSelector
187 self.referenceCatalogLoader.doApplyColorTerms = False
188 self.referenceCatalogLoader.refObjLoader.requireProperMotion = False
189 self.referenceCatalogLoader.refObjLoader.anyFilterMapsToThis = "phot_g_mean"
192class AstrometricCatalogMatchVisitTask(AstrometricCatalogMatchTask):
193 """Match a visit-level catalog to a reference catalog"""
195 ConfigClass = AstrometricCatalogMatchVisitConfig
196 _DefaultName = "analysisToolsAstrometricCatalogMatchVisit"
198 def runQuantum(self, butlerQC, inputRefs, outputRefs):
199 # Docs inherited from base class
201 inputs = butlerQC.get(inputRefs)
203 columns = [self.config.idColumn, "coord_ra", "coord_dec", "detector"]
204 columns.extend(self.config.extraColumns)
205 for selectorAction in [
206 self.config.selectorActions,
207 self.config.sourceSelectorActions,
208 self.config.extraColumnSelectors,
209 ]:
210 for selector in selectorAction:
211 selectorSchema = selector.getFormattedInputSchema()
212 columns += [s[0] for s in selectorSchema]
214 table = inputs["catalog"].get(parameters={"columns": columns})
216 loaderTask = LoadReferenceCatalogTask(
217 config=self.config.referenceCatalogLoader,
218 dataIds=[ref.dataId for ref in inputRefs.refCat],
219 name=inputs["refCat"][0].ref.datasetType.name,
220 refCats=inputs["refCat"],
221 )
223 visitSummaryTable = inputs.pop("visitSummaryTable")
224 loadedRefCat, badDetectors = self._loadRefCat(loaderTask, visitSummaryTable)
225 for badDetector in badDetectors:
226 mask = table["detector"] != badDetector
227 self.log.info(
228 "Trimming %d sources from detector %s with incomplete or missing astrometry.",
229 len(table) - sum(mask),
230 badDetector,
231 )
232 table = table[mask]
233 outputs = self.run(targetCatalog=table, refCatalog=loadedRefCat, bands=self.config.bands)
235 butlerQC.put(outputs, outputRefs)
237 def _loadRefCat(self, loaderTask, visitSummaryTable):
238 """Make a reference catalog with coordinates in degrees
240 Parameters
241 ----------
242 visitSummaryTable : `lsst.afw.table.ExposureCatalog`
243 The table of visit information
245 Returns
246 -------
247 refCat : `astropy.table.Table`
248 Reference catalog.
249 badDetectors : `list` [ `int` ]
250 IDs of detectors with invalid visit summaries (should be skipped).
251 """
252 # Get convex hull around the detectors, then get its center and radius
253 corners = []
254 badDetectors = []
255 for visSum in visitSummaryTable:
256 for ra, dec in zip(visSum["raCorners"], visSum["decCorners"]):
257 # If the coordinates are nan skip this detector.
258 if not np.isfinite(ra) or not np.isfinite(dec):
259 badDetectors.append(visSum.getId())
260 break
261 corners.append(lsst.geom.SpherePoint(ra, dec, units=lsst.geom.degrees).getVector())
262 if not corners:
263 raise pipeBase.NoWorkFound("No visit summary corners were finite.")
264 visitBoundingCircle = lsst.sphgeom.ConvexPolygon.convexHull(corners).getBoundingCircle()
265 center = lsst.geom.SpherePoint(visitBoundingCircle.getCenter())
266 radius = visitBoundingCircle.getOpeningAngle()
268 # Get the observation date of the visit
269 epoch = visSum.getVisitInfo().getDate().toAstropy()
271 # Load the reference catalog in the skyCircle of the detectors, then
272 # convert the coordinates to degrees and convert the catalog to a
273 # dataframe
275 filterName = self.config.referenceCatalogLoader.refObjLoader.anyFilterMapsToThis
276 loadedRefCat = loaderTask.getSkyCircleCatalog(center, radius, filterName, epoch=epoch)
278 return Table(loadedRefCat), badDetectors