Coverage for python/lsst/ip/diffim/getTemplate.py: 21%

343 statements  

« prev     ^ index     » next       coverage.py v6.4.2, created at 2022-07-15 12:19 +0000

1# 

2# LSST Data Management System 

3# Copyright 2016 LSST Corporation. 

4# 

5# This product includes software developed by the 

6# LSST Project (http://www.lsst.org/). 

7# 

8# This program is free software: you can redistribute it and/or modify 

9# it under the terms of the GNU General Public License as published by 

10# the Free Software Foundation, either version 3 of the License, or 

11# (at your option) any later version. 

12# 

13# This program is distributed in the hope that it will be useful, 

14# but WITHOUT ANY WARRANTY; without even the implied warranty of 

15# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 

16# GNU General Public License for more details. 

17# 

18# You should have received a copy of the LSST License Statement and 

19# the GNU General Public License along with this program. If not, 

20# see <http://www.lsstcorp.org/LegalNotices/>. 

21# 

22 

23import numpy as np 

24 

25import lsst.afw.image as afwImage 

26import lsst.geom as geom 

27import lsst.afw.geom as afwGeom 

28import lsst.afw.table as afwTable 

29import lsst.afw.math as afwMath 

30import lsst.pex.config as pexConfig 

31import lsst.pipe.base as pipeBase 

32from lsst.skymap import BaseSkyMap 

33from lsst.daf.butler import DeferredDatasetHandle 

34from lsst.ip.diffim.dcrModel import DcrModel 

35from lsst.meas.algorithms import CoaddPsf, CoaddPsfConfig 

36 

37__all__ = ["GetCoaddAsTemplateTask", "GetCoaddAsTemplateConfig", 

38 "GetCalexpAsTemplateTask", "GetCalexpAsTemplateConfig", 

39 "GetTemplateTask", "GetTemplateConfig", 

40 "GetDcrTemplateTask", "GetDcrTemplateConfig", 

41 "GetMultiTractCoaddTemplateTask", "GetMultiTractCoaddTemplateConfig"] 

42 

43 

44class GetCoaddAsTemplateConfig(pexConfig.Config): 

45 templateBorderSize = pexConfig.Field( 

46 dtype=int, 

47 default=20, 

48 doc="Number of pixels to grow the requested template image to account for warping" 

49 ) 

50 coaddName = pexConfig.Field( 

51 doc="coadd name: typically one of 'deep', 'goodSeeing', or 'dcr'", 

52 dtype=str, 

53 default="deep", 

54 ) 

55 warpType = pexConfig.Field( 

56 doc="Warp type of the coadd template: one of 'direct' or 'psfMatched'", 

57 dtype=str, 

58 default="direct", 

59 ) 

60 

61 

62class GetCoaddAsTemplateTask(pipeBase.Task): 

63 """Subtask to retrieve coadd for use as an image difference template. 

64 

65 This is the default getTemplate Task to be run as a subtask by 

66 ``pipe.tasks.ImageDifferenceTask``. The main methods are ``run()`` and 

67 ``runGen3()``. 

68 

69 Notes 

70 ----- 

71 From the given skymap, the closest tract is selected; multiple tracts are 

72 not supported. The assembled template inherits the WCS of the selected 

73 skymap tract and the resolution of the template exposures. Overlapping box 

74 regions of the input template patches are pixel by pixel copied into the 

75 assembled template image. There is no warping or pixel resampling. 

76 

77 Pixels with no overlap of any available input patches are set to ``nan`` value 

78 and ``NO_DATA`` flagged. 

79 """ 

80 

81 ConfigClass = GetCoaddAsTemplateConfig 

82 _DefaultName = "GetCoaddAsTemplateTask" 

83 

84 def runDataRef(self, exposure, sensorRef, templateIdList=None): 

85 """Gen2 task entry point. Retrieve and mosaic a template coadd exposure 

86 that overlaps the science exposure. 

87 

88 Parameters 

89 ---------- 

90 exposure: `lsst.afw.image.Exposure` 

91 an exposure for which to generate an overlapping template 

92 sensorRef : TYPE 

93 a Butler data reference that can be used to obtain coadd data 

94 templateIdList : TYPE, optional 

95 list of data ids, unused here, in the case of coadd template 

96 

97 Returns 

98 ------- 

99 result : `lsst.pipe.base.Struct` 

100 - ``exposure`` : `lsst.afw.image.ExposureF` 

101 a template coadd exposure assembled out of patches 

102 - ``sources`` : None for this subtask 

103 """ 

104 skyMap = sensorRef.get(datasetType=self.config.coaddName + "Coadd_skyMap") 

105 tractInfo, patchList, skyCorners = self.getOverlapPatchList(exposure, skyMap) 

106 

107 availableCoaddRefs = dict() 

108 for patchInfo in patchList: 

109 patchNumber = tractInfo.getSequentialPatchIndex(patchInfo) 

110 patchArgDict = dict( 

111 datasetType=self.getCoaddDatasetName() + "_sub", 

112 bbox=patchInfo.getOuterBBox(), 

113 tract=tractInfo.getId(), 

114 patch="%s,%s" % (patchInfo.getIndex()[0], patchInfo.getIndex()[1]), 

115 ) 

116 

117 if sensorRef.datasetExists(**patchArgDict): 

118 self.log.info("Reading patch %s", patchArgDict) 

