23 "SingleBandMeasurementDriverConfig",
24 "SingleBandMeasurementDriverTask",
25 "MultiBandMeasurementDriverConfig",
26 "MultiBandMeasurementDriverTask",
27 "ForcedMeasurementDriverConfig",
28 "ForcedMeasurementDriverTask",
33from abc
import ABCMeta, abstractmethod
38import lsst.afw.image
as afwImage
45import lsst.meas.extensions.scarlet
as scarlet
49from lsst.meas.extensions.scarlet.deconvolveExposureTask
import DeconvolveExposureTask
52logging.basicConfig(level=logging.INFO)
56 """Base configuration for measurement driver tasks.
58 This class provides foundational configuration for its subclasses to handle
59 single-band and multi-band data. It defines variance scaling, detection,
60 deblending, measurement, aperture correction, and catalog calculation
61 subtasks, which are intended to be executed in sequence by the driver task.
64 doScaleVariance = Field[bool](doc=
"Scale variance plane using empirical noise?", default=
False)
67 doc=
"Subtask to rescale variance plane", target=measAlgorithms.ScaleVarianceTask
70 doDetect = Field[bool](doc=
"Run the source detection algorithm?", default=
True)
73 doc=
"Subtask to detect sources in the image", target=measAlgorithms.SourceDetectionTask
76 doDeblend = Field[bool](doc=
"Run the source deblending algorithm?", default=
True)
79 doMeasure = Field[bool](doc=
"Run the source measurement algorithm?", default=
True)
82 doc=
"Subtask to measure sources and populate the output catalog",
83 target=measBase.SingleFrameMeasurementTask,
86 psfCache = Field[int](
87 doc=
"Maximum number of PSFs to cache, preventing repeated PSF evaluations at the same "
88 "point across different measurement plugins. Defaults to -1, which auto-sizes the cache "
89 "based on the plugin count.",
93 checkUnitsParseStrict = Field[str](
94 doc=
"Strictness of Astropy unit compatibility check, can be 'raise', 'warn' or 'silent'",
98 doApCorr = Field[bool](
99 doc=
"Apply aperture corrections? If yes, your image must have an aperture correction map",
104 doc=
"Subtask to apply aperture corrections",
105 target=measBase.ApplyApCorrTask,
108 doRunCatalogCalculation = Field[bool](doc=
"Run catalogCalculation task?", default=
False)
111 doc=
"Subtask to run catalogCalculation plugins on catalog", target=measBase.CatalogCalculationTask
120 "doRunCatalogCalculation",
124 """Ensure that at least one processing step is enabled."""
127 if not any(getattr(self, opt)
for opt
in self.
doOptions):
128 raise ValueError(f
"At least one of these options must be enabled: {self.doOptions}")
131 raise ValueError(
"Aperture correction requires measurement to be enabled.")
134 raise ValueError(
"Catalog calculation requires measurement to be enabled.")
138 """Base class for the mid-level driver running variance scaling, detection,
139 deblending, measurement, apperture correction, and catalog calculation in
142 Users don't need to Butlerize their input data, which is a significant
143 advantage for quick data exploration and testing. This driver simplifies
144 the process of applying measurement algorithms to images by abstracting
145 away low-level implementation details such as Schema and table boilerplate.
146 It's a convenient way to process images into catalogs with a user-friendly
147 interface for non-developers while allowing extensive configuration and
148 integration into unit tests for developers. It also considerably improves
149 how demos and workflows are showcased in Jupyter notebooks.
154 Schema used to create the output `~lsst.afw.table.SourceCatalog`,
155 modified in place with fields that will be written by this task.
157 Schema of Footprint Peaks that will be passed to the deblender.
159 Additional kwargs to pass to lsst.pipe.base.Task.__init__()
163 Subclasses (e.g., single-band vs. multi-band) share most methods and config
164 options but differ in handling and validating inputs by overriding the base
165 config class and any methods that require their own logic.
168 ConfigClass = MeasurementDriverBaseConfig
169 _DefaultName =
"measurementDriverBase"
185 self.
deblend: measDeblender.SourceDeblendTask | scarlet.ScarletDeblendTask
186 self.
measurement: measBase.SingleFrameMeasurementTask | measBase.ForcedMeasurementTask
195 super().
__setattr__(
"initSchema", copy.deepcopy(schema))
198 """Prevent external modifications of the initial Schema."""
199 if name ==
"initSchema":
200 raise AttributeError(f
"Cannot modify {name} directly")
204 def run(self, *args, **kwargs) -> pipeBase.Struct:
205 """Run the measurement driver task. Subclasses must implement this
206 method using their own logic to handle single-band or multi-band data.
208 raise NotImplementedError(
"This is not implemented on the base class")
212 catalog: afwTable.SourceCatalog |
None,
214 """Perform validation and adjustments of inputs without heavy
220 Catalog to be extended by the driver task.
226 if catalog
is not None:
228 "An input catalog was given to bypass detection, but 'doDetect' is still on"
232 raise RuntimeError(
"Cannot run without detection if no 'catalog' is provided")
235 """Initialize the Schema to be used for constructing the subtasks.
237 Though it may seem clunky, this workaround is necessary to ensure
238 Schema consistency across all subtasks.
243 Catalog from which to extract the Schema. If not provided, the
244 user-provided Schema and if that is also not provided during
245 initialization, a minimal Schema will be used.
257 self.
schema = afwTable.SourceTable.makeMinimalSchema()
262 if self.
schema is not None:
264 "Both a catalog and a Schema were provided; using the Schema from the catalog only"
268 catalogSchema = catalog.schema
281 self.
mapper.addMinimalSchema(catalogSchema,
True)
287 if isinstance(self, ForcedMeasurementDriverTask):
291 self.
schema.addField(
"deblend_nChild",
"I",
"Needed for minimal forced photometry schema")
294 """Add coordinate error fields to the schema in-place if they are not
300 Schema to be checked for coordinate error fields.
303 errorField
in schema.getNames()
304 for errorField
in (
"coord_raErr",
"coord_decErr",
"coord_ra_dec_Cov")
306 afwTable.CoordKey.addErrorFields(schema)
309 """Construct subtasks based on the configuration and the Schema."""
310 if self.
schema is None and any(
311 getattr(self.
config, attr)
for attr
in self.
config.doOptions
if attr !=
"doScaleVariance"
314 "Cannot create requested subtasks without a Schema; "
315 "ensure one is provided explicitly or via a catalog"
318 if self.
config.doScaleVariance:
319 self.makeSubtask(
"scaleVariance")
321 if isinstance(self, ForcedMeasurementDriverTask):
324 self.makeSubtask(
"measurement", refSchema=self.
schema)
336 self.makeSubtask(
"applyApCorr", schema=self.
measurement.schema)
340 if self.
config.doRunCatalogCalculation:
341 self.makeSubtask(
"catalogCalculation", schema=self.
measurement.schema)
344 self.makeSubtask(
"detection", schema=self.
schema)
350 self.makeSubtask(
"measurement", schema=self.
schema)
353 self.makeSubtask(
"applyApCorr", schema=self.
measurement.schema)
355 if self.
config.doRunCatalogCalculation:
356 self.makeSubtask(
"catalogCalculation", schema=self.
schema)
359 self, catalog: afwTable.SourceCatalog |
None
361 """Ensure subtasks are properly initialized according to the
362 configuration and the provided catalog.
367 Optional catalog to be used for initializing the Schema and the
373 Updated catalog to be passed to the subtasks, if it was provided.
383 self.
schema.checkUnits(parse_strict=self.
config.checkUnitsParseStrict)
392 """Scale the variance plane of an exposure to match the observed
398 Exposure on which to run the variance scaling algorithm.
400 Band associated with the exposure. Used for logging.
402 self.log.info(f
"Scaling variance plane for {band} band")
404 exposure.getMetadata().add(
"VARIANCE_SCALE", varScale)
409 """Make a catalog or catalogs contiguous if they are not already.
414 Catalog or dictionary of catalogs with bands as keys to be made
420 Contiguous catalog or dictionary of contiguous catalogs.
422 if isinstance(catalog, dict):
423 for band, cat
in catalog.items():
424 if not cat.isContiguous():
425 self.log.info(f
"{band}-band catalog is not contiguous; making it contiguous")
426 catalog[band] = cat.copy(deep=
True)
428 if not catalog.isContiguous():
429 self.log.info(
"Catalog is not contiguous; making it contiguous")
430 catalog = catalog.copy(deep=
True)
434 """Update the Schema of the provided catalog to incorporate changes
435 made by the configured subtasks.
440 Catalog to be updated with the Schema changes.
445 Catalog with the updated Schema.
453 updatedCatalog.extend(catalog, mapper=self.
mapper)
457 return updatedCatalog
462 """Run the detection subtask to identify sources in the image.
467 Exposure on which to run the detection algorithm.
469 Generator for unique source IDs.
474 A catalog containing detected sources.
476 A list of background models obtained from the detection process,
479 self.log.info(f
"Running detection on a {exposure.width}x{exposure.height} pixel exposure")
483 table = afwTable.SourceTable.make(self.
schema, idGenerator.make_table_id_factory())
486 detections = self.
detection.run(table, exposure)
487 catalog = detections.sources
491 if hasattr(detections,
"background")
and detections.background:
492 for bg
in detections.background:
493 backgroundList.append(bg)
495 return catalog, backgroundList
499 """Run the deblending subtask to separate blended sources. Subclasses
500 must implement this method to handle task-specific deblending logic.
502 raise NotImplementedError(
"This is not implemented on the base class")
506 exposure: afwImage.Exposure,
508 idGenerator: measBase.IdGenerator,
511 """Run the measurement subtask to compute properties of sources.
516 Exposure on which to run the measurement algorithm.
518 Catalog containing sources on which to run the measurement subtask.
520 Generator for unique source IDs.
522 Reference catalog to be used for forced measurements, if any.
523 If not provided, the measurement will be run on the sources in the
524 catalog in a standard manner without reference.
529 refWcs = exposure.getWcs()
536 exposureId=idGenerator.catalog_id,
540 self.
measurement.run(measCat=catalog, exposure=exposure, exposureId=idGenerator.catalog_id)
545 """Apply aperture corrections to the catalog.
550 Exposure on which to apply aperture corrections.
552 Catalog to be corrected using the aperture correction map from
555 Generator for unique source IDs.
557 apCorrMap = exposure.getInfo().getApCorrMap()
558 if apCorrMap
is None:
560 "Image does not have valid aperture correction map for catalog id "
561 f
"{idGenerator.catalog_id}; skipping aperture correction"
564 self.
applyApCorr.run(catalog=catalog, apCorrMap=apCorrMap)
567 """Run the catalog calculation plugins on the catalog.
572 Catalog to be processed by the catalog calculation subtask.
578 exposure: afwImage.Exposure,
580 idGenerator: measBase.IdGenerator,
581 band: str =
"a single",
584 """Process a catalog through measurement, aperture correction, and
585 catalog calculation subtasks.
590 Exposure associated with the catalog.
592 Catalog to be processed by the subtasks.
594 Generator for unique source IDs.
596 Band associated with the exposure and catalog. Used for logging.
598 Reference catalog for forced measurements. If not provided, the
599 measurement will be run on the sources in the catalog in a standard
600 manner without reference.
605 Catalog after processing through the configured subtasks.
609 if self.
config.psfCache > 0:
611 exposure.psf.setCacheCapacity(self.
config.psfCache)
619 exposure.psf.setCacheCapacity(2 * len(self.
config.measurement.plugins.names))
624 f
"Measuring {len(catalog)} sources in {band} band "
625 f
"using '{self.measurement.__class__.__name__}'"
634 self.log.info(f
"Applying aperture corrections to {band} band")
638 if self.
config.doRunCatalogCalculation:
639 self.log.info(f
"Running catalog calculation on {band} band")
643 f
"Finished processing for {band} band; output catalog has {catalog.schema.getFieldCount()} "
644 f
"fields and {len(catalog)} records"
651 """Configuration for the single-band measurement driver task."""
653 deblend =
ConfigurableField(target=measDeblender.SourceDeblendTask, doc=
"Deblender for single-band data.")
657 """Mid-level driver for processing single-band data.
659 Offers a helper method for direct handling of raw image data in addition to
660 the standard single-band exposure.
664 Here is an example of how to use this class to run variance scaling,
665 detection, deblending, and measurement on a single-band exposure:
667 >>> from lsst.pipe.tasks.measurementDriver import (
668 ... SingleBandMeasurementDriverConfig,
669 ... SingleBandMeasurementDriverTask,
671 >>> import lsst.meas.extensions.shapeHSM # To register its plugins
672 >>> config = SingleBandMeasurementDriverConfig()
673 >>> config.doScaleVariance = True
674 >>> config.doDetect = True
675 >>> config.doDeblend = True
676 >>> config.doMeasure = True
677 >>> config.scaleVariance.background.binSize = 64
678 >>> config.detection.thresholdValue = 5.5
679 >>> config.deblend.tinyFootprintSize = 3
680 >>> config.measurement.plugins.names |= [
681 ... "base_SdssCentroid",
682 ... "base_SdssShape",
683 ... "ext_shapeHSM_HsmSourceMoments",
685 >>> config.measurement.slots.psfFlux = None
686 >>> config.measurement.doReplaceWithNoise = False
687 >>> exposure = butler.get("deepCoadd", dataId=...)
688 >>> driver = SingleBandMeasurementDriverTask(config=config)
689 >>> results = driver.run(exposure)
690 >>> results.catalog.writeFits("meas_catalog.fits")
692 Alternatively, if an exposure is not available, the driver can also process
701 >>> results = driver.runFromImage(
702 ... image, mask, variance, wcs, psf, photoCalib
704 >>> results.catalog.writeFits("meas_catalog.fits")
707 ConfigClass = SingleBandMeasurementDriverConfig
708 _DefaultName =
"singleBandMeasurementDriver"
709 _Deblender =
"meas_deblender"
719 exposure: afwImage.Exposure,
721 idGenerator: measBase.IdGenerator |
None =
None,
722 ) -> pipeBase.Struct:
723 """Process a single-band exposure through the configured subtasks and
724 return the results as a struct.
729 The exposure on which to run the driver task.
731 Catalog to be extended by the driver task. If not provided, an
732 empty catalog will be created and populated.
734 Object that generates source IDs and provides random seeds.
739 Results as a struct with attributes:
742 Catalog containing the measured sources
743 (`~lsst.afw.table.SourceCatalog`).
745 List of backgrounds (`list[~lsst.afw.math.Background]`). Only
746 populated if detection is enabled.
756 if idGenerator
is None:
757 idGenerator = measBase.IdGenerator()
761 if self.
config.doScaleVariance:
766 catalog, backgroundList = self.
_detectSources(exposure, idGenerator)
768 self.log.info(
"Skipping detection; using detections from the provided catalog")
769 backgroundList =
None
775 self.log.info(
"Skipping deblending")
781 return pipeBase.Struct(catalog=catalog, backgroundList=backgroundList)
789 psf: afwDetection.Psf | np.ndarray =
None,
792 idGenerator: measBase.IdGenerator =
None,
793 ) -> pipeBase.Struct:
794 """Convert image data to an `Exposure`, then run it through the
800 Input image data. Will be converted into an `Exposure` before
803 Mask data for the image. Used if ``image`` is a bare `array` or
806 Variance plane data for the image.
808 World Coordinate System to associate with the exposure that will
809 be created from ``image``.
811 PSF model for the exposure.
813 Photometric calibration model for the exposure.
815 Catalog to be extended by the driver task. If not provided, a new
816 catalog will be created during detection and populated.
818 Generator for unique source IDs.
823 Results as a struct with attributes:
826 Catalog containing the measured sources
827 (`~lsst.afw.table.SourceCatalog`).
829 List of backgrounds (`list[~lsst.afw.math.Background]`).
832 if isinstance(image, np.ndarray):
833 image = afwImage.makeImageFromArray(image)
834 if isinstance(mask, np.ndarray):
835 mask = afwImage.makeMaskFromArray(mask)
836 if isinstance(variance, np.ndarray):
837 variance = afwImage.makeImageFromArray(variance)
846 raise TypeError(f
"Unsupported 'image' type: {type(image)}")
849 if isinstance(psf, np.ndarray):
854 psf = afwDetection.KernelPsf(kernel)
855 elif not isinstance(psf, afwDetection.Psf):
856 raise TypeError(f
"Unsupported 'psf' type: {type(psf)}")
859 if photoCalib
is not None:
860 exposure.setPhotoCalib(photoCalib)
862 return self.
run(exposure, catalog=catalog, idGenerator=idGenerator)
867 """Run single-band deblending given an exposure and a catalog.
872 Exposure on which to run the deblending algorithm.
874 Catalog containing sources to be deblended.
879 Catalog after deblending, with sources separated into their
880 individual components if they were deblended.
882 self.log.info(f
"Deblending using '{self._Deblender}' on {len(catalog)} detection footprints")
883 self.
deblend.run(exposure=exposure, sources=catalog)
890 """Configuration for the multi-band measurement driver task."""
893 target=scarlet.ScarletDeblendTask, doc=
"Scarlet deblender for multi-band data"
896 doConserveFlux = Field[bool](
897 doc=
"Whether to use the deblender models as templates to re-distribute the flux from "
898 "the 'exposure' (True), or to perform measurements on the deblender model footprints.",
902 measureOnlyInRefBand = Field[bool](
903 doc=
"If True, all measurements downstream of deblending run only in the reference band that "
904 "was used for detection; otherwise, they are performed in all available bands, generating a "
905 "catalog for each. Regardless of this setting, deblending still uses all available bands.",
909 removeScarletData = Field[bool](
910 doc=
"Whether or not to remove `ScarletBlendData` for each blend in order to save memory. "
911 "If set to True, some sources may end up with missing footprints in catalogs other than the "
912 "reference-band catalog, leading to failures in subsequent measurements that require footprints. "
913 "For example, keep this False if `measureOnlyInRefBand` is set to False and "
914 "`measurement.doReplaceWithNoise` to True, in order to make the footprints available in "
915 "non-reference bands in addition to the reference band.",
919 updateFluxColumns = Field[bool](
920 doc=
"Whether or not to update the `deblend_*` columns in the catalog. This should only be "
921 "True when the input catalog schema already contains those columns.",
927 """Mid-level driver for processing multi-band data.
929 The default behavior is to run detection on the reference band, use all
930 available bands for deblending, and then process everything downstream
931 separately for each band making per-band catalogs unless configured
932 otherwise. This subclass provides functionality for handling a singe-band
933 exposure and a list of single-band exposures in addition to a standard
938 Here is an example of how to use this class to run variance scaling,
939 detection, deblending, measurement, and aperture correction on a multi-band
942 >>> from lsst.afw.image import MultibandExposure
943 >>> from lsst.pipe.tasks.measurementDriver import (
944 ... MultiBandMeasurementDriverConfig,
945 ... MultiBandMeasurementDriverTask,
947 >>> import lsst.meas.extensions.shapeHSM # To register its plugins
948 >>> config = MultiBandMeasurementDriverConfig()
949 >>> config.doScaleVariance = True
950 >>> config.doDetect = True
951 >>> config.doDeblend = True
952 >>> config.doMeasure = True
953 >>> config.doApCorr = True
954 >>> config.scaleVariance.background.binSize = 64
955 >>> config.detection.thresholdValue = 5.5
956 >>> config.deblend.minSNR = 42.0
957 >>> config.deblend.maxIter = 20
958 >>> config.measurement.plugins.names |= [
959 ... "base_SdssCentroid",
960 ... "base_SdssShape",
961 ... "ext_shapeHSM_HsmSourceMoments",
963 >>> config.measurement.slots.psfFlux = None
964 >>> config.measurement.doReplaceWithNoise = False
965 >>> config.applyApCorr.doFlagApCorrFailures = False
966 >>> mExposure = MultibandExposure.fromButler(
967 ... butler, ["g", "r", "i"], "deepCoadd_calexp", ...
969 >>> driver = MultiBandMeasurementDriverTask(config=config)
970 >>> results = driver.run(mExposure, "r")
971 >>> for band, catalog in results.catalogs.items():
972 ... catalog.writeFits(f"meas_catalog_{band}.fits")
975 ConfigClass = MultiBandMeasurementDriverConfig
976 _DefaultName =
"multiBandMeasurementDriver"
977 _Deblender =
"scarlet"
992 refBand: str |
None =
None,
993 bands: list[str] |
None =
None,
995 idGenerator: measBase.IdGenerator =
None,
996 ) -> pipeBase.Struct:
997 """Process an exposure through the configured subtasks while using
998 multi-band information for deblending.
1003 Multi-band data containing images of the same shape and region of
1004 the sky. May be a `MultibandExposure`, a single-band exposure
1005 (i.e., `Exposure`), or a list of single-band exposures associated
1006 with different bands in which case ``bands`` must be provided. If a
1007 single-band exposure is given, it will be treated as a
1008 `MultibandExposure` that contains only that one band whose name may
1009 be "unknown" unless either ``bands`` or ``refBand`` is provided.
1011 Multi-band deconvolved images of the same shape and region of the
1012 sky. Follows the same type conventions as ``mExposure``. If not
1013 provided, the deblender will run the deconvolution internally
1014 using the provided ``mExposure``.
1016 Reference band to use for detection. Not required for single-band
1017 exposures. If `measureOnlyInRefBand` is enabled while detection is
1018 disabled and a catalog of detected sources is provided, this
1019 should specify the band the sources were detected on (or the band
1020 you want to use to perform measurements on exclusively). If
1021 `measureOnlyInRefBand` is disabled instead in the latter scenario,
1022 ``refBand`` does not need to be provided.
1024 List of bands associated with the exposures in ``mExposure``. Only
1025 required if ``mExposure`` is a list of single-band exposures. If
1026 provided for a multi-band exposure, it will be used to only process
1027 that subset of bands from the available ones in the exposure.
1029 Catalog to be extended by the driver task. If not provided, a new
1030 catalog will be created and populated.
1032 Generator for unique source IDs.
1037 Results as a struct with attributes:
1040 Dictionary of catalogs containing the measured sources with
1041 bands as keys (`dict[str, ~lsst.afw.table.SourceCatalog]`). If
1042 `measureOnlyInRefBand` is enabled or deblending is disabled,
1043 this will only contain the reference-band catalog; otherwise,
1044 it will contain a catalog for each band.
1046 List of backgrounds (`list[~lsst.afw.math.Background]`). Will
1047 be None if detection is disabled.
1049 Multiband scarlet models produced during deblending
1050 (`~lsst.scarlet.lite.io.ScarletModelData`). Will be None if
1051 deblending is disabled.
1056 mExposure, mDeconvolved, refBand, bands, catalog
1063 if idGenerator
is None:
1064 idGenerator = measBase.IdGenerator()
1068 if self.
config.doScaleVariance:
1070 for band
in mExposure.bands:
1075 catalog, backgroundList = self.
_detectSources(mExposure[refBand], idGenerator)
1077 self.log.info(
"Skipping detection; using detections from provided catalog")
1078 backgroundList =
None
1081 if self.
config.doDeblend:
1085 "Skipping deblending; proceeding with the provided catalog in the reference band"
1087 catalogs = {refBand: catalog}
1092 for band, catalog
in catalogs.items():
1093 exposure = mExposure[band]
1094 self.
_processCatalog(exposure, catalog, idGenerator, band=f
"'{band}'")
1096 return pipeBase.Struct(catalogs=catalogs, backgroundList=backgroundList, modelData=self.
modelData)
1102 refBand: str |
None,
1103 bands: list[str] |
None,
1106 """Perform validation and adjustments of inputs without heavy
1112 Multi-band data to be processed by the driver task.
1114 Multi-band deconvolved data to be processed by the driver task.
1116 Reference band to use for detection.
1118 List of bands associated with the exposures in ``mExposure``.
1120 Catalog to be extended by the driver task.
1125 Multi-band exposure to be processed by the driver task. If the
1126 input was not already a `MultibandExposure` (optionally with the
1127 relevant ``bands``), it is converted into one and returned
1128 here; otherwise, the original input is returned unchanged.
1130 Multi-band deconvolved exposure to be processed by the driver task.
1131 Same adjustments apply as for ``mExposure`` except that it is
1132 optional and may be returned as None if not provided as input.
1134 Reference band to use for detection after potential adjustments.
1135 If not provided in the input, and only one band is set to be
1136 processed, ``refBand`` will be chosen to be the only existing band
1137 in the ``bands`` list, or `mExposure.bands`, and if neither is
1138 provided, it will be set to "unknown" for single-band exposures
1139 processed by this multi-band driver.
1141 List of bands associated with the exposures in ``mExposure`` after
1142 potential adjustments. If not provided in the input, it will be set
1143 to a list containing only the provided (or inferred as "unknown")
1152 if bands
is not None:
1153 if not isinstance(bands, list):
1154 raise TypeError(f
"Expected 'bands' to be a list, got {type(bands)}")
1155 if not all(isinstance(b, str)
for b
in bands):
1156 raise TypeError(f
"All elements in 'bands' must be strings, got {[type(b) for b in bands]}")
1158 if refBand
is not None:
1159 if not isinstance(refBand, str):
1160 raise TypeError(f
"Reference band must be a string, got {type(refBand)}")
1164 if bands
is not None:
1165 if any(b
not in mExposure.bands
for b
in bands):
1167 f
"Some of the provided {bands=} are not present in {mExposure.bands=}"
1170 f
"Using {bands=} out of the available {mExposure.bands=} in the multi-band exposures"
1172 elif isinstance(mExposure, list):
1174 raise ValueError(
"The 'bands' list must be provided if 'mExposure' is a list")
1175 if len(bands) != len(mExposure):
1176 raise ValueError(
"Number of bands and exposures must match")
1178 if bands
is not None:
1181 f
"{bands=}, if provided, must only contain a single band "
1182 "if 'mExposure' is a single-band exposure"
1185 raise TypeError(f
"Unsupported 'mExposure' type: {type(mExposure)}")
1190 if bands
is not None:
1191 if any(b
not in mDeconvolved.bands
for b
in bands):
1193 f
"Some of the provided {bands=} are not present in {mDeconvolved.bands=}"
1195 elif isinstance(mDeconvolved, list):
1197 raise ValueError(
"The 'bands' list must be provided if 'mDeconvolved' is a list")
1198 if len(bands) != len(mDeconvolved):
1199 raise ValueError(
"Number of bands and deconvolved exposures must match")
1201 if bands
is not None:
1204 f
"{bands=}, if provided, must only contain a single band "
1205 "if 'mDeconvolved' is a single-band exposure"
1208 raise TypeError(f
"Unsupported {type(mDeconvolved)=}")
1211 if bands
is not None and len(bands) != 1:
1213 f
"{bands=}, if provided, must only contain a single band "
1214 "if one of 'mExposure' or 'mDeconvolved' is a single-band exposure"
1216 if bands
is None and refBand
is None:
1219 elif bands
is None and refBand
is not None:
1221 elif bands
is not None and refBand
is None:
1230 if mExposure.bands != mDeconvolved.bands:
1232 "The bands in 'mExposure' and 'mDeconvolved' must match; "
1233 f
"got {mExposure.bands} and {mDeconvolved.bands}"
1236 if len(mExposure.bands) == 1:
1241 self.log.info(f
"Running '{self._Deblender}' in single-band mode; make sure it was intended!")
1243 refBand = mExposure.bands[0]
1245 "No reference band provided for single-band data; "
1246 f
"using the only available band ('{refBand}') as the reference band"
1250 if self.
config.measureOnlyInRefBand:
1251 measInfo =
"and everything downstream of deblending"
1254 "while subtasks downstream of deblending will be run in each of "
1255 f
"the {mExposure.bands} bands"
1257 self.log.info(f
"Using '{refBand}' as the reference band for detection {measInfo}")
1261 raise ValueError(
"Reference band must be provided for multi-band data")
1263 if refBand
not in mExposure.bands:
1264 raise ValueError(f
"Requested {refBand=} is not in {mExposure.bands=}")
1266 if bands
is not None and refBand
not in bands:
1267 raise ValueError(f
"Reference {refBand=} is not in {bands=}")
1269 return mExposure, mDeconvolved, refBand, bands
1273 mExposure: afwImage.MultibandExposure,
1278 """Run multi-band deblending given a multi-band exposure and a catalog.
1283 Multi-band exposure on which to run the deblending algorithm.
1285 Multi-band deconvolved exposure to use for deblending. If None,
1286 the deblender will create it internally using the provided
1289 Catalog containing sources to be deblended.
1291 Reference band used for detection or the band to use for
1292 measurements if `measureOnlyInRefBand` is enabled.
1297 Dictionary of catalogs containing the deblended sources. If
1298 `measureOnlyInRefBand` is enabled, this will only contain the
1299 reference-band catalog; otherwise, it will contain a catalog for
1302 Multiband scarlet models produced during deblending.
1304 self.log.info(f
"Deblending using '{self._Deblender}' on {len(catalog)} detection footprints")
1306 if mDeconvolved
is None:
1308 deconvolvedCoadds = []
1309 deconvolveTask = DeconvolveExposureTask()
1310 for coadd
in mExposure:
1311 deconvolvedCoadd = deconvolveTask.run(coadd, catalog).deconvolved
1312 deconvolvedCoadds.append(deconvolvedCoadd)
1313 mDeconvolved = afwImage.MultibandExposure.fromExposures(mExposure.bands, deconvolvedCoadds)
1316 result = self.
deblend.run(mExposure, mDeconvolved, catalog)
1317 catalog = result.deblendedCatalog
1318 modelData = result.scarletModelData
1321 bands = [refBand]
if self.
config.measureOnlyInRefBand
else mExposure.bands
1323 catalogs = {band: catalog.copy(deep=
True)
for band
in bands}
1326 imageForRedistribution = mExposure[band]
if self.
config.doConserveFlux
else None
1327 scarlet.io.updateCatalogFootprints(
1328 modelData=modelData,
1329 catalog=catalogs[band],
1331 imageForRedistribution=imageForRedistribution,
1332 removeScarletData=self.
config.removeScarletData,
1333 updateFluxColumns=self.
config.updateFluxColumns,
1341 bands: list[str] |
None,
1343 """Convert a single-band exposure or a list of single-band exposures to
1344 a `MultibandExposure` if not already of that type.
1346 No conversion will be done if ``mExposureData`` is already a
1347 `MultibandExposure` except it will be subsetted to the bands provided.
1352 Input multi-band data.
1354 List of bands associated with the exposures in ``mExposure``. Only
1355 required if ``mExposure`` is a list of single-band exposures. If
1356 provided while ``mExposureData`` is a ``MultibandExposure``, it
1357 will be used to select a specific subset of bands from the
1363 Converted multi-band exposure.
1366 if bands
and not set(bands).issubset(mExposureData.bands):
1368 f
"Requested bands {bands} are not a subset of available bands: {mExposureData.bands}"
1370 return mExposureData[bands,]
if bands
and len(bands) > 1
else mExposureData
1371 elif isinstance(mExposureData, list):
1372 mExposure = afwImage.MultibandExposure.fromExposures(bands, mExposureData)
1376 mExposure = afwImage.MultibandExposure.fromExposures(bands, [mExposureData])
1382 for band, exposure
in zip(bands, mExposureData):
1383 mExposure[band].setWcs(exposure.getWcs())
1389 """Configuration for the forced measurement driver task."""
1392 target=measBase.ForcedMeasurementTask,
1393 doc=
"Measurement task for forced measurements. This should be a "
1394 "measurement task that does not perform detection.",
1398 """Set default values for the configuration.
1400 This method overrides the base class method to ensure that `doDetect`
1401 is set to `False` by default, as this task is intended for forced
1402 measurements where detection is not performed. Also, it sets some
1403 default measurement plugins by default.
1411 "base_TransformedCentroidFromCoord",
1413 "base_CircularApertureFlux",
1417 """Validate the configuration.
1419 This method overrides the base class validation to ensure that
1420 `doDetect` is set to `False`, as this task is intended for forced
1421 measurements where detection is not performed.
1426 "ForcedMeasurementDriverTask should not perform detection or "
1427 "deblending; set doDetect=False and doDeblend=False"
1430 raise ValueError(
"ForcedMeasurementDriverTask must perform measurements; set doMeasure=True")
1434 """Forced measurement driver task for single-band data.
1436 This task is the 'forced' version of the `SingleBandMeasurementDriverTask`,
1437 intended as a convenience function for performing forced photometry on an
1438 input image given a set of IDs and RA/Dec coordinates. It is designed as a
1439 public-facing interface, allowing users to measure sources without
1440 explicitly instantiating and running pipeline tasks.
1444 Here is an example of how to use this class to run forced measurements on
1445 an exposure using an Astropy table containing source IDs and RA/Dec
1448 >>> from lsst.pipe.tasks.measurementDriver import (
1449 ... ForcedMeasurementDriverConfig,
1450 ... ForcedMeasurementDriverTask,
1452 >>> import astropy.table
1453 >>> import lsst.afw.image as afwImage
1454 >>> config = ForcedMeasurementDriverConfig()
1455 >>> config.doScaleVariance = True
1456 >>> config.scaleVariance.background.binSize = 32
1457 >>> config.doApCorr = True
1458 >>> config.measurement.plugins.names = [
1459 ... "base_PixelFlags",
1460 ... "base_TransformedCentroidFromCoord",
1462 ... "base_CircularApertureFlux",
1464 >>> config.measurement.slots.psfFlux = "base_PsfFlux"
1465 >>> config.measurement.slots.centroid = "base_TransformedCentroidFromCoord"
1466 >>> config.measurement.slots.shape = None
1467 >>> config.measurement.doReplaceWithNoise = False
1468 >>> calexp = butler.get("deepCoadd_calexp", dataId=...)
1469 >>> objtable = butler.get(
1470 ... "objectTable", dataId=..., storageClass="ArrowAstropy"
1472 >>> table = objtable[:5].copy()["objectId", "coord_ra", "coord_dec"]
1473 >>> driver = ForcedMeasurementDriverTask(config=config)
1474 >>> results = driver.runFromAstropy(
1477 ... id_column_name="objectId",
1478 ... ra_column_name="coord_ra",
1479 ... dec_column_name="coord_dec",
1480 ... psf_footprint_scaling=3.0,
1482 >>> results.writeFits("forced_meas_catalog.fits")
1485 ConfigClass = ForcedMeasurementDriverConfig
1486 _DefaultName =
"forcedMeasurementDriver"
1489 """Initialize the forced measurement driver task."""
1496 table: astropy.table.Table,
1499 id_column_name: str =
"objectId",
1500 ra_column_name: str =
"coord_ra",
1501 dec_column_name: str =
"coord_dec",
1502 psf_footprint_scaling: float = 3.0,
1503 idGenerator: measBase.IdGenerator |
None =
None,
1504 ) -> astropy.table.Table:
1505 """Run forced measurements on an exposure using an Astropy table.
1510 Astropy table containing source IDs and RA/Dec coordinates.
1511 Must contain columns with names specified by `id_column_name`,
1512 `ra_column_name`, and `dec_column_name`.
1514 Exposure on which to run the forced measurements.
1516 Name of the column containing source IDs in the table.
1518 Name of the column containing RA coordinates in the table.
1520 Name of the column containing Dec coordinates in the table.
1521 psf_footprint_scaling :
1522 Scaling factor to apply to the PSF second-moments ellipse in order
1523 to determine the footprint boundary.
1525 Object that generates source IDs and provides random seeds.
1526 If not provided, a new `IdGenerator` will be created.
1531 Astropy table containing the measured sources with columns
1532 corresponding to the source IDs, RA, Dec, from the input table, and
1533 additional measurement columns defined in the configuration.
1536 coord_unit = self.
_ensureValidInputs(table, exposure, id_column_name, ra_column_name, dec_column_name)
1539 if idGenerator
is None:
1540 idGenerator = measBase.IdGenerator()
1543 refWcs = exposure.getWcs()
1553 table, columns=[id_column_name, ra_column_name, dec_column_name], coord_unit=coord_unit
1557 bbox = exposure.getBBox()
1558 for record
in refCat:
1559 localPoint = refWcs.skyToPixel(record.getCoord())
1561 assert bbox.contains(localIntPoint), (
1562 f
"Center for record {record.getId()} is not in exposure; this should be guaranteed by "
1567 if self.
config.doScaleVariance:
1574 exposure, refCat, refWcs, idFactory=idGenerator.make_table_id_factory()
1580 self.
measurement.attachPsfShapeFootprints(catalog, exposure, scaling=psf_footprint_scaling)
1584 catalog = self.
_processCatalog(exposure, catalog, idGenerator, refCat=refCat)
1587 result = catalog.asAstropy()
1595 def run(self, *args, **kwargs):
1596 raise NotImplementedError(
1597 "The run method is not implemented for `ForcedMeasurementDriverTask`. "
1598 "Use `runFromAstropy` instead."
1602 raise NotImplementedError(
1603 "The `runFromImage` method is not implemented for `ForcedMeasurementDriverTask`. "
1604 "Use `runFromAstropy` instead."
1609 table: astropy.table.Table,
1611 id_column_name: str,
1612 ra_column_name: str,
1613 dec_column_name: str,
1615 """Validate the inputs for the forced measurement task.
1620 Astropy table containing source IDs and RA/Dec coordinates.
1622 Exposure on which to run the forced measurements.
1624 Name of the column containing source IDs in the table.
1626 Name of the column containing RA coordinates in the table.
1628 Name of the column containing Dec coordinates in the table.
1633 Unit of the sky coordinates extracted from the table.
1635 if not isinstance(table, astropy.table.Table):
1636 raise TypeError(f
"Expected 'table' to be an astropy Table, got {type(table)}")
1638 if table[ra_column_name].unit == table[dec_column_name].unit:
1639 if table[ra_column_name].unit == astropy.units.deg:
1640 coord_unit =
"degrees"
1641 elif table[ra_column_name].unit == astropy.units.rad:
1642 coord_unit =
"radians"
1645 coord_unit = str(table[ra_column_name].unit)
1647 raise ValueError(
"RA and Dec columns must have the same unit")
1650 raise TypeError(f
"Expected 'exposure' to be an Exposure, got {type(exposure)}")
1652 for col
in [id_column_name, ra_column_name, dec_column_name]:
1653 if col
not in table.colnames:
1654 raise ValueError(f
"Column '{col}' not found in the input table")
1660 table: astropy.table.Table,
1661 columns: list[str] = [
"id",
"ra",
"dec"],
1662 coord_unit: str =
"degrees",
1664 """Convert an Astropy Table to a minimal LSST SourceCatalog.
1666 This is intended for use with the forced measurement subtask, which
1667 expects a `SourceCatalog` input with a minimal schema containing `id`,
1673 Astropy Table containing source IDs and sky coordinates.
1675 Names of the columns in the order [id, ra, dec], where `ra` and
1676 `dec` are in degrees by default. If the coordinates are in radians,
1677 set `coord_unit` to "radians".
1679 Unit of the sky coordinates. Can be either "degrees" or "radians".
1683 outputCatalog : `lsst.afw.table.SourceCatalog`
1684 A SourceCatalog with minimal schema populated from the input table.
1689 If `coord_unit` is not "degrees" or "radians".
1690 If `columns` does not contain exactly 3 items.
1692 If any of the specified columns are missing from the input table.
1698 if coord_unit
not in [
"degrees",
"radians"]:
1699 raise ValueError(f
"Invalid coordinate unit '{coord_unit}'; must be 'degrees' or 'radians'")
1701 if len(columns) != 3:
1702 raise ValueError(
"`columns` must contain exactly three elements for [id, ra, dec]")
1704 idCol, raCol, decCol = columns
1707 if col
not in table.colnames:
1708 raise KeyError(f
"Missing required column: '{col}'")
1711 outputCatalog.reserve(len(table))
1714 outputRecord = outputCatalog.addNew()
1715 outputRecord.setId(row[idCol])
1716 outputRecord.setCoord(
1720 return outputCatalog
A 2-dimensional celestial WCS that transform pixels to ICRS RA/Dec, using the LSST standard for pixel...
A class to contain the data, WCS, and other information needed to describe an image of the sky.
A class to represent a 2-dimensional array of pixels.
Represent a 2-dimensional array of bitmask pixels.
A class to manipulate images, masks, and variance as a single object.
The photometric calibration of an exposure.
A kernel created from an Image.
Defines the fields and offsets for a table.
A mapping between the keys of two Schemas, used to copy data between them.
Point in an unspecified spherical coordinate system.
__init__(self, *args, **kwargs)
astropy.table.Table runFromAstropy(self, astropy.table.Table table, afwImage.Exposure exposure, *, str id_column_name="objectId", str ra_column_name="coord_ra", str dec_column_name="coord_dec", float psf_footprint_scaling=3.0, measBase.IdGenerator|None idGenerator=None)
runFromImage(self, *args, **kwargs)
run(self, *args, **kwargs)
_makeMinimalSourceCatalogFromAstropy(self, astropy.table.Table table, list[str] columns=["id", "ra", "dec"], str coord_unit="degrees")
None _ensureValidInputs(self, astropy.table.Table table, afwImage.Exposure exposure, str id_column_name, str ra_column_name, str dec_column_name)
_ensureValidInputs(self, afwTable.SourceCatalog|None catalog)
pipeBase.Struct run(self, *args, **kwargs)
_applyApCorr(self, afwImage.Exposure exposure, afwTable.SourceCatalog catalog, measBase.IdGenerator idGenerator)
afwTable.Schema initSchema
afwTable.SchemaMapper mapper
_scaleVariance(self, afwImage.Exposure exposure, str band="a single")
_deblendSources(self, *args, **kwargs)
afwTable.SchemaMapper schema
__setattr__(self, name, value)
_addCoordErrorFieldsIfMissing(self, afwTable.Schema schema)
measAlgorithms.ScaleVarianceTask scaleVariance
afwTable.SourceCatalog _processCatalog(self, afwImage.Exposure exposure, afwTable.SourceCatalog catalog, measBase.IdGenerator idGenerator, str band="a single", afwTable.SourceCatalog|None refCat=None)
measAlgorithms.SourceDetectionTask detection
afwTable.SourceCatalog|dict[str, afwTable.SourceCatalog] _toContiguous(self, afwTable.SourceCatalog|dict[str, afwTable.SourceCatalog] catalog)
tuple[afwTable.SourceCatalog, afwMath.BackgroundList] _detectSources(self, afwImage.Exposure|afwImage.MultibandExposure exposure, measBase.IdGenerator idGenerator)
__init__(self, afwTable.Schema schema=None, afwTable.Schema peakSchema=None, **dict kwargs)
measDeblender.SourceDeblendTask|scarlet.ScarletDeblendTask deblend
_measureSources(self, afwImage.Exposure exposure, afwTable.SourceCatalog catalog, measBase.IdGenerator idGenerator, afwTable.SourceCatalog|None refCat=None)
measBase.CatalogCalculationTask catalogCalculation
afwTable.SourceCatalog|None _prepareSchemaAndSubtasks(self, afwTable.SourceCatalog|None catalog)
_initializeSchema(self, afwTable.SourceCatalog catalog=None)
measBase.SingleFrameMeasurementTask|measBase.ForcedMeasurementTask measurement
measBase.ApplyApCorrTask applyApCorr
_runCatalogCalculation(self, afwTable.SourceCatalog catalog)
afwTable.SourceCatalog _updateCatalogSchema(self, afwTable.SourceCatalog catalog)
pipeBase.Struct run(self, afwImage.MultibandExposure|list[afwImage.Exposure]|afwImage.Exposure mExposure, afwImage.MultibandExposure|list[afwImage.Exposure]|afwImage.Exposure|None mDeconvolved=None, str|None refBand=None, list[str]|None bands=None, afwTable.SourceCatalog catalog=None, measBase.IdGenerator idGenerator=None)
afwImage.MultibandExposure _buildMultibandExposure(self, afwImage.MultibandExposure|list[afwImage.Exposure]|afwImage.Exposure mExposureData, list[str]|None bands)
__init__(self, *args, **kwargs)
tuple[afwImage.MultibandExposure, afwImage.MultibandExposure, str, list[str]|None] _ensureValidInputs(self, afwImage.MultibandExposure|list[afwImage.Exposure]|afwImage.Exposure mExposure, afwImage.MultibandExposure|list[afwImage.Exposure]|afwImage.Exposure|None mDeconvolved, str|None refBand, list[str]|None bands, afwTable.SourceCatalog|None catalog=None)
tuple[dict[str, afwTable.SourceCatalog], scl.io.ScarletModelData] _deblendSources(self, afwImage.MultibandExposure mExposure, afwImage.MultibandExposure|None mDeconvolved, afwTable.SourceCatalog catalog, str refBand)
scl.io.ScarletModelData modelData
afwTable.SourceCatalog _deblendSources(self, afwImage.Exposure exposure, afwTable.SourceCatalog catalog)
pipeBase.Struct runFromImage(self, afwImage.MaskedImage|afwImage.Image|np.ndarray image, afwImage.Mask|np.ndarray mask=None, afwImage.Image|np.ndarray variance=None, afwGeom.SkyWcs wcs=None, afwDetection.Psf|np.ndarray psf=None, afwImage.PhotoCalib photoCalib=None, afwTable.SourceCatalog catalog=None, measBase.IdGenerator idGenerator=None)
__init__(self, *args, **kwargs)
pipeBase.Struct run(self, afwImage.Exposure exposure, afwTable.SourceCatalog|None catalog=None, measBase.IdGenerator|None idGenerator=None)
MaskedImage< ImagePixelT, MaskPixelT, VariancePixelT > * makeMaskedImage(typename std::shared_ptr< Image< ImagePixelT > > image, typename std::shared_ptr< Mask< MaskPixelT > > mask=Mask< MaskPixelT >(), typename std::shared_ptr< Image< VariancePixelT > > variance=Image< VariancePixelT >())
A function to return a MaskedImage of the correct type (cf.
std::shared_ptr< Exposure< ImagePixelT, MaskPixelT, VariancePixelT > > makeExposure(MaskedImage< ImagePixelT, MaskPixelT, VariancePixelT > &mimage, std::shared_ptr< geom::SkyWcs const > wcs=std::shared_ptr< geom::SkyWcs const >())
A function to return an Exposure of the correct type (cf.