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

Shortcuts 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

283 statements  

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 "GetMultiTractCoaddTemplateTask", "GetMultiTractCoaddTemplateConfig"] 

40 

41 

42class GetCoaddAsTemplateConfig(pexConfig.Config): 

43 templateBorderSize = pexConfig.Field( 

44 dtype=int, 

45 default=10, 

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

47 ) 

48 coaddName = pexConfig.Field( 

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

50 dtype=str, 

51 default="deep", 

52 ) 

53 numSubfilters = pexConfig.Field( 

54 doc="Number of subfilters in the DcrCoadd. Used only if ``coaddName``='dcr'", 

55 dtype=int, 

56 default=3, 

57 ) 

58 effectiveWavelength = pexConfig.Field( 

59 doc="Effective wavelength of the filter. Used only if ``coaddName``='dcr'", 

60 optional=True, 

61 dtype=float, 

62 ) 

63 bandwidth = pexConfig.Field( 

64 doc="Bandwidth of the physical filter. Used only if ``coaddName``='dcr'", 

65 optional=True, 

66 dtype=float, 

67 ) 

68 warpType = pexConfig.Field( 

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

70 dtype=str, 

71 default="direct", 

72 ) 

73 

74 def validate(self): 

75 if self.coaddName == 'dcr': 

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

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

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

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

80 

81 

82class GetCoaddAsTemplateTask(pipeBase.Task): 

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

84 

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

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

87 ``runGen3()``. 

88 

89 Notes 

90 ----- 

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

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

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

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

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

96 

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

98 and ``NO_DATA`` flagged. 

99 """ 

100 

101 ConfigClass = GetCoaddAsTemplateConfig 

102 _DefaultName = "GetCoaddAsTemplateTask" 

103 

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

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

106 that overlaps the science exposure. 

107 

108 Parameters 

109 ---------- 

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

111 an exposure for which to generate an overlapping template 

112 sensorRef : TYPE 

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

114 templateIdList : TYPE, optional 

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

116 

117 Returns 

118 ------- 

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

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

121 a template coadd exposure assembled out of patches 

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

123 """ 

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

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

126 

127 availableCoaddRefs = dict() 

128 for patchInfo in patchList: 

129 patchNumber = tractInfo.getSequentialPatchIndex(patchInfo) 

130 patchArgDict = dict( 

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

132 bbox=patchInfo.getOuterBBox(), 

133 tract=tractInfo.getId(), 

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

135 subfilter=0, 

136 numSubfilters=self.config.numSubfilters, 

137 ) 

138 

139 if sensorRef.datasetExists(**patchArgDict): 

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

141 availableCoaddRefs[patchNumber] = patchArgDict 

142 

143 templateExposure = self.run( 

144 tractInfo, patchList, skyCorners, availableCoaddRefs, 

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

146 ) 

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

148 

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

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

151 that overlaps the science exposure. 

152 

153 Parameters 

154 ---------- 

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

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

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

158 Butler like object that supports getting data by DatasetRef. 

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

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

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

162 Iterable of references to the available template coadd patches. 

163 

164 Returns 

165 ------- 

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

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

168 a template coadd exposure assembled out of patches 

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

170 """ 

171 skyMap = butlerQC.get(skyMapRef) 

172 coaddExposureRefs = butlerQC.get(coaddExposureRefs) 

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

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

175 tractInfo = skyMap[tracts[0]] 

176 else: 

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

178 "Use GetMultiTractCoaddTemplateTask instead.") 

179 

180 detectorBBox = exposure.getBBox() 

181 detectorWcs = exposure.getWcs() 

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

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

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

185 

186 availableCoaddRefs = dict() 

187 overlappingArea = 0 

188 for coaddRef in coaddExposureRefs: 

189 dataId = coaddRef.dataId 

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

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

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

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

194 if patchPolygon.intersection(detectorPolygon): 

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

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

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

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

199 if dataId['patch'] in availableCoaddRefs: 

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

201 else: 

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

203 else: 

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

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

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

207 

208 if overlappingArea == 0: 

209 templateExposure = None 

210 pixGood = 0 

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

212 else: 

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

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

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

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

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

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

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

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

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

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

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

224 

225 def getOverlapPatchList(self, exposure, skyMap): 

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

227 

228 Parameters 

229 ---------- 

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

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

232 

233 skyMap : `lsst.skymap.BaseSkyMap` 

234 SkyMap object that corresponds to the template coadd. 

235 

236 Returns 

237 ------- 

238 result : `tuple` of 

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

240 The selected tract. 

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

242 List of all overlap patches of the selected tract. 

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

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

245 """ 