119 availableCoaddRefs[patchNumber] = patchArgDict 

120 

121 templateExposure = self.run( 

122 tractInfo, patchList, skyCorners, availableCoaddRefs, 

123 sensorRef=sensorRef, visitInfo=exposure.getInfo().getVisitInfo() 

124 ) 

125 return pipeBase.Struct(exposure=templateExposure, sources=None) 

126 

127 def runQuantum(self, exposure, butlerQC, skyMapRef, coaddExposureRefs): 

128 """Gen3 task entry point. Retrieve and mosaic a template coadd exposure 

129 that overlaps the science exposure. 

130 

131 Parameters 

132 ---------- 

133 exposure : `lsst.afw.image.Exposure` 

134 The science exposure to define the sky region of the template coadd. 

135 butlerQC : `lsst.pipe.base.ButlerQuantumContext` 

136 Butler like object that supports getting data by DatasetRef. 

137 skyMapRef : `lsst.daf.butler.DatasetRef` 

138 Reference to SkyMap object that corresponds to the template coadd. 

139 coaddExposureRefs : iterable of `lsst.daf.butler.DeferredDatasetRef` 

140 Iterable of references to the available template coadd patches. 

141 

142 Returns 

143 ------- 

144 result : `lsst.pipe.base.Struct` 

145 - ``exposure`` : `lsst.afw.image.ExposureF` 

146 a template coadd exposure assembled out of patches 

147 - ``sources`` : `None` for this subtask 

148 """ 

149 self.log.warn("GetCoaddAsTemplateTask is deprecated. Use GetTemplateTask instead.") 

150 skyMap = butlerQC.get(skyMapRef) 

151 coaddExposureRefs = butlerQC.get(coaddExposureRefs) 

152 tracts = [ref.dataId['tract'] for ref in coaddExposureRefs] 

153 if tracts.count(tracts[0]) == len(tracts): 

154 tractInfo = skyMap[tracts[0]] 

155 else: 

156 raise RuntimeError("Templates constructed from multiple Tracts not supported by this task. " 

157 "Use GetTemplateTask instead.") 

158 

159 detectorBBox = exposure.getBBox() 

160 detectorWcs = exposure.getWcs() 

161 detectorCorners = detectorWcs.pixelToSky(geom.Box2D(detectorBBox).getCorners()) 

162 validPolygon = exposure.getInfo().getValidPolygon() 

163 detectorPolygon = validPolygon if validPolygon else geom.Box2D(detectorBBox) 

164 

165 availableCoaddRefs = dict() 

166 overlappingArea = 0 

167 for coaddRef in coaddExposureRefs: 

168 dataId = coaddRef.dataId 

169 patchWcs = skyMap[dataId['tract']].getWcs() 

170 patchBBox = skyMap[dataId['tract']][dataId['patch']].getOuterBBox() 

171 patchCorners = patchWcs.pixelToSky(geom.Box2D(patchBBox).getCorners()) 

172 patchPolygon = afwGeom.Polygon(detectorWcs.skyToPixel(patchCorners)) 

173 if patchPolygon.intersection(detectorPolygon): 

174 overlappingArea += patchPolygon.intersectionSingle(detectorPolygon).calculateArea() 

175 if self.config.coaddName == 'dcr': 

176 self.log.info("Using template input tract=%s, patch=%s, subfilter=%s", 

177 dataId['tract'], dataId['patch'], dataId['subfilter']) 

178 if dataId['patch'] in availableCoaddRefs: 

179 availableCoaddRefs[dataId['patch']].append(coaddRef) 

180 else: 

181 availableCoaddRefs[dataId['patch']] = [coaddRef, ] 

182 else: 

183 self.log.info("Using template input tract=%s, patch=%s", 

184 dataId['tract'], dataId['patch']) 

185 availableCoaddRefs[dataId['patch']] = coaddRef 

186 

187 if overlappingArea == 0: 

188 templateExposure = None 

189 pixGood = 0 

190 self.log.warning("No overlapping template patches found") 

191 else: 

192 patchList = [tractInfo[patch] for patch in availableCoaddRefs.keys()] 

193 templateExposure = self.run(tractInfo, patchList, detectorCorners, availableCoaddRefs, 

194 visitInfo=exposure.getInfo().getVisitInfo()) 

195 # Count the number of pixels with the NO_DATA mask bit set 

196 # counting NaN pixels is insufficient because pixels without data are often intepolated over) 

197 pixNoData = np.count_nonzero(templateExposure.mask.array 

198 & templateExposure.mask.getPlaneBitMask('NO_DATA')) 

199 pixGood = templateExposure.getBBox().getArea() - pixNoData 

200 self.log.info("template has %d good pixels (%.1f%%)", pixGood, 

201 100*pixGood/templateExposure.getBBox().getArea()) 

202 return pipeBase.Struct(exposure=templateExposure, sources=None, area=pixGood) 

203 

204 def getOverlapPatchList(self, exposure, skyMap): 

