Coverage for python/lsst/pipe/tasks/makeWarp.py: 18%

227 statements  

« prev     ^ index     » next       coverage.py v7.5.1, created at 2024-05-09 03:58 -0700

1# This file is part of pipe_tasks. 

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__all__ = ["MakeWarpTask", "MakeWarpConfig"] 

23 

24import logging 

25import numpy 

26 

27import lsst.pex.config as pexConfig 

28import lsst.afw.image as afwImage 

29import lsst.coadd.utils as coaddUtils 

30import lsst.pipe.base as pipeBase 

31import lsst.pipe.base.connectionTypes as connectionTypes 

32import lsst.utils as utils 

33import lsst.geom 

34from lsst.daf.butler import DeferredDatasetHandle 

35from lsst.meas.base import DetectorVisitIdGeneratorConfig 

36from lsst.meas.algorithms import CoaddPsf, CoaddPsfConfig, GaussianPsfFactory 

37from lsst.skymap import BaseSkyMap 

38from lsst.utils.timer import timeMethod 

39from .coaddBase import CoaddBaseTask, makeSkyInfo, reorderAndPadList 

40from .warpAndPsfMatch import WarpAndPsfMatchTask 

41from collections.abc import Iterable 

42 

43log = logging.getLogger(__name__) 

44 

45 

46class MakeWarpConnections(pipeBase.PipelineTaskConnections, 

47 dimensions=("tract", "patch", "skymap", "instrument", "visit"), 

48 defaultTemplates={"coaddName": "deep", 

49 "calexpType": ""}): 

50 calExpList = connectionTypes.Input( 

51 doc="Input exposures to be resampled and optionally PSF-matched onto a SkyMap projection/patch", 

52 name="{calexpType}calexp", 

53 storageClass="ExposureF", 

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

55 multiple=True, 

56 deferLoad=True, 

57 ) 

58 backgroundList = connectionTypes.Input( 

59 doc="Input backgrounds to be added back into the calexp if bgSubtracted=False", 

60 name="calexpBackground", 

61 storageClass="Background", 

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

63 multiple=True, 

64 ) 

65 skyCorrList = connectionTypes.Input( 

66 doc="Input Sky Correction to be subtracted from the calexp if doApplySkyCorr=True", 

67 name="skyCorr", 

68 storageClass="Background", 

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

70 multiple=True, 

71 ) 

72 skyMap = connectionTypes.Input( 

73 doc="Input definition of geometry/bbox and projection/wcs for warped exposures", 

74 name=BaseSkyMap.SKYMAP_DATASET_TYPE_NAME, 

75 storageClass="SkyMap", 

76 dimensions=("skymap",), 

77 ) 

78 direct = connectionTypes.Output( 

79 doc=("Output direct warped exposure (previously called CoaddTempExp), produced by resampling ", 

80 "calexps onto the skyMap patch geometry."), 

81 name="{coaddName}Coadd_directWarp", 

82 storageClass="ExposureF", 

83 dimensions=("tract", "patch", "skymap", "visit", "instrument"), 

84 ) 

85 psfMatched = connectionTypes.Output( 

86 doc=("Output PSF-Matched warped exposure (previously called CoaddTempExp), produced by resampling ", 

87 "calexps onto the skyMap patch geometry and PSF-matching to a model PSF."), 

88 name="{coaddName}Coadd_psfMatchedWarp", 

89 storageClass="ExposureF", 

90 dimensions=("tract", "patch", "skymap", "visit", "instrument"), 

91 ) 

92 visitSummary = connectionTypes.Input( 

93 doc="Input visit-summary catalog with updated calibration objects.", 

94 name="finalVisitSummary", 

95 storageClass="ExposureCatalog", 

96 dimensions=("instrument", "visit",), 

97 ) 

98 

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

100 if config.bgSubtracted: 

101 del self.backgroundList 

102 if not config.doApplySkyCorr: 

103 del self.skyCorrList 

104 if not config.makeDirect: 

105 del self.direct 

106 if not config.makePsfMatched: 

107 del self.psfMatched 

108 

109 

110class MakeWarpConfig(pipeBase.PipelineTaskConfig, CoaddBaseTask.ConfigClass, 

111 pipelineConnections=MakeWarpConnections): 

112 """Config for MakeWarpTask.""" 

113 

114 warpAndPsfMatch = pexConfig.ConfigurableField( 

115 target=WarpAndPsfMatchTask, 

116 doc="Task to warp and PSF-match calexp", 

117 ) 