246 expWcs = exposure.getWcs() 

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

248 expBoxD.grow(self.config.templateBorderSize) 

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

250 tractInfo = skyMap.findTract(ctrSkyPos) 

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

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

253 patchList = tractInfo.findPatchList(skyCorners) 

254 

255 if not patchList: 

256 raise RuntimeError("No suitable tract found") 

257 

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

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

260 

261 return (tractInfo, patchList, skyCorners) 

262 

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

264 sensorRef=None, visitInfo=None): 

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

266 copying of pixels from overlapping patch regions. 

267 

268 Parameters 

269 ---------- 

270 skyMap : `lsst.skymap.BaseSkyMap` 

271 SkyMap object that corresponds to the template coadd. 

272 tractInfo : `lsst.skymap.TractInfo` 

273 The selected tract. 

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

275 Patches to consider for making the template exposure. 

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

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

278 availableCoaddRefs : `dict` [`int`] 

279 Dictionary of spatially relevant retrieved coadd patches, 

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

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

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

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

284 Butler data reference to get coadd data. 

285 Must be `None` for Gen3. 

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

287 VisitInfo to make dcr model. 

288 

289 Returns 

290 ------- 

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

292 The created template exposure. 

293 """ 

294 coaddWcs = tractInfo.getWcs() 

295 

296 # compute coadd bbox 

297 coaddBBox = geom.Box2D() 

298 for skyPos in skyCorners: 

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

300 coaddBBox = geom.Box2I(coaddBBox) 

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

302 

303 coaddExposure = afwImage.ExposureF(coaddBBox, coaddWcs) 

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

305 nPatchesFound = 0 

306 coaddFilterLabel = None 

307 coaddPsf = None 

308 coaddPhotoCalib = None 

309 for patchInfo in patchList: 

310 patchNumber = tractInfo.getSequentialPatchIndex(patchInfo) 

311 patchSubBBox = patchInfo.getOuterBBox() 

312 patchSubBBox.clip(coaddBBox) 

313 if patchNumber not in availableCoaddRefs: 

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

315 continue 

316 if patchSubBBox.isEmpty(): 

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

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

319 else: 

320 tract = availableCoaddRefs[patchNumber]['tract'] 

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

322 continue 

323 

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

325 patchInnerBBox = patchInfo.getInnerBBox() 

326 patchInnerBBox.clip(coaddBBox) 

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

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

329 availableCoaddRefs[patchNumber]) 

330 continue 

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

332 availableCoaddRefs[patchNumber]) 

333 

334 if sensorRef: 

335 dcrModel = DcrModel.fromDataRef(sensorRef, 

336 self.config.effectiveWavelength, 

337 self.config.bandwidth, 

338 **availableCoaddRefs[patchNumber]) 

339 else: 

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

341 self.config.effectiveWavelength, 

342 self.config.bandwidth) 

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

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

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

346 # previous patches. 

347 # Shrink the BBox to remove the contaminated pixels, 

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

349 dcrBBox = geom.Box2I(patchSubBBox) 

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

351 dcrBBox.include(patchInnerBBox) 

352 coaddPatch = dcrModel.buildMatchedExposure(bbox=dcrBBox, 

353 visitInfo=visitInfo) 

354 else: 

355 if sensorRef is None: 

356 # Gen3 

357 coaddPatch = availableCoaddRefs[patchNumber].get() 

358 else: 

359 # Gen2 

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

361 nPatchesFound += 1 

362 

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

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

365 overlapBox = coaddPatch.getBBox() 

366 overlapBox.clip(coaddBBox) 

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

368 

369 if coaddFilterLabel is None: 

370 coaddFilterLabel = coaddPatch.getFilterLabel() 

371 

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

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

374 coaddPsf = coaddPatch.getPsf() 

375 

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

377 if coaddPhotoCalib is None: 

378 coaddPhotoCalib = coaddPatch.getPhotoCalib() 

379 

380 if coaddPhotoCalib is None: 

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

382 if nPatchesFound == 0: 

383 raise RuntimeError("No patches found!") 

384 if coaddPsf is None: 

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

386 

387 coaddExposure.setPhotoCalib(coaddPhotoCalib) 

388 coaddExposure.setPsf(coaddPsf) 

389 coaddExposure.setFilterLabel(coaddFilterLabel) 

390 return coaddExposure 

391 

392 def getCoaddDatasetName(self): 

393 """Return coadd name for given task config 

394 

395 Returns 