205 """Select the relevant tract and its patches that overlap with the science exposure. 

206 

207 Parameters 

208 ---------- 

209 exposure : `lsst.afw.image.Exposure` 

210 The science exposure to define the sky region of the template coadd. 

211 

212 skyMap : `lsst.skymap.BaseSkyMap` 

213 SkyMap object that corresponds to the template coadd. 

214 

215 Returns 

216 ------- 

217 result : `tuple` of 

218 - ``tractInfo`` : `lsst.skymap.TractInfo` 

219 The selected tract. 

220 - ``patchList`` : `list` of `lsst.skymap.PatchInfo` 

221 List of all overlap patches of the selected tract. 

222 - ``skyCorners`` : `list` of `lsst.geom.SpherePoint` 

223 Corners of the exposure in the sky in the order given by `lsst.geom.Box2D.getCorners`. 

224 """ 

225 expWcs = exposure.getWcs() 

226 expBoxD = geom.Box2D(exposure.getBBox()) 

227 expBoxD.grow(self.config.templateBorderSize) 

228 ctrSkyPos = expWcs.pixelToSky(expBoxD.getCenter()) 

229 tractInfo = skyMap.findTract(ctrSkyPos) 

230 self.log.info("Using skyMap tract %s", tractInfo.getId()) 

231 skyCorners = [expWcs.pixelToSky(pixPos) for pixPos in expBoxD.getCorners()] 

232 patchList = tractInfo.findPatchList(skyCorners) 

233 

234 if not patchList: 

235 raise RuntimeError("No suitable tract found") 

236 

237 self.log.info("Assembling %d coadd patches", len(patchList)) 

238 self.log.info("exposure dimensions=%s", exposure.getDimensions()) 

239 

240 return (tractInfo, patchList, skyCorners) 

241 

242 def run(self, tractInfo, patchList, skyCorners, availableCoaddRefs, 

243 sensorRef=None, visitInfo=None): 

244 """Gen2 and gen3 shared code: determination of exposure dimensions and 

245 copying of pixels from overlapping patch regions. 

246 

247 Parameters 

248 ---------- 

249 skyMap : `lsst.skymap.BaseSkyMap` 

250 SkyMap object that corresponds to the template coadd. 

251 tractInfo : `lsst.skymap.TractInfo` 

252 The selected tract. 

253 patchList : iterable of `lsst.skymap.patchInfo.PatchInfo` 

254 Patches to consider for making the template exposure. 

255 skyCorners : list of `lsst.geom.SpherePoint` 

256 Sky corner coordinates to be covered by the template exposure. 

257 availableCoaddRefs : `dict` [`int`] 

258 Dictionary of spatially relevant retrieved coadd patches, 

259 indexed by their sequential patch number. In Gen3 mode, values are 

260 `lsst.daf.butler.DeferredDatasetHandle` and ``.get()`` is called, 

261 in Gen2 mode, ``sensorRef.get(**coaddef)`` is called to retrieve the coadd. 

262 sensorRef : `lsst.daf.persistence.ButlerDataRef`, Gen2 only 

263 Butler data reference to get coadd data. 

264 Must be `None` for Gen3. 

265 visitInfo : `lsst.afw.image.VisitInfo`, Gen2 only 

266 VisitInfo to make dcr model. 

267 

268 Returns 

269 ------- 

270 templateExposure: `lsst.afw.image.ExposureF` 

271 The created template exposure. 

272 """ 

273 coaddWcs = tractInfo.getWcs() 

274 

275 # compute coadd bbox 

276 coaddBBox = geom.Box2D() 

277 for skyPos in skyCorners: 

278 coaddBBox.include(coaddWcs.skyToPixel(skyPos)) 

279 coaddBBox = geom.Box2I(coaddBBox) 

280 self.log.info("coadd dimensions=%s", coaddBBox.getDimensions()) 

281 

282 coaddExposure = afwImage.ExposureF(coaddBBox, coaddWcs) 

283 coaddExposure.maskedImage.set(np.nan, afwImage.Mask.getPlaneBitMask("NO_DATA"), np.nan) 

284 nPatchesFound = 0 

285 coaddFilterLabel = None 

286 coaddPsf = None 

287 coaddPhotoCalib = None 

288 for patchInfo in patchList: 

289 patchNumber = tractInfo.getSequentialPatchIndex(patchInfo) 

290 patchSubBBox = patchInfo.getOuterBBox() 

291 patchSubBBox.clip(coaddBBox) 

292 if patchNumber not in availableCoaddRefs: 

293 self.log.warning("skip patch=%d; patch does not exist for this coadd", patchNumber) 

294 continue 

295 if patchSubBBox.isEmpty(): 

296 if isinstance(availableCoaddRefs[patchNumber], DeferredDatasetHandle): 

297 tract = availableCoaddRefs[patchNumber].dataId['tract'] 

298 else: 

299 tract = availableCoaddRefs[patchNumber]['tract'] 

300 self.log.info("skip tract=%d patch=%d; no overlapping pixels", tract, patchNumber) 

301 continue 

302 

303 if self.config.coaddName == 'dcr': 

304 patchInnerBBox = patchInfo.getInnerBBox() 

305 patchInnerBBox.clip(coaddBBox) 

306 if np.min(patchInnerBBox.getDimensions()) <= 2*self.config.templateBorderSize: 

307 self.log.info("skip tract=%(tract)s, patch=%(patch)s; too few pixels.", 

308 availableCoaddRefs[patchNumber]) 

309 continue 

310 self.log.info("Constructing DCR-matched template for patch %s", 

311 availableCoaddRefs[patchNumber]) 

312 

313 dcrModel = DcrModel.fromQuantum(availableCoaddRefs[patchNumber], 

314 self.config.effectiveWavelength, 

315 self.config.bandwidth) 

