Coverage for python / lsst / pipe / tasks / drpAssociationPipe.py: 21%

168 statements  

« prev     ^ index     » next       coverage.py v7.13.1, created at 2026-01-09 01:51 -0800

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"""Pipeline for running DiaSource association in a DRP context. 

23""" 

24 

25__all__ = ["DrpAssociationPipeTask", 

26 "DrpAssociationPipeConfig", 

27 "DrpAssociationPipeConnections"] 

28 

29import os 

30 

31import astropy.table as tb 

32import numpy as np 

33import pandas as pd 

34 

35 

36from lsst.pipe.tasks.ssoAssociation import SolarSystemAssociationTask 

37import lsst.geom as geom 

38import lsst.pex.config as pexConfig 

39import lsst.pipe.base as pipeBase 

40from lsst.meas.base import SkyMapIdGeneratorConfig 

41from lsst.skymap import BaseSkyMap 

42 

43from .coaddBase import makeSkyInfo 

44from .schemaUtils import convertDataFrameToSdmSchema, readSdmSchemaFile 

45from .simpleAssociation import SimpleAssociationTask 

46 

47 

48class DrpAssociationPipeConnections(pipeBase.PipelineTaskConnections, 

49 dimensions=("tract", "patch", "skymap"), 

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

51 "warpTypeSuffix": "", 

52 "fakesType": ""}): 

53 diaSourceTables = pipeBase.connectionTypes.Input( 

54 doc="Set of catalogs of calibrated DiaSources.", 

55 name="{fakesType}{coaddName}Diff_diaSrcTable", 

56 storageClass="ArrowAstropy", 

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

58 deferLoad=True, 

59 multiple=True 

60 ) 

61 ssObjectTableRefs = pipeBase.connectionTypes.Input( 

62 doc="Reference to catalogs of SolarSolarSystem objects expected to be " 

63 "observable in each (visit, detector).", 

64 name="preloaded_ss_object_visit", 

65 storageClass="ArrowAstropy", 

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

67 minimum=0, 

68 deferLoad=True, 

69 multiple=True 

70 ) 

71 skyMap = pipeBase.connectionTypes.Input( 

72 doc="Input definition of geometry/bbox and projection/wcs for coadded " 

73 "exposures", 

74 name=BaseSkyMap.SKYMAP_DATASET_TYPE_NAME, 

75 storageClass="SkyMap", 

76 dimensions=("skymap", ), 

77 ) 

78 finalVisitSummaryRefs = pipeBase.connectionTypes.Input( 

79 doc="Reference to finalVisitSummary of each exposure, containing " 

80 "visitInfo, bbox, and wcs.", 

81 name="finalVisitSummary", 

82 storageClass="ExposureCatalog", 

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

84 deferLoad=True, 

85 multiple=True 

86 ) 

87 assocDiaSourceTable = pipeBase.connectionTypes.Output( 

88 doc="Catalog of DiaSources covering the patch and associated with a " 

89 "DiaObject.", 

90 name="{fakesType}{coaddName}Diff_assocDiaSrcTable", 

91 storageClass="DataFrame", 

92 dimensions=("tract", "patch"), 

93 ) 

94 associatedSsSources = pipeBase.connectionTypes.Output( 

95 doc="Optional output storing ssSource data computed during association.", 

96 name="{fakesType}{coaddName}Diff_assocSsSrcTable", 

97 storageClass="ArrowAstropy", 

98 dimensions=("tract", "patch"), 

99 ) 

100 unassociatedSsObjects = pipeBase.connectionTypes.Output( 

101 doc="Expected locations of ssObjects with no associated source.", 

102 name="{fakesType}{coaddName}Diff_unassocSsObjTable", 

103 storageClass="ArrowAstropy", 

104 dimensions=("tract", "patch"), 

105 ) 

106 diaObjectTable = pipeBase.connectionTypes.Output( 

107 doc="Catalog of DiaObjects created from spatially associating " 

108 "DiaSources.", 

109 name="{fakesType}{coaddName}Diff_diaObjTable", 

110 storageClass="DataFrame", 

111 dimensions=("tract", "patch"), 

112 ) 

113 

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

115 super().__init__(config=config) 

116 

117 if not config.doSolarSystemAssociation: 

