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

136 statements  

« prev     ^ index     » next       coverage.py v7.2.7, created at 2023-07-23 09:18 +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 

31import pandas as pd 

32 

33import lsst.dax.apdb as daxApdb 

34from lsst.meas.base import DetectorVisitIdGeneratorConfig, DiaObjectCalculationTask 

35import lsst.pex.config as pexConfig 

36import lsst.pipe.base as pipeBase 

37import lsst.pipe.base.connectionTypes as connTypes 

38from lsst.utils.timer import timeMethod 

39 

40from lsst.ap.association import ( 

41 AssociationTask, 

42 DiaForcedSourceTask, 

43 LoadDiaCatalogsTask, 

44 PackageAlertsTask) 

45from lsst.ap.association.ssoAssociation import SolarSystemAssociationTask 

46 

47__all__ = ("DiaPipelineConfig", 

48 "DiaPipelineTask", 

49 "DiaPipelineConnections") 

50 

51 

52class DiaPipelineConnections( 

53 pipeBase.PipelineTaskConnections, 

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

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

56 """Butler connections for DiaPipelineTask. 

57 """ 

58 diaSourceTable = connTypes.Input( 

59 doc="Catalog of calibrated DiaSources.", 

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

61 storageClass="DataFrame", 

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

63 ) 

64 solarSystemObjectTable = connTypes.Input( 

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

66 "this detectorVisit.", 

67 name="visitSsObjects", 

68 storageClass="DataFrame", 

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

70 ) 

71 diffIm = connTypes.Input( 

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

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

74 storageClass="ExposureF", 

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

76 ) 

77 exposure = connTypes.Input( 

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

79 "image differencing.", 

80 name="{fakesType}calexp", 

81 storageClass="ExposureF", 

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

83 ) 

84 template = connTypes.Input( 

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

86 "matched.", 

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

88 storageClass="ExposureF", 

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

90 ) 

91 apdbMarker = connTypes.Output( 

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

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

94 name="apdb_marker", 

95 storageClass="Config", 

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

97 ) 

98 associatedDiaSources = connTypes.Output( 

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

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

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

102 storageClass="DataFrame", 

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

104 ) 

105 diaForcedSources = connTypes.Output( 

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

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

108 storageClass="DataFrame", 

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

110 ) 

111 diaObjects = connTypes.Output( 

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

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

114 storageClass="DataFrame", 

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

116 ) 

117 

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

119 super().__init__(config=config) 

120 

121 if not config.doWriteAssociatedSources: 

122 self.outputs.remove("associatedDiaSources") 

123 self.outputs.remove("diaForcedSources") 

124 self.outputs.remove("diaObjects") 

125 if not config.doSolarSystemAssociation: 

126 self.inputs.remove("solarSystemObjectTable") 

127 

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

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

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

131 of the activator. 

132 

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

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

135 

136 Parameters 

137 ---------- 

138 inputs : `dict` 

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

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

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

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

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

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

145 dictionaries are guaranteed to be temporary copies that are true 

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

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

148 outputs : `dict` 

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

150 label : `str` 

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

152 diagnostic messages). 

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

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

155 diagnostic messages). 

156 

157 Returns 

158 ------- 

159 adjusted_inputs : `dict` 

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

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

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

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

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

165 `PipelineTask.runQuantum`. 

166 adjusted_outputs : `dict` 

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

168 interpretation as ``adjusted_inputs``. 

169 

170 Raises 

171 ------ 

172 ScalarError 

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

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

175 NoWorkFound 

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

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

178 quantum should be pruned or skipped. 

179 FileNotFoundError 

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

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

182 `PrerequisiteInput` connection. 

183 """ 

184 _, refs = inputs["diffIm"] 

185 for ref in refs: 

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

187 raise ValueError( 

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

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

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

191 "the validBands list in DiaPipelineConfig and add the " 

192 "appropriate columns to the Apdb schema.") 

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

194 

195 

196class DiaPipelineConfig(pipeBase.PipelineTaskConfig, 

197 pipelineConnections=DiaPipelineConnections): 

198 """Config for DiaPipelineTask. 