316 # The edge pixels of the DcrCoadd may contain artifacts due to missing data. 

317 # Each patch has significant overlap, and the contaminated edge pixels in 

318 # a new patch will overwrite good pixels in the overlap region from 

319 # previous patches. 

320 # Shrink the BBox to remove the contaminated pixels, 

321 # but make sure it is only the overlap region that is reduced. 

322 dcrBBox = geom.Box2I(patchSubBBox) 

323 dcrBBox.grow(-self.config.templateBorderSize) 

324 dcrBBox.include(patchInnerBBox) 

325 coaddPatch = dcrModel.buildMatchedExposure(bbox=dcrBBox, 

326 visitInfo=visitInfo) 

327 else: 

328 if sensorRef is None: 

329 # Gen3 

330 coaddPatch = availableCoaddRefs[patchNumber].get() 

331 else: 

332 # Gen2 

333 coaddPatch = sensorRef.get(**availableCoaddRefs[patchNumber]) 

334 nPatchesFound += 1 

335 

336 # Gen2 get() seems to clip based on bbox kwarg but we removed bbox 

337 # calculation from caller code. Gen3 also does not do this. 

338 overlapBox = coaddPatch.getBBox() 

339 overlapBox.clip(coaddBBox) 

340 coaddExposure.maskedImage.assign(coaddPatch.maskedImage[overlapBox], overlapBox) 

341 

342 if coaddFilterLabel is None: 

343 coaddFilterLabel = coaddPatch.getFilter() 

344 

345 # Retrieve the PSF for this coadd tract, if not already retrieved 

346 if coaddPsf is None and coaddPatch.hasPsf(): 

347 coaddPsf = coaddPatch.getPsf() 

348 

349 # Retrieve the calibration for this coadd tract, if not already retrieved 

350 if coaddPhotoCalib is None: 

351 coaddPhotoCalib = coaddPatch.getPhotoCalib() 

352 

353 if coaddPhotoCalib is None: 

354 raise RuntimeError("No coadd PhotoCalib found!") 

355 if nPatchesFound == 0: 

356 raise RuntimeError("No patches found!") 

357 if coaddPsf is None: 

358 raise RuntimeError("No coadd Psf found!") 

359 

360 coaddExposure.setPhotoCalib(coaddPhotoCalib) 

361 coaddExposure.setPsf(coaddPsf) 

362 coaddExposure.setFilter(coaddFilterLabel) 

363 return coaddExposure 

364 

365 def getCoaddDatasetName(self): 

366 """Return coadd name for given task config 

367 

368 Returns 

369 ------- 

370 CoaddDatasetName : `string` 

371 

372 TODO: This nearly duplicates a method in CoaddBaseTask (DM-11985) 

373 """ 

374 warpType = self.config.warpType 

375 suffix = "" if warpType == "direct" else warpType[0].upper() + warpType[1:] 

376 return self.config.coaddName + "Coadd" + suffix 

377 

378 

379class GetCalexpAsTemplateConfig(pexConfig.Config): 

380 doAddCalexpBackground = pexConfig.Field( 

381 dtype=bool, 

382 default=True, 

383 doc="Add background to calexp before processing it." 

384 ) 

385 

386 

387class GetCalexpAsTemplateTask(pipeBase.Task): 

388 """Subtask to retrieve calexp of the same ccd number as the science image SensorRef 

389 for use as an image difference template. Only gen2 supported. 

390 

391 To be run as a subtask by pipe.tasks.ImageDifferenceTask. 

392 Intended for use with simulations and surveys that repeatedly visit the same pointing. 

393 This code was originally part of Winter2013ImageDifferenceTask. 

394 """ 

395 

396 ConfigClass = GetCalexpAsTemplateConfig 

397 _DefaultName = "GetCalexpAsTemplateTask" 

398 

399 def run(self, exposure, sensorRef, templateIdList): 

400 """Return a calexp exposure with based on input sensorRef. 

401 

402 Construct a dataId based on the sensorRef.dataId combined 

403 with the specifications from the first dataId in templateIdList 

404 

405 Parameters 

406 ---------- 

407 exposure : `lsst.afw.image.Exposure` 

408 exposure (unused) 

409 sensorRef : `list` of `lsst.daf.persistence.ButlerDataRef` 

410 Data reference of the calexp(s) to subtract from. 

411 templateIdList : `list` of `lsst.daf.persistence.ButlerDataRef` 

412 Data reference of the template calexp to be subtraced. 

413 Can be incomplete, fields are initialized from `sensorRef`. 

414 If there are multiple items, only the first one is used. 

415 

416 Returns 

417 ------- 

418 result : `struct` 

419 

420 return a pipeBase.Struct: 

421 

422 - ``exposure`` : a template calexp 

423 - ``sources`` : source catalog measured on the template 

424 """ 

425 

426 if len(templateIdList) == 0: 

427 raise RuntimeError("No template data reference supplied.") 

428 if len(templateIdList) > 1: 

429 self.log.warning("Multiple template data references supplied. Using the first one only.") 

430 

431 templateId = sensorRef.dataId.copy() 

432 templateId.update(templateIdList[0]) 

433 

434 self.log.info("Fetching calexp (%s) as template.", templateId) 

435 

436 butler = sensorRef.getButler() 

437 template = butler.get(datasetType="calexp", dataId=templateId) 

