Coverage for python/lsst/ap/association/diaPipe.py: 24%

179 statements  

« prev     ^ index     » next       coverage.py v7.4.0, created at 2024-01-03 12:06 +0000

1# 

2# LSST Data Management System 

3# Copyright 2008-2016 AURA/LSST. 

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 <https://www.lsstcorp.org/LegalNotices/>. 

21# 

22 

23"""PipelineTask for associating DiaSources with previous DiaObjects. 

24 

25Additionally performs forced photometry on the calibrated and difference 

26images at the updated locations of DiaObjects. 

27 

28Currently loads directly from the Apdb rather than pre-loading. 

29""" 

30 

31__all__ = ("DiaPipelineConfig", 

32 "DiaPipelineTask", 

33 "DiaPipelineConnections") 

34 

35import numpy as np 

36import pandas as pd 

37 

38from lsst.daf.base import DateTime 

39import lsst.dax.apdb as daxApdb 

40from lsst.meas.base import DetectorVisitIdGeneratorConfig, DiaObjectCalculationTask 

41import lsst.pex.config as pexConfig 

42import lsst.pipe.base as pipeBase 

43import lsst.pipe.base.connectionTypes as connTypes 

44from lsst.utils.timer import timeMethod 

45 

46from lsst.ap.association import ( 

47 AssociationTask, 

48 DiaForcedSourceTask, 

49 LoadDiaCatalogsTask, 

50 PackageAlertsTask) 

51from lsst.ap.association.ssoAssociation import SolarSystemAssociationTask 

52 

53 

54class DiaPipelineConnections( 

55 pipeBase.PipelineTaskConnections, 

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

57 defaultTemplates={"coaddName": "deep", "fakesType": ""}): 

58 """Butler connections for DiaPipelineTask. 

59 """ 

60 diaSourceTable = connTypes.Input( 

61 doc="Catalog of calibrated DiaSources.", 

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

63 storageClass="DataFrame", 

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

65 ) 

66 solarSystemObjectTable = connTypes.Input( 

67 doc="Catalog of SolarSolarSystem objects expected to be observable in " 

68 "this detectorVisit.", 

69 name="visitSsObjects", 

70 storageClass="DataFrame", 

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

72 ) 

73 diffIm = connTypes.Input( 

74 doc="Difference image on which the DiaSources were detected.", 

75 name="{fakesType}{coaddName}Diff_differenceExp", 

76 storageClass="ExposureF", 

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

78 ) 

79 exposure = connTypes.Input( 

80 doc="Calibrated exposure differenced with a template image during " 

81 "image differencing.", 

82 name="{fakesType}calexp", 

83 storageClass="ExposureF", 

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

85 ) 

86 template = connTypes.Input( 

87 doc="Warped template used to create `subtractedExposure`. Not PSF " 

88 "matched.", 

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

90 storageClass="ExposureF", 

91 name="{fakesType}{coaddName}Diff_templateExp", 

92 ) 

93 apdbMarker = connTypes.Output( 

94 doc="Marker dataset storing the configuration of the Apdb for each " 

95 "visit/detector. Used to signal the completion of the pipeline.", 

96 name="apdb_marker", 

97 storageClass="Config", 

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

99 ) 

100 associatedDiaSources = connTypes.Output( 

101 doc="Optional output storing the DiaSource catalog after matching, " 

102 "calibration, and standardization for insertion into the Apdb.", 

103 name="{fakesType}{coaddName}Diff_assocDiaSrc", 

104 storageClass="DataFrame", 

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

106 ) 

107 diaForcedSources = connTypes.Output( 

108 doc="Optional output storing the forced sources computed at the diaObject positions.", 

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

110 storageClass="DataFrame", 

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

112 ) 

113 diaObjects = connTypes.Output( 

114 doc="Optional output storing the updated diaObjects associated to these sources.", 

115 name="{fakesType}{coaddName}Diff_diaObject", 

116 storageClass="DataFrame", 

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

118 ) 

119 longTrailedSources = pipeBase.connectionTypes.Output( 

120 doc="Optional output temporarily storing long trailed diaSources.", 

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

122 storageClass="DataFrame", 

123 name="{fakesType}{coaddName}Diff_longTrailedSrc", 

124 ) 

125 

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

127 super().__init__(config=config) 

128 

129 if not config.doWriteAssociatedSources: 

130 self.outputs.remove("associatedDiaSources") 