118 del self.ssObjectTableRefs 

119 del self.associatedSsSources 

120 del self.unassociatedSsObjects 

121 del self.finalVisitSummaryRefs 

122 

123 

124class DrpAssociationPipeConfig( 

125 pipeBase.PipelineTaskConfig, 

126 pipelineConnections=DrpAssociationPipeConnections): 

127 associator = pexConfig.ConfigurableField( 

128 target=SimpleAssociationTask, 

129 doc="Task used to associate DiaSources with DiaObjects.", 

130 ) 

131 solarSystemAssociator = pexConfig.ConfigurableField( 

132 target=SolarSystemAssociationTask, 

133 doc="Task used to associate DiaSources with SolarSystemObjects.", 

134 ) 

135 doAddDiaObjectCoords = pexConfig.Field( 

136 dtype=bool, 

137 default=True, 

138 doc="Do pull diaObject's average coordinate as coord_ra and coord_dec" 

139 "Duplicates information, but needed for bulk ingest into qserv." 

140 ) 

141 doWriteEmptyTables = pexConfig.Field( 

142 dtype=bool, 

143 default=False, 

144 doc="If True, construct and write out empty diaSource and diaObject " 

145 "tables. If False, raise NoWorkFound" 

146 ) 

147 doSolarSystemAssociation = pexConfig.Field( 

148 dtype=bool, 

149 default=True, 

150 doc="Process SolarSystem objects through the pipeline.", 

151 ) 

152 doUseSchema = pexConfig.Field( 

153 dtype=bool, 

154 default=False, 

155 doc="Use an existing schema to coerce the data types of the output columns." 

156 ) 

157 schemaDir = pexConfig.Field( 

158 dtype=str, 

159 doc="Path to the directory containing schema definitions.", 

160 default=os.path.join("${SDM_SCHEMAS_DIR}", 

161 "yml"), 

162 ) 

163 schemaFile = pexConfig.Field( 

164 dtype=str, 

165 doc="Yaml file specifying the schema of the output catalog.", 

166 default="lsstcam.yaml", 

167 ) 

168 idGenerator = SkyMapIdGeneratorConfig.make_field() 

169 

170 

171class DrpAssociationPipeTask(pipeBase.PipelineTask): 

172 """Driver pipeline for loading DiaSource catalogs in a patch/tract 

173 region and associating them. 

174 """ 

175 ConfigClass = DrpAssociationPipeConfig 

176 _DefaultName = "drpAssociation" 

177 

178 def __init__(self, **kwargs): 

179 super().__init__(**kwargs) 

180 self.makeSubtask('associator') 

181 

182 if self.config.doUseSchema: 

183 schemaFile = os.path.join(self.config.schemaDir, self.config.schemaFile) 

184 self.schema = readSdmSchemaFile(schemaFile) 

185 else: 

186 self.schema = None 

187 if self.config.doSolarSystemAssociation: 

188 self.makeSubtask("solarSystemAssociator") 

189 

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

191 inputs = butlerQC.get(inputRefs) 

192 

193 inputs["tractId"] = butlerQC.quantum.dataId["tract"] 

194 inputs["patchId"] = butlerQC.quantum.dataId["patch"] 

195 inputs["idGenerator"] = self.config.idGenerator.apply(butlerQC.quantum.dataId) 

196 if not self.config.doSolarSystemAssociation: 

197 inputs["ssObjectTableRefs"] = [] 

198 inputs["finalVisitSummaryRefs"] = [] 

199 outputs = self.run(**inputs) 

200 butlerQC.put(outputs, outputRefs) 

201 

202 def run(self, 

203 diaSourceTables, 

204 ssObjectTableRefs, 

205 skyMap, 

206 finalVisitSummaryRefs, 

207 tractId, 

208 patchId, 

209 idGenerator=None): 