199 """ 

200 coaddName = pexConfig.Field( 

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

202 dtype=str, 

203 default="deep", 

204 ) 

205 apdb = daxApdb.ApdbSql.makeField( 

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

207 "DiaObjects. Must already be initialized.", 

208 ) 

209 validBands = pexConfig.ListField( 

210 dtype=str, 

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

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

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

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

215 ) 

216 diaCatalogLoader = pexConfig.ConfigurableField( 

217 target=LoadDiaCatalogsTask, 

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

219 ) 

220 associator = pexConfig.ConfigurableField( 

221 target=AssociationTask, 

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

223 ) 

224 doSolarSystemAssociation = pexConfig.Field( 

225 dtype=bool, 

226 default=False, 

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

228 ) 

229 solarSystemAssociator = pexConfig.ConfigurableField( 

230 target=SolarSystemAssociationTask, 

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

232 ) 

233 diaCalculation = pexConfig.ConfigurableField( 

234 target=DiaObjectCalculationTask, 

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

236 ) 

237 diaForcedSource = pexConfig.ConfigurableField( 

238 target=DiaForcedSourceTask, 

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

240 "difference images.", 

241 ) 

242 alertPackager = pexConfig.ConfigurableField( 

243 target=PackageAlertsTask, 

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

245 ) 

246 doPackageAlerts = pexConfig.Field( 

247 dtype=bool, 

248 default=False, 

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

250 "write them to disk.", 

251 ) 

252 doWriteAssociatedSources = pexConfig.Field( 

253 dtype=bool, 

254 default=False, 

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

256 "formatted following the Science Data Model.", 

257 ) 

258 idGenerator = DetectorVisitIdGeneratorConfig.make_field() 

259 

260 def setDefaults(self): 

261 self.apdb.dia_object_index = "baseline" 

262 self.apdb.dia_object_columns = [] 

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

264 "ap_nDiaSources", 

265 "ap_diaObjectFlag", 

266 "ap_meanFlux", 

267 "ap_percentileFlux", 

268 "ap_sigmaFlux", 

269 "ap_chi2Flux", 

270 "ap_madFlux", 

271 "ap_skewFlux", 

272 "ap_minMaxFlux", 

273 "ap_maxSlopeFlux", 

274 "ap_meanErrFlux", 

275 "ap_linearFit", 

276 "ap_stetsonJ", 

277 "ap_meanTotFlux", 

278 "ap_sigmaTotFlux"] 

279 

280 

281class DiaPipelineTask(pipeBase.PipelineTask): 

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

283 (DIA) Objects and Sources. 

284 """ 

285 ConfigClass = DiaPipelineConfig 

286 _DefaultName = "diaPipe" 

287 

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

289 super().__init__(**kwargs) 

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

291 self.makeSubtask("diaCatalogLoader") 

292 self.makeSubtask("associator") 

293 self.makeSubtask("diaCalculation") 

294 self.makeSubtask("diaForcedSource") 

295 if self.config.doPackageAlerts: 

296 self.makeSubtask("alertPackager") 

297 if self.config.doSolarSystemAssociation: 

298 self.makeSubtask("solarSystemAssociator") 

299 

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

301 inputs = butlerQC.get(inputRefs) 

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

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

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

305 inputs["ccdExposureIdBits"] = None 

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

307 if not self.config.doSolarSystemAssociation: 

308 inputs["solarSystemObjectTable"] = None 

309 

310 outputs = self.run(**inputs) 

311 

312 butlerQC.put(outputs, outputRefs) 

313 

314 @timeMethod 

315 def run(self, 

316 diaSourceTable, 

317 solarSystemObjectTable, 

318 diffIm, 

319 exposure, 

320 template, 

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

322 band, 

323 idGenerator=None): 

324 """Process DiaSources and DiaObjects. 

325 

326 Load previous DiaObjects and their DiaSource history. Calibrate the 

327 values in the diaSourceCat. Associate new DiaSources with previous 

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

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

330 

331 Parameters 

332 ---------- 

333 diaSourceTable : `pandas.DataFrame` 

334 Newly detected DiaSources. 

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

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

337 were detected. 

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

339 Calibrated exposure differenced with a template to create 

340 ``diffIm``. 

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

342 Template exposure used to create diffIm. 

343 ccdExposureIdBits : `int` 

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

345 favor of ``idGenerator``, and ignored if that is present. Pass 

346 `None` explicitly to avoid a deprecation warning (a default is 

347 impossible given that later positional arguments are not 

348 defaulted). 

349 band : `str` 

350 The band in which the new DiaSources were detected. 

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

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

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

354 

355 Returns 

356 ------- 

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

358 Results struct with components. 

359 

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

361 that this ccdVisit has completed successfully. 

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

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

364 DiaSources. (`pandas.DataFrame`) 

365 """ 