438 if self.config.doAddCalexpBackground: 

439 templateBg = butler.get(datasetType="calexpBackground", dataId=templateId) 

440 mi = template.getMaskedImage() 

441 mi += templateBg.getImage() 

442 

443 if not template.hasPsf(): 

444 raise pipeBase.TaskError("Template has no psf") 

445 

446 templateSources = butler.get(datasetType="src", dataId=templateId) 

447 return pipeBase.Struct(exposure=template, 

448 sources=templateSources) 

449 

450 def runDataRef(self, *args, **kwargs): 

451 return self.run(*args, **kwargs) 

452 

453 def runQuantum(self, **kwargs): 

454 raise NotImplementedError("Calexp template is not supported with gen3 middleware") 

455 

456 

457class GetTemplateConnections(pipeBase.PipelineTaskConnections, 

458 dimensions=("instrument", "visit", "detector", "skymap"), 

459 defaultTemplates={"coaddName": "goodSeeing", 

460 "warpTypeSuffix": "", 

461 "fakesType": ""}): 

462 bbox = pipeBase.connectionTypes.Input( 

463 doc="BBoxes of calexp used determine geometry of output template", 

464 name="{fakesType}calexp.bbox", 

465 storageClass="Box2I", 

466 dimensions=("instrument", "visit", "detector"), 

467 ) 

468 wcs = pipeBase.connectionTypes.Input( 

469 doc="WCS of the calexp that we want to fetch the template for", 

470 name="{fakesType}calexp.wcs", 

471 storageClass="Wcs", 

472 dimensions=("instrument", "visit", "detector"), 

473 ) 

474 skyMap = pipeBase.connectionTypes.Input( 

475 doc="Input definition of geometry/bbox and projection/wcs for template exposures", 

476 name=BaseSkyMap.SKYMAP_DATASET_TYPE_NAME, 

477 dimensions=("skymap", ), 

478 storageClass="SkyMap", 

479 ) 

480 # TODO DM-31292: Add option to use global external wcs from jointcal 

481 # Needed for DRP HSC 

482 coaddExposures = pipeBase.connectionTypes.Input( 

483 doc="Input template to match and subtract from the exposure", 

484 dimensions=("tract", "patch", "skymap", "band"), 

485 storageClass="ExposureF", 

486 name="{fakesType}{coaddName}Coadd{warpTypeSuffix}", 

487 multiple=True, 

488 deferLoad=True 

489 ) 

490 outputExposure = pipeBase.connectionTypes.Output( 

491 doc="Warped template used to create `subtractedExposure`.", 

492 dimensions=("instrument", "visit", "detector"), 

493 storageClass="ExposureF", 

494 name="{fakesType}{coaddName}Diff_templateExp{warpTypeSuffix}", 

495 ) 

496 

497 

498class GetTemplateConfig(pipeBase.PipelineTaskConfig, 

499 pipelineConnections=GetTemplateConnections): 

500 templateBorderSize = pexConfig.Field( 

501 dtype=int, 

502 default=20, 

503 doc="Number of pixels to grow the requested template image to account for warping" 

504 ) 

505 warp = pexConfig.ConfigField( 

506 dtype=afwMath.Warper.ConfigClass, 

507 doc="warper configuration", 

508 ) 

509 coaddPsf = pexConfig.ConfigField( 

510 doc="Configuration for CoaddPsf", 

511 dtype=CoaddPsfConfig, 

512 ) 

513 

514 def setDefaults(self): 

515 self.warp.warpingKernelName = 'lanczos5' 

516 self.coaddPsf.warpingKernelName = 'lanczos5' 

517 

518 

519class GetTemplateTask(pipeBase.PipelineTask): 

520 ConfigClass = GetTemplateConfig 

521 _DefaultName = "getTemplate" 

522 

523 def __init__(self, *args, **kwargs): 

524 super().__init__(*args, **kwargs) 

525 self.warper = afwMath.Warper.fromConfig(self.config.warp) 

526 

527 def runQuantum(self, butlerQC, inputRefs, outputRefs): 

528 # Read in all inputs. 

529 inputs = butlerQC.get(inputRefs) 

530 results = self.getOverlappingExposures(inputs) 

531 inputs["coaddExposures"] = results.coaddExposures 

532 inputs["dataIds"] = results.dataIds 

533 outputs = self.run(**inputs) 

534 butlerQC.put(outputs, outputRefs) 

535 

536 def getOverlappingExposures(self, inputs): 

537 """Return lists of coadds and their corresponding dataIds that overlap the detector. 

538 

539 The spatial index in the registry has generous padding and often supplies 

540 patches near, but not directly overlapping the detector. 

541 Filters inputs so that we don't have to read in all input coadds. 

542 

543 Parameters 

544 ---------- 

545 inputs : `dict` of task Inputs, containing: 

546 - coaddExposureRefs : list of elements of type 

547 `lsst.daf.butler.DeferredDatasetHandle` of 

548 `lsst.afw.image.Exposure` 

549 Data references to exposures that might overlap the detector. 

550 - bbox : `lsst.geom.Box2I` 

551 Template Bounding box of the detector geometry onto which to 

552 resample the coaddExposures 

553 - skyMap : `lsst.skymap.SkyMap` 

554 Input definition of geometry/bbox and projection/wcs for template exposures 

555 - wcs : `lsst.afw.geom.SkyWcs` 

556 Template WCS onto which to resample the coaddExposures 

557 

558 Returns 

559 ------- 

560 result : `lsst.pipe.base.Struct` containing these fields: 

561 - coaddExposures : `list` of elements of type `lsst.afw.image.Exposure` 

562 Coadd exposures that overlap the detector. 

563 - dataIds : `list` of `lsst.daf.butler.DataCoordinate` 

564 Data IDs of the coadd exposures that overlap the detector. 

565 

566 Raises 

567 ------ 

568 NoWorkFound 

569 Raised if no patches overlap the input detector bbox 

570 """ 