131 self.outputs.remove("diaForcedSources") 

132 self.outputs.remove("diaObjects") 

133 if not config.doSolarSystemAssociation: 

134 self.inputs.remove("solarSystemObjectTable") 

135 if not config.associator.doTrailedSourceFilter: 

136 self.outputs.remove("longTrailedSources") 

137 

138 def adjustQuantum(self, inputs, outputs, label, dataId): 

139 """Override to make adjustments to `lsst.daf.butler.DatasetRef` objects 

140 in the `lsst.daf.butler.core.Quantum` during the graph generation stage 

141 of the activator. 

142 

143 This implementation checks to make sure that the filters in the dataset 

144 are compatible with AP processing as set by the Apdb/DPDD schema. 

145 

146 Parameters 

147 ---------- 

148 inputs : `dict` 

149 Dictionary whose keys are an input (regular or prerequisite) 

150 connection name and whose values are a tuple of the connection 

151 instance and a collection of associated `DatasetRef` objects. 

152 The exact type of the nested collections is unspecified; it can be 

153 assumed to be multi-pass iterable and support `len` and ``in``, but 

154 it should not be mutated in place. In contrast, the outer 

155 dictionaries are guaranteed to be temporary copies that are true 

156 `dict` instances, and hence may be modified and even returned; this 

157 is especially useful for delegating to `super` (see notes below). 

158 outputs : `dict` 

159 Dict of output datasets, with the same structure as ``inputs``. 

160 label : `str` 

161 Label for this task in the pipeline (should be used in all 

162 diagnostic messages). 

163 data_id : `lsst.daf.butler.DataCoordinate` 

164 Data ID for this quantum in the pipeline (should be used in all 

165 diagnostic messages). 

166 

167 Returns 

168 ------- 

169 adjusted_inputs : `dict` 

170 Dict of the same form as ``inputs`` with updated containers of 

171 input `DatasetRef` objects. Connections that are not changed 

172 should not be returned at all. Datasets may only be removed, not 

173 added. Nested collections may be of any multi-pass iterable type, 

174 and the order of iteration will set the order of iteration within 

175 `PipelineTask.runQuantum`. 

176 adjusted_outputs : `dict` 

177 Dict of updated output datasets, with the same structure and 

178 interpretation as ``adjusted_inputs``. 

179 

180 Raises 

181 ------ 

182 ScalarError 

183 Raised if any `Input` or `PrerequisiteInput` connection has 

184 ``multiple`` set to `False`, but multiple datasets. 

185 NoWorkFound 

186 Raised to indicate that this quantum should not be run; not enough 

187 datasets were found for a regular `Input` connection, and the 

188 quantum should be pruned or skipped. 

189 FileNotFoundError 

190 Raised to cause QuantumGraph generation to fail (with the message 

191 included in this exception); not enough datasets were found for a 

192 `PrerequisiteInput` connection. 

193 """ 

194 _, refs = inputs["diffIm"] 

195 for ref in refs: 

196 if ref.dataId["band"] not in self.config.validBands: 

197 raise ValueError( 

198 f"Requested '{ref.dataId['band']}' not in " 

199 "DiaPipelineConfig.validBands. To process bands not in " 

200 "the standard Rubin set (ugrizy) you must add the band to " 

201 "the validBands list in DiaPipelineConfig and add the " 

202 "appropriate columns to the Apdb schema.") 

203 return super().adjustQuantum(inputs, outputs, label, dataId) 

204 

205 

206class DiaPipelineConfig(pipeBase.PipelineTaskConfig, 

207 pipelineConnections=DiaPipelineConnections): 

208 """Config for DiaPipelineTask. 

209 """ 

210 coaddName = pexConfig.Field( 

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

212 dtype=str, 

213 default="deep", 

214 ) 

215 apdb = daxApdb.ApdbSql.makeField( 

216 doc="Database connection for storing associated DiaSources and " 

217 "DiaObjects. Must already be initialized.", 

218 ) 

219 validBands = pexConfig.ListField( 

220 dtype=str, 

221 default=["u", "g", "r", "i", "z", "y"], 

222 doc="List of bands that are valid for AP processing. To process a " 

223 "band not on this list, the appropriate band specific columns " 

224 "must be added to the Apdb schema in dax_apdb.", 

225 ) 