366 # Load the DiaObjects and DiaSource history. 

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

368 

369 # Associate new DiaSources with existing DiaObjects. 

370 assocResults = self.associator.run(diaSourceTable, 

371 loaderResult.diaObjects) 

372 if self.config.doSolarSystemAssociation: 

373 ssoAssocResult = self.solarSystemAssociator.run( 

374 assocResults.unAssocDiaSources, 

375 solarSystemObjectTable, 

376 diffIm) 

377 createResults = self.createNewDiaObjects( 

378 ssoAssocResult.unAssocDiaSources) 

379 associatedDiaSources = pd.concat( 

380 [assocResults.matchedDiaSources, 

381 ssoAssocResult.ssoAssocDiaSources, 

382 createResults.diaSources]) 

383 nTotalSsObjects = ssoAssocResult.nTotalSsObjects 

384 nAssociatedSsObjects = ssoAssocResult.nAssociatedSsObjects 

385 else: 

386 createResults = self.createNewDiaObjects( 

387 assocResults.unAssocDiaSources) 

388 associatedDiaSources = pd.concat( 

389 [assocResults.matchedDiaSources, 

390 createResults.diaSources]) 

391 nTotalSsObjects = 0 

392 nAssociatedSsObjects = 0 

393 

394 # Create new DiaObjects from unassociated diaSources. 

395 self._add_association_meta_data(assocResults.nUpdatedDiaObjects, 

396 assocResults.nUnassociatedDiaObjects, 

397 createResults.nNewDiaObjects, 

398 nTotalSsObjects, 

399 nAssociatedSsObjects) 

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

401 # have been made. 

