Coverage for python/lsst/faro/base/MatchedCatalogBase.py: 25%
103 statements
« prev ^ index » next coverage.py v7.5.0, created at 2024-04-25 19:08 +0000
« prev ^ index » next coverage.py v7.5.0, created at 2024-04-25 19:08 +0000
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.afw.table as afwTable
23import lsst.pipe.base as pipeBase
24import lsst.pex.config as pexConfig
25import lsst.geom as geom
26from lsst.utils.logging import PeriodicLogger
27import numpy as np
29from lsst.faro.utils.matcher import matchCatalogs
31__all__ = (
32 "MatchedBaseConnections",
33 "MatchedBaseConfig",
34 "MatchedBaseTask",
35 "MatchedTractBaseTask",
36)
39class MatchedBaseConnections(
40 pipeBase.PipelineTaskConnections,
41 dimensions=(),
42 defaultTemplates={"coaddName": "deep"}
43):
44 sourceCatalogs = pipeBase.connectionTypes.Input(
45 doc="Source catalogs to match up.",
46 dimensions=("instrument", "visit", "detector", "band"),
47 storageClass="SourceCatalog",
48 name="src",
49 multiple=True,
50 )
51 visitSummary = pipeBase.connectionTypes.Input(
52 doc="Exposure catalog with WCS and PhotoCalib this detector+visit combination.",
53 dimensions=("instrument", "visit"),
54 storageClass="ExposureCatalog",
55 name="finalVisitSummary",
56 multiple=True,
57 )
58 skyMap = pipeBase.connectionTypes.Input(
59 doc="Input definition of geometry/bbox and projection/wcs for warped exposures",
60 name="skyMap",
61 storageClass="SkyMap",
62 dimensions=("skymap",),
63 )
66class MatchedBaseConfig(
67 pipeBase.PipelineTaskConfig, pipelineConnections=MatchedBaseConnections
68):
69 match_radius = pexConfig.Field(
70 doc="Match radius in arcseconds.", dtype=float, default=1
71 )
72 snrMin = pexConfig.Field(
73 doc="Minimum SNR for a source to be included.",
74 dtype=float, default=200
75 )
76 snrMax = pexConfig.Field(
77 doc="Maximum SNR for a source to be included.",
78 dtype=float, default=np.Inf
79 )
80 brightMagCut = pexConfig.Field(
81 doc="Bright limit of catalog entries to include.", dtype=float, default=10.0
82 )
83 faintMagCut = pexConfig.Field(
84 doc="Faint limit of catalog entries to include.", dtype=float, default=30.0
85 )
86 selectExtended = pexConfig.Field(
87 doc="Whether to select extended sources", dtype=bool, default=False
88 )
91class MatchedBaseTask(pipeBase.PipelineTask):
93 ConfigClass = MatchedBaseConfig
94 _DefaultName = "matchedBaseTask"
96 def __init__(self, config: MatchedBaseConfig, *args, **kwargs):
97 super().__init__(*args, config=config, **kwargs)
98 self.radius = self.config.match_radius
99 self.level = "patch"
101 def run(
102 self,
103 sourceCatalogs,
104 photoCalibs,
105 astromCalibs,
106 dataIds,
107 wcs,
108 box,
109 ):
110 self.log.info("Running catalog matching")
111 periodicLog = PeriodicLogger(self.log)
112 radius = geom.Angle(self.radius, geom.arcseconds)
113 if len(sourceCatalogs) < 2:
114 self.log.warning("%s valid input catalogs: ", len(sourceCatalogs))
115 out_matched = afwTable.SimpleCatalog()
116 else:
117 srcvis, matched = matchCatalogs(
118 sourceCatalogs, photoCalibs, astromCalibs, dataIds, radius,
119 self.config, logger=self.log
120 )
121 self.log.verbose("Finished matching catalogs.")
123 # Trim the output to the patch bounding box
124 out_matched = type(matched)(matched.schema)
125 self.log.info("%s sources in matched catalog.", len(matched))
126 for record_index, record in enumerate(matched):
127 if box.contains(wcs.skyToPixel(record.getCoord())):
128 out_matched.append(record)
129 periodicLog.log("Checked %d records for trimming out of %d.", record_index + 1, len(matched))
131 self.log.info(
132 "%s sources when trimmed to %s boundaries.", len(out_matched), self.level
133 )
134 return pipeBase.Struct(outputCatalog=out_matched)
136 def get_box_wcs(self, skymap, oid):
137 tract_info = skymap.generateTract(oid["tract"])
138 wcs = tract_info.getWcs()
139 patch_info = tract_info.getPatchInfo(oid["patch"])
140 patch_box = patch_info.getInnerBBox()
141 self.log.info("Running tract: %s and patch: %s", oid["tract"], oid["patch"])
142 return patch_box, wcs
144 def runQuantum(self, butlerQC, inputRefs, outputRefs):
145 inputs = butlerQC.get(inputRefs)
146 self.log.verbose("Inputs obtained from the butler.")
147 oid = dict(outputRefs.outputCatalog.dataId.required)
148 skymap = inputs["skyMap"]
149 del inputs["skyMap"]
150 box, wcs = self.get_box_wcs(skymap, oid)
151 # Cast to float to handle fractional pixels
152 box = geom.Box2D(box)
153 inputs["dataIds"] = [el.dataId for el in inputRefs.sourceCatalogs]
154 inputs["wcs"] = wcs
155 inputs["box"] = box
156 visitSummary = inputs.pop("visitSummary")
158 flatVisitSummaryList = np.hstack(visitSummary)
159 visitSummaryVisitIdList = np.array(
160 [calib["visit"] for calib in flatVisitSummaryList]
161 )
162 visitSummaryDetectorIdList = np.array(
163 [calib["id"] for calib in flatVisitSummaryList]
164 )
166 remove_indices = []
167 inputs["photoCalibs"] = [None] * len(inputs["dataIds"])
168 inputs["astromCalibs"] = [None] * len(inputs["dataIds"])
170 for i in range(len(inputs["dataIds"])):
171 dataId = inputs["dataIds"][i]
172 detector = dataId["detector"]
173 visit = dataId["visit"]
174 row_find = (visitSummaryVisitIdList == visit) & (
175 visitSummaryDetectorIdList == detector
176 )
177 if np.sum(row_find) < 1:
178 self.log.warning("Detector id %s not found in visit summary "
179 "for visit %s and will not be used.",
180 detector, visit)
181 inputs["photoCalibs"][i] = None
182 inputs["astromCalibs"][i] = None
183 remove_indices.append(i)
184 else:
185 row = flatVisitSummaryList[row_find]
186 inputs["photoCalibs"][i] = row[0].getPhotoCalib()
187 inputs["astromCalibs"][i] = row[0].getWcs()
189 # Remove datasets that didn't have matching external calibs
190 remove_indices = np.unique(np.array(remove_indices))
191 if len(remove_indices) > 0:
192 for ind in sorted(remove_indices, reverse=True):
193 del inputs['sourceCatalogs'][ind]
194 del inputs['dataIds'][ind]
195 del inputs['photoCalibs'][ind]
196 del inputs['astromCalibs'][ind]
198 outputs = self.run(**inputs)
199 butlerQC.put(outputs, outputRefs)
202class MatchedTractBaseTask(MatchedBaseTask):
204 ConfigClass = MatchedBaseConfig
205 _DefaultName = "matchedTractBaseTask"
207 def __init__(self, config: MatchedBaseConfig, *args, **kwargs):
208 super().__init__(*args, config=config, **kwargs)
209 self.radius = self.config.match_radius
210 self.level = "tract"
212 def get_box_wcs(self, skymap, oid):
213 tract_info = skymap.generateTract(oid["tract"])
214 wcs = tract_info.getWcs()
215 tract_box = tract_info.getBBox()
216 self.log.info("Running tract: %s", oid["tract"])
217 return tract_box, wcs