396 ------- 

397 CoaddDatasetName : `string` 

398 

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

400 """ 

401 warpType = self.config.warpType 

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

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

404 

405 

406class GetCalexpAsTemplateConfig(pexConfig.Config): 

407 doAddCalexpBackground = pexConfig.Field( 

408 dtype=bool, 

409 default=True, 

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

411 ) 

412 

413 

414class GetCalexpAsTemplateTask(pipeBase.Task): 

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

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

417 

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

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

420 This code was originally part of Winter2013ImageDifferenceTask. 

421 """ 

422 

423 ConfigClass = GetCalexpAsTemplateConfig 

424 _DefaultName = "GetCalexpAsTemplateTask" 

425 

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

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

428 

429 Construct a dataId based on the sensorRef.dataId combined 

430 with the specifications from the first dataId in templateIdList 

431 

432 Parameters 

433 ---------- 

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

435 exposure (unused) 

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

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

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

439 Data reference of the template calexp to be subtraced. 

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

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

442 

443 Returns 

444 ------- 

445 result : `struct` 

446 

447 return a pipeBase.Struct: 

448 

449 - ``exposure`` : a template calexp 

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

451 """ 

452 

453 if len(templateIdList) == 0: 

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

455 if len(templateIdList) > 1: 

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

457 

458 templateId = sensorRef.dataId.copy() 

459 templateId.update(templateIdList[0]) 

460 

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

462 

463 butler = sensorRef.getButler() 

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

465 if self.config.doAddCalexpBackground: 

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

467 mi = template.getMaskedImage() 

468 mi += templateBg.getImage() 

469 

470 if not template.hasPsf(): 

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

472 

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

474 return pipeBase.Struct(exposure=template, 

475 sources=templateSources) 

476 

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

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

479 

480 def runQuantum(self, **kwargs): 

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

482 

483 

484class GetMultiTractCoaddTemplateConnections(pipeBase.PipelineTaskConnections, 

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

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

487 "warpTypeSuffix": "", 

488 "fakesType": ""}): 

489 bbox = pipeBase.connectionTypes.Input( 

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

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

492 storageClass="Box2I", 

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

494 ) 

495 wcs = pipeBase.connectionTypes.Input( 

496 doc="WCSs of calexps that we want to fetch the template for", 

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

498 storageClass="Wcs", 

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

500 ) 

501 skyMap = pipeBase.connectionTypes.Input( 

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

503 name=BaseSkyMap.SKYMAP_DATASET_TYPE_NAME, 

504 dimensions=("skymap", ), 

505 storageClass="SkyMap", 

506 ) 

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

508 # Needed for DRP HSC 

509 coaddExposures = pipeBase.connectionTypes.Input( 

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

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

512 storageClass="ExposureF", 

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

514 multiple=True, 

515 deferLoad=True 

516 ) 

517 outputExposure = pipeBase.connectionTypes.Output( 

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

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

520 storageClass="ExposureF", 

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

522 ) 

523 

524 

525class GetMultiTractCoaddTemplateConfig(pipeBase.PipelineTaskConfig, GetCoaddAsTemplateConfig, 

526 pipelineConnections=GetMultiTractCoaddTemplateConnections): 

527 warp = pexConfig.ConfigField( 

528 dtype=afwMath.Warper.ConfigClass, 

529 doc="warper configuration", 

530 ) 

531 coaddPsf = pexConfig.ConfigField( 

532 doc="Configuration for CoaddPsf", 

533 dtype=CoaddPsfConfig, 

534 ) 

535 

536 def setDefaults(self): 

537 self.warp.warpingKernelName = 'lanczos5' 

538 self.coaddPsf.warpingKernelName = 'lanczos5' 

539 

540 

541class GetMultiTractCoaddTemplateTask(pipeBase.PipelineTask): 

542 ConfigClass = GetMultiTractCoaddTemplateConfig 

543 _DefaultName = "getMultiTractCoaddTemplateTask" 

544 

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

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

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

548 

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

550 # Read in all inputs. 

551 inputs = butlerQC.get(inputRefs) 

552 inputs['coaddExposures'] = self.getOverlappingExposures(inputs) 

553 # SkyMap only needed for filtering without 

554 inputs.pop('skyMap') 

555 outputs = self.run(**inputs) 

556 butlerQC.put(outputs, outputRefs) 

557 

558 def getOverlappingExposures(self, inputs): 

559 """Return list of coaddExposure DeferredDatasetHandles that overlap detector 

560 

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

562 patches near, but not directly overlapping the detector. 

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