402 updatedDiaObjectIds = associatedDiaSources["diaObjectId"][ 

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

404 associatedDiaSources.set_index(["diaObjectId", 

405 "band", 

406 "diaSourceId"], 

407 drop=False, 

408 inplace=True) 

409 

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

411 diaObjects = pd.concat( 

412 [loaderResult.diaObjects, 

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

414 sort=True) 

415 if self.testDataFrameIndex(diaObjects): 

416 raise RuntimeError( 

417 "Duplicate DiaObjects created after association. This is " 

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

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

420 "failure in Association while matching and creating new " 

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

422 mergedDiaSourceHistory = pd.concat( 

423 [loaderResult.diaSources, associatedDiaSources], 

424 sort=True) 

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

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

427 # to the Apdb. 

428 if self.testDataFrameIndex(mergedDiaSourceHistory): 

429 raise RuntimeError( 

430 "Duplicate DiaSources found after association and merging " 

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

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

433 "was an unexpected failure in Association while matching " 

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

435 

436 # Compute DiaObject Summary statistics from their full DiaSource 

437 # history. 

438 diaCalResult = self.diaCalculation.run( 

439 diaObjects, 

440 mergedDiaSourceHistory, 

441 updatedDiaObjectIds, 

442 [band]) 

443 # Test for duplication in the updated DiaObjects. 

444 if self.testDataFrameIndex(diaCalResult.diaObjectCat): 

445 raise RuntimeError( 

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

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

448 "reported. Exiting.") 

449 if self.testDataFrameIndex(diaCalResult.updatedDiaObjects): 

450 raise RuntimeError( 

451 "Duplicate DiaObjects (updated) created after " 

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

453 "reported. Exiting.") 

454 

455 # Force photometer on the Difference and Calibrated exposures using 

456 # the new and updated DiaObject locations. 

457 diaForcedSources = self.diaForcedSource.run( 

458 diaCalResult.diaObjectCat, 

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

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

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

462 # emit our own. 

463 ccdExposureIdBits, 

464 exposure, 

465 diffIm, 

466 idGenerator=idGenerator) 

467 

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

469 # Apdb. 

470 self.apdb.store( 

471 exposure.visitInfo.date, 

472 diaCalResult.updatedDiaObjects, 

473 associatedDiaSources, 

474 diaForcedSources) 

475 

476 if self.config.doPackageAlerts: 

477 if len(loaderResult.diaForcedSources) > 1: 

478 diaForcedSources = pd.concat( 

479 [diaForcedSources, loaderResult.diaForcedSources], 

480 sort=True) 

481 if self.testDataFrameIndex(diaForcedSources): 

482 self.log.warning( 

483 "Duplicate DiaForcedSources created after merge with " 

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

485 "problems. Dropping duplicates.") 

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

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

488 # expected. 

489 diaForcedSources = diaForcedSources.groupby( 

490 diaForcedSources.index).first() 

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

492 diaForcedSources.set_index( 

493 ["diaObjectId", "diaForcedSourceId"], 

494 drop=False, 

495 inplace=True) 

496 self.alertPackager.run(associatedDiaSources, 

497 diaCalResult.diaObjectCat, 

498 loaderResult.diaSources, 

499 diaForcedSources, 

500 diffIm, 

501 template) 

502 

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

504 associatedDiaSources=associatedDiaSources, 

505 diaForcedSources=diaForcedSources, 

506 diaObjects=diaObjects, 

507 ) 

508 

509 def createNewDiaObjects(self, unAssocDiaSources): 

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

511 for unassociated DiaSources. 

512 

513 Parameters 

514 ---------- 

515 unAssocDiaSources : `pandas.DataFrame` 

516 Set of DiaSources to create new DiaObjects from. 

517 

518 Returns 

519 ------- 

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

521 Results struct containing: 

522 

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

524 (`pandas.DataFrame`) 

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

526 unassociated DiaSources. (`pandas.DataFrame`) 

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

528 """ 

529 if len(unAssocDiaSources) == 0: 

530 tmpObj = self._initialize_dia_object(0) 

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

532 columns=tmpObj.keys()) 

533 else: 

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

535 self._initialize_dia_object) 

536 unAssocDiaSources["diaObjectId"] = unAssocDiaSources["diaSourceId"] 

537 return pipeBase.Struct(diaSources=unAssocDiaSources, 

538 newDiaObjects=newDiaObjects, 

539 nNewDiaObjects=len(newDiaObjects)) 

540 

541 def _initialize_dia_object(self, objId): 

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

543 Ppdb. 

544 

545 Parameters 

546 ---------- 

547 objid : `int` 

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

549 

550 Returns 

551 ------- 

552 diaObject : `dict` 

553 Newly created DiaObject with keys: 

554 

555 ``diaObjectId`` 

556 Unique DiaObjectId (`int`). 

557 ``pmParallaxNdata`` 

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

559 ``nearbyObj1`` 

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

561 ``nearbyObj2`` 

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

563 ``nearbyObj3`` 

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

565 ``?_psfFluxNdata`` 

566 Number of data points used to calculate point source flux 

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

568 """ 

569 new_dia_object = {"diaObjectId": objId, 

570 "pmParallaxNdata": 0, 

571 "nearbyObj1": 0, 

572 "nearbyObj2": 0, 

573 "nearbyObj3": 0, 

574 "flags": 0} 

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

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

577 return pd.Series(data=new_dia_object) 

578 

579 def testDataFrameIndex(self, df): 

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

581 

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

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

584 

585 Parameters 

586 ---------- 

587 df : `pandas.DataFrame` 

588 DataFrame to text. 

589 

590 Returns 

591 ------- 

592 `bool` 

593 True if DataFrame contains duplicate rows. 

594 """ 

595 return df.index.has_duplicates 

596 

597 def _add_association_meta_data(self, 

598 nUpdatedDiaObjects, 

599 nUnassociatedDiaObjects, 

600 nNewDiaObjects, 

601 nTotalSsObjects, 

602 nAssociatedSsObjects): 

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

604 

605 Parameters 

606 ---------- 

607 nUpdatedDiaObjects : `int` 

608 Number of previous DiaObjects associated and updated in this 

609 ccdVisit. 

610 nUnassociatedDiaObjects : `int` 

611 Number of previous DiaObjects that were not associated or updated 

612 in this ccdVisit. 

613 nNewDiaObjects : `int` 

614 Number of newly created DiaObjects for this ccdVisit. 

615 nTotalSsObjects : `int` 

616 Number of SolarSystemObjects within the observable detector 

617 area. 

618 nAssociatedSsObjects : `int` 

619 Number of successfully associated SolarSystemObjects. 

620 """ 

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

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

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

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

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