118 doWrite = pexConfig.Field( 

119 doc="persist <coaddName>Coadd_<warpType>Warp", 

120 dtype=bool, 

121 default=True, 

122 ) 

123 bgSubtracted = pexConfig.Field( 

124 doc="Work with a background subtracted calexp?", 

125 dtype=bool, 

126 default=True, 

127 ) 

128 coaddPsf = pexConfig.ConfigField( 

129 doc="Configuration for CoaddPsf", 

130 dtype=CoaddPsfConfig, 

131 ) 

132 makeDirect = pexConfig.Field( 

133 doc="Make direct Warp/Coadds", 

134 dtype=bool, 

135 default=True, 

136 ) 

137 makePsfMatched = pexConfig.Field( 

138 doc="Make Psf-Matched Warp/Coadd?", 

139 dtype=bool, 

140 default=False, 

141 ) 

142 modelPsf = GaussianPsfFactory.makeField(doc="Model Psf factory") 

143 useVisitSummaryPsf = pexConfig.Field( 

144 doc=( 

145 "If True, use the PSF model and aperture corrections from the 'visitSummary' connection. " 

146 "If False, use the PSF model and aperture corrections from the 'exposure' connection. " 

147 ), 

148 dtype=bool, 

149 default=True, 

150 ) 

151 doWriteEmptyWarps = pexConfig.Field( 

152 dtype=bool, 

153 default=False, 

154 doc="Write out warps even if they are empty" 

155 ) 

156 hasFakes = pexConfig.Field( 

157 doc="Should be set to True if fake sources have been inserted into the input data.", 

158 dtype=bool, 

159 default=False, 

160 ) 

161 doApplySkyCorr = pexConfig.Field( 

162 dtype=bool, 

163 default=False, 

164 doc="Apply sky correction?", 

165 ) 

166 idGenerator = DetectorVisitIdGeneratorConfig.make_field() 

167 

168 def validate(self): 

169 CoaddBaseTask.ConfigClass.validate(self) 

170 

171 if not self.makePsfMatched and not self.makeDirect: 

172 raise ValueError("At least one of config.makePsfMatched and config.makeDirect must be True") 

173 if self.warpAndPsfMatch.warp.cacheSize != self.coaddPsf.cacheSize: 

174 # This is an incomplete check: usually the CoaddPsf cache size 

175 # configured here in MakeWarpTask is superseded by the one in 

176 # AssembleCoaddTask. A pipeline contract in the drp_pipe is 

177 # present to check that. 

178 raise ValueError("Image warping cache size and CoaddPSf warping cache size do not agree.") 

179 

180 def setDefaults(self): 

181 CoaddBaseTask.ConfigClass.setDefaults(self) 

182 self.warpAndPsfMatch.warp.cacheSize = 0 

183 self.coaddPsf.cacheSize = 0 

184 self.warpAndPsfMatch.psfMatch.kernel.active.kernelSize = self.matchingKernelSize 

185 

186 

187class MakeWarpTask(CoaddBaseTask): 

188 """Warp and optionally PSF-Match calexps onto an a common projection. 

189 

190 Warp and optionally PSF-Match calexps onto a common projection, by 

191 performing the following operations: 

192 - Group calexps by visit/run 

193 - For each visit, generate a Warp by calling method @ref run. 

194 `run` loops over the visit's calexps calling 

195 `~lsst.pipe.tasks.warpAndPsfMatch.WarpAndPsfMatchTask` on each visit 

196 

197 """ 

198 ConfigClass = MakeWarpConfig 

199 _DefaultName = "makeWarp" 

200 

201 def __init__(self, **kwargs): 

202 CoaddBaseTask.__init__(self, **kwargs) 

203 self.makeSubtask("warpAndPsfMatch") 

204 if self.config.hasFakes: 

205 self.calexpType = "fakes_calexp" 

206 else: 

207 self.calexpType = "calexp" 

208 

209 @utils.inheritDoc(pipeBase.PipelineTask) 

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

211 # Docstring to be augmented with info from PipelineTask.runQuantum 

212 """Notes 

213 ----- 

214 Obtain the list of input detectors from calExpList. Sort them by 

215 detector order (to ensure reproducibility). Then ensure all input 

216 lists are in the same sorted detector order. 

217 """ 

218 detectorOrder = [ref.datasetRef.dataId['detector'] for ref in inputRefs.calExpList] 

219 detectorOrder.sort() 