571 # Check that the patches actually overlap the detector 

572 # Exposure's validPolygon would be more accurate 

573 detectorPolygon = geom.Box2D(inputs['bbox']) 

574 overlappingArea = 0 

575 coaddExposureList = [] 

576 dataIds = [] 

577 for coaddRef in inputs['coaddExposures']: 

578 dataId = coaddRef.dataId 

579 patchWcs = inputs['skyMap'][dataId['tract']].getWcs() 

580 patchBBox = inputs['skyMap'][dataId['tract']][dataId['patch']].getOuterBBox() 

581 patchCorners = patchWcs.pixelToSky(geom.Box2D(patchBBox).getCorners()) 

582 patchPolygon = afwGeom.Polygon(inputs['wcs'].skyToPixel(patchCorners)) 

583 if patchPolygon.intersection(detectorPolygon): 

584 overlappingArea += patchPolygon.intersectionSingle(detectorPolygon).calculateArea() 

585 self.log.info("Using template input tract=%s, patch=%s" % 

586 (dataId['tract'], dataId['patch'])) 

587 coaddExposureList.append(coaddRef.get()) 

588 dataIds.append(dataId) 

589 

590 if not overlappingArea: 

591 raise pipeBase.NoWorkFound('No patches overlap detector') 

592 

593 return pipeBase.Struct(coaddExposures=coaddExposureList, 

594 dataIds=dataIds) 

595 

596 def run(self, coaddExposures, bbox, wcs, dataIds, **kwargs): 

597 """Warp coadds from multiple tracts to form a template for image diff. 

598 

599 Where the tracts overlap, the resulting template image is averaged. 

600 The PSF on the template is created by combining the CoaddPsf on each 

601 template image into a meta-CoaddPsf. 

602 

603 Parameters 

604 ---------- 

605 coaddExposures : `list` of `lsst.afw.image.Exposure` 

606 Coadds to be mosaicked 

607 bbox : `lsst.geom.Box2I` 

608 Template Bounding box of the detector geometry onto which to 

609 resample the coaddExposures 

610 wcs : `lsst.afw.geom.SkyWcs` 

611 Template WCS onto which to resample the coaddExposures 

612 dataIds : `list` of `lsst.daf.butler.DataCoordinate` 

613 Record of the tract and patch of each coaddExposure. 

614 **kwargs 

615 Any additional keyword parameters. 

616 

617 Returns 

618 ------- 

619 result : `lsst.pipe.base.Struct` containing 

620 - ``outputExposure`` : a template coadd exposure assembled out of patches 

621 """ 

622 # Table for CoaddPSF 

623 tractsSchema = afwTable.ExposureTable.makeMinimalSchema() 

624 tractKey = tractsSchema.addField('tract', type=np.int32, doc='Which tract') 

625 patchKey = tractsSchema.addField('patch', type=np.int32, doc='Which patch') 

626 weightKey = tractsSchema.addField('weight', type=float, doc='Weight for each tract, should be 1') 

627 tractsCatalog = afwTable.ExposureCatalog(tractsSchema) 

628 

629 finalWcs = wcs 

630 bbox.grow(self.config.templateBorderSize) 

631 finalBBox = bbox 

632 

633 nPatchesFound = 0 

634 maskedImageList = [] 

635 weightList = [] 

636 

637 for coaddExposure, dataId in zip(coaddExposures, dataIds): 

638 

639 # warp to detector WCS 

640 warped = self.warper.warpExposure(finalWcs, coaddExposure, maxBBox=finalBBox) 

641 

642 # Check if warped image is viable 

643 if not np.any(np.isfinite(warped.image.array)): 

644 self.log.info("No overlap for warped %s. Skipping" % dataId) 

645 continue 

646 

647 exp = afwImage.ExposureF(finalBBox, finalWcs) 

648 exp.maskedImage.set(np.nan, afwImage.Mask.getPlaneBitMask("NO_DATA"), np.nan) 

649 exp.maskedImage.assign(warped.maskedImage, warped.getBBox()) 

650 

651 maskedImageList.append(exp.maskedImage) 

652 weightList.append(1) 

653 record = tractsCatalog.addNew() 

654 record.setPsf(coaddExposure.getPsf()) 

655 record.setWcs(coaddExposure.getWcs()) 

656 record.setPhotoCalib(coaddExposure.getPhotoCalib()) 

657 record.setBBox(coaddExposure.getBBox()) 

658 record.setValidPolygon(afwGeom.Polygon(geom.Box2D(coaddExposure.getBBox()).getCorners())) 

659 record.set(tractKey, dataId['tract']) 

660 record.set(patchKey, dataId['patch']) 

661 record.set(weightKey, 1.) 

662 nPatchesFound += 1 

663 

664 if nPatchesFound == 0: 