226 diaCatalogLoader = pexConfig.ConfigurableField( 

227 target=LoadDiaCatalogsTask, 

228 doc="Task to load DiaObjects and DiaSources from the Apdb.", 

229 ) 

230 associator = pexConfig.ConfigurableField( 

231 target=AssociationTask, 

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

233 ) 

234 doSolarSystemAssociation = pexConfig.Field( 

235 dtype=bool, 

236 default=False, 

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

238 ) 

239 solarSystemAssociator = pexConfig.ConfigurableField( 

240 target=SolarSystemAssociationTask, 

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

242 ) 

243 diaCalculation = pexConfig.ConfigurableField( 

244 target=DiaObjectCalculationTask, 

245 doc="Task to compute summary statistics for DiaObjects.", 

246 ) 

247 diaForcedSource = pexConfig.ConfigurableField( 

248 target=DiaForcedSourceTask, 

249 doc="Task used for force photometer DiaObject locations in direct and " 

250 "difference images.", 

251 ) 

252 alertPackager = pexConfig.ConfigurableField( 

253 target=PackageAlertsTask, 

254 doc="Subtask for packaging Ap data into alerts.", 

255 ) 

256 doPackageAlerts = pexConfig.Field( 

257 dtype=bool, 

258 default=False, 

259 doc="Package Dia-data into serialized alerts for distribution and " 

260 "write them to disk.", 

261 ) 

262 doWriteAssociatedSources = pexConfig.Field( 

263 dtype=bool, 

264 default=False, 

265 doc="Write out associated DiaSources, DiaForcedSources, and DiaObjects, " 

266 "formatted following the Science Data Model.", 

267 ) 

268 imagePixelMargin = pexConfig.RangeField( 

269 dtype=int, 

270 default=10, 

271 min=0, 

272 doc="Pad the image by this many pixels before removing off-image " 

273 "diaObjects for association.", 

274 ) 

275 idGenerator = DetectorVisitIdGeneratorConfig.make_field() 

276 

277 def setDefaults(self): 

278 self.apdb.dia_object_index = "baseline" 

279 self.apdb.dia_object_columns = [] 

280 self.diaCalculation.plugins = ["ap_meanPosition", 

281 "ap_nDiaSources", 

282 "ap_diaObjectFlag", 

283 "ap_meanFlux", 

284 "ap_percentileFlux", 

285 "ap_sigmaFlux", 

286 "ap_chi2Flux", 

287 "ap_madFlux", 

288 "ap_skewFlux", 

289 "ap_minMaxFlux", 

290 "ap_maxSlopeFlux", 

291 "ap_meanErrFlux", 

292 "ap_linearFit", 

293 "ap_stetsonJ", 

294 "ap_meanTotFlux", 

295 "ap_sigmaTotFlux"] 

296 

297 

298class DiaPipelineTask(pipeBase.PipelineTask): 

299 """Task for loading, associating and storing Difference Image Analysis 

300 (DIA) Objects and Sources. 

301 """ 

302 ConfigClass = DiaPipelineConfig 

303 _DefaultName = "diaPipe" 

304 

305 def __init__(self, initInputs=None, **kwargs): 

306 super().__init__(**kwargs) 

307 self.apdb = self.config.apdb.apply() 

308 self.makeSubtask("diaCatalogLoader") 

309 self.makeSubtask("associator") 

310 self.makeSubtask("diaCalculation") 

311 self.makeSubtask("diaForcedSource") 

312 if self.config.doPackageAlerts: 

313 self.makeSubtask("alertPackager") 

314 if self.config.doSolarSystemAssociation: 

315 self.makeSubtask("solarSystemAssociator") 

316 

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

318 inputs = butlerQC.get(inputRefs) 

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

320 # Need to set ccdExposureIdBits (now deprecated) to None and pass it, 

321 # since there are non-optional positional arguments after it. 

322 inputs["ccdExposureIdBits"] = None 

323 inputs["band"] = butlerQC.quantum.dataId["band"] 

324 if not self.config.doSolarSystemAssociation: 

325 inputs["solarSystemObjectTable"] = None 

326 

327 outputs = self.run(**inputs) 

328 

329 butlerQC.put(outputs, outputRefs) 

330 

331 @timeMethod 

332 def run(self, 

333 diaSourceTable, 

334 solarSystemObjectTable, 

335 diffIm, 

336 exposure, 

337 template, 

338 ccdExposureIdBits, # TODO: remove on DM-38687. 

339 band, 

340 idGenerator=None): 