220 inputRefs = reorderRefs(inputRefs, detectorOrder, dataIdKey='detector') 

221 

222 # Read in all inputs. 

223 inputs = butlerQC.get(inputRefs) 

224 

225 # Construct skyInfo expected by `run`. We remove the SkyMap itself 

226 # from the dictionary so we can pass it as kwargs later. 

227 skyMap = inputs.pop("skyMap") 

228 quantumDataId = butlerQC.quantum.dataId 

229 skyInfo = makeSkyInfo(skyMap, tractId=quantumDataId['tract'], patchId=quantumDataId['patch']) 

230 

231 # Construct list of input DataIds expected by `run`. 

232 dataIdList = [ref.datasetRef.dataId for ref in inputRefs.calExpList] 

233 # Construct list of packed integer IDs expected by `run`. 

234 ccdIdList = [ 

235 self.config.idGenerator.apply(dataId).catalog_id 

236 for dataId in dataIdList 

237 ] 

238 

239 # Check early that the visitSummary contains everything we need. 

240 visitSummary = inputs["visitSummary"] 

241 bboxList = [] 

242 wcsList = [] 

243 for dataId in dataIdList: 

244 row = visitSummary.find(dataId["detector"]) 

245 if row is None: 

246 raise RuntimeError( 

247 f"Unexpectedly incomplete visitSummary provided to makeWarp: {dataId} is missing." 

248 ) 

249 bboxList.append(row.getBBox()) 

250 wcsList.append(row.getWcs()) 

251 inputs["bboxList"] = bboxList 

252 inputs["wcsList"] = wcsList 

253 

254 # Do an initial selection on inputs with complete wcs/photoCalib info. 

255 # Qualifying calexps will be read in the following call. 

256 completeIndices = self._prepareCalibratedExposures(**inputs) 

257 inputs = self.filterInputs(indices=completeIndices, inputs=inputs) 

258 

259 # Do another selection based on the configured selection task 

260 # (using updated WCSs to determine patch overlap if an external 

261 # calibration was applied). 

262 cornerPosList = lsst.geom.Box2D(skyInfo.bbox).getCorners() 

263 coordList = [skyInfo.wcs.pixelToSky(pos) for pos in cornerPosList] 

264 goodIndices = self.select.run(**inputs, coordList=coordList, dataIds=dataIdList) 

265 inputs = self.filterInputs(indices=goodIndices, inputs=inputs) 

266 

267 # Extract integer visitId requested by `run`. 

268 visitId = dataIdList[0]["visit"] 

269 

270 results = self.run(**inputs, 

271 visitId=visitId, 

272 ccdIdList=[ccdIdList[i] for i in goodIndices], 

273 dataIdList=[dataIdList[i] for i in goodIndices], 

274 skyInfo=skyInfo) 

275 if self.config.makeDirect and results.exposures["direct"] is not None: 

276 butlerQC.put(results.exposures["direct"], outputRefs.direct) 

277 if self.config.makePsfMatched and results.exposures["psfMatched"] is not None: 

278 butlerQC.put(results.exposures["psfMatched"], outputRefs.psfMatched) 

279 

280 @timeMethod 

281 def run(self, calExpList, ccdIdList, skyInfo, visitId=0, dataIdList=None, **kwargs): 

282 """Create a Warp from inputs. 

283 

284 We iterate over the multiple calexps in a single exposure to construct 

285 the warp (previously called a coaddTempExp) of that exposure to the 

286 supplied tract/patch. 

287 

288 Pixels that receive no pixels are set to NAN; this is not correct 

289 (violates LSST algorithms group policy), but will be fixed up by 

290 interpolating after the coaddition. 

291 

292 calexpRefList : `list` 

293 List of data references for calexps that (may) 

294 overlap the patch of interest. 

295 skyInfo : `lsst.pipe.base.Struct` 

296 Struct from `~lsst.pipe.base.coaddBase.makeSkyInfo()` with 

297 geometric information about the patch. 

298 visitId : `int` 

299 Integer identifier for visit, for the table that will 

300 produce the CoaddPsf. 

301 

302 Returns 

303 ------- 

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

305 Results as a struct with attributes: 

306 

307 ``exposures`` 

308 A dictionary containing the warps requested: 

309 "direct": direct warp if ``config.makeDirect`` 

310 "psfMatched": PSF-matched warp if ``config.makePsfMatched`` 

311 (`dict`). 

312 """ 

