Coverage for python/lsst/meas/base/references.py: 34%

92 statements  

« prev     ^ index     » next       coverage.py v6.4.2, created at 2022-07-13 11:17 +0000

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 

22""" 

23Subtasks for creating the reference catalogs used in forced measurement. 

24""" 

25 

26import lsst.geom 

27import lsst.pex.config 

28import lsst.pipe.base 

29 

30__all__ = ("BaseReferencesTask", "CoaddSrcReferencesTask") 

31 

32 

33class BaseReferencesConfig(lsst.pex.config.Config): 

34 """Default configuration for reference source selection. 

35 """ 

36 

37 removePatchOverlaps = lsst.pex.config.Field( 

38 doc="Only include reference sources for each patch that lie within the patch's inner bbox", 

39 dtype=bool, 

40 default=True 

41 ) 

42 filter = lsst.pex.config.Field( 

43 doc="Bandpass for reference sources; None indicates chi-squared detections.", 

44 dtype=str, 

45 optional=True 

46 ) 

47 

48 

49class BaseReferencesTask(lsst.pipe.base.Task): 

50 """Base class for forced photometry subtask that fetches reference sources. 

51 

52 Parameters 

53 ---------- 

54 schema : `lsst.afw.table.Schema`, optional 

55 The schema of the reference catalog. 

56 butler : `lsst.daf.persistence.butler.Butler`, optional 

57 A butler that will allow the task to load its schema from disk. 

58 

59 Notes 

60 ----- 

61 At least one of the ``schema`` and ``butler`` arguments must be present; 

62 if both are, ``schema`` takes precedence. 

63 

64 ``BaseReferencesTask`` defines the required API for the references task, 

65 which consists of: 

66 

67 - ``getSchema(butler)`` 

68 - ``fetchInPatches(butler, tract, filter, patchList)`` 

69 - ``fetchInBox(self, butler, tract, filter, bbox, wcs)`` 

70 - the ``removePatchOverlaps`` config option 

71 

72 It also provides the ``subset`` method, which may be of use to derived 

73 classes when reimplementing ``fetchInBox``. 

74 """ 

75 

76 ConfigClass = BaseReferencesConfig 

77 """Configuration class associated with this task (`lsst.pex.config.Config`). 

78 """ 

79 

80 def __init__(self, butler=None, schema=None, **kwargs): 

81 lsst.pipe.base.Task.__init__(self, **kwargs) 

82 

83 def getSchema(self, butler): 

84 """Return the schema for the reference sources. 

85 

86 Parameters 

87 ---------- 

88 butler : `lsst.daf.persistence.butler.Butler` 

89 Data butler from which the schema will be fetched. 

90 

91 Notes 

92 ----- 

93 Must be available even before any data has been processed. 

94 """ 

95 raise NotImplementedError("BaseReferencesTask is pure abstract, and cannot be used directly.") 

96 

97 def getWcs(self, dataRef): 

98 """Return the WCS for reference sources. 

99 

100 Parameters 

101 ---------- 

102 dataRef : `lsst.daf.persistence.ButlerDataRef` 

103 The data reference from which the WCS will be fetched. This must 

104 include the tract in its dataId. 

105 """ 

106 raise NotImplementedError("BaseReferencesTask is pure abstract, and cannot be used directly.") 

107 

108 def fetchInBox(self, dataRef, bbox, wcs): 

109 """Return reference sources within a given bounding box. 

110 

111 Reference sources are selected if they overlap a region defined by a 

112 pixel-coordinate bounding box and corresponding WCS. 

113 

114 Parameters 

115 ---------- 

116 dataRef : `lsst.daf.persistence.ButlerDataRef` 

117 Butler data reference. The implied data ID must contain the 

118 ``tract`` key. 

119 bbox : `lsst.afw.geom.Box2I` or `lsst.afw.geom.Box2D` 

120 Defines the selection region in pixel coordinates. 

121 wcs : `lsst.afw.image.SkyWcs` 

122 Maps ``bbox`` to sky coordinates. 

123 

124 Returns 

125 ------- 

126 sources : iterable of `~lsst.afw.table.SourceRecord` 

127 Reference sources. May be any Python iterable, including a lazy 

128 iterator. 

129 

130 Notes 

131 ----- 

132 The returned set of sources should be complete and close to minimal. 

133 """ 

134 raise NotImplementedError("BaseReferencesTask is pure abstract, and cannot be used directly.") 

135 

136 def fetchInPatches(self, dataRef, patchList): 

137 """Return reference sources within one or more patches. 

138 

139 Parameters 

140 ---------- 

141 dataRef : `lsst.daf.persistence.ButlerDataRef` 

142 Butler data reference. The implied data ID must contain the 

143 ``tract`` key. 

144 patchList : `list` of `lsst.skymap.PatchInfo` 

145 Patches for which to fetch reference sources. 

146 

147 Returns 

148 ------- 

149 sources : iterable of `~lsst.afw.table.SourceRecord` 

150 Reference sources. May be any Python iterable, including a lazy 

151 iterator. 

152 

153 Notes 

154 ----- 

155 The returned set of sources should be complete and close to minimal. 

156 

157 If ``config.removePatchOverlaps`` is `True`, only sources within each 

158 patch's "inner" bounding box should be returned. 

159 """ 