341 """Process DiaSources and DiaObjects. 

342 

343 Load previous DiaObjects and their DiaSource history. Calibrate the 

344 values in the diaSourceCat. Associate new DiaSources with previous 

345 DiaObjects. Run forced photometry at the updated DiaObject locations. 

346 Store the results in the Alert Production Database (Apdb). 

347 

348 Parameters 

349 ---------- 

350 diaSourceTable : `pandas.DataFrame` 

351 Newly detected DiaSources. 

352 diffIm : `lsst.afw.image.ExposureF` 

353 Difference image exposure in which the sources in ``diaSourceCat`` 

354 were detected. 

355 exposure : `lsst.afw.image.ExposureF` 

356 Calibrated exposure differenced with a template to create 

357 ``diffIm``. 

358 template : `lsst.afw.image.ExposureF` 

359 Template exposure used to create diffIm. 

360 ccdExposureIdBits : `int` 

361 Number of bits used for a unique ``ccdVisitId``. Deprecated in 

362 favor of ``idGenerator``, and ignored if that is present (will be 

363 removed after v26). Pass `None` explicitly to avoid a deprecation 

364 warning (a default is impossible given that later positional 

365 arguments are not defaulted). 

366 band : `str` 

367 The band in which the new DiaSources were detected. 

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

369 Object that generates source IDs and random number generator seeds. 

370 Will be required after ``ccdExposureIdBits`` is removed. 

371 

372 Returns 

373 ------- 

374 results : `lsst.pipe.base.Struct` 

375 Results struct with components. 

376 

377 - ``apdbMaker`` : Marker dataset to store in the Butler indicating 

378 that this ccdVisit has completed successfully. 

379 (`lsst.dax.apdb.ApdbConfig`) 

380 - ``associatedDiaSources`` : Catalog of newly associated 

381 DiaSources. (`pandas.DataFrame`) 

382 """ 

383 # Load the DiaObjects and DiaSource history. 

384 loaderResult = self.diaCatalogLoader.run(diffIm, self.apdb) 

385 if len(loaderResult.diaObjects) > 0: 

386 diaObjects = self.purgeDiaObjects(diffIm.getBBox(), diffIm.getWcs(), loaderResult.diaObjects, 

387 buffer=self.config.imagePixelMargin) 

388 else: 

389 diaObjects = loaderResult.diaObjects 

390 

391 # Associate new DiaSources with existing DiaObjects. 

392 assocResults = self.associator.run(diaSourceTable, diaObjects, 

393 exposure_time=diffIm.visitInfo.exposureTime) 

394 

395 if self.config.doSolarSystemAssociation: 

396 ssoAssocResult = self.solarSystemAssociator.run( 

397 assocResults.unAssocDiaSources, 

398 solarSystemObjectTable, 

399 diffIm) 

400 createResults = self.createNewDiaObjects( 

401 ssoAssocResult.unAssocDiaSources) 

402 toAssociate = [] 

403 if len(assocResults.matchedDiaSources) > 0: 

404 toAssociate.append(assocResults.matchedDiaSources) 

405 if len(ssoAssocResult.ssoAssocDiaSources) > 0: 

406 toAssociate.append(ssoAssocResult.ssoAssocDiaSources) 

407 toAssociate.append(createResults.diaSources) 

408 associatedDiaSources = pd.concat(toAssociate) 

409 nTotalSsObjects = ssoAssocResult.nTotalSsObjects 

410 nAssociatedSsObjects = ssoAssocResult.nAssociatedSsObjects 

411 else: 

412 createResults = self.createNewDiaObjects( 

413 assocResults.unAssocDiaSources) 

414 toAssociate = [] 

415 if len(assocResults.matchedDiaSources) > 0: 

416 toAssociate.append(assocResults.matchedDiaSources) 

417 toAssociate.append(createResults.diaSources) 

418 associatedDiaSources = pd.concat(toAssociate) 

419 nTotalSsObjects = 0 

420 nAssociatedSsObjects = 0 

421 

422 # Create new DiaObjects from unassociated diaSources. 

423 self._add_association_meta_data(assocResults.nUpdatedDiaObjects, 

424 assocResults.nUnassociatedDiaObjects, 

425 createResults.nNewDiaObjects, 

426 nTotalSsObjects, 

427 nAssociatedSsObjects) 

