Coverage for python/lsst/analysis/tools/tasks/catalogMatch.py: 33%
95 statements
« prev ^ index » next coverage.py v7.3.2, created at 2023-12-08 13:17 +0000
« prev ^ index » next coverage.py v7.3.2, created at 2023-12-08 13:17 +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__ = ("CatalogMatchConfig", "CatalogMatchTask")
25import lsst.geom
26import lsst.pex.config as pexConfig
27import lsst.pipe.base as pipeBase
28import numpy as np
29from astropy.table import Table, hstack
30from astropy.time import Time
31from lsst.pipe.tasks.configurableActions import ConfigurableActionStructField
32from lsst.pipe.tasks.loadReferenceCatalog import LoadReferenceCatalogTask
33from lsst.skymap import BaseSkyMap
34from smatch import Matcher
36from ..actions.vector import CoaddPlotFlagSelector, GalaxySelector, SnSelector, StarSelector
37from ..interfaces import VectorAction
40class CatalogMatchConnections(
41 pipeBase.PipelineTaskConnections,
42 dimensions=("tract", "skymap"),
43 defaultTemplates={"targetCatalog": "objectTable_tract", "refCatalog": "ps1_pv3_3pi_20170110"},
44):
45 catalog = pipeBase.connectionTypes.Input(
46 doc="The tract-wide catalog to make plots from.",
47 storageClass="ArrowAstropy",
48 name="{targetCatalog}",
49 dimensions=("tract", "skymap"),
50 deferLoad=True,
51 )
53 refCat = pipeBase.connectionTypes.PrerequisiteInput(
54 doc="The reference catalog to match to loaded input catalog sources.",
55 name="{refCatalog}",
56 storageClass="SimpleCatalog",
57 dimensions=("skypix",),
58 deferLoad=True,
59 multiple=True,
60 )
62 skymap = pipeBase.connectionTypes.Input(
63 doc="The skymap for the tract",
64 storageClass="SkyMap",
65 name=BaseSkyMap.SKYMAP_DATASET_TYPE_NAME,
66 dimensions=("skymap",),
67 )
69 matchedCatalog = pipeBase.connectionTypes.Output(
70 doc="Catalog with matched target and reference objects with separations",
71 name="{targetCatalog}_{refCatalog}_match",
72 storageClass="ArrowAstropy",
73 dimensions=("tract", "skymap"),
74 )
77class CatalogMatchConfig(pipeBase.PipelineTaskConfig, pipelineConnections=CatalogMatchConnections):
78 referenceCatalogLoader = pexConfig.ConfigurableField(
79 target=LoadReferenceCatalogTask,
80 doc="Reference catalog loader",
81 )
83 epoch = pexConfig.Field[float](
84 doc="Epoch to which reference objects are shifted.",
85 default=2015.0,
86 )
88 filterNames = pexConfig.ListField[str](
89 doc="Physical filter names to persist downstream.",
90 default=["u", "g", "r", "i", "z", "y"],
91 )
93 selectorBands = pexConfig.ListField[str](
94 doc="Band to use when selecting objects, primarily for extendedness.",
95 default=["i"],
96 )
98 selectorActions = ConfigurableActionStructField[VectorAction](
99 doc="Which selectors to use to narrow down the data for QA plotting.",
100 default={"flagSelector": CoaddPlotFlagSelector()},
101 )
103 sourceSelectorActions = ConfigurableActionStructField[VectorAction](
104 doc="What types of sources to use.",
105 default={"sourceSelector": StarSelector()},
106 )
108 extraColumnSelectors = ConfigurableActionStructField[VectorAction](
109 doc="Other selectors that are not used in this task, but whose columns" "may be needed downstream",
110 default={"selector1": SnSelector(), "selector2": GalaxySelector()},
111 )
113 extraColumns = pexConfig.ListField[str](
114 doc="Other catalog columns to persist to downstream tasks",
115 default=["x", "y", "patch", "ebv"],
116 )
118 extraPerBandColumns = pexConfig.ListField[str](
119 doc="Other columns to load that should be loaded for each band individually.",
120 default=["cModelFlux"],
121 )
123 matchRadius = pexConfig.Field[float](
124 doc="The radius to use for matching, in arcsecs.",
125 default=1.0,
126 )
128 raColumn = pexConfig.Field[str](doc="RA column.", default="coord_ra")
129 decColumn = pexConfig.Field[str](doc="Dec column.", default="coord_dec")
130 patchColumn = pexConfig.Field[str](doc="Patch column.", default="patch")
132 def setDefaults(self):
133 super().setDefaults()
134 self.referenceCatalogLoader.doReferenceSelection = False
135 self.referenceCatalogLoader.doApplyColorTerms = False
138class CatalogMatchTask(pipeBase.PipelineTask):
139 """The base task for matching catalogs. Figures out which columns
140 it needs to grab for the downstream tasks and then matches the
141 two tables together and returns the matched and joined table
142 including the extra columns.
143 """
145 ConfigClass = CatalogMatchConfig
146 _DefaultName = "analysisToolsCatalogMatch"
148 def runQuantum(self, butlerQC, inputRefs, outputRefs):
149 """Implemented in the inherited tasks"""
150 pass
152 def run(self, *, catalog, loadedRefCat, bands):
153 """Takes the two catalogs and returns the matched one.
155 Parameters
156 ----------
157 `catalog` : astropy.table.Table
158 The catalog to be matched
159 `loadedRefCat` : astropy.table.Table
160 The loaded reference catalog
161 `bands` : list
162 A list of bands to apply the selectors in
164 Returns
165 -------
166 `matchedCatalog` : astropy.table.Table
168 Notes
169 -----
170 Performs an RA/Dec match that returns the closest match
171 within the match radius which defaults to 1.0 arcsecond.
172 Applies the suffix, _target, to the catalog being matched
173 and _ref to the reference catalog.
174 """
175 # Apply the selectors to the catalog
176 mask = np.ones(len(catalog), dtype=bool)
177 for selector in self.config.sourceSelectorActions:
178 for band in self.config.selectorBands:
179 mask &= selector(catalog, band=band).astype(bool)
181 targetCatalog = catalog[mask]
183 if (len(targetCatalog) == 0) or (len(loadedRefCat) == 0):
184 refMatchIndices = np.array([], dtype=np.int64)
185 targetMatchIndices = np.array([], dtype=np.int64)
186 dists = np.array([], dtype=np.float64)
187 else:
188 # Run the matcher.
190 # This all assumes that everything is in degrees.
191 # Which I think is okay, but the current task
192 # allows different units. Need to configure match
193 # radius, either in this task or a subtask.
194 with Matcher(loadedRefCat["ra"], loadedRefCat["dec"]) as m:
195 idx, refMatchIndices, targetMatchIndices, dists = m.query_radius(
196 targetCatalog[self.config.raColumn],
197 targetCatalog[self.config.decColumn],
198 self.config.matchRadius / 3600.0,
199 return_indices=True,
200 )
202 # Convert degrees to arcseconds.
203 dists *= 3600.0
205 targetCatalogMatched = targetCatalog[targetMatchIndices]
206 loadedRefCatMatched = loadedRefCat[refMatchIndices]
208 targetCols = targetCatalogMatched.columns.copy()
209 for col in targetCols:
210 targetCatalogMatched.rename_column(col, col + "_target")
211 refCols = loadedRefCatMatched.columns.copy()
212 for col in refCols:
213 loadedRefCatMatched.rename_column(col, col + "_ref")
215 for i, band in enumerate(bands):
216 loadedRefCatMatched[band + "_mag_ref"] = loadedRefCatMatched["refMag_ref"][:, i]
217 loadedRefCatMatched[band + "_magErr_ref"] = loadedRefCatMatched["refMagErr_ref"][:, i]
218 loadedRefCatMatched.remove_column("refMag_ref")
219 loadedRefCatMatched.remove_column("refMagErr_ref")
220 tMatched = hstack([targetCatalogMatched, loadedRefCatMatched], join_type="exact")
221 tMatched["matchDistance"] = dists
223 return pipeBase.Struct(matchedCatalog=tMatched)
225 def prepColumns(self, bands):
226 """Get all the columns needed for downstream tasks.
227 Both those from the selectors and those specified in the
228 config options.
229 """
231 bandColumns = []
232 for band in bands:
233 for col in self.config.extraPerBandColumns:
234 bandColumns.append(band + "_" + col)
236 columns = (
237 [
238 self.config.raColumn,
239 self.config.decColumn,
240 ]
241 + self.config.extraColumns.list()
242 + bandColumns
243 )
245 if self.config.patchColumn != "":
246 columns.append(self.config.patchColumn)
248 selectorBands = list(set(list(bands) + self.config.selectorBands.list()))
249 for selectorAction in [
250 self.config.selectorActions,
251 self.config.sourceSelectorActions,
252 self.config.extraColumnSelectors,
253 ]:
254 for selector in selectorAction:
255 for band in selectorBands:
256 selectorSchema = selector.getFormattedInputSchema(band=band)
257 columns += [s[0] for s in selectorSchema]
259 return columns
261 def _loadRefCat(self, loaderTask, tractInfo):
262 """Load the reference catalog that covers the
263 catalog that is to be matched to.
265 Parameters
266 ----------
267 `loaderTask` :
268 lsst.pipe.tasks.loadReferenceCatalog.loadReferenceCatalogTask
269 `tractInfo` : lsst.skymap.tractInfo.ExplicitTractInfo
270 The tract information to get the sky location from
272 Returns
273 -------
274 `loadedRefCat` : astropy.table.Table
275 The reference catalog that covers the input catalog.
276 """
277 boundingCircle = tractInfo.getOuterSkyPolygon().getBoundingCircle()
278 center = lsst.geom.SpherePoint(boundingCircle.getCenter())
279 radius = boundingCircle.getOpeningAngle()
281 epoch = Time(self.config.epoch, format="decimalyear")
283 # This is always going to return degrees.
284 try:
285 loadedRefCat = loaderTask.getSkyCircleCatalog(
286 center, radius, self.config.filterNames, epoch=epoch
287 )
288 except RuntimeError as e:
289 raise pipeBase.NoWorkFound(e)
291 return Table(loadedRefCat)