564 

565 Parameters 

566 ---------- 

567 inputs : `dict` of task Inputs 

568 

569 Returns 

570 ------- 

571 coaddExposures : list of elements of type 

572 `lsst.daf.butler.DeferredDatasetHandle` of 

573 `lsst.afw.image.Exposure` 

574 

575 Raises 

576 ------ 

577 NoWorkFound 

578 Raised if no patches overlap the input detector bbox 

579 """ 

580 # Check that the patches actually overlap the detector 

581 # Exposure's validPolygon would be more accurate 

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

583 overlappingArea = 0 

584 coaddExposureList = [] 

585 for coaddRef in inputs['coaddExposures']: 

586 dataId = coaddRef.dataId 

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

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

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

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

591 if patchPolygon.intersection(detectorPolygon): 

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

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

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

595 coaddExposureList.append(coaddRef) 

596 

597 if not overlappingArea: 

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

599 

600 return coaddExposureList 

601 

602 def run(self, coaddExposures, bbox, wcs): 

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

604 

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

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

607 template image into a meta-CoaddPsf. 

608 

609 Parameters 

610 ---------- 

611 coaddExposures: list of DeferredDatasetHandle to `lsst.afw.image.Exposure` 

612 Coadds to be mosaicked 

613 bbox : `lsst.geom.Box2I` 

614 Template Bounding box of the detector geometry onto which to 

615 resample the coaddExposures 

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

617 Template WCS onto which to resample the coaddExposures 

618 

619 Returns 

620 ------- 

621 result : `struct` 

622 return a pipeBase.Struct: 

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

624 

625 

626 Raises 

627 ------ 

628 NoWorkFound 

629 Raised if no patches overlatp the input detector bbox 

630 

631 """ 

632 # Table for CoaddPSF 

633 tractsSchema = afwTable.ExposureTable.makeMinimalSchema() 

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

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

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

637 tractsCatalog = afwTable.ExposureCatalog(tractsSchema) 

638 

639 finalWcs = wcs 

640 bbox.grow(self.config.templateBorderSize) 

641 finalBBox = bbox 

642 

643 nPatchesFound = 0 

644 maskedImageList = [] 

645 weightList = [] 

646 

647 for coaddExposure in coaddExposures: 

648 coaddPatch = coaddExposure.get() 

649 

650 # warp to detector WCS 

651 warped = self.warper.warpExposure(finalWcs, coaddPatch, maxBBox=finalBBox) 

652 

653 # Check if warped image is viable 

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

655 self.log.info("No overlap for warped %s. Skipping" % coaddExposure.ref.dataId) 

656 continue 

657 

658 exp = afwImage.ExposureF(finalBBox, finalWcs) 

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

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

661 

662 maskedImageList.append(exp.maskedImage) 

663 weightList.append(1) 

664 record = tractsCatalog.addNew() 

665 record.setPsf(coaddPatch.getPsf()) 

666 record.setWcs(coaddPatch.getWcs()) 

667 record.setPhotoCalib(coaddPatch.getPhotoCalib()) 

668 record.setBBox(coaddPatch.getBBox()) 

669 record.setValidPolygon(afwGeom.Polygon(geom.Box2D(coaddPatch.getBBox()).getCorners())) 

670 record.set(tractKey, coaddExposure.ref.dataId['tract']) 

671 record.set(patchKey, coaddExposure.ref.dataId['patch']) 

672 record.set(weightKey, 1.) 

673 nPatchesFound += 1 

674 

675 if nPatchesFound == 0: 

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

677 

678 # Combine images from individual patches together 

679 statsFlags = afwMath.stringToStatisticsProperty('MEAN') 

680 statsCtrl = afwMath.StatisticsControl() 

681 statsCtrl.setNanSafe(True) 

682 statsCtrl.setWeighted(True) 

683 statsCtrl.setCalcErrorFromInputVariance(True) 

684 

685 templateExposure = afwImage.ExposureF(finalBBox, finalWcs) 

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

687 xy0 = templateExposure.getXY0() 

688 # Do not mask any values 

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

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

691 templateExposure.maskedImage.setXY0(xy0) 

692 

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

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

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

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

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

698 

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

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

701 if coaddPsf is None: 

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

703 

704 templateExposure.setPsf(coaddPsf) 

705 templateExposure.setFilterLabel(coaddPatch.getFilterLabel()) 

706 templateExposure.setPhotoCalib(coaddPatch.getPhotoCalib()) 

707 return pipeBase.Struct(outputExposure=templateExposure)