428 # Index the DiaSource catalog for this visit after all associations 

429 # have been made. 

430 updatedDiaObjectIds = associatedDiaSources["diaObjectId"][ 

431 associatedDiaSources["diaObjectId"] != 0].to_numpy() 

432 associatedDiaSources.set_index(["diaObjectId", 

433 "band", 

434 "diaSourceId"], 

435 drop=False, 

436 inplace=True) 

437 

438 # Append new DiaObjects and DiaSources to their previous history. 

439 diaObjects = pd.concat( 

440 [diaObjects, 

441 createResults.newDiaObjects.set_index("diaObjectId", drop=False)], 

442 sort=True) 

443 if self.testDataFrameIndex(diaObjects): 

444 raise RuntimeError( 

445 "Duplicate DiaObjects created after association. This is " 

446 "likely due to re-running data with an already populated " 

447 "Apdb. If this was not the case then there was an unexpected " 

448 "failure in Association while matching and creating new " 

449 "DiaObjects and should be reported. Exiting.") 

450 

451 if len(loaderResult.diaSources) > 0: 

452 # We need to coerce the types of loaderResult.diaSources 

453 # to be the same as associatedDiaSources, thanks to pandas 

454 # datetime issues (DM-41100). And we may as well coerce 

455 # all the columns to ensure consistency for future compatibility. 

456 for name, dtype in associatedDiaSources.dtypes.items(): 

457 if name in loaderResult.diaSources.columns and loaderResult.diaSources[name].dtype != dtype: 

458 self.log.debug( 

459 "Coercing loaderResult.diaSources column %s from %s to %s", 

460 name, 

461 str(loaderResult.diaSources[name].dtype), 

462 str(dtype), 

463 ) 

464 loaderResult.diaSources[name] = loaderResult.diaSources[name].astype(dtype) 

465 

466 mergedDiaSourceHistory = pd.concat( 

467 [loaderResult.diaSources, associatedDiaSources], 

468 sort=True) 

469 else: 

470 mergedDiaSourceHistory = pd.concat([associatedDiaSources], sort=True) 

471 

472 # Test for DiaSource duplication first. If duplicates are found, 

473 # this likely means this is duplicate data being processed and sent 

474 # to the Apdb. 

475 if self.testDataFrameIndex(mergedDiaSourceHistory): 

476 raise RuntimeError( 

477 "Duplicate DiaSources found after association and merging " 

478 "with history. This is likely due to re-running data with an " 

479 "already populated Apdb. If this was not the case then there " 

480 "was an unexpected failure in Association while matching " 

481 "sources to objects, and should be reported. Exiting.") 

482 

483 # Compute DiaObject Summary statistics from their full DiaSource 

484 # history. 

485 diaCalResult = self.diaCalculation.run( 

486 diaObjects, 

487 mergedDiaSourceHistory, 

488 updatedDiaObjectIds, 

489 [band]) 

490 # Test for duplication in the updated DiaObjects. 

491 if self.testDataFrameIndex(diaCalResult.diaObjectCat): 

492 raise RuntimeError( 

493 "Duplicate DiaObjects (loaded + updated) created after " 

494 "DiaCalculation. This is unexpected behavior and should be " 

495 "reported. Exiting.") 

496 if self.testDataFrameIndex(diaCalResult.updatedDiaObjects): 

497 raise RuntimeError( 

498 "Duplicate DiaObjects (updated) created after " 

499 "DiaCalculation. This is unexpected behavior and should be " 

500 "reported. Exiting.") 

501 

502 # Force photometer on the Difference and Calibrated exposures using 

503 # the new and updated DiaObject locations. 

504 diaForcedSources = self.diaForcedSource.run( 

505 diaCalResult.diaObjectCat, 

506 diaCalResult.updatedDiaObjects.loc[:, "diaObjectId"].to_numpy(), 

507 # Passing a ccdExposureIdBits here that isn't None will make 

508 # diaForcedSource emit a deprecation warning, so we don't have to 

509 # emit our own. 

510 ccdExposureIdBits, 

511 exposure, 

512 diffIm, 

513 idGenerator=idGenerator) 

514 

515 # Store DiaSources, updated DiaObjects, and DiaForcedSources in the 

516 # Apdb. 

517 self.apdb.store( 

518 DateTime.now(), 

519 diaCalResult.updatedDiaObjects, 

520 associatedDiaSources, 

521 diaForcedSources) 