160 raise NotImplementedError("BaseReferencesTask is pure abstract, and cannot be used directly.") 

161 

162 def subset(self, sources, bbox, wcs): 

163 """Filter a list of sources to only those within the bounding box. 

164 

165 Parameters 

166 ---------- 

167 sources : iterable of `~lsst.afw.table.SourceRecord` 

168 Reference sources. May be any Python iterable, including a lazy 

169 iterator. 

170 bbox : `lsst.afw.geom.Box2I` or `lsst.afw.geom.Box2D` 

171 Defines the selection region. 

172 wcs : `lsst.afw.image.SkyWcs` 

173 Maps ``bbox`` to sky coordinates. 

174 

175 Returns 

176 ------- 

177 sources : iterable of `~lsst.afw.table.SourceRecord` 

178 Filtered sources. May be any Python iterable, including a lazy 

179 iterator. 

180 

181 Notes 

182 ----- 

183 Instead of filtering sources directly via their positions, we filter 

184 based on the positions of parent objects, then include or discard all 

185 children based on their parent's status. This is necessary to support 

186 replacement with noise in measurement, which requires all child 

187 sources have their parent present. 

188 

189 This is not a part of the required `BaseReferencesTask` interface; 

190 it's a convenience function used in implementing `fetchInBox` that may 

191 be of use to subclasses. 

192 """ 

193 boxD = lsst.geom.Box2D(bbox) 

194 # We're passed an arbitrary iterable, but we need a catalog so we can 

195 # iterate over parents and then children. 

196 catalog = lsst.afw.table.SourceCatalog(self.schema) 

197 catalog.extend(sources) 

198 # catalog must be sorted by parent ID for lsst.afw.table.getChildren 

199 # to work 

200 catalog.sort(lsst.afw.table.SourceTable.getParentKey()) 

201 # Iterate over objects that have no parent. 

202 parentSources = catalog.getChildren(0) 

203 skyCoordList = [source.getCoord() for source in parentSources] 

204 pixelPosList = wcs.skyToPixel(skyCoordList) 

205 parentList = [parent for parent, pixel in zip(parentSources, pixelPosList) if boxD.contains(pixel)] 

206 childrenIter = catalog.getChildren((parent.getId() for parent in parentList)) 

207 for parent, children in zip(parentList, childrenIter): 

208 yield parent 

209 yield from children 

210 

211 

212class CoaddSrcReferencesConfig(BaseReferencesTask.ConfigClass): 

213 """Default configuration for coadd reference source selection. 

214 """ 

215 

216 coaddName = lsst.pex.config.Field( 

217 doc="Coadd name: typically one of deep or goodSeeing.", 

218 dtype=str, 

219 default="deep", 

220 ) 

221 skipMissing = lsst.pex.config.Field( 

222 doc="Silently skip patches where the reference catalog does not exist.", 

223 dtype=bool, 

224 default=False 

225 ) 

226 

227 def validate(self): 

228 if (self.coaddName == "chiSquared") != (self.filter is None): 

229 raise lsst.pex.config.FieldValidationError( 

230 field=CoaddSrcReferencesConfig.coaddName, 

231 config=self, 

232 msg="filter may be None if and only if coaddName is chiSquared" 

233 ) 

234 

235 

236class CoaddSrcReferencesTask(BaseReferencesTask): 

237 """Select reference sources by loading the “coadd source” dataset directly. 

238 

239 The name of the dataset to read is generated by appending the 

240 `datasetSuffix` attribute to the string ``Coadd_``. The dataset is then 

241 read directly from disk using the Butler. 

242 

243 Parameters 

244 ---------- 

245 schema : `lsst.afw.table.Schema`, optional 

246 The schema of the detection catalogs used as input to this one. 

247 butler : `lsst.daf.persistence.butler.Butler`, optional 

248 A Butler used to read the input schema from disk. Required if 

249 ``schema`` is `None`. 

250 

251 Notes 

252 ----- 

253 The task will set its own ``self.schema`` attribute to the schema of the 

254 output merged catalog. 

255 """ 

256 

257 ConfigClass = CoaddSrcReferencesConfig 

258 """Configuration class associated with this task (`lsst.pex.config.Config`). 

259 """ 

260 

261 datasetSuffix = "src" 

262 """Suffix to append to ``Coadd_`` to generate the dataset name (`str`). 

263 """ 

264 

265 def __init__(self, butler=None, schema=None, **kwargs): 

266 BaseReferencesTask.__init__(self, butler=butler, schema=schema, **kwargs) 

267 if schema is None: 

268 assert butler is not None, "No butler nor schema provided" 

269 schema = butler.get("{}Coadd_{}_schema".format(self.config.coaddName, self.datasetSuffix), 

270 immediate=True).getSchema() 

271 self.schema = schema 

272 

273 def getWcs(self, dataRef): 