313 warpTypeList = self.getWarpTypeList() 

314 

315 totGoodPix = {warpType: 0 for warpType in warpTypeList} 

316 didSetMetadata = {warpType: False for warpType in warpTypeList} 

317 warps = {warpType: self._prepareEmptyExposure(skyInfo) for warpType in warpTypeList} 

318 inputRecorder = {warpType: self.inputRecorder.makeCoaddTempExpRecorder(visitId, len(calExpList)) 

319 for warpType in warpTypeList} 

320 

321 modelPsf = self.config.modelPsf.apply() if self.config.makePsfMatched else None 

322 if dataIdList is None: 

323 dataIdList = ccdIdList 

324 

325 for calExpInd, (calExp, ccdId, dataId) in enumerate(zip(calExpList, ccdIdList, dataIdList)): 

326 self.log.info("Processing calexp %d of %d for this Warp: id=%s", 

327 calExpInd+1, len(calExpList), dataId) 

328 try: 

329 warpedAndMatched = self.warpAndPsfMatch.run(calExp, modelPsf=modelPsf, 

330 wcs=skyInfo.wcs, maxBBox=skyInfo.bbox, 

331 makeDirect=self.config.makeDirect, 

332 makePsfMatched=self.config.makePsfMatched) 

333 except Exception as e: 

334 self.log.warning("WarpAndPsfMatch failed for calexp %s; skipping it: %s", dataId, e) 

335 continue 

336 try: 

337 numGoodPix = {warpType: 0 for warpType in warpTypeList} 

338 for warpType in warpTypeList: 

339 exposure = warpedAndMatched.getDict()[warpType] 

340 if exposure is None: 

341 continue 

342 warp = warps[warpType] 

343 if didSetMetadata[warpType]: 

344 mimg = exposure.getMaskedImage() 

345 mimg *= (warp.getPhotoCalib().getInstFluxAtZeroMagnitude() 

346 / exposure.getPhotoCalib().getInstFluxAtZeroMagnitude()) 

347 del mimg 

348 numGoodPix[warpType] = coaddUtils.copyGoodPixels( 

349 warp.getMaskedImage(), exposure.getMaskedImage(), self.getBadPixelMask()) 

350 totGoodPix[warpType] += numGoodPix[warpType] 

351 self.log.debug("Calexp %s has %d good pixels in this patch (%.1f%%) for %s", 

352 dataId, numGoodPix[warpType], 

353 100.0*numGoodPix[warpType]/skyInfo.bbox.getArea(), warpType) 

354 if numGoodPix[warpType] > 0 and not didSetMetadata[warpType]: 

355 warp.info.id = exposure.info.id 

356 warp.setPhotoCalib(exposure.getPhotoCalib()) 

357 warp.setFilter(exposure.getFilter()) 

358 warp.getInfo().setVisitInfo(exposure.getInfo().getVisitInfo()) 

359 # PSF replaced with CoaddPsf after loop if and only if 

360 # creating direct warp. 

361 warp.setPsf(exposure.getPsf()) 

362 didSetMetadata[warpType] = True 

363 

364 # Need inputRecorder for CoaddApCorrMap for both direct and 

365 # PSF-matched. 

366 inputRecorder[warpType].addCalExp(calExp, ccdId, numGoodPix[warpType]) 

367 

368 except Exception as e: 

369 self.log.warning("Error processing calexp %s; skipping it: %s", dataId, e) 

370 continue 

371 

372 for warpType in warpTypeList: 

373 self.log.info("%sWarp has %d good pixels (%.1f%%)", 

374 warpType, totGoodPix[warpType], 100.0*totGoodPix[warpType]/skyInfo.bbox.getArea()) 

375 

376 if totGoodPix[warpType] > 0 and didSetMetadata[warpType]: 

377 inputRecorder[warpType].finish(warps[warpType], totGoodPix[warpType]) 

378 if warpType == "direct": 

379 warps[warpType].setPsf( 

380 CoaddPsf(inputRecorder[warpType].coaddInputs.ccds, skyInfo.wcs, 

381 self.config.coaddPsf.makeControl())) 

382 else: 

383 if not self.config.doWriteEmptyWarps: 

384 # No good pixels. Exposure still empty. 

385 warps[warpType] = None 

386 # NoWorkFound is unnecessary as the downstream tasks will 

387 # adjust the quantum accordingly. 

388 

389 result = pipeBase.Struct(exposures=warps) 

390 return result 

391 