522 

523 if self.config.doPackageAlerts: 

524 if len(loaderResult.diaForcedSources) > 1: 

525 # We need to coerce the types of loaderResult.diaForcedSources 

526 # to be the same as associatedDiaSources, thanks to pandas 

527 # datetime issues (DM-41100). And we may as well coerce 

528 # all the columns to ensure consistency for future compatibility. 

529 for name, dtype in diaForcedSources.dtypes.items(): 

530 if name in loaderResult.diaForcedSources.columns and \ 

531 loaderResult.diaForcedSources[name].dtype != dtype: 

532 self.log.debug( 

533 "Coercing loaderResult.diaForcedSources column %s from %s to %s", 

534 name, 

535 str(loaderResult.diaForcedSources[name].dtype), 

536 str(dtype), 

537 ) 

538 loaderResult.diaForcedSources[name] = ( 

539 loaderResult.diaForcedSources[name].astype(dtype) 

540 ) 

541 diaForcedSources = pd.concat( 

542 [diaForcedSources, loaderResult.diaForcedSources], 

543 sort=True) 

544 if self.testDataFrameIndex(diaForcedSources): 

545 self.log.warning( 

546 "Duplicate DiaForcedSources created after merge with " 

547 "history and new sources. This may cause downstream " 

548 "problems. Dropping duplicates.") 

549 # Drop duplicates via index and keep the first appearance. 

550 # Reset due to the index shape being slight different than 

551 # expected. 

552 diaForcedSources = diaForcedSources.groupby( 

553 diaForcedSources.index).first() 

554 diaForcedSources.reset_index(drop=True, inplace=True) 

555 diaForcedSources.set_index( 

556 ["diaObjectId", "diaForcedSourceId"], 

557 drop=False, 

558 inplace=True) 

559 self.alertPackager.run(associatedDiaSources, 

560 diaCalResult.diaObjectCat, 

561 loaderResult.diaSources, 

562 diaForcedSources, 

563 diffIm, 

564 template) 

565 

566 return pipeBase.Struct(apdbMarker=self.config.apdb.value, 

567 associatedDiaSources=associatedDiaSources, 

568 diaForcedSources=diaForcedSources, 

569 diaObjects=diaObjects, 

570 longTrailedSources=assocResults.longTrailedSources 

571 ) 

572 

573 def createNewDiaObjects(self, unAssocDiaSources): 

574 """Loop through the set of DiaSources and create new DiaObjects 

575 for unassociated DiaSources. 

576 

577 Parameters 

578 ---------- 

579 unAssocDiaSources : `pandas.DataFrame` 

580 Set of DiaSources to create new DiaObjects from. 

581 

582 Returns 

583 ------- 

584 results : `lsst.pipe.base.Struct` 

585 Results struct containing: 

586 

587 - ``diaSources`` : DiaSource catalog with updated DiaObject ids. 

588 (`pandas.DataFrame`) 

589 - ``newDiaObjects`` : Newly created DiaObjects from the 

590 unassociated DiaSources. (`pandas.DataFrame`) 

591 - ``nNewDiaObjects`` : Number of newly created diaObjects.(`int`) 

592 """ 

593 if len(unAssocDiaSources) == 0: 

594 tmpObj = self._initialize_dia_object(0) 

595 newDiaObjects = pd.DataFrame(data=[], 

596 columns=tmpObj.keys()) 

597 else: 

598 newDiaObjects = unAssocDiaSources["diaSourceId"].apply( 

599 self._initialize_dia_object) 

600 unAssocDiaSources["diaObjectId"] = unAssocDiaSources["diaSourceId"] 

601 return pipeBase.Struct(diaSources=unAssocDiaSources, 

602 newDiaObjects=newDiaObjects, 

603 nNewDiaObjects=len(newDiaObjects)) 

604 

605 def _initialize_dia_object(self, objId): 

606 """Create a new DiaObject with values required to be initialized by the 

607 Ppdb. 

608 

609 Parameters 

610 ---------- 

611 objid : `int` 

612 ``diaObjectId`` value for the of the new DiaObject. 

613 

614 Returns 

615 ------- 

616 diaObject : `dict` 

617 Newly created DiaObject with keys: 

618 

619 ``diaObjectId`` 

620 Unique DiaObjectId (`int`). 

621 ``pmParallaxNdata`` 

622 Number of data points used for parallax calculation (`int`). 

623 ``nearbyObj1`` 

624 Id of the a nearbyObject in the Object table (`int`). 

625 ``nearbyObj2`` 

626 Id of the a nearbyObject in the Object table (`int`). 

627 ``nearbyObj3`` 

628 Id of the a nearbyObject in the Object table (`int`). 

629 ``?_psfFluxNdata`` 

630 Number of data points used to calculate point source flux 

631 summary statistics in each bandpass (`int`). 

632 """ 

