Coverage for python/lsst/faro/base/MatchedCatalogBase.py : 22%

Hot-keys on this page
r m x p toggle line displays
j k next/prev highlighted chunk
0 (zero) top of page
1 (one) first highlighted chunk
1# This file is part of faro.
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/>.
22import lsst.pipe.base as pipeBase
23import lsst.pex.config as pexConfig
24import lsst.geom as geom
25import numpy as np
27from lsst.faro.utils.matcher import matchCatalogs
29__all__ = (
30 "MatchedBaseConnections",
31 "MatchedBaseConfig",
32 "MatchedBaseTask",
33 "MatchedTractBaseTask",
34)
37class MatchedBaseConnections(
38 pipeBase.PipelineTaskConnections,
39 dimensions=(),
40 defaultTemplates={
41 "coaddName": "deep",
42 "photoCalibName": "calexp.photoCalib",
43 "wcsName": "calexp.wcs",
44 "externalPhotoCalibName": "fgcm",
45 "externalWcsName": "jointcal",
46 },
47):
48 sourceCatalogs = pipeBase.connectionTypes.Input(
49 doc="Source catalogs to match up.",
50 dimensions=("instrument", "visit", "detector", "band"),
51 storageClass="SourceCatalog",
52 name="src",
53 multiple=True,
54 )
55 photoCalibs = pipeBase.connectionTypes.Input(
56 doc="Photometric calibration object.",
57 dimensions=("instrument", "visit", "detector", "band"),
58 storageClass="PhotoCalib",
59 name="{photoCalibName}",
60 multiple=True,
61 )
62 astromCalibs = pipeBase.connectionTypes.Input(
63 doc="WCS for the catalog.",
64 dimensions=("instrument", "visit", "detector", "band"),
65 storageClass="Wcs",
66 name="{wcsName}",
67 multiple=True,
68 )
69 externalSkyWcsTractCatalog = pipeBase.connectionTypes.Input(
70 doc=(
71 "Per-tract, per-visit wcs calibrations. These catalogs use the detector "
72 "id for the catalog id, sorted on id for fast lookup."
73 ),
74 name="{externalWcsName}SkyWcsCatalog",
75 storageClass="ExposureCatalog",
76 dimensions=("instrument", "visit", "tract", "band"),
77 multiple=True,
78 )
79 externalSkyWcsGlobalCatalog = pipeBase.connectionTypes.Input(
80 doc=(
81 "Per-visit wcs calibrations computed globally (with no tract information). "
82 "These catalogs use the detector id for the catalog id, sorted on id for "
83 "fast lookup."
84 ),
85 name="{externalWcsName}SkyWcsCatalog",
86 storageClass="ExposureCatalog",
87 dimensions=("instrument", "visit", "band"),
88 multiple=True,
89 )
90 externalPhotoCalibTractCatalog = pipeBase.connectionTypes.Input(
91 doc=(
92 "Per-tract, per-visit photometric calibrations. These catalogs use the "
93 "detector id for the catalog id, sorted on id for fast lookup."
94 ),
95 name="{externalPhotoCalibName}PhotoCalibCatalog",
96 storageClass="ExposureCatalog",
97 dimensions=("instrument", "visit", "tract", "band"),
98 multiple=True,
99 )
100 externalPhotoCalibGlobalCatalog = pipeBase.connectionTypes.Input(
101 doc=(
102 "Per-visit photometric calibrations computed globally (with no tract "
103 "information). These catalogs use the detector id for the catalog id, "
104 "sorted on id for fast lookup."
105 ),
106 name="{externalPhotoCalibName}PhotoCalibCatalog",
107 storageClass="ExposureCatalog",
108 dimensions=("instrument", "visit", "band"),
109 multiple=True,
110 )
111 skyMap = pipeBase.connectionTypes.Input(
112 doc="Input definition of geometry/bbox and projection/wcs for warped exposures",
113 name="skyMap",
114 storageClass="SkyMap",
115 dimensions=("skymap",),
116 )
118 def __init__(self, *, config=None):
119 super().__init__(config=config)
120 if config.doApplyExternalSkyWcs:
121 if config.useGlobalExternalSkyWcs:
122 self.inputs.remove("externalSkyWcsTractCatalog")
123 else:
124 self.inputs.remove("externalSkyWcsGlobalCatalog")
125 else:
126 self.inputs.remove("externalSkyWcsTractCatalog")
127 self.inputs.remove("externalSkyWcsGlobalCatalog")
128 if config.doApplyExternalPhotoCalib:
129 if config.useGlobalExternalPhotoCalib:
130 self.inputs.remove("externalPhotoCalibTractCatalog")
131 else:
132 self.inputs.remove("externalPhotoCalibGlobalCatalog")
133 else:
134 self.inputs.remove("externalPhotoCalibTractCatalog")
135 self.inputs.remove("externalPhotoCalibGlobalCatalog")
138class MatchedBaseConfig(
139 pipeBase.PipelineTaskConfig, pipelineConnections=MatchedBaseConnections
140):
141 match_radius = pexConfig.Field(
142 doc="Match radius in arcseconds.", dtype=float, default=1
143 )
144 doApplyExternalSkyWcs = pexConfig.Field(
145 doc="Whether or not to use the external wcs.", dtype=bool, default=False
146 )
147 useGlobalExternalSkyWcs = pexConfig.Field(
148 doc="Whether or not to use the global external wcs.", dtype=bool, default=False
149 )
150 doApplyExternalPhotoCalib = pexConfig.Field(
151 doc="Whether or not to use the external photoCalib.", dtype=bool, default=False
152 )
153 useGlobalExternalPhotoCalib = pexConfig.Field(
154 doc="Whether or not to use the global external photoCalib.",
155 dtype=bool,
156 default=False,
157 )
160class MatchedBaseTask(pipeBase.PipelineTask):
162 ConfigClass = MatchedBaseConfig
163 _DefaultName = "matchedBaseTask"
165 def __init__(self, config: MatchedBaseConfig, *args, **kwargs):
166 super().__init__(*args, config=config, **kwargs)
167 self.radius = self.config.match_radius
168 self.level = "patch"
170 def run(
171 self,
172 sourceCatalogs,
173 photoCalibs,
174 astromCalibs,
175 dataIds,
176 wcs,
177 box,
178 doApplyExternalSkyWcs=False,
179 doApplyExternalPhotoCalib=False,
180 ):
181 self.log.info("Running catalog matching")
182 radius = geom.Angle(self.radius, geom.arcseconds)
183 srcvis, matched = matchCatalogs(
184 sourceCatalogs, photoCalibs, astromCalibs, dataIds, radius, logger=self.log
185 )
186 # Trim the output to the patch bounding box
187 out_matched = type(matched)(matched.schema)
188 self.log.info("%s sources in matched catalog.", len(matched))
189 for record in matched:
190 if box.contains(wcs.skyToPixel(record.getCoord())):
191 out_matched.append(record)
192 self.log.info(
193 "%s sources when trimmed to %s boundaries.", len(out_matched), self.level
194 )
195 return pipeBase.Struct(outputCatalog=out_matched)
197 def get_box_wcs(self, skymap, oid):
198 tract_info = skymap.generateTract(oid["tract"])
199 wcs = tract_info.getWcs()
200 patch_info = tract_info.getPatchInfo(oid["patch"])
201 patch_box = patch_info.getInnerBBox()
202 self.log.info("Running tract: %s and patch: %s", oid["tract"], oid["patch"])
203 return patch_box, wcs
205 def runQuantum(self, butlerQC, inputRefs, outputRefs):
206 inputs = butlerQC.get(inputRefs)
207 oid = outputRefs.outputCatalog.dataId.byName()
208 skymap = inputs["skyMap"]
209 del inputs["skyMap"]
210 box, wcs = self.get_box_wcs(skymap, oid)
211 # Cast to float to handle fractional pixels
212 box = geom.Box2D(box)
213 inputs["dataIds"] = [
214 butlerQC.registry.expandDataId(el.dataId) for el in inputRefs.sourceCatalogs
215 ]
216 inputs["wcs"] = wcs
217 inputs["box"] = box
218 inputs["doApplyExternalSkyWcs"] = self.config.doApplyExternalSkyWcs
219 inputs["doApplyExternalPhotoCalib"] = self.config.doApplyExternalPhotoCalib
221 if self.config.doApplyExternalPhotoCalib:
222 if self.config.useGlobalExternalPhotoCalib:
223 externalPhotoCalibCatalog = inputs.pop(
224 "externalPhotoCalibGlobalCatalog"
225 )
226 else:
227 externalPhotoCalibCatalog = inputs.pop("externalPhotoCalibTractCatalog")
229 flatPhotoCalibList = np.hstack(externalPhotoCalibCatalog)
230 visitPhotoCalibList = np.array(
231 [calib["visit"] for calib in flatPhotoCalibList]
232 )
233 detectorPhotoCalibList = np.array(
234 [calib["id"] for calib in flatPhotoCalibList]
235 )
237 if self.config.doApplyExternalSkyWcs:
238 if self.config.useGlobalExternalSkyWcs:
239 externalSkyWcsCatalog = inputs.pop("externalSkyWcsGlobalCatalog")
240 else:
241 externalSkyWcsCatalog = inputs.pop("externalSkyWcsTractCatalog")
243 flatSkyWcsList = np.hstack(externalSkyWcsCatalog)
244 visitSkyWcsList = np.array([calib["visit"] for calib in flatSkyWcsList])
245 detectorSkyWcsList = np.array([calib["id"] for calib in flatSkyWcsList])
247 if self.config.doApplyExternalPhotoCalib:
248 for i in range(len(inputs["dataIds"])):
249 dataId = inputs["dataIds"][i]
250 detector = dataId["detector"]
251 visit = dataId["visit"]
252 calib_find = (visitPhotoCalibList == visit) & (
253 detectorPhotoCalibList == detector
254 )
255 row = flatPhotoCalibList[calib_find]
256 externalPhotoCalib = row[0].getPhotoCalib()
257 inputs["photoCalibs"][i] = externalPhotoCalib
259 if self.config.doApplyExternalSkyWcs:
260 for i in range(len(inputs["dataIds"])):
261 dataId = inputs["dataIds"][i]
262 detector = dataId["detector"]
263 visit = dataId["visit"]
264 calib_find = (visitSkyWcsList == visit) & (
265 detectorSkyWcsList == detector
266 )
267 row = flatSkyWcsList[calib_find]
268 externalSkyWcs = row[0].getWcs()
269 inputs["astromCalibs"][i] = externalSkyWcs
271 outputs = self.run(**inputs)
272 butlerQC.put(outputs, outputRefs)
275class MatchedTractBaseTask(MatchedBaseTask):
277 ConfigClass = MatchedBaseConfig
278 _DefaultName = "matchedTractBaseTask"
280 def __init__(self, config: MatchedBaseConfig, *args, **kwargs):
281 super().__init__(*args, config=config, **kwargs)
282 self.radius = self.config.match_radius
283 self.level = "tract"
285 def get_box_wcs(self, skymap, oid):
286 tract_info = skymap.generateTract(oid["tract"])
287 wcs = tract_info.getWcs()
288 tract_box = tract_info.getBBox()
289 self.log.info("Running tract: %s", oid["tract"])
290 return tract_box, wcs