392 def filterInputs(self, indices, inputs): 

393 """Filter task inputs by their indices. 

394 

395 Parameters 

396 ---------- 

397 indices : `list` [`int`] 

398 inputs : `dict` [`list`] 

399 A dictionary of input connections to be passed to run. 

400 

401 Returns 

402 ------- 

403 inputs : `dict` [`list`] 

404 Task inputs with their lists filtered by indices. 

405 """ 

406 for key in inputs.keys(): 

407 # Only down-select on list inputs 

408 if isinstance(inputs[key], list): 

409 inputs[key] = [inputs[key][ind] for ind in indices] 

410 return inputs 

411 

412 def _prepareCalibratedExposures(self, *, visitSummary, calExpList=[], wcsList=None, 

413 backgroundList=None, skyCorrList=None, **kwargs): 

414 """Calibrate and add backgrounds to input calExpList in place. 

415 

416 Parameters 

417 ---------- 

418 visitSummary : `lsst.afw.table.ExposureCatalog` 

419 Exposure catalog with potentially all calibrations. Attributes set 

420 to `None` are ignored. 

421 calExpList : `list` [`lsst.afw.image.Exposure` or 

422 `lsst.daf.butler.DeferredDatasetHandle`] 

423 Sequence of calexps to be modified in place. 

424 wcsList : `list` [`lsst.afw.geom.SkyWcs`] 

425 The WCSs of the calexps in ``calExpList``. These will be used to 

426 determine if the calexp should be used in the warp. The list is 

427 dynamically updated with the WCSs from the visitSummary. 

428 backgroundList : `list` [`lsst.afw.math.backgroundList`], optional 

429 Sequence of backgrounds to be added back in if bgSubtracted=False. 

430 skyCorrList : `list` [`lsst.afw.math.backgroundList`], optional 

431 Sequence of background corrections to be subtracted if 

432 doApplySkyCorr=True. 

433 **kwargs 

434 Additional keyword arguments. 

435 

436 Returns 

437 ------- 

438 indices : `list` [`int`] 

439 Indices of ``calExpList`` and friends that have valid 

440 photoCalib/skyWcs. 

441 """ 

442 wcsList = len(calExpList)*[None] if wcsList is None else wcsList 

443 backgroundList = len(calExpList)*[None] if backgroundList is None else backgroundList 

444 skyCorrList = len(calExpList)*[None] if skyCorrList is None else skyCorrList 

445 

446 includeCalibVar = self.config.includeCalibVar 

447 

448 indices = [] 

449 for index, (calexp, background, skyCorr) in enumerate(zip(calExpList, 

450 backgroundList, 

451 skyCorrList)): 

452 if isinstance(calexp, DeferredDatasetHandle): 

453 calexp = calexp.get() 

454 

455 if not self.config.bgSubtracted: 

456 calexp.maskedImage += background.getImage() 

457 

458 detectorId = calexp.info.getDetector().getId() 

459 

460 # Load all calibrations from visitSummary. 

461 row = visitSummary.find(detectorId) 

462 if row is None: 

463 raise RuntimeError( 

464 f"Unexpectedly incomplete visitSummary: detector={detectorId} is missing." 

465 ) 

466 if (photoCalib := row.getPhotoCalib()) is not None: 

467 calexp.setPhotoCalib(photoCalib) 

468 else: 

469 self.log.warning( 

470 "Detector id %d for visit %d has None for photoCalib in the visitSummary and will " 

471 "not be used in the warp", detectorId, row["visit"], 

472 ) 

473 continue 

474 if (skyWcs := row.getWcs()) is not None: 

475 calexp.setWcs(skyWcs) 

476 wcsList[index] = skyWcs 

477 else: 

478 self.log.warning( 

479 "Detector id %d for visit %d has None for wcs in the visitSummary and will " 

480 "not be used in the warp", detectorId, row["visit"], 

481 ) 

482 continue 

483 if self.config.useVisitSummaryPsf: 

484 if (psf := row.getPsf()) is not None: 

485 calexp.setPsf(psf) 

486 else: 

487 self.log.warning( 

488 "Detector id %d for visit %d has None for psf in the visitSummary and will " 

489 "not be used in the warp", detectorId, row["visit"], 

490 ) 

491 continue 

492 if (apCorrMap := row.getApCorrMap()) is not None: 

493 calexp.info.setApCorrMap(apCorrMap) 

494 else: 