210 """Trim DiaSources to the current Patch and run association. 

211 

212 Takes in the set of DiaSource catalogs that covers the current patch, 

213 trims them to the dimensions of the patch, and [TODO: eventually] 

214 runs association on the concatenated DiaSource Catalog. 

215 

216 Parameters 

217 ---------- 

218 diaSourceTables : `list` of `lsst.daf.butler.DeferredDatasetHandle` 

219 Set of DiaSource catalogs potentially covering this patch/tract. 

220 ssObjectTableRefs : `list` of `lsst.daf.butler.DeferredDatasetHandle` 

221 Set of known SSO ephemerides potentially covering this patch/tract. 

222 skyMap : `lsst.skymap.BaseSkyMap` 

223 SkyMap defining the patch/tract 

224 finalVisitSummaryRefs : `list` of `lsst.daf.butler.DeferredDatasetHandle` 

225 Reference to finalVisitSummary of each exposure potentially 

226 covering this patch/tract, which contain visitInfo, bbox, and wcs 

227 tractId : `int` 

228 Id of current tract being processed. 

229 patchId : `int` 

230 Id of current patch being processed. 

231 idGenerator : `lsst.meas.base.IdGenerator`, optional 

232 Object that generates Object IDs and random number generator seeds. 

233 

234 Returns 

235 ------- 

236 output : `lsst.pipe.base.Struct` 

237 Results struct with attributes: 

238 

239 ``assocDiaSourceTable`` 

240 Table of DiaSources with updated value for diaObjectId. 

241 (`pandas.DataFrame`) 

242 ``diaObjectTable`` 

243 Table of DiaObjects from matching DiaSources 

244 (`pandas.DataFrame`). 

245 """ 

246 self.log.info("Running DPR Association on patch %i, tract %i...", 

247 patchId, tractId) 

248 

249 skyInfo = makeSkyInfo(skyMap, tractId, patchId) 

250 

251 # Get the patch bounding box. 

252 innerPatchBox = geom.Box2D(skyInfo.patchInfo.getInnerBBox()) 

253 outerPatchBox = geom.Box2D(skyInfo.patchInfo.getOuterBBox()) 

254 innerTractSkyRegion = skyInfo.tractInfo.getInnerSkyRegion() 

255 

256 # Keep track of our diaCats, ssObject cats, and finalVisitSummaries by their (visit, detector) IDs 

257 diaIdDict = prepareCatalogDict(diaSourceTables, useVisitDetector=True) 

258 ssObjectIdDict = prepareCatalogDict(ssObjectTableRefs, useVisitDetector=False) 

259 finalVisitSummaryIdDict = prepareCatalogDict(finalVisitSummaryRefs, useVisitDetector=False) 

260 

261 diaSourceHistory, ssSourceHistory, unassociatedSsObjectHistory = [], [], [] 

262 nSsSrc, nSsObj = 0, 0 

263 visits = set([v for v, _ in diaIdDict.keys()]) 

264 for visit in visits: 

265 # visit summaries and Solar System catalogs are per-visit, so only 

266 # load them once for all detectors with that visit 

267 visitSummary = finalVisitSummaryIdDict[visit].get() if visit in finalVisitSummaryIdDict else None 

268 ssCat = ssObjectIdDict[visit].get() if visit in ssObjectIdDict else None 

269 detectors = [det for (v, det) in diaIdDict.keys() if v == visit] 

270 for detector in detectors: 

271 diaCat = diaIdDict[(visit, detector)].get() 

272 nDiaSrcIn = len(diaCat) 

273 if (ssCat is not None) and (visitSummary is not None): 

274 ssoAssocResult = self.runSolarSystemAssociation(diaCat, 

275 ssCat.copy(), 

276 visitSummary=visitSummary, 

277 patchBbox=innerPatchBox, 

278 patchWcs=skyInfo.wcs, 

279 innerTractSkyRegion=innerTractSkyRegion, 

280 detector=detector, 

281 visit=visit, 

282 ) 

283 

284 nSsSrc = len(ssoAssocResult.associatedSsSources) 

285 nSsObj = len(ssoAssocResult.unassociatedSsObjects) 

286 # If diaSources were associated with Solar System objects, 

287 # remove them from the catalog so they won't create new 

288 # diaObjects or be associated with other diaObjects. 

289 diaCat = ssoAssocResult.unassociatedDiaSources 

290 else: 

291 nSsSrc, nSsObj = 0, 0 

292 

293 # Only trim diaSources to the outer bbox of the patch, so that 

294 # diaSources near the patch boundary can be associated. 

295 # DiaObjects will be trimmed to the inner patch bbox, and any 