274 """Return the WCS for reference sources. 

275 

276 Parameters 

277 ---------- 

278 dataRef : `lsst.daf.persistence.ButlerDataRef` 

279 Butler data reference. Must includ the trac in its dataId. 

280 """ 

281 skyMap = dataRef.get(self.config.coaddName + "Coadd_skyMap", immediate=True) 

282 return skyMap[dataRef.dataId["tract"]].getWcs() 

283 

284 def fetchInPatches(self, dataRef, patchList): 

285 """Fetch the source catalog using the Butler. 

286 

287 Parameters 

288 ---------- 

289 dataRef : `lsst.daf.persistence.ButlerDataRef` 

290 Butler data reference. The implied data ID must contain the 

291 ``tract`` key. 

292 patchList : `list` of `lsst.skymap.PatchInfo` 

293 Patches for which to fetch reference sources. 

294 

295 Returns 

296 ------- 

297 sources : iterable of `~lsst.afw.table.SourceRecord` 

298 Reference sources. May be any Python iterable, including a lazy 

299 iterator. 

300 

301 Notes 

302 ----- 

303 An implementation of `BaseReferencesTask.fetchInPatches` that loads 

304 ``Coadd_`` + `datasetSuffix` catalogs using the butler. 

305 """ 

306 dataset = "{}Coadd_{}".format(self.config.coaddName, self.datasetSuffix) 

307 tract = dataRef.dataId["tract"] 

308 butler = dataRef.butlerSubset.butler 

309 for patch in patchList: 

310 dataId = {'tract': tract, 'patch': "%d,%d" % patch.getIndex()} 

311 if self.config.filter is not None: 

312 dataId['filter'] = self.config.filter 

313 

314 if not butler.datasetExists(dataset, dataId): 

315 if self.config.skipMissing: 

316 continue 

317 raise lsst.pipe.base.TaskError("Reference %s doesn't exist" % (dataId,)) 

318 self.log.info("Getting references in %s", dataId) 

319 catalog = butler.get(dataset, dataId, immediate=True) 

320 if self.config.removePatchOverlaps: 

321 bbox = lsst.geom.Box2D(patch.getInnerBBox()) 

322 for source in catalog: 

323 if bbox.contains(source.getCentroid()): 

324 yield source 

325 else: 

326 for source in catalog: 

327 yield source 

328 

329 def fetchInBox(self, dataRef, bbox, wcs, pad=0): 

330 """Return reference sources within a given bounding box. 

331 

332 Reference sources are selected if they overlap a region defined by a 

333 pixel-coordinate bounding box and corresponding WCS. 

334 

335 Parameters 

336 ---------- 

337 dataRef : `lsst.daf.persistence.ButlerDataRef` 

338 Butler data reference. The implied data ID must contain the 

339 ``tract`` key. 

340 bbox : `lsst.afw.geom.Box2I` or `lsst.afw.geom.Box2D` 

341 Defines the selection region in pixel coordinates. 

342 wcs : `lsst.afw.image.SkyWcs` 

343 Maps ``bbox`` to sky coordinates. 

344 pad : `int` 

345 a buffer to grow the bounding box by after catalogs have been loaded, but 

346 before filtering them to include just the given bounding box. 

347 

348 Returns 

349 ------- 

350 sources : iterable of `~lsst.afw.table.SourceRecord` 

351 Reference sources. May be any Python iterable, including a lazy 

352 iterator. 

353 """ 

354 skyMap = dataRef.get(self.config.coaddName + "Coadd_skyMap", immediate=True) 

355 tract = skyMap[dataRef.dataId["tract"]] 

356 coordList = [wcs.pixelToSky(corner) for corner in lsst.geom.Box2D(bbox).getCorners()] 

357 self.log.info("Getting references in region with corners %s [degrees]", 

358 ", ".join("(%s)" % (coord.getPosition(lsst.geom.degrees),) for coord in coordList)) 

359 patchList = tract.findPatchList(coordList) 

360 # After figuring out which patch catalogs to read from the bbox, pad out the bbox if desired 

361 # But don't add any new patches while padding 

362 if pad: 

363 bbox.grow(pad) 

364 return self.subset(self.fetchInPatches(dataRef, patchList), bbox, wcs) 

365 

366 

367class MultiBandReferencesConfig(CoaddSrcReferencesTask.ConfigClass): 

368 """Default configuration for multi-band reference source selection. 

369 """ 

370 

371 def validate(self): 

372 if self.filter is not None: 

373 raise lsst.pex.config.FieldValidationError( 

374 field=MultiBandReferencesConfig.filter, 

375 config=self, 

376 msg="Filter should not be set for the multiband processing scheme") 

377 # Delegate to ultimate base class, because the direct one has a check we don't want. 

378 BaseReferencesTask.ConfigClass.validate(self) 

379 

380 

381class MultiBandReferencesTask(CoaddSrcReferencesTask): 

382 """Loads references from the multi-band processing scheme. 

383 """ 

384 

385 ConfigClass = MultiBandReferencesConfig 

386 datasetSuffix = "ref" # Documented in superclass