Coverage for python/lsst/ap/association/diaPipe.py: 24%
178 statements
« prev ^ index » next coverage.py v7.4.1, created at 2024-02-08 10:30 +0000
« prev ^ index » next coverage.py v7.4.1, created at 2024-02-08 10:30 +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#
23"""PipelineTask for associating DiaSources with previous DiaObjects.
25Additionally performs forced photometry on the calibrated and difference
26images at the updated locations of DiaObjects.
28Currently loads directly from the Apdb rather than pre-loading.
29"""
31__all__ = ("DiaPipelineConfig",
32 "DiaPipelineTask",
33 "DiaPipelineConnections")
35import numpy as np
36import pandas as pd
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
46from lsst.ap.association import (
47 AssociationTask,
48 DiaForcedSourceTask,
49 LoadDiaCatalogsTask,
50 PackageAlertsTask)
51from lsst.ap.association.ssoAssociation import SolarSystemAssociationTask
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 )
126 def __init__(self, *, config=None):
127 super().__init__(config=config)
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")
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.
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.
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).
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``.
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)
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()
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"]
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"
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")
317 def runQuantum(self, butlerQC, inputRefs, outputRefs):
318 inputs = butlerQC.get(inputRefs)
319 inputs["idGenerator"] = self.config.idGenerator.apply(butlerQC.quantum.dataId)
320 inputs["band"] = butlerQC.quantum.dataId["band"]
321 if not self.config.doSolarSystemAssociation:
322 inputs["solarSystemObjectTable"] = None
324 outputs = self.run(**inputs)
326 butlerQC.put(outputs, outputRefs)
328 @timeMethod
329 def run(self,
330 diaSourceTable,
331 solarSystemObjectTable,
332 diffIm,
333 exposure,
334 template,
335 band,
336 idGenerator):
337 """Process DiaSources and DiaObjects.
339 Load previous DiaObjects and their DiaSource history. Calibrate the
340 values in the diaSourceCat. Associate new DiaSources with previous
341 DiaObjects. Run forced photometry at the updated DiaObject locations.
342 Store the results in the Alert Production Database (Apdb).
344 Parameters
345 ----------
346 diaSourceTable : `pandas.DataFrame`
347 Newly detected DiaSources.
348 diffIm : `lsst.afw.image.ExposureF`
349 Difference image exposure in which the sources in ``diaSourceCat``
350 were detected.
351 exposure : `lsst.afw.image.ExposureF`
352 Calibrated exposure differenced with a template to create
353 ``diffIm``.
354 template : `lsst.afw.image.ExposureF`
355 Template exposure used to create diffIm.
356 band : `str`
357 The band in which the new DiaSources were detected.
358 idGenerator : `lsst.meas.base.IdGenerator`
359 Object that generates source IDs and random number generator seeds.
361 Returns
362 -------
363 results : `lsst.pipe.base.Struct`
364 Results struct with components.
366 - ``apdbMaker`` : Marker dataset to store in the Butler indicating
367 that this ccdVisit has completed successfully.
368 (`lsst.dax.apdb.ApdbConfig`)
369 - ``associatedDiaSources`` : Catalog of newly associated
370 DiaSources. (`pandas.DataFrame`)
371 """
372 # Load the DiaObjects and DiaSource history.
373 loaderResult = self.diaCatalogLoader.run(diffIm, self.apdb)
374 if len(loaderResult.diaObjects) > 0:
375 diaObjects = self.purgeDiaObjects(diffIm.getBBox(), diffIm.getWcs(), loaderResult.diaObjects,
376 buffer=self.config.imagePixelMargin)
377 else:
378 diaObjects = loaderResult.diaObjects
380 # Associate new DiaSources with existing DiaObjects.
381 assocResults = self.associator.run(diaSourceTable, diaObjects,
382 exposure_time=diffIm.visitInfo.exposureTime)
384 if self.config.doSolarSystemAssociation:
385 ssoAssocResult = self.solarSystemAssociator.run(
386 assocResults.unAssocDiaSources,
387 solarSystemObjectTable,
388 diffIm)
389 createResults = self.createNewDiaObjects(
390 ssoAssocResult.unAssocDiaSources)
391 toAssociate = []
392 if len(assocResults.matchedDiaSources) > 0:
393 toAssociate.append(assocResults.matchedDiaSources)
394 if len(ssoAssocResult.ssoAssocDiaSources) > 0:
395 toAssociate.append(ssoAssocResult.ssoAssocDiaSources)
396 toAssociate.append(createResults.diaSources)
397 associatedDiaSources = pd.concat(toAssociate)
398 nTotalSsObjects = ssoAssocResult.nTotalSsObjects
399 nAssociatedSsObjects = ssoAssocResult.nAssociatedSsObjects
400 else:
401 createResults = self.createNewDiaObjects(
402 assocResults.unAssocDiaSources)
403 toAssociate = []
404 if len(assocResults.matchedDiaSources) > 0:
405 toAssociate.append(assocResults.matchedDiaSources)
406 toAssociate.append(createResults.diaSources)
407 associatedDiaSources = pd.concat(toAssociate)
408 nTotalSsObjects = 0
409 nAssociatedSsObjects = 0
411 # Create new DiaObjects from unassociated diaSources.
412 self._add_association_meta_data(assocResults.nUpdatedDiaObjects,
413 assocResults.nUnassociatedDiaObjects,
414 createResults.nNewDiaObjects,
415 nTotalSsObjects,
416 nAssociatedSsObjects)
417 # Index the DiaSource catalog for this visit after all associations
418 # have been made.
419 updatedDiaObjectIds = associatedDiaSources["diaObjectId"][
420 associatedDiaSources["diaObjectId"] != 0].to_numpy()
421 associatedDiaSources.set_index(["diaObjectId",
422 "band",
423 "diaSourceId"],
424 drop=False,
425 inplace=True)
427 # Append new DiaObjects and DiaSources to their previous history.
428 diaObjects = pd.concat(
429 [diaObjects,
430 createResults.newDiaObjects.set_index("diaObjectId", drop=False)],
431 sort=True)
432 if self.testDataFrameIndex(diaObjects):
433 raise RuntimeError(
434 "Duplicate DiaObjects created after association. This is "
435 "likely due to re-running data with an already populated "
436 "Apdb. If this was not the case then there was an unexpected "
437 "failure in Association while matching and creating new "
438 "DiaObjects and should be reported. Exiting.")
440 if len(loaderResult.diaSources) > 0:
441 # We need to coerce the types of loaderResult.diaSources
442 # to be the same as associatedDiaSources, thanks to pandas
443 # datetime issues (DM-41100). And we may as well coerce
444 # all the columns to ensure consistency for future compatibility.
445 for name, dtype in associatedDiaSources.dtypes.items():
446 if name in loaderResult.diaSources.columns and loaderResult.diaSources[name].dtype != dtype:
447 self.log.debug(
448 "Coercing loaderResult.diaSources column %s from %s to %s",
449 name,
450 str(loaderResult.diaSources[name].dtype),
451 str(dtype),
452 )
453 loaderResult.diaSources[name] = loaderResult.diaSources[name].astype(dtype)
455 mergedDiaSourceHistory = pd.concat(
456 [loaderResult.diaSources, associatedDiaSources],
457 sort=True)
458 else:
459 mergedDiaSourceHistory = pd.concat([associatedDiaSources], sort=True)
461 # Test for DiaSource duplication first. If duplicates are found,
462 # this likely means this is duplicate data being processed and sent
463 # to the Apdb.
464 if self.testDataFrameIndex(mergedDiaSourceHistory):
465 raise RuntimeError(
466 "Duplicate DiaSources found after association and merging "
467 "with history. This is likely due to re-running data with an "
468 "already populated Apdb. If this was not the case then there "
469 "was an unexpected failure in Association while matching "
470 "sources to objects, and should be reported. Exiting.")
472 # Compute DiaObject Summary statistics from their full DiaSource
473 # history.
474 diaCalResult = self.diaCalculation.run(
475 diaObjects,
476 mergedDiaSourceHistory,
477 updatedDiaObjectIds,
478 [band])
479 # Test for duplication in the updated DiaObjects.
480 if self.testDataFrameIndex(diaCalResult.diaObjectCat):
481 raise RuntimeError(
482 "Duplicate DiaObjects (loaded + updated) created after "
483 "DiaCalculation. This is unexpected behavior and should be "
484 "reported. Exiting.")
485 if self.testDataFrameIndex(diaCalResult.updatedDiaObjects):
486 raise RuntimeError(
487 "Duplicate DiaObjects (updated) created after "
488 "DiaCalculation. This is unexpected behavior and should be "
489 "reported. Exiting.")
491 # Force photometer on the Difference and Calibrated exposures using
492 # the new and updated DiaObject locations.
493 diaForcedSources = self.diaForcedSource.run(
494 diaCalResult.diaObjectCat,
495 diaCalResult.updatedDiaObjects.loc[:, "diaObjectId"].to_numpy(),
496 exposure,
497 diffIm,
498 idGenerator=idGenerator)
500 # Store DiaSources, updated DiaObjects, and DiaForcedSources in the
501 # Apdb.
502 self.apdb.store(
503 DateTime.now(),
504 diaCalResult.updatedDiaObjects,
505 associatedDiaSources,
506 diaForcedSources)
508 if self.config.doPackageAlerts:
509 if len(loaderResult.diaForcedSources) > 1:
510 # We need to coerce the types of loaderResult.diaForcedSources
511 # to be the same as associatedDiaSources, thanks to pandas
512 # datetime issues (DM-41100). And we may as well coerce
513 # all the columns to ensure consistency for future compatibility.
514 for name, dtype in diaForcedSources.dtypes.items():
515 if name in loaderResult.diaForcedSources.columns and \
516 loaderResult.diaForcedSources[name].dtype != dtype:
517 self.log.debug(
518 "Coercing loaderResult.diaForcedSources column %s from %s to %s",
519 name,
520 str(loaderResult.diaForcedSources[name].dtype),
521 str(dtype),
522 )
523 loaderResult.diaForcedSources[name] = (
524 loaderResult.diaForcedSources[name].astype(dtype)
525 )
526 diaForcedSources = pd.concat(
527 [diaForcedSources, loaderResult.diaForcedSources],
528 sort=True)
529 if self.testDataFrameIndex(diaForcedSources):
530 self.log.warning(
531 "Duplicate DiaForcedSources created after merge with "
532 "history and new sources. This may cause downstream "
533 "problems. Dropping duplicates.")
534 # Drop duplicates via index and keep the first appearance.
535 # Reset due to the index shape being slight different than
536 # expected.
537 diaForcedSources = diaForcedSources.groupby(
538 diaForcedSources.index).first()
539 diaForcedSources.reset_index(drop=True, inplace=True)
540 diaForcedSources.set_index(
541 ["diaObjectId", "diaForcedSourceId"],
542 drop=False,
543 inplace=True)
544 self.alertPackager.run(associatedDiaSources,
545 diaCalResult.diaObjectCat,
546 loaderResult.diaSources,
547 diaForcedSources,
548 diffIm,
549 template)
551 return pipeBase.Struct(apdbMarker=self.config.apdb.value,
552 associatedDiaSources=associatedDiaSources,
553 diaForcedSources=diaForcedSources,
554 diaObjects=diaObjects,
555 longTrailedSources=assocResults.longTrailedSources
556 )
558 def createNewDiaObjects(self, unAssocDiaSources):
559 """Loop through the set of DiaSources and create new DiaObjects
560 for unassociated DiaSources.
562 Parameters
563 ----------
564 unAssocDiaSources : `pandas.DataFrame`
565 Set of DiaSources to create new DiaObjects from.
567 Returns
568 -------
569 results : `lsst.pipe.base.Struct`
570 Results struct containing:
572 - ``diaSources`` : DiaSource catalog with updated DiaObject ids.
573 (`pandas.DataFrame`)
574 - ``newDiaObjects`` : Newly created DiaObjects from the
575 unassociated DiaSources. (`pandas.DataFrame`)
576 - ``nNewDiaObjects`` : Number of newly created diaObjects.(`int`)
577 """
578 if len(unAssocDiaSources) == 0:
579 tmpObj = self._initialize_dia_object(0)
580 newDiaObjects = pd.DataFrame(data=[],
581 columns=tmpObj.keys())
582 else:
583 newDiaObjects = unAssocDiaSources["diaSourceId"].apply(
584 self._initialize_dia_object)
585 unAssocDiaSources["diaObjectId"] = unAssocDiaSources["diaSourceId"]
586 return pipeBase.Struct(diaSources=unAssocDiaSources,
587 newDiaObjects=newDiaObjects,
588 nNewDiaObjects=len(newDiaObjects))
590 def _initialize_dia_object(self, objId):
591 """Create a new DiaObject with values required to be initialized by the
592 Ppdb.
594 Parameters
595 ----------
596 objid : `int`
597 ``diaObjectId`` value for the of the new DiaObject.
599 Returns
600 -------
601 diaObject : `dict`
602 Newly created DiaObject with keys:
604 ``diaObjectId``
605 Unique DiaObjectId (`int`).
606 ``pmParallaxNdata``
607 Number of data points used for parallax calculation (`int`).
608 ``nearbyObj1``
609 Id of the a nearbyObject in the Object table (`int`).
610 ``nearbyObj2``
611 Id of the a nearbyObject in the Object table (`int`).
612 ``nearbyObj3``
613 Id of the a nearbyObject in the Object table (`int`).
614 ``?_psfFluxNdata``
615 Number of data points used to calculate point source flux
616 summary statistics in each bandpass (`int`).
617 """
618 new_dia_object = {"diaObjectId": objId,
619 "pmParallaxNdata": 0,
620 "nearbyObj1": 0,
621 "nearbyObj2": 0,
622 "nearbyObj3": 0,
623 "flags": 0}
624 for f in ["u", "g", "r", "i", "z", "y"]:
625 new_dia_object["%s_psfFluxNdata" % f] = 0
626 return pd.Series(data=new_dia_object)
628 def testDataFrameIndex(self, df):
629 """Test the sorted DataFrame index for duplicates.
631 Wrapped as a separate function to allow for mocking of the this task
632 in unittesting. Default of a mock return for this test is True.
634 Parameters
635 ----------
636 df : `pandas.DataFrame`
637 DataFrame to text.
639 Returns
640 -------
641 `bool`
642 True if DataFrame contains duplicate rows.
643 """
644 return df.index.has_duplicates
646 def _add_association_meta_data(self,
647 nUpdatedDiaObjects,
648 nUnassociatedDiaObjects,
649 nNewDiaObjects,
650 nTotalSsObjects,
651 nAssociatedSsObjects):
652 """Store summaries of the association step in the task metadata.
654 Parameters
655 ----------
656 nUpdatedDiaObjects : `int`
657 Number of previous DiaObjects associated and updated in this
658 ccdVisit.
659 nUnassociatedDiaObjects : `int`
660 Number of previous DiaObjects that were not associated or updated
661 in this ccdVisit.
662 nNewDiaObjects : `int`
663 Number of newly created DiaObjects for this ccdVisit.
664 nTotalSsObjects : `int`
665 Number of SolarSystemObjects within the observable detector
666 area.
667 nAssociatedSsObjects : `int`
668 Number of successfully associated SolarSystemObjects.
669 """
670 self.metadata.add('numUpdatedDiaObjects', nUpdatedDiaObjects)
671 self.metadata.add('numUnassociatedDiaObjects', nUnassociatedDiaObjects)
672 self.metadata.add('numNewDiaObjects', nNewDiaObjects)
673 self.metadata.add('numTotalSolarSystemObjects', nTotalSsObjects)
674 self.metadata.add('numAssociatedSsObjects', nAssociatedSsObjects)
676 def purgeDiaObjects(self, bbox, wcs, diaObjCat, buffer=0):
677 """Drop diaObjects that are outside the exposure bounding box.
679 Parameters
680 ----------
681 bbox : `lsst.geom.Box2I`
682 Bounding box of the exposure.
683 wcs : `lsst.afw.geom.SkyWcs`
684 Coordinate system definition (wcs) for the exposure.
685 diaObjCat : `pandas.DataFrame`
686 DiaObjects loaded from the Apdb.
687 buffer : `int`, optional
688 Width, in pixels, to pad the exposure bounding box.
690 Returns
691 -------
692 diaObjCat : `pandas.DataFrame`
693 DiaObjects loaded from the Apdb, restricted to the exposure
694 bounding box.
695 """
696 try:
697 bbox.grow(buffer)
698 raVals = diaObjCat.ra.to_numpy()
699 decVals = diaObjCat.dec.to_numpy()
700 xVals, yVals = wcs.skyToPixelArray(raVals, decVals, degrees=True)
701 selector = bbox.contains(xVals, yVals)
702 nPurged = np.sum(~selector)
703 if nPurged > 0:
704 diaObjCat = diaObjCat[selector].copy()
705 self.log.info(f"Dropped {nPurged} diaObjects that were outside the bbox "
706 f"leaving {len(diaObjCat)} in the catalog")
707 except Exception as e:
708 self.log.warning("Error attempting to check diaObject history: %s", e)
709 return diaObjCat