296 # diaSources associated with dropped diaObjects will also be dropped 

297 diaInPatch = self._trimToPatch(diaCat.to_pandas(), outerPatchBox, skyInfo.wcs) 

298 

299 nDiaSrc = diaInPatch.sum() 

300 

301 self.log.info( 

302 "Read DiaSource catalog of length %i from visit %i, " 

303 "detector %i. Found %i sources within the patch/tract " 

304 "footprint, including %i associated with SSOs.", 

305 nDiaSrcIn, visit, detector, nDiaSrc + nSsSrc, nSsSrc) 

306 

307 if nDiaSrc > 0: 

308 diaSourceHistory.append(diaCat[diaInPatch]) 

309 if nSsSrc > 0: 

310 ssSourceHistory.append(ssoAssocResult.associatedSsSources) 

311 if nSsObj > 0: 

312 unassociatedSsObjectHistory.append(ssoAssocResult.unassociatedSsObjects) 

313 

314 # After looping over all of the detector-level catalogs that overlap the 

315 # patch, combine them into patch-level catalogs 

316 diaSourceHistoryCat = self._stackCatalogs(diaSourceHistory) 

317 if self.config.doSolarSystemAssociation: 

318 ssSourceHistoryCat = self._stackCatalogs(ssSourceHistory, remove_columns=['ra', 'dec']) 

319 nSsSrcTotal = len(ssSourceHistoryCat) if ssSourceHistoryCat else 0 

320 unassociatedSsObjectHistoryCat = self._stackCatalogs(unassociatedSsObjectHistory) 

321 nSsObjTotal = len(unassociatedSsObjectHistoryCat) if unassociatedSsObjectHistoryCat else 0 

322 self.log.info("Found %i ssSources and %i missing ssObjects in patch %i, tract %i", 

323 nSsSrcTotal, nSsObjTotal, patchId, tractId) 

324 else: 

325 ssSourceHistoryCat = None 

326 unassociatedSsObjectHistoryCat = None 

327 

328 if (not diaSourceHistory) and (not ssSourceHistory): 

329 if not self.config.doWriteEmptyTables: 

330 raise pipeBase.NoWorkFound("Found no overlapping DIASources to associate.") 

331 

332 self.log.info("Found %i DiaSources overlapping patch %i, tract %i", 

333 len(diaSourceHistoryCat), patchId, tractId) 

334 

335 diaSourceTable = diaSourceHistoryCat.to_pandas() 

336 diaSourceTable.set_index("diaSourceId", drop=False) 

337 # Now run diaObject association on the stacked remaining diaSources 

338 assocResult = self.associator.run(diaSourceTable, idGenerator=idGenerator) 

339 

340 # Drop any diaObjects that were created outside the inner region of the 

341 # patch. These will be associated in the overlapping patch instead. 

342 objectsInTractPatch = self._trimToPatch(assocResult.diaObjects, 

343 innerPatchBox, 

344 skyInfo.wcs, 

345 innerTractSkyRegion=innerTractSkyRegion) 

346 diaObjects = assocResult.diaObjects[objectsInTractPatch] 

347 # Instead of dropping diaSources based on their patch, assign them to a 

348 # patch based on whether their diaObject was inside. This means that 

349 # some diaSources in the patch catalog will actually have coordinates 

350 # just outside the patch. 

351 assocDiaSources = self.dropDiaSourceByDiaObjectId(assocResult.diaObjects[~objectsInTractPatch].index, 

352 assocResult.assocDiaSources) 

353 

354 self.log.info("Associated DiaSources into %i DiaObjects", len(diaObjects)) 

355 

356 if self.config.doAddDiaObjectCoords: 

357 assocDiaSources = self._addDiaObjectCoords(diaObjects, assocDiaSources) 

358 if self.config.doUseSchema: 

359 diaObjects = convertDataFrameToSdmSchema(self.schema, diaObjects, tableName="DiaObject") 

360 assocDiaSources = convertDataFrameToSdmSchema(self.schema, assocDiaSources, tableName="DiaSource") 

361 

362 return pipeBase.Struct( 

363 diaObjectTable=diaObjects, 

364 assocDiaSourceTable=assocDiaSources, 

365 associatedSsSources=ssSourceHistoryCat, 

366 unassociatedSsObjects=unassociatedSsObjectHistoryCat, 

367 ) 