665 raise pipeBase.NoWorkFound("No patches found to overlap detector") 

666 

667 # Combine images from individual patches together 

668 statsFlags = afwMath.stringToStatisticsProperty('MEAN') 

669 statsCtrl = afwMath.StatisticsControl() 

670 statsCtrl.setNanSafe(True) 

671 statsCtrl.setWeighted(True) 

672 statsCtrl.setCalcErrorFromInputVariance(True) 

673 

674 templateExposure = afwImage.ExposureF(finalBBox, finalWcs) 

675 templateExposure.maskedImage.set(np.nan, afwImage.Mask.getPlaneBitMask("NO_DATA"), np.nan) 

676 xy0 = templateExposure.getXY0() 

677 # Do not mask any values 

678 templateExposure.maskedImage = afwMath.statisticsStack(maskedImageList, statsFlags, statsCtrl, 

679 weightList, clipped=0, maskMap=[]) 

680 templateExposure.maskedImage.setXY0(xy0) 

681 

682 # CoaddPsf centroid not only must overlap image, but must overlap the part of 

683 # image with data. Use centroid of region with data 

684 boolmask = templateExposure.mask.array & templateExposure.mask.getPlaneBitMask('NO_DATA') == 0 

685 maskx = afwImage.makeMaskFromArray(boolmask.astype(afwImage.MaskPixel)) 

686 centerCoord = afwGeom.SpanSet.fromMask(maskx, 1).computeCentroid() 

687 

688 ctrl = self.config.coaddPsf.makeControl() 

689 coaddPsf = CoaddPsf(tractsCatalog, finalWcs, centerCoord, ctrl.warpingKernelName, ctrl.cacheSize) 

690 if coaddPsf is None: 

691 raise RuntimeError("CoaddPsf could not be constructed") 

692 

693 templateExposure.setPsf(coaddPsf) 

694 templateExposure.setFilter(coaddExposure.getFilter()) 

695 templateExposure.setPhotoCalib(coaddExposure.getPhotoCalib()) 

696 return pipeBase.Struct(outputExposure=templateExposure) 

697 

698 

699class GetDcrTemplateConnections(GetTemplateConnections, 

700 dimensions=("instrument", "visit", "detector", "skymap"), 

701 defaultTemplates={"coaddName": "dcr", 

702 "warpTypeSuffix": "", 

703 "fakesType": ""}): 

704 visitInfo = pipeBase.connectionTypes.Input( 

705 doc="VisitInfo of calexp used to determine observing conditions.", 

706 name="{fakesType}calexp.visitInfo", 

707 storageClass="VisitInfo", 

708 dimensions=("instrument", "visit", "detector"), 

709 ) 

710 dcrCoadds = pipeBase.connectionTypes.Input( 

711 doc="Input DCR template to match and subtract from the exposure", 

712 name="{fakesType}dcrCoadd{warpTypeSuffix}", 

713 storageClass="ExposureF", 

714 dimensions=("tract", "patch", "skymap", "band", "subfilter"), 

715 multiple=True, 

716 deferLoad=True 

717 ) 

718 

719 def __init__(self, *, config=None): 

720 super().__init__(config=config) 

721 self.inputs.remove("coaddExposures") 

722 

723 

724class GetDcrTemplateConfig(GetTemplateConfig, 

725 pipelineConnections=GetDcrTemplateConnections): 

726 numSubfilters = pexConfig.Field( 

727 doc="Number of subfilters in the DcrCoadd.", 

728 dtype=int, 

729 default=3, 

730 ) 

731 effectiveWavelength = pexConfig.Field( 

732 doc="Effective wavelength of the filter.", 

733 optional=False, 

734 dtype=float, 

735 ) 

736 bandwidth = pexConfig.Field( 

737 doc="Bandwidth of the physical filter.", 

738 optional=False, 

739 dtype=float, 

740 ) 

741 

742 def validate(self): 

743 if self.effectiveWavelength is None or self.bandwidth is None: 

744 raise ValueError("The effective wavelength and bandwidth of the physical filter " 

745 "must be set in the getTemplate config for DCR coadds. " 

746 "Required until transmission curves are used in DM-13668.") 

747 

748 

749class GetDcrTemplateTask(GetTemplateTask): 

750 ConfigClass = GetDcrTemplateConfig 

751 _DefaultName = "getDcrTemplate" 

752 

753 def getOverlappingExposures(self, inputs): 

754 """Return lists of coadds and their corresponding dataIds that overlap the detector. 

755 

756 The spatial index in the registry has generous padding and often supplies 

757 patches near, but not directly overlapping the detector. 

758 Filters inputs so that we don't have to read in all input coadds. 

759 

760 Parameters 

761 ---------- 

762 inputs : `dict` of task Inputs, containing: 

763 - coaddExposureRefs : `list` of elements of type 

764 `lsst.daf.butler.DeferredDatasetHandle` of 

765 `lsst.afw.image.Exposure` 

766 Data references to exposures that might overlap the detector. 

767 - bbox : `lsst.geom.Box2I` 

768 Template Bounding box of the detector geometry onto which to 

769 resample the coaddExposures 

770 - skyMap : `lsst.skymap.SkyMap` 

771 Input definition of geometry/bbox and projection/wcs for template exposures 

772 - wcs : `lsst.afw.geom.SkyWcs` 

773 Template WCS onto which to resample the coaddExposures 

774 - visitInfo : `lsst.afw.image.VisitInfo` 

775 Metadata for the science image. 

776 

777 Returns 

778 ------- 

779 result : `lsst.pipe.base.Struct` containing these fields: 

780 - coaddExposures : `list` of elements of type `lsst.afw.image.Exposure` 

781 Coadd exposures that overlap the detector. 

782 - dataIds : `list` of `lsst.daf.butler.DataCoordinate` 

783 Data IDs of the coadd exposures that overlap the detector. 

784 

785 Raises 

786 ------ 

787 NoWorkFound 

788 Raised if no patches overlatp the input detector bbox 

789 """ 

