Coverage for python/lsst/ap/association/diaPipe.py: 23%
198 statements
« prev ^ index » next coverage.py v7.5.1, created at 2024-05-07 03:43 -0700
« prev ^ index » next coverage.py v7.5.1, created at 2024-05-07 03:43 -0700
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")
36import warnings
38import numpy as np
39import pandas as pd
41from lsst.daf.base import DateTime
42import lsst.dax.apdb as daxApdb
43from lsst.meas.base import DetectorVisitIdGeneratorConfig, DiaObjectCalculationTask
44import lsst.pex.config as pexConfig
45import lsst.pipe.base as pipeBase
46import lsst.pipe.base.connectionTypes as connTypes
47from lsst.utils.timer import timeMethod
49from lsst.ap.association import (
50 AssociationTask,
51 DiaForcedSourceTask,
52 LoadDiaCatalogsTask,
53 PackageAlertsTask)
54from lsst.ap.association.ssoAssociation import SolarSystemAssociationTask
57class DiaPipelineConnections(
58 pipeBase.PipelineTaskConnections,
59 dimensions=("instrument", "visit", "detector"),
60 defaultTemplates={"coaddName": "deep", "fakesType": ""}):
61 """Butler connections for DiaPipelineTask.
62 """
63 diaSourceTable = connTypes.Input(
64 doc="Catalog of calibrated DiaSources.",
65 name="{fakesType}{coaddName}Diff_diaSrcTable",
66 storageClass="DataFrame",
67 dimensions=("instrument", "visit", "detector"),
68 )
69 solarSystemObjectTable = connTypes.Input(
70 doc="Catalog of SolarSolarSystem objects expected to be observable in "
71 "this detectorVisit.",
72 name="visitSsObjects",
73 storageClass="DataFrame",
74 dimensions=("instrument", "visit"),
75 )
76 diffIm = connTypes.Input(
77 doc="Difference image on which the DiaSources were detected.",
78 name="{fakesType}{coaddName}Diff_differenceExp",
79 storageClass="ExposureF",
80 dimensions=("instrument", "visit", "detector"),
81 )
82 exposure = connTypes.Input(
83 doc="Calibrated exposure differenced with a template image during "
84 "image differencing.",
85 name="{fakesType}calexp",
86 storageClass="ExposureF",
87 dimensions=("instrument", "visit", "detector"),
88 )
89 template = connTypes.Input(
90 doc="Warped template used to create `subtractedExposure`. Not PSF "
91 "matched.",
92 dimensions=("instrument", "visit", "detector"),
93 storageClass="ExposureF",
94 name="{fakesType}{coaddName}Diff_templateExp",
95 )
96 apdbMarker = connTypes.Output(
97 doc="Marker dataset storing the configuration of the Apdb for each "
98 "visit/detector. Used to signal the completion of the pipeline.",
99 name="apdb_marker",
100 storageClass="Config",
101 dimensions=("instrument", "visit", "detector"),
102 )
103 associatedDiaSources = connTypes.Output(
104 doc="Optional output storing the DiaSource catalog after matching, "
105 "calibration, and standardization for insertion into the Apdb.",
106 name="{fakesType}{coaddName}Diff_assocDiaSrc",
107 storageClass="DataFrame",
108 dimensions=("instrument", "visit", "detector"),
109 )
110 diaForcedSources = connTypes.Output(
111 doc="Optional output storing the forced sources computed at the diaObject positions.",
112 name="{fakesType}{coaddName}Diff_diaForcedSrc",
113 storageClass="DataFrame",
114 dimensions=("instrument", "visit", "detector"),
115 )
116 diaObjects = connTypes.Output(
117 doc="Optional output storing the updated diaObjects associated to these sources.",
118 name="{fakesType}{coaddName}Diff_diaObject",
119 storageClass="DataFrame",
120 dimensions=("instrument", "visit", "detector"),
121 )
122 longTrailedSources = pipeBase.connectionTypes.Output(
123 doc="Optional output temporarily storing long trailed diaSources.",
124 dimensions=("instrument", "visit", "detector"),
125 storageClass="DataFrame",
126 name="{fakesType}{coaddName}Diff_longTrailedSrc",
127 )
129 def __init__(self, *, config=None):
130 super().__init__(config=config)
132 if not config.doWriteAssociatedSources:
133 self.outputs.remove("associatedDiaSources")
134 self.outputs.remove("diaForcedSources")
135 self.outputs.remove("diaObjects")
136 elif not config.doRunForcedMeasurement:
137 self.outputs.remove("diaForcedSources")
138 if not config.doSolarSystemAssociation:
139 self.inputs.remove("solarSystemObjectTable")
140 if not config.associator.doTrailedSourceFilter:
141 self.outputs.remove("longTrailedSources")
143 def adjustQuantum(self, inputs, outputs, label, dataId):
144 """Override to make adjustments to `lsst.daf.butler.DatasetRef` objects
145 in the `lsst.daf.butler.core.Quantum` during the graph generation stage
146 of the activator.
148 This implementation checks to make sure that the filters in the dataset
149 are compatible with AP processing as set by the Apdb/DPDD schema.
151 Parameters
152 ----------
153 inputs : `dict`
154 Dictionary whose keys are an input (regular or prerequisite)
155 connection name and whose values are a tuple of the connection
156 instance and a collection of associated `DatasetRef` objects.
157 The exact type of the nested collections is unspecified; it can be
158 assumed to be multi-pass iterable and support `len` and ``in``, but
159 it should not be mutated in place. In contrast, the outer
160 dictionaries are guaranteed to be temporary copies that are true
161 `dict` instances, and hence may be modified and even returned; this
162 is especially useful for delegating to `super` (see notes below).
163 outputs : `dict`
164 Dict of output datasets, with the same structure as ``inputs``.
165 label : `str`
166 Label for this task in the pipeline (should be used in all
167 diagnostic messages).
168 data_id : `lsst.daf.butler.DataCoordinate`
169 Data ID for this quantum in the pipeline (should be used in all
170 diagnostic messages).
172 Returns
173 -------
174 adjusted_inputs : `dict`
175 Dict of the same form as ``inputs`` with updated containers of
176 input `DatasetRef` objects. Connections that are not changed
177 should not be returned at all. Datasets may only be removed, not
178 added. Nested collections may be of any multi-pass iterable type,
179 and the order of iteration will set the order of iteration within
180 `PipelineTask.runQuantum`.
181 adjusted_outputs : `dict`
182 Dict of updated output datasets, with the same structure and
183 interpretation as ``adjusted_inputs``.
185 Raises
186 ------
187 ScalarError
188 Raised if any `Input` or `PrerequisiteInput` connection has
189 ``multiple`` set to `False`, but multiple datasets.
190 NoWorkFound
191 Raised to indicate that this quantum should not be run; not enough
192 datasets were found for a regular `Input` connection, and the
193 quantum should be pruned or skipped.
194 FileNotFoundError
195 Raised to cause QuantumGraph generation to fail (with the message
196 included in this exception); not enough datasets were found for a
197 `PrerequisiteInput` connection.
198 """
199 _, refs = inputs["diffIm"]
200 for ref in refs:
201 if ref.dataId["band"] not in self.config.validBands:
202 raise ValueError(
203 f"Requested '{ref.dataId['band']}' not in "
204 "DiaPipelineConfig.validBands. To process bands not in "
205 "the standard Rubin set (ugrizy) you must add the band to "
206 "the validBands list in DiaPipelineConfig and add the "
207 "appropriate columns to the Apdb schema.")
208 return super().adjustQuantum(inputs, outputs, label, dataId)
211class DiaPipelineConfig(pipeBase.PipelineTaskConfig,
212 pipelineConnections=DiaPipelineConnections):
213 """Config for DiaPipelineTask.
214 """
215 coaddName = pexConfig.Field(
216 doc="coadd name: typically one of deep, goodSeeing, or dcr",
217 dtype=str,
218 default="deep",
219 )
220 apdb = pexConfig.ConfigurableField( # TODO: remove on DM-43419
221 target=daxApdb.ApdbSql,
222 doc="Database connection for storing associated DiaSources and "
223 "DiaObjects. Must already be initialized.",
224 deprecated="This field has been replaced by ``apdb_config_url``; set "
225 "``doConfigureApdb=False`` to use it. Will be removed after v28.",
226 )
227 apdb_config_url = pexConfig.Field(
228 dtype=str,
229 default=None,
230 optional=False,
231 doc="A config file specifying the APDB and its connection parameters, "
232 "typically written by the apdb-cli command-line utility. "
233 "The database must already be initialized.",
234 )
235 validBands = pexConfig.ListField(
236 dtype=str,
237 default=["u", "g", "r", "i", "z", "y"],
238 doc="List of bands that are valid for AP processing. To process a "
239 "band not on this list, the appropriate band specific columns "
240 "must be added to the Apdb schema in dax_apdb.",
241 )
242 diaCatalogLoader = pexConfig.ConfigurableField(
243 target=LoadDiaCatalogsTask,
244 doc="Task to load DiaObjects and DiaSources from the Apdb.",
245 )
246 associator = pexConfig.ConfigurableField(
247 target=AssociationTask,
248 doc="Task used to associate DiaSources with DiaObjects.",
249 )
250 doSolarSystemAssociation = pexConfig.Field(
251 dtype=bool,
252 default=False,
253 doc="Process SolarSystem objects through the pipeline.",
254 )
255 solarSystemAssociator = pexConfig.ConfigurableField(
256 target=SolarSystemAssociationTask,
257 doc="Task used to associate DiaSources with SolarSystemObjects.",
258 )
259 diaCalculation = pexConfig.ConfigurableField(
260 target=DiaObjectCalculationTask,
261 doc="Task to compute summary statistics for DiaObjects.",
262 )
263 doLoadForcedSources = pexConfig.Field(
264 dtype=bool,
265 default=True,
266 deprecated="Added to allow disabling forced sources for performance "
267 "reasons during the ops rehearsal. "
268 "It is expected to be removed.",
269 doc="Load forced DiaSource history from the APDB? "
270 "This should only be turned off for debugging purposes.",
271 )
272 doRunForcedMeasurement = pexConfig.Field(
273 dtype=bool,
274 default=True,
275 deprecated="Added to allow disabling forced sources for performance "
276 "reasons during the ops rehearsal. "
277 "It is expected to be removed.",
278 doc="Run forced measurement on all of the diaObjects? "
279 "This should only be turned off for debugging purposes.",
280 )
281 diaForcedSource = pexConfig.ConfigurableField(
282 target=DiaForcedSourceTask,
283 doc="Task used for force photometer DiaObject locations in direct and "
284 "difference images.",
285 )
286 alertPackager = pexConfig.ConfigurableField(
287 target=PackageAlertsTask,
288 doc="Subtask for packaging Ap data into alerts.",
289 )
290 doPackageAlerts = pexConfig.Field(
291 dtype=bool,
292 default=False,
293 doc="Package Dia-data into serialized alerts for distribution and "
294 "write them to disk.",
295 )
296 doWriteAssociatedSources = pexConfig.Field(
297 dtype=bool,
298 default=True,
299 doc="Write out associated DiaSources, DiaForcedSources, and DiaObjects, "
300 "formatted following the Science Data Model.",
301 )
302 imagePixelMargin = pexConfig.RangeField(
303 dtype=int,
304 default=10,
305 min=0,
306 doc="Pad the image by this many pixels before removing off-image "
307 "diaObjects for association.",
308 )
309 idGenerator = DetectorVisitIdGeneratorConfig.make_field()
310 doConfigureApdb = pexConfig.Field( # TODO: remove on DM-43419
311 dtype=bool,
312 default=True,
313 doc="Use the deprecated ``apdb`` sub-config to set up the APDB, "
314 "instead of the new config (``apdb_config_url``). This field is "
315 "provided for backward-compatibility ONLY and will be removed "
316 "without notice after v28.",
317 )
319 def setDefaults(self):
320 self.apdb.dia_object_index = "baseline"
321 self.apdb.dia_object_columns = []
322 self.diaCalculation.plugins = ["ap_meanPosition",
323 "ap_nDiaSources",
324 "ap_meanFlux",
325 "ap_percentileFlux",
326 "ap_sigmaFlux",
327 "ap_chi2Flux",
328 "ap_madFlux",
329 "ap_skewFlux",
330 "ap_minMaxFlux",
331 "ap_maxSlopeFlux",
332 "ap_meanErrFlux",
333 "ap_linearFit",
334 "ap_stetsonJ",
335 "ap_meanTotFlux",
336 "ap_sigmaTotFlux"]
338 # TODO: remove on DM-43419
339 def validate(self):
340 # Sidestep Config.validate to avoid validating uninitialized fields we're not using.
341 skip = {"apdb_config_url"} if self.doConfigureApdb else {"apdb"}
342 for name, field in self._fields.items():
343 if name not in skip:
344 field.validate(self)
346 # It's possible to use apdb without setting it, bypassing the deprecation warning.
347 if self.doConfigureApdb:
348 warnings.warn("Config field DiaPipelineConfig.apdb is deprecated: "
349 # Workaround for DM-44051
350 "This field has been replaced by ``apdb_config_url``; set "
351 "``doConfigureApdb=False`` to use it. Will be removed after v28.",
352 FutureWarning)
355class DiaPipelineTask(pipeBase.PipelineTask):
356 """Task for loading, associating and storing Difference Image Analysis
357 (DIA) Objects and Sources.
358 """
359 ConfigClass = DiaPipelineConfig
360 _DefaultName = "diaPipe"
362 def __init__(self, initInputs=None, **kwargs):
363 super().__init__(**kwargs)
364 if self.config.doConfigureApdb:
365 self.apdb = self.config.apdb.apply()
366 else:
367 self.apdb = daxApdb.Apdb.from_uri(self.config.apdb_config_url)
368 self.makeSubtask("diaCatalogLoader")
369 self.makeSubtask("associator")
370 self.makeSubtask("diaCalculation")
371 if self.config.doRunForcedMeasurement:
372 self.makeSubtask("diaForcedSource")
373 if self.config.doPackageAlerts:
374 self.makeSubtask("alertPackager")
375 if self.config.doSolarSystemAssociation:
376 self.makeSubtask("solarSystemAssociator")
378 def runQuantum(self, butlerQC, inputRefs, outputRefs):
379 inputs = butlerQC.get(inputRefs)
380 inputs["idGenerator"] = self.config.idGenerator.apply(butlerQC.quantum.dataId)
381 inputs["band"] = butlerQC.quantum.dataId["band"]
382 if not self.config.doSolarSystemAssociation:
383 inputs["solarSystemObjectTable"] = None
385 outputs = self.run(**inputs)
387 butlerQC.put(outputs, outputRefs)
389 @timeMethod
390 def run(self,
391 diaSourceTable,
392 solarSystemObjectTable,
393 diffIm,
394 exposure,
395 template,
396 band,
397 idGenerator):
398 """Process DiaSources and DiaObjects.
400 Load previous DiaObjects and their DiaSource history. Calibrate the
401 values in the diaSourceCat. Associate new DiaSources with previous
402 DiaObjects. Run forced photometry at the updated DiaObject locations.
403 Store the results in the Alert Production Database (Apdb).
405 Parameters
406 ----------
407 diaSourceTable : `pandas.DataFrame`
408 Newly detected DiaSources.
409 diffIm : `lsst.afw.image.ExposureF`
410 Difference image exposure in which the sources in ``diaSourceCat``
411 were detected.
412 exposure : `lsst.afw.image.ExposureF`
413 Calibrated exposure differenced with a template to create
414 ``diffIm``.
415 template : `lsst.afw.image.ExposureF`
416 Template exposure used to create diffIm.
417 band : `str`
418 The band in which the new DiaSources were detected.
419 idGenerator : `lsst.meas.base.IdGenerator`
420 Object that generates source IDs and random number generator seeds.
422 Returns
423 -------
424 results : `lsst.pipe.base.Struct`
425 Results struct with components.
427 - ``apdbMaker`` : Marker dataset to store in the Butler indicating
428 that this ccdVisit has completed successfully.
429 (`lsst.dax.apdb.ApdbConfig`)
430 - ``associatedDiaSources`` : Catalog of newly associated
431 DiaSources. (`pandas.DataFrame`)
432 """
433 # Load the DiaObjects and DiaSource history.
434 loaderResult = self.diaCatalogLoader.run(diffIm, self.apdb,
435 doLoadForcedSources=self.config.doLoadForcedSources)
436 if len(loaderResult.diaObjects) > 0:
437 diaObjects = self.purgeDiaObjects(diffIm.getBBox(), diffIm.getWcs(), loaderResult.diaObjects,
438 buffer=self.config.imagePixelMargin)
439 else:
440 diaObjects = loaderResult.diaObjects
442 # Associate new DiaSources with existing DiaObjects.
443 assocResults = self.associator.run(diaSourceTable, diaObjects,
444 exposure_time=diffIm.visitInfo.exposureTime)
446 if self.config.doSolarSystemAssociation:
447 ssoAssocResult = self.solarSystemAssociator.run(
448 assocResults.unAssocDiaSources,
449 solarSystemObjectTable,
450 diffIm)
451 createResults = self.createNewDiaObjects(
452 ssoAssocResult.unAssocDiaSources)
453 toAssociate = []
454 if len(assocResults.matchedDiaSources) > 0:
455 toAssociate.append(assocResults.matchedDiaSources)
456 if len(ssoAssocResult.ssoAssocDiaSources) > 0:
457 toAssociate.append(ssoAssocResult.ssoAssocDiaSources)
458 toAssociate.append(createResults.diaSources)
459 associatedDiaSources = pd.concat(toAssociate)
460 nTotalSsObjects = ssoAssocResult.nTotalSsObjects
461 nAssociatedSsObjects = ssoAssocResult.nAssociatedSsObjects
462 else:
463 createResults = self.createNewDiaObjects(
464 assocResults.unAssocDiaSources)
465 toAssociate = []
466 if len(assocResults.matchedDiaSources) > 0:
467 toAssociate.append(assocResults.matchedDiaSources)
468 toAssociate.append(createResults.diaSources)
469 associatedDiaSources = pd.concat(toAssociate)
470 nTotalSsObjects = 0
471 nAssociatedSsObjects = 0
473 # Create new DiaObjects from unassociated diaSources.
474 self._add_association_meta_data(assocResults.nUpdatedDiaObjects,
475 assocResults.nUnassociatedDiaObjects,
476 createResults.nNewDiaObjects,
477 nTotalSsObjects,
478 nAssociatedSsObjects)
479 # Index the DiaSource catalog for this visit after all associations
480 # have been made.
481 updatedDiaObjectIds = associatedDiaSources["diaObjectId"][
482 associatedDiaSources["diaObjectId"] != 0].to_numpy()
483 associatedDiaSources.set_index(["diaObjectId",
484 "band",
485 "diaSourceId"],
486 drop=False,
487 inplace=True)
489 # Append new DiaObjects and DiaSources to their previous history.
490 diaObjects = pd.concat(
491 [diaObjects,
492 createResults.newDiaObjects.set_index("diaObjectId", drop=False)],
493 sort=True)
494 if self.testDataFrameIndex(diaObjects):
495 raise RuntimeError(
496 "Duplicate DiaObjects created after association. This is "
497 "likely due to re-running data with an already populated "
498 "Apdb. If this was not the case then there was an unexpected "
499 "failure in Association while matching and creating new "
500 "DiaObjects and should be reported. Exiting.")
502 if len(loaderResult.diaSources) > 0:
503 # We need to coerce the types of loaderResult.diaSources
504 # to be the same as associatedDiaSources, thanks to pandas
505 # datetime issues (DM-41100). And we may as well coerce
506 # all the columns to ensure consistency for future compatibility.
507 for name, dtype in associatedDiaSources.dtypes.items():
508 if name in loaderResult.diaSources.columns and loaderResult.diaSources[name].dtype != dtype:
509 self.log.debug(
510 "Coercing loaderResult.diaSources column %s from %s to %s",
511 name,
512 str(loaderResult.diaSources[name].dtype),
513 str(dtype),
514 )
515 loaderResult.diaSources[name] = loaderResult.diaSources[name].astype(dtype)
517 mergedDiaSourceHistory = pd.concat(
518 [loaderResult.diaSources, associatedDiaSources],
519 sort=True)
520 else:
521 mergedDiaSourceHistory = pd.concat([associatedDiaSources], sort=True)
523 # Test for DiaSource duplication first. If duplicates are found,
524 # this likely means this is duplicate data being processed and sent
525 # to the Apdb.
526 if self.testDataFrameIndex(mergedDiaSourceHistory):
527 raise RuntimeError(
528 "Duplicate DiaSources found after association and merging "
529 "with history. This is likely due to re-running data with an "
530 "already populated Apdb. If this was not the case then there "
531 "was an unexpected failure in Association while matching "
532 "sources to objects, and should be reported. Exiting.")
534 # Compute DiaObject Summary statistics from their full DiaSource
535 # history.
536 diaCalResult = self.diaCalculation.run(
537 diaObjects,
538 mergedDiaSourceHistory,
539 updatedDiaObjectIds,
540 [band])
541 # Test for duplication in the updated DiaObjects.
542 if self.testDataFrameIndex(diaCalResult.diaObjectCat):
543 raise RuntimeError(
544 "Duplicate DiaObjects (loaded + updated) created after "
545 "DiaCalculation. This is unexpected behavior and should be "
546 "reported. Exiting.")
547 if self.testDataFrameIndex(diaCalResult.updatedDiaObjects):
548 raise RuntimeError(
549 "Duplicate DiaObjects (updated) created after "
550 "DiaCalculation. This is unexpected behavior and should be "
551 "reported. Exiting.")
553 if self.config.doRunForcedMeasurement:
554 # Force photometer on the Difference and Calibrated exposures using
555 # the new and updated DiaObject locations.
556 diaForcedSources = self.diaForcedSource.run(
557 diaCalResult.diaObjectCat,
558 diaCalResult.updatedDiaObjects.loc[:, "diaObjectId"].to_numpy(),
559 exposure,
560 diffIm,
561 idGenerator=idGenerator)
562 else:
563 # alertPackager needs correct columns
564 diaForcedSources = pd.DataFrame(columns=[
565 "diaForcedSourceId", "diaObjectID", "ccdVisitID", "psfFlux", "psfFluxErr",
566 "x", "y", "midpointMjdTai", "band",
567 ])
569 # Store DiaSources, updated DiaObjects, and DiaForcedSources in the
570 # Apdb.
571 self.apdb.store(
572 DateTime.now().toAstropy(),
573 diaCalResult.updatedDiaObjects,
574 associatedDiaSources,
575 diaForcedSources)
577 if self.config.doPackageAlerts:
578 if len(loaderResult.diaForcedSources) > 1:
579 # We need to coerce the types of loaderResult.diaForcedSources
580 # to be the same as associatedDiaSources, thanks to pandas
581 # datetime issues (DM-41100). And we may as well coerce
582 # all the columns to ensure consistency for future compatibility.
583 for name, dtype in diaForcedSources.dtypes.items():
584 if name in loaderResult.diaForcedSources.columns and \
585 loaderResult.diaForcedSources[name].dtype != dtype:
586 self.log.debug(
587 "Coercing loaderResult.diaForcedSources column %s from %s to %s",
588 name,
589 str(loaderResult.diaForcedSources[name].dtype),
590 str(dtype),
591 )
592 loaderResult.diaForcedSources[name] = (
593 loaderResult.diaForcedSources[name].astype(dtype)
594 )
595 diaForcedSources = pd.concat(
596 [diaForcedSources, loaderResult.diaForcedSources],
597 sort=True)
598 if self.testDataFrameIndex(diaForcedSources):
599 self.log.warning(
600 "Duplicate DiaForcedSources created after merge with "
601 "history and new sources. This may cause downstream "
602 "problems. Dropping duplicates.")
603 # Drop duplicates via index and keep the first appearance.
604 # Reset due to the index shape being slight different than
605 # expected.
606 diaForcedSources = diaForcedSources.groupby(
607 diaForcedSources.index).first()
608 diaForcedSources.reset_index(drop=True, inplace=True)
609 diaForcedSources.set_index(
610 ["diaObjectId", "diaForcedSourceId"],
611 drop=False,
612 inplace=True)
613 self.alertPackager.run(associatedDiaSources,
614 diaCalResult.diaObjectCat,
615 loaderResult.diaSources,
616 diaForcedSources,
617 diffIm,
618 exposure,
619 template,
620 doRunForcedMeasurement=self.config.doRunForcedMeasurement,
621 )
623 # For historical reasons, apdbMarker is a Config even if it's not meant to be read.
624 # A default Config is the cheapest way to satisfy the storage class.
625 marker = self.config.apdb.value if self.config.doConfigureApdb else pexConfig.Config()
626 return pipeBase.Struct(apdbMarker=marker,
627 associatedDiaSources=associatedDiaSources,
628 diaForcedSources=diaForcedSources,
629 diaObjects=diaObjects,
630 longTrailedSources=assocResults.longTrailedSources
631 )
633 def createNewDiaObjects(self, unAssocDiaSources):
634 """Loop through the set of DiaSources and create new DiaObjects
635 for unassociated DiaSources.
637 Parameters
638 ----------
639 unAssocDiaSources : `pandas.DataFrame`
640 Set of DiaSources to create new DiaObjects from.
642 Returns
643 -------
644 results : `lsst.pipe.base.Struct`
645 Results struct containing:
647 - ``diaSources`` : DiaSource catalog with updated DiaObject ids.
648 (`pandas.DataFrame`)
649 - ``newDiaObjects`` : Newly created DiaObjects from the
650 unassociated DiaSources. (`pandas.DataFrame`)
651 - ``nNewDiaObjects`` : Number of newly created diaObjects.(`int`)
652 """
653 if len(unAssocDiaSources) == 0:
654 tmpObj = self._initialize_dia_object(0)
655 newDiaObjects = pd.DataFrame(data=[],
656 columns=tmpObj.keys())
657 else:
658 newDiaObjects = unAssocDiaSources["diaSourceId"].apply(
659 self._initialize_dia_object)
660 unAssocDiaSources["diaObjectId"] = unAssocDiaSources["diaSourceId"]
661 return pipeBase.Struct(diaSources=unAssocDiaSources,
662 newDiaObjects=newDiaObjects,
663 nNewDiaObjects=len(newDiaObjects))
665 def _initialize_dia_object(self, objId):
666 """Create a new DiaObject with values required to be initialized by the
667 Ppdb.
669 Parameters
670 ----------
671 objid : `int`
672 ``diaObjectId`` value for the of the new DiaObject.
674 Returns
675 -------
676 diaObject : `dict`
677 Newly created DiaObject with keys:
679 ``diaObjectId``
680 Unique DiaObjectId (`int`).
681 ``pmParallaxNdata``
682 Number of data points used for parallax calculation (`int`).
683 ``nearbyObj1``
684 Id of the a nearbyObject in the Object table (`int`).
685 ``nearbyObj2``
686 Id of the a nearbyObject in the Object table (`int`).
687 ``nearbyObj3``
688 Id of the a nearbyObject in the Object table (`int`).
689 ``?_psfFluxNdata``
690 Number of data points used to calculate point source flux
691 summary statistics in each bandpass (`int`).
692 """
693 new_dia_object = {"diaObjectId": objId,
694 "pmParallaxNdata": 0,
695 "nearbyObj1": 0,
696 "nearbyObj2": 0,
697 "nearbyObj3": 0}
698 for f in ["u", "g", "r", "i", "z", "y"]:
699 new_dia_object["%s_psfFluxNdata" % f] = 0
700 return pd.Series(data=new_dia_object)
702 def testDataFrameIndex(self, df):
703 """Test the sorted DataFrame index for duplicates.
705 Wrapped as a separate function to allow for mocking of the this task
706 in unittesting. Default of a mock return for this test is True.
708 Parameters
709 ----------
710 df : `pandas.DataFrame`
711 DataFrame to text.
713 Returns
714 -------
715 `bool`
716 True if DataFrame contains duplicate rows.
717 """
718 return df.index.has_duplicates
720 def _add_association_meta_data(self,
721 nUpdatedDiaObjects,
722 nUnassociatedDiaObjects,
723 nNewDiaObjects,
724 nTotalSsObjects,
725 nAssociatedSsObjects):
726 """Store summaries of the association step in the task metadata.
728 Parameters
729 ----------
730 nUpdatedDiaObjects : `int`
731 Number of previous DiaObjects associated and updated in this
732 ccdVisit.
733 nUnassociatedDiaObjects : `int`
734 Number of previous DiaObjects that were not associated or updated
735 in this ccdVisit.
736 nNewDiaObjects : `int`
737 Number of newly created DiaObjects for this ccdVisit.
738 nTotalSsObjects : `int`
739 Number of SolarSystemObjects within the observable detector
740 area.
741 nAssociatedSsObjects : `int`
742 Number of successfully associated SolarSystemObjects.
743 """
744 self.metadata.add('numUpdatedDiaObjects', nUpdatedDiaObjects)
745 self.metadata.add('numUnassociatedDiaObjects', nUnassociatedDiaObjects)
746 self.metadata.add('numNewDiaObjects', nNewDiaObjects)
747 self.metadata.add('numTotalSolarSystemObjects', nTotalSsObjects)
748 self.metadata.add('numAssociatedSsObjects', nAssociatedSsObjects)
750 def purgeDiaObjects(self, bbox, wcs, diaObjCat, buffer=0):
751 """Drop diaObjects that are outside the exposure bounding box.
753 Parameters
754 ----------
755 bbox : `lsst.geom.Box2I`
756 Bounding box of the exposure.
757 wcs : `lsst.afw.geom.SkyWcs`
758 Coordinate system definition (wcs) for the exposure.
759 diaObjCat : `pandas.DataFrame`
760 DiaObjects loaded from the Apdb.
761 buffer : `int`, optional
762 Width, in pixels, to pad the exposure bounding box.
764 Returns
765 -------
766 diaObjCat : `pandas.DataFrame`
767 DiaObjects loaded from the Apdb, restricted to the exposure
768 bounding box.
769 """
770 try:
771 bbox.grow(buffer)
772 raVals = diaObjCat.ra.to_numpy()
773 decVals = diaObjCat.dec.to_numpy()
774 xVals, yVals = wcs.skyToPixelArray(raVals, decVals, degrees=True)
775 selector = bbox.contains(xVals, yVals)
776 nPurged = np.sum(~selector)
777 if nPurged > 0:
778 diaObjCat = diaObjCat[selector].copy()
779 self.log.info(f"Dropped {nPurged} diaObjects that were outside the bbox "
780 f"leaving {len(diaObjCat)} in the catalog")
781 except Exception as e:
782 self.log.warning("Error attempting to check diaObject history: %s", e)
783 return diaObjCat