633 new_dia_object = {"diaObjectId": objId, 

634 "pmParallaxNdata": 0, 

635 "nearbyObj1": 0, 

636 "nearbyObj2": 0, 

637 "nearbyObj3": 0, 

638 "flags": 0} 

639 for f in ["u", "g", "r", "i", "z", "y"]: 

640 new_dia_object["%s_psfFluxNdata" % f] = 0 

641 return pd.Series(data=new_dia_object) 

642 

643 def testDataFrameIndex(self, df): 

644 """Test the sorted DataFrame index for duplicates. 

645 

646 Wrapped as a separate function to allow for mocking of the this task 

647 in unittesting. Default of a mock return for this test is True. 

648 

649 Parameters 

650 ---------- 

651 df : `pandas.DataFrame` 

652 DataFrame to text. 

653 

654 Returns 

655 ------- 

656 `bool` 

657 True if DataFrame contains duplicate rows. 

658 """ 

659 return df.index.has_duplicates 

660 

661 def _add_association_meta_data(self, 

662 nUpdatedDiaObjects, 

663 nUnassociatedDiaObjects, 

664 nNewDiaObjects, 

665 nTotalSsObjects, 

666 nAssociatedSsObjects): 

667 """Store summaries of the association step in the task metadata. 

668 

669 Parameters 

670 ---------- 

671 nUpdatedDiaObjects : `int` 

672 Number of previous DiaObjects associated and updated in this 

673 ccdVisit. 

674 nUnassociatedDiaObjects : `int` 

675 Number of previous DiaObjects that were not associated or updated 

676 in this ccdVisit. 

677 nNewDiaObjects : `int` 

678 Number of newly created DiaObjects for this ccdVisit. 

679 nTotalSsObjects : `int` 

680 Number of SolarSystemObjects within the observable detector 

681 area. 

682 nAssociatedSsObjects : `int` 

683 Number of successfully associated SolarSystemObjects. 

684 """ 

685 self.metadata.add('numUpdatedDiaObjects', nUpdatedDiaObjects) 

686 self.metadata.add('numUnassociatedDiaObjects', nUnassociatedDiaObjects) 

687 self.metadata.add('numNewDiaObjects', nNewDiaObjects) 

688 self.metadata.add('numTotalSolarSystemObjects', nTotalSsObjects) 

689 self.metadata.add('numAssociatedSsObjects', nAssociatedSsObjects) 

690 

691 def purgeDiaObjects(self, bbox, wcs, diaObjCat, buffer=0): 

692 """Drop diaObjects that are outside the exposure bounding box. 

693 

694 Parameters 

695 ---------- 

696 bbox : `lsst.geom.Box2I` 

697 Bounding box of the exposure. 

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

699 Coordinate system definition (wcs) for the exposure. 

700 diaObjCat : `pandas.DataFrame` 

701 DiaObjects loaded from the Apdb. 

702 buffer : `int`, optional 

703 Width, in pixels, to pad the exposure bounding box. 

704 

705 Returns 

706 ------- 

707 diaObjCat : `pandas.DataFrame` 

708 DiaObjects loaded from the Apdb, restricted to the exposure 

709 bounding box. 

710 """ 

711 try: 

712 bbox.grow(buffer) 

713 raVals = diaObjCat.ra.to_numpy() 

714 decVals = diaObjCat.dec.to_numpy() 

715 xVals, yVals = wcs.skyToPixelArray(raVals, decVals, degrees=True) 

716 selector = bbox.contains(xVals, yVals) 

717 nPurged = np.sum(~selector) 

718 if nPurged > 0: 

719 diaObjCat = diaObjCat[selector].copy() 

720 self.log.info(f"Dropped {nPurged} diaObjects that were outside the bbox " 

721 f"leaving {len(diaObjCat)} in the catalog") 

722 except Exception as e: 

723 self.log.warning("Error attempting to check diaObject history: %s", e) 

724 return diaObjCat