790 # Check that the patches actually overlap the detector 

791 # Exposure's validPolygon would be more accurate 

792 detectorPolygon = geom.Box2D(inputs["bbox"]) 

793 overlappingArea = 0 

794 coaddExposureRefList = [] 

795 dataIds = [] 

796 patchList = dict() 

797 for coaddRef in inputs["dcrCoadds"]: 

798 dataId = coaddRef.dataId 

799 patchWcs = inputs["skyMap"][dataId['tract']].getWcs() 

800 patchBBox = inputs["skyMap"][dataId['tract']][dataId['patch']].getOuterBBox() 

801 patchCorners = patchWcs.pixelToSky(geom.Box2D(patchBBox).getCorners()) 

802 patchPolygon = afwGeom.Polygon(inputs["wcs"].skyToPixel(patchCorners)) 

803 if patchPolygon.intersection(detectorPolygon): 

804 overlappingArea += patchPolygon.intersectionSingle(detectorPolygon).calculateArea() 

805 self.log.info("Using template input tract=%s, patch=%s, subfilter=%s" % 

806 (dataId['tract'], dataId['patch'], dataId["subfilter"])) 

807 coaddExposureRefList.append(coaddRef) 

808 if dataId['tract'] in patchList: 

809 patchList[dataId['tract']].append(dataId['patch']) 

810 else: 

811 patchList[dataId['tract']] = [dataId['patch'], ] 

812 dataIds.append(dataId) 

813 

814 if not overlappingArea: 

815 raise pipeBase.NoWorkFound('No patches overlap detector') 

816 

817 self.checkPatchList(patchList) 

818 

819 coaddExposures = self.getDcrModel(patchList, inputs['dcrCoadds'], inputs['visitInfo']) 

820 return pipeBase.Struct(coaddExposures=coaddExposures, 

821 dataIds=dataIds) 

822 

823 def checkPatchList(self, patchList): 

824 """Check that all of the DcrModel subfilters are present for each patch. 

825 

826 Parameters 

827 ---------- 

828 patchList : `dict` 

829 Dict of the patches containing valid data for each tract 

830 

831 Raises 

832 ------ 

833 RuntimeError 

834 If the number of exposures found for a patch does not match the number of subfilters. 

835 """ 

836 for tract in patchList: 

837 for patch in set(patchList[tract]): 

838 if patchList[tract].count(patch) != self.config.numSubfilters: 

839 raise RuntimeError("Invalid number of DcrModel subfilters found: %d vs %d expected", 

840 patchList[tract].count(patch), self.config.numSubfilters) 

841 

842 def getDcrModel(self, patchList, coaddRefs, visitInfo): 

843 """Build DCR-matched coadds from a list of exposure references. 

844 

845 Parameters 

846 ---------- 

847 patchList : `dict` 

848 Dict of the patches containing valid data for each tract 

849 coaddRefs : `list` of elements of type 

850 `lsst.daf.butler.DeferredDatasetHandle` of 

851 `lsst.afw.image.Exposure` 

852 Data references to DcrModels that overlap the detector. 

853 visitInfo : `lsst.afw.image.VisitInfo` 

854 Metadata for the science image. 

855 

856 Returns 

857 ------- 

858 `list` of elements of type `lsst.afw.image.Exposure` 

859 Coadd exposures that overlap the detector. 

860 """ 

861 coaddExposureList = [] 

862 for tract in patchList: 

863 for patch in set(patchList[tract]): 

864 coaddRefList = [coaddRef for coaddRef in coaddRefs 

865 if _selectDataRef(coaddRef, tract, patch)] 

866 

867 dcrModel = DcrModel.fromQuantum(coaddRefList, 

868 self.config.effectiveWavelength, 

869 self.config.bandwidth, 

870 self.config.numSubfilters) 

871 coaddExposureList.append(dcrModel.buildMatchedExposure(visitInfo=visitInfo)) 

872 return coaddExposureList 

873 

874 

875def _selectDataRef(coaddRef, tract, patch): 

876 condition = (coaddRef.dataId['tract'] == tract) & (coaddRef.dataId['patch'] == patch) 

877 return condition 

878 

879 

880class GetMultiTractCoaddTemplateConfig(GetTemplateConfig): 

881 pass 

882 

883 

884class GetMultiTractCoaddTemplateTask(GetTemplateTask): 

885 ConfigClass = GetMultiTractCoaddTemplateConfig 

886 _DefaultName = "getMultiTractCoaddTemplate" 

887 

888 def __init__(self, *args, **kwargs): 

889 super().__init__(*args, **kwargs) 

890 self.log.warn("GetMultiTractCoaddTemplateTask is deprecated. Use GetTemplateTask instead.")