495 self.log.warning( 

496 "Detector id %d for visit %d has None for apCorrMap in the visitSummary and will " 

497 "not be used in the warp", detectorId, row["visit"], 

498 ) 

499 continue 

500 else: 

501 if calexp.getPsf() is None: 

502 self.log.warning( 

503 "Detector id %d for visit %d has None for psf for the calexp and will " 

504 "not be used in the warp", detectorId, row["visit"], 

505 ) 

506 continue 

507 if calexp.info.getApCorrMap() is None: 

508 self.log.warning( 

509 "Detector id %d for visit %d has None for apCorrMap in the calexp and will " 

510 "not be used in the warp", detectorId, row["visit"], 

511 ) 

512 continue 

513 

514 # Calibrate the image. 

515 calexp.maskedImage = photoCalib.calibrateImage(calexp.maskedImage, 

516 includeScaleUncertainty=includeCalibVar) 

517 calexp.maskedImage /= photoCalib.getCalibrationMean() 

518 # TODO: The images will have a calibration of 1.0 everywhere once 

519 # RFC-545 is implemented. 

520 # exposure.setCalib(afwImage.Calib(1.0)) 

521 

522 # Apply skycorr 

523 if self.config.doApplySkyCorr: 

524 calexp.maskedImage -= skyCorr.getImage() 

525 

526 indices.append(index) 

527 calExpList[index] = calexp 

528 

529 return indices 

530 

531 @staticmethod 

532 def _prepareEmptyExposure(skyInfo): 

533 """Produce an empty exposure for a given patch. 

534 

535 Parameters 

536 ---------- 

537 skyInfo : `lsst.pipe.base.Struct` 

538 Struct from `~lsst.pipe.base.coaddBase.makeSkyInfo()` with 

539 geometric information about the patch. 

540 

541 Returns 

542 ------- 

543 exp : `lsst.afw.image.exposure.ExposureF` 

544 An empty exposure for a given patch. 

545 """ 

546 exp = afwImage.ExposureF(skyInfo.bbox, skyInfo.wcs) 

547 exp.getMaskedImage().set(numpy.nan, afwImage.Mask 

548 .getPlaneBitMask("NO_DATA"), numpy.inf) 

549 return exp 

550 

551 def getWarpTypeList(self): 

552 """Return list of requested warp types per the config. 

553 """ 

554 warpTypeList = [] 

555 if self.config.makeDirect: 

556 warpTypeList.append("direct") 

557 if self.config.makePsfMatched: 

558 warpTypeList.append("psfMatched") 

559 return warpTypeList 

560 

561 

562def reorderRefs(inputRefs, outputSortKeyOrder, dataIdKey): 

563 """Reorder inputRefs per outputSortKeyOrder. 

564 

565 Any inputRefs which are lists will be resorted per specified key e.g., 

566 'detector.' Only iterables will be reordered, and values can be of type 

567 `lsst.pipe.base.connections.DeferredDatasetRef` or 

568 `lsst.daf.butler.core.datasets.ref.DatasetRef`. 

569 

570 Returned lists of refs have the same length as the outputSortKeyOrder. 

571 If an outputSortKey not in the inputRef, then it will be padded with None. 

572 If an inputRef contains an inputSortKey that is not in the 

573 outputSortKeyOrder it will be removed. 

574 

575 Parameters 

576 ---------- 

577 inputRefs : `lsst.pipe.base.connections.QuantizedConnection` 

578 Input references to be reordered and padded. 

579 outputSortKeyOrder : `iterable` 

580 Iterable of values to be compared with inputRef's dataId[dataIdKey]. 

581 dataIdKey : `str` 

582 The data ID key in the dataRefs to compare with the outputSortKeyOrder. 

583 

584 Returns 

585 ------- 

586 inputRefs : `lsst.pipe.base.connections.QuantizedConnection` 

587 Quantized Connection with sorted DatasetRef values sorted if iterable. 

588 """ 

589 for connectionName, refs in inputRefs: 

590 if isinstance(refs, Iterable): 

591 if hasattr(refs[0], "dataId"): 

592 inputSortKeyOrder = [ref.dataId[dataIdKey] for ref in refs] 

593 else: 

594 inputSortKeyOrder = [ref.datasetRef.dataId[dataIdKey] for ref in refs] 

595 if inputSortKeyOrder != outputSortKeyOrder: 

596 setattr(inputRefs, connectionName, 

597 reorderAndPadList(refs, inputSortKeyOrder, outputSortKeyOrder)) 

598 return inputRefs