368 

369 def _stackCatalogs(self, catalogs, remove_columns=None, empty=None): 

370 """Stack a list of catalogs. 

371 

372 Parameters 

373 ---------- 

374 catalogs : `list` of `astropy.table.Table` 

375 Input catalogs with the same columns to be combined. 

376 remove_columns : `list` of `str` or None, optional 

377 List of column names to drop from the tables before stacking. 

378 

379 Returns 

380 ------- 

381 `astropy.table.Table` 

382 The combined catalog. 

383 """ 

384 if catalogs: 

385 sourceHistory = tb.vstack(catalogs) 

386 if remove_columns is not None: 

387 sourceHistory.remove_columns(remove_columns) 

388 return sourceHistory 

389 else: 

390 return empty 

391 

392 def runSolarSystemAssociation(self, diaCat, ssCat, 

393 visitSummary, 

394 patchBbox, 

395 patchWcs, 

396 innerTractSkyRegion, 

397 detector, 

398 visit, 

399 ): 

400 """Run Solar System object association and filter the results. 

401 

402 Parameters 

403 ---------- 

404 diaCat : `pandas.DataFrame` 

405 Catalog of detected diaSources on the image difference. 

406 ssCat : `astropy.table.Table` 

407 Catalog of predicted coordinates of known Solar System objects. 

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

409 Table of calibration and metadata for all detectors in a visit. 

410 patchBbox : `lsst.geom.Box2D` 

411 Bounding box of the patch. 

412 patchWcs : `lsst.geom.SkyWcs` 

413 Wcs of the tract containing the patch. 

414 innerTractSkyRegion : `lsst.sphgeom.Box` 

415 Region defining the inner non-overlapping part of a tract. 

416 detector : `int` 

417 Detector number of the science exposure. 

418 visit : `int` 

419 Visit number of the science exposure. 

420 

421 Returns 

422 ------- 

423 ssoAssocResult : `lsst.pipe.base.Struct` 

424 Results struct with attributes: 

425 

426 ``associatedSsSources`` 

427 Table of DiaSources associated with Solar System objects. 

428 (`astropy.table.Table`) 

429 ``associatedSsDiaSources`` 

430 Table of Solar System objects associated with DiaSources. 

431 (`astropy.table.Table`). 

432 ``unassociatedSsObjects`` 

433 Table of Solar System objects in the patch not associated with 

434 any DiaSource (`astropy.table.Table`). 

435 ``unassociatedDiaSources`` 

436 Table of DiaSources not associated with any Solar System object 

437 (`astropy.table.Table`). 

438 """ 

439 # Get the exposure metadata from the detector's row in the visitSummary table. 

440 ssoAssocResult = self.solarSystemAssociator.run( 

441 diaCat, 

442 ssCat, 

443 visitInfo=visitSummary.find(detector).visitInfo, 

444 bbox=visitSummary.find(detector).getBBox(), 

445 wcs=visitSummary.find(detector).wcs, 

446 ) 

447 

448 ssInTractPatch = self._trimToPatch(ssoAssocResult.associatedSsSources.to_pandas(), 

449 patchBbox, 

450 patchWcs, 

451 innerTractSkyRegion=innerTractSkyRegion) 

452 associatedSsSources = ssoAssocResult.associatedSsSources[ssInTractPatch].copy() 

453 assocDiaSrcIds = set(associatedSsSources['diaSourceId']) 

454 diaSrcMask = [diaId in assocDiaSrcIds for diaId in ssoAssocResult.ssoAssocDiaSources['diaSourceId']] 

455 associatedSsDiaSources = ssoAssocResult.ssoAssocDiaSources[np.array(diaSrcMask)] 

456 

457 ssObjInTractPatch = self._trimToPatch(ssoAssocResult.unassociatedSsObjects.to_pandas(), 

458 patchBbox, 

459 patchWcs, 

460 innerTractSkyRegion=innerTractSkyRegion) 

461 unassociatedSsObjects = ssoAssocResult.unassociatedSsObjects[ssObjInTractPatch].copy() 

462 # Update the table of Solar System objects that were not found with the 

463 # visit and detector where they were predicted. 

464 if len(unassociatedSsObjects) > 0: 

465 unassociatedSsObjects['visit'] = visit 

466 unassociatedSsObjects['detector'] = detector 

467 

468 return pipeBase.Struct( 

469 associatedSsSources=associatedSsSources, 

470 associatedSsDiaSources=associatedSsDiaSources, 

471 unassociatedSsObjects=unassociatedSsObjects, 

472 unassociatedDiaSources=ssoAssocResult.unAssocDiaSources 

473 ) 

474 

475 def _addDiaObjectCoords(self, objects, sources): 

476 obj = objects[['ra', 'dec']].rename(columns={"ra": "coord_ra", "dec": "coord_dec"}) 

477 df = pd.merge(sources.reset_index(), obj, left_on='diaObjectId', right_index=True, 

478 how='inner').set_index('diaSourceId') 

479 return df 

480 

481 def _trimToPatch(self, cat, patchBox, wcs, innerTractSkyRegion=None): 

482 """Create generator testing if a set of DiaSources are in the 

483 patch/tract. 

484 

485 Parameters 

486 ---------- 

487 cat : `pandas.DataFrame` 

488 Catalog of DiaSources to test within patch/tract. 

489 patchBox : `lsst.geom.Box2D` 

490 Bounding box of the patch. 

491 wcs : `lsst.geom.SkyWcs` 

492 Wcs of the tract. 

493 innerTractSkyRegion : `lsst.sphgeom.Box`, optional 

494 Region defining the inner non-overlapping part of a tract. 

495 

496 Returns 

497 ------- 

498 isInPatch : `numpy.ndarray`, (N,) 

499 Booleans representing if the DiaSources are contained within the 

500 current patch and tract. 

501 """ 

502 isInPatch = np.zeros(len(cat), dtype=bool) 

503 

504 for idx, row in cat.reset_index().iterrows(): 

505 spPoint = geom.SpherePoint(row["ra"], row["dec"], geom.degrees) 

506 pxCoord = wcs.skyToPixel(spPoint) 

507 ra_rad = np.deg2rad(row["ra"]) 

508 dec_rad = np.deg2rad(row["dec"]) 

509 isInPatch[idx] = patchBox.contains(pxCoord) 

510 

511 if innerTractSkyRegion is not None: 

512 isInPatch[idx] &= innerTractSkyRegion.contains(ra_rad, dec_rad) 

513 

514 return isInPatch 

515 

516 def dropDiaSourceByDiaObjectId(self, droppedDiaObjectIds, diaSources): 

517 """Drop diaSources with diaObject IDs in the supplied list. 

518 

519 Parameters 

520 ---------- 

521 droppedDiaObjectIds : `pandas.DataFrame` 

522 DiaObjectIds to match and drop from the list of diaSources. 

523 diaSources : `pandas.DataFrame` 

524 Catalog of diaSources to check and filter. 

525 

526 Returns 

527 ------- 

528 filteredDiaSources : `pandas.DataFrame` 

529 The input diaSources with any rows matching the listed diaObjectIds 

530 removed. 

531 """ 

532 toDrop = diaSources['diaObjectId'].isin(droppedDiaObjectIds) 

533 

534 # Keep only rows that do NOT match 

535 return diaSources.loc[~toDrop].copy() 

536 

537 

538def prepareCatalogDict(dataRefList, useVisitDetector=True): 

539 """Prepare lookup tables of the data references. 

540 

541 Parameters 

542 ---------- 

543 dataRefList : `list` of `lsst.daf.butler.DeferredDatasetHandle` 

544 The data references to make a lookup table for. 

545 useVisitDetector : `bool`, optional 

546 Use both visit and detector in the dict key? If False, use only visit. 

547 

548 Returns 

549 ------- 

550 `dict` of `lsst.daf.butler.DeferredDatasetHandle` 

551 Lookup table of the data references by visit (and optionally detector) 

552 """ 

553 dataDict = {} 

554 

555 if useVisitDetector: 

556 for dataRef in dataRefList: 

557 dataDict[(dataRef.dataId["visit"], dataRef.dataId["detector"])] = dataRef 

558 else: 

559 for dataRef in dataRefList: 

560 dataDict[dataRef.dataId["visit"]] = dataRef 

561 return dataDict