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
48from lsst.meas.extensions.scarlet.deconvolveExposureTask
import DeconvolveExposureTask
51logging.basicConfig(level=logging.INFO)
55 """Base configuration for measurement driver tasks.
57 This class provides foundational configuration for its subclasses to handle
58 single-band and multi-band data. It defines variance scaling, detection,
59 deblending, measurement, aperture correction, and catalog calculation
60 subtasks, which are intended to be executed in sequence by the driver task.
63 doScaleVariance = Field[bool](doc=
"Scale variance plane using empirical noise?", default=
False)
66 doc=
"Subtask to rescale variance plane", target=measAlgorithms.ScaleVarianceTask
69 doDetect = Field[bool](doc=
"Run the source detection algorithm?", default=
True)
72 doc=
"Subtask to detect sources in the image", target=measAlgorithms.SourceDetectionTask
75 doDeblend = Field[bool](doc=
"Run the source deblending algorithm?", default=
True)
78 doMeasure = Field[bool](doc=
"Run the source measurement algorithm?", default=
True)
81 doc=
"Subtask to measure sources and populate the output catalog",
82 target=measBase.SingleFrameMeasurementTask,
85 psfCache = Field[int](
86 doc=
"Maximum number of PSFs to cache, preventing repeated PSF evaluations at the same "
87 "point across different measurement plugins. Defaults to -1, which auto-sizes the cache "
88 "based on the plugin count.",
92 checkUnitsParseStrict = Field[str](
93 doc=
"Strictness of Astropy unit compatibility check, can be 'raise', 'warn' or 'silent'",
97 doApCorr = Field[bool](
98 doc=
"Apply aperture corrections? If yes, your image must have an aperture correction map",
103 doc=
"Subtask to apply aperture corrections",
104 target=measBase.ApplyApCorrTask,
107 doRunCatalogCalculation = Field[bool](doc=
"Run catalogCalculation task?", default=
False)
110 doc=
"Subtask to run catalogCalculation plugins on catalog", target=measBase.CatalogCalculationTask
119 "doRunCatalogCalculation",
123 """Ensure that at least one processing step is enabled."""
126 if not any(getattr(self, opt)
for opt
in self.
doOptions):
127 raise ValueError(f
"At least one of these options must be enabled: {self.doOptions}")
130 raise ValueError(
"Aperture correction requires measurement to be enabled.")
133 raise ValueError(
"Catalog calculation requires measurement to be enabled.")
137 """Base class for the mid-level driver running variance scaling, detection,
138 deblending, measurement, apperture correction, and catalog calculation in
141 Users don't need to Butlerize their input data, which is a significant
142 advantage for quick data exploration and testing. This driver simplifies
143 the process of applying measurement algorithms to images by abstracting
144 away low-level implementation details such as Schema and table boilerplate.
145 It's a convenient way to process images into catalogs with a user-friendly
146 interface for non-developers while allowing extensive configuration and
147 integration into unit tests for developers. It also considerably improves
148 how demos and workflows are showcased in Jupyter notebooks.
153 Schema used to create the output `~lsst.afw.table.SourceCatalog`,
154 modified in place with fields that will be written by this task.
156 Schema of Footprint Peaks that will be passed to the deblender.
158 Additional kwargs to pass to lsst.pipe.base.Task.__init__()
162 Subclasses (e.g., single-band vs. multi-band) share most methods and config
163 options but differ in handling and validating inputs by overriding the base
164 config class and any methods that require their own logic.
167 ConfigClass = MeasurementDriverBaseConfig
168 _DefaultName =
"measurementDriverBase"
184 self.
deblend: measDeblender.SourceDeblendTask | scarlet.ScarletDeblendTask
185 self.
measurement: measBase.SingleFrameMeasurementTask | measBase.ForcedMeasurementTask
194 super().
__setattr__(
"initSchema", copy.deepcopy(schema))
197 """Prevent external modifications of the initial Schema."""
198 if name ==
"initSchema":
199 raise AttributeError(f
"Cannot modify {name} directly")
203 def run(self, *args, **kwargs) -> pipeBase.Struct:
204 """Run the measurement driver task. Subclasses must implement this
205 method using their own logic to handle single-band or multi-band data.
207 raise NotImplementedError(
"This is not implemented on the base class")
211 catalog: afwTable.SourceCatalog |
None,
213 """Perform validation and adjustments of inputs without heavy
219 Catalog to be extended by the driver task.
225 if catalog
is not None:
227 "An input catalog was given to bypass detection, but 'doDetect' is still on"
231 raise RuntimeError(
"Cannot run without detection if no 'catalog' is provided")
234 """Initialize the Schema to be used for constructing the subtasks.
236 Though it may seem clunky, this workaround is necessary to ensure
237 Schema consistency across all subtasks.
242 Catalog from which to extract the Schema. If not provided, the
243 user-provided Schema and if that is also not provided during
244 initialization, a minimal Schema will be used.
256 self.
schema = afwTable.SourceTable.makeMinimalSchema()
261 if self.
schema is not None:
263 "Both a catalog and a Schema were provided; using the Schema from the catalog only"
267 catalogSchema = catalog.schema
280 self.
mapper.addMinimalSchema(catalogSchema,
True)
286 if isinstance(self, ForcedMeasurementDriverTask):
290 self.
schema.addField(
"deblend_nChild",
"I",
"Needed for minimal forced photometry schema")
293 """Add coordinate error fields to the schema in-place if they are not
299 Schema to be checked for coordinate error fields.
302 errorField
in schema.getNames()
303 for errorField
in (
"coord_raErr",
"coord_decErr",
"coord_ra_dec_Cov")
305 afwTable.CoordKey.addErrorFields(schema)
308 """Construct subtasks based on the configuration and the Schema."""
309 if self.
schema is None and any(
310 getattr(self.
config, attr)
for attr
in self.
config.doOptions
if attr !=
"doScaleVariance"
313 "Cannot create requested subtasks without a Schema; "
314 "ensure one is provided explicitly or via a catalog"
317 if self.
config.doScaleVariance:
318 self.makeSubtask(
"scaleVariance")
320 if isinstance(self, ForcedMeasurementDriverTask):
323 self.makeSubtask(
"measurement", refSchema=self.
schema)
335 self.makeSubtask(
"applyApCorr", schema=self.
measurement.schema)
339 if self.
config.doRunCatalogCalculation:
340 self.makeSubtask(
"catalogCalculation", schema=self.
measurement.schema)
343 self.makeSubtask(
"detection", schema=self.
schema)
349 self.makeSubtask(
"measurement", schema=self.
schema)
352 self.makeSubtask(
"applyApCorr", schema=self.
measurement.schema)
354 if self.
config.doRunCatalogCalculation:
355 self.makeSubtask(
"catalogCalculation", schema=self.
schema)
358 self, catalog: afwTable.SourceCatalog |
None
360 """Ensure subtasks are properly initialized according to the
361 configuration and the provided catalog.
366 Optional catalog to be used for initializing the Schema and the
372 Updated catalog to be passed to the subtasks, if it was provided.
382 self.
schema.checkUnits(parse_strict=self.
config.checkUnitsParseStrict)
391 """Scale the variance plane of an exposure to match the observed
397 Exposure on which to run the variance scaling algorithm.
399 Band associated with the exposure. Used for logging.
401 self.log.info(f
"Scaling variance plane for {band} band")
403 exposure.getMetadata().add(
"VARIANCE_SCALE", varScale)
408 """Make a catalog or catalogs contiguous if they are not already.
413 Catalog or dictionary of catalogs with bands as keys to be made
419 Contiguous catalog or dictionary of contiguous catalogs.
421 if isinstance(catalog, dict):
422 for band, cat
in catalog.items():
423 if not cat.isContiguous():
424 self.log.info(f
"{band}-band catalog is not contiguous; making it contiguous")
425 catalog[band] = cat.copy(deep=
True)
427 if not catalog.isContiguous():
428 self.log.info(
"Catalog is not contiguous; making it contiguous")
429 catalog = catalog.copy(deep=
True)
433 """Update the Schema of the provided catalog to incorporate changes
434 made by the configured subtasks.
439 Catalog to be updated with the Schema changes.
444 Catalog with the updated Schema.
452 updatedCatalog.extend(catalog, mapper=self.
mapper)
456 return updatedCatalog
461 """Run the detection subtask to identify sources in the image.
466 Exposure on which to run the detection algorithm.
468 Generator for unique source IDs.
473 A catalog containing detected sources.
475 A list of background models obtained from the detection process,
478 self.log.info(f
"Running detection on a {exposure.width}x{exposure.height} pixel exposure")
482 table = afwTable.SourceTable.make(self.
schema, idGenerator.make_table_id_factory())
485 detections = self.
detection.run(table, exposure)
486 catalog = detections.sources
490 if hasattr(detections,
"background")
and detections.background:
491 for bg
in detections.background:
492 backgroundList.append(bg)
494 return catalog, backgroundList
498 """Run the deblending subtask to separate blended sources. Subclasses
499 must implement this method to handle task-specific deblending logic.
501 raise NotImplementedError(
"This is not implemented on the base class")
505 exposure: afwImage.Exposure,
507 idGenerator: measBase.IdGenerator,
510 """Run the measurement subtask to compute properties of sources.
515 Exposure on which to run the measurement algorithm.
517 Catalog containing sources on which to run the measurement subtask.
519 Generator for unique source IDs.
521 Reference catalog to be used for forced measurements, if any.
522 If not provided, the measurement will be run on the sources in the
523 catalog in a standard manner without reference.
528 refWcs = exposure.getWcs()
535 exposureId=idGenerator.catalog_id,
539 self.
measurement.run(measCat=catalog, exposure=exposure, exposureId=idGenerator.catalog_id)
544 """Apply aperture corrections to the catalog.
549 Exposure on which to apply aperture corrections.
551 Catalog to be corrected using the aperture correction map from
554 Generator for unique source IDs.
556 apCorrMap = exposure.getInfo().getApCorrMap()
557 if apCorrMap
is None:
559 "Image does not have valid aperture correction map for catalog id "
560 f
"{idGenerator.catalog_id}; skipping aperture correction"
563 self.
applyApCorr.run(catalog=catalog, apCorrMap=apCorrMap)
566 """Run the catalog calculation plugins on the catalog.
571 Catalog to be processed by the catalog calculation subtask.
577 exposure: afwImage.Exposure,
579 idGenerator: measBase.IdGenerator,
580 band: str =
"a single",
583 """Process a catalog through measurement, aperture correction, and
584 catalog calculation subtasks.
589 Exposure associated with the catalog.
591 Catalog to be processed by the subtasks.
593 Generator for unique source IDs.
595 Band associated with the exposure and catalog. Used for logging.
597 Reference catalog for forced measurements. If not provided, the
598 measurement will be run on the sources in the catalog in a standard
599 manner without reference.
604 Catalog after processing through the configured subtasks.
608 if self.
config.psfCache > 0:
610 exposure.psf.setCacheCapacity(self.
config.psfCache)
618 exposure.psf.setCacheCapacity(2 * len(self.
config.measurement.plugins.names))
623 f
"Measuring {len(catalog)} sources in {band} band "
624 f
"using '{self.measurement.__class__.__name__}'"
633 self.log.info(f
"Applying aperture corrections to {band} band")
637 if self.
config.doRunCatalogCalculation:
638 self.log.info(f
"Running catalog calculation on {band} band")
642 f
"Finished processing for {band} band; output catalog has {catalog.schema.getFieldCount()} "
643 f
"fields and {len(catalog)} records"
650 """Configuration for the single-band measurement driver task."""
652 deblend =
ConfigurableField(target=measDeblender.SourceDeblendTask, doc=
"Deblender for single-band data.")
656 """Mid-level driver for processing single-band data.
658 Offers a helper method for direct handling of raw image data in addition to
659 the standard single-band exposure.
663 Here is an example of how to use this class to run variance scaling,
664 detection, deblending, and measurement on a single-band exposure:
666 >>> from lsst.pipe.tasks.measurementDriver import (
667 ... SingleBandMeasurementDriverConfig,
668 ... SingleBandMeasurementDriverTask,
670 >>> import lsst.meas.extensions.shapeHSM # To register its plugins
671 >>> config = SingleBandMeasurementDriverConfig()
672 >>> config.doScaleVariance = True
673 >>> config.doDetect = True
674 >>> config.doDeblend = True
675 >>> config.doMeasure = True
676 >>> config.scaleVariance.background.binSize = 64
677 >>> config.detection.thresholdValue = 5.5
678 >>> config.deblend.tinyFootprintSize = 3
679 >>> config.measurement.plugins.names |= [
680 ... "base_SdssCentroid",
681 ... "base_SdssShape",
682 ... "ext_shapeHSM_HsmSourceMoments",
684 >>> config.measurement.slots.psfFlux = None
685 >>> config.measurement.doReplaceWithNoise = False
686 >>> exposure = butler.get("deepCoadd", dataId=...)
687 >>> driver = SingleBandMeasurementDriverTask(config=config)
688 >>> results = driver.run(exposure)
689 >>> results.catalog.writeFits("meas_catalog.fits")
691 Alternatively, if an exposure is not available, the driver can also process
700 >>> results = driver.runFromImage(
701 ... image, mask, variance, wcs, psf, photoCalib
703 >>> results.catalog.writeFits("meas_catalog.fits")
706 ConfigClass = SingleBandMeasurementDriverConfig
707 _DefaultName =
"singleBandMeasurementDriver"
708 _Deblender =
"meas_deblender"
718 exposure: afwImage.Exposure,
720 idGenerator: measBase.IdGenerator |
None =
None,
721 ) -> pipeBase.Struct:
722 """Process a single-band exposure through the configured subtasks and
723 return the results as a struct.
728 The exposure on which to run the driver task.
730 Catalog to be extended by the driver task. If not provided, an
731 empty catalog will be created and populated.
733 Object that generates source IDs and provides random seeds.
738 Results as a struct with attributes:
741 Catalog containing the measured sources
742 (`~lsst.afw.table.SourceCatalog`).
744 List of backgrounds (`list[~lsst.afw.math.Background]`). Only
745 populated if detection is enabled.
755 if idGenerator
is None:
756 idGenerator = measBase.IdGenerator()
760 if self.
config.doScaleVariance:
765 catalog, backgroundList = self.
_detectSources(exposure, idGenerator)
767 self.log.info(
"Skipping detection; using detections from the provided catalog")
768 backgroundList =
None
774 self.log.info(
"Skipping deblending")
780 return pipeBase.Struct(catalog=catalog, backgroundList=backgroundList)
788 psf: afwDetection.Psf | np.ndarray =
None,
791 idGenerator: measBase.IdGenerator =
None,
792 ) -> pipeBase.Struct:
793 """Convert image data to an `Exposure`, then run it through the
799 Input image data. Will be converted into an `Exposure` before
802 Mask data for the image. Used if ``image`` is a bare `array` or
805 Variance plane data for the image.
807 World Coordinate System to associate with the exposure that will
808 be created from ``image``.
810 PSF model for the exposure.
812 Photometric calibration model for the exposure.
814 Catalog to be extended by the driver task. If not provided, a new
815 catalog will be created during detection and populated.
817 Generator for unique source IDs.
822 Results as a struct with attributes:
825 Catalog containing the measured sources
826 (`~lsst.afw.table.SourceCatalog`).
828 List of backgrounds (`list[~lsst.afw.math.Background]`).
831 if isinstance(image, np.ndarray):
832 image = afwImage.makeImageFromArray(image)
833 if isinstance(mask, np.ndarray):
834 mask = afwImage.makeMaskFromArray(mask)
835 if isinstance(variance, np.ndarray):
836 variance = afwImage.makeImageFromArray(variance)
845 raise TypeError(f
"Unsupported 'image' type: {type(image)}")
848 if isinstance(psf, np.ndarray):
853 psf = afwDetection.KernelPsf(kernel)
854 elif not isinstance(psf, afwDetection.Psf):
855 raise TypeError(f
"Unsupported 'psf' type: {type(psf)}")
858 if photoCalib
is not None:
859 exposure.setPhotoCalib(photoCalib)
861 return self.
run(exposure, catalog=catalog, idGenerator=idGenerator)
866 """Run single-band deblending given an exposure and a catalog.
871 Exposure on which to run the deblending algorithm.
873 Catalog containing sources to be deblended.
878 Catalog after deblending, with sources separated into their
879 individual components if they were deblended.
881 self.log.info(f
"Deblending using '{self._Deblender}' on {len(catalog)} detection footprints")
882 self.
deblend.run(exposure=exposure, sources=catalog)
889 """Configuration for the multi-band measurement driver task."""
892 target=scarlet.ScarletDeblendTask, doc=
"Scarlet deblender for multi-band data"
895 doConserveFlux = Field[bool](
896 doc=
"Whether to use the deblender models as templates to re-distribute the flux from "
897 "the 'exposure' (True), or to perform measurements on the deblender model footprints.",
901 measureOnlyInRefBand = Field[bool](
902 doc=
"If True, all measurements downstream of deblending run only in the reference band that "
903 "was used for detection; otherwise, they are performed in all available bands, generating a "
904 "catalog for each. Regardless of this setting, deblending still uses all available bands.",
908 removeScarletData = Field[bool](
909 doc=
"Whether or not to remove `ScarletBlendData` for each blend in order to save memory. "
910 "If set to True, some sources may end up with missing footprints in catalogs other than the "
911 "reference-band catalog, leading to failures in subsequent measurements that require footprints. "
912 "For example, keep this False if `measureOnlyInRefBand` is set to False and "
913 "`measurement.doReplaceWithNoise` to True, in order to make the footprints available in "
914 "non-reference bands in addition to the reference band.",
918 updateFluxColumns = Field[bool](
919 doc=
"Whether or not to update the `deblend_*` columns in the catalog. This should only be "
920 "True when the input catalog schema already contains those columns.",
926 """Mid-level driver for processing multi-band data.
928 The default behavior is to run detection on the reference band, use all
929 available bands for deblending, and then process everything downstream
930 separately for each band making per-band catalogs unless configured
931 otherwise. This subclass provides functionality for handling a singe-band
932 exposure and a list of single-band exposures in addition to a standard
937 Here is an example of how to use this class to run variance scaling,
938 detection, deblending, measurement, and aperture correction on a multi-band
941 >>> from lsst.afw.image import MultibandExposure
942 >>> from lsst.pipe.tasks.measurementDriver import (
943 ... MultiBandMeasurementDriverConfig,
944 ... MultiBandMeasurementDriverTask,
946 >>> import lsst.meas.extensions.shapeHSM # To register its plugins
947 >>> config = MultiBandMeasurementDriverConfig()
948 >>> config.doScaleVariance = True
949 >>> config.doDetect = True
950 >>> config.doDeblend = True
951 >>> config.doMeasure = True
952 >>> config.doApCorr = True
953 >>> config.scaleVariance.background.binSize = 64
954 >>> config.detection.thresholdValue = 5.5
955 >>> config.deblend.minSNR = 42.0
956 >>> config.deblend.maxIter = 20
957 >>> config.measurement.plugins.names |= [
958 ... "base_SdssCentroid",
959 ... "base_SdssShape",
960 ... "ext_shapeHSM_HsmSourceMoments",
962 >>> config.measurement.slots.psfFlux = None
963 >>> config.measurement.doReplaceWithNoise = False
964 >>> config.applyApCorr.doFlagApCorrFailures = False
965 >>> mExposure = MultibandExposure.fromButler(
966 ... butler, ["g", "r", "i"], "deepCoadd_calexp", ...
968 >>> driver = MultiBandMeasurementDriverTask(config=config)
969 >>> results = driver.run(mExposure, "r")
970 >>> for band, catalog in results.catalogs.items():
971 ... catalog.writeFits(f"meas_catalog_{band}.fits")
974 ConfigClass = MultiBandMeasurementDriverConfig
975 _DefaultName =
"multiBandMeasurementDriver"
976 _Deblender =
"scarlet"
991 refBand: str |
None =
None,
992 bands: list[str] |
None =
None,
994 idGenerator: measBase.IdGenerator =
None,
995 ) -> pipeBase.Struct:
996 """Process an exposure through the configured subtasks while using
997 multi-band information for deblending.
1002 Multi-band data containing images of the same shape and region of
1003 the sky. May be a `MultibandExposure`, a single-band exposure
1004 (i.e., `Exposure`), or a list of single-band exposures associated
1005 with different bands in which case ``bands`` must be provided. If a
1006 single-band exposure is given, it will be treated as a
1007 `MultibandExposure` that contains only that one band whose name may
1008 be "unknown" unless either ``bands`` or ``refBand`` is provided.
1010 Multi-band deconvolved images of the same shape and region of the
1011 sky. Follows the same type conventions as ``mExposure``. If not
1012 provided, the deblender will run the deconvolution internally
1013 using the provided ``mExposure``.
1015 Reference band to use for detection. Not required for single-band
1016 exposures. If `measureOnlyInRefBand` is enabled while detection is
1017 disabled and a catalog of detected sources is provided, this
1018 should specify the band the sources were detected on (or the band
1019 you want to use to perform measurements on exclusively). If
1020 `measureOnlyInRefBand` is disabled instead in the latter scenario,
1021 ``refBand`` does not need to be provided.
1023 List of bands associated with the exposures in ``mExposure``. Only
1024 required if ``mExposure`` is a list of single-band exposures. If
1025 provided for a multi-band exposure, it will be used to only process
1026 that subset of bands from the available ones in the exposure.
1028 Catalog to be extended by the driver task. If not provided, a new
1029 catalog will be created and populated.
1031 Generator for unique source IDs.
1036 Results as a struct with attributes:
1039 Dictionary of catalogs containing the measured sources with
1040 bands as keys (`dict[str, ~lsst.afw.table.SourceCatalog]`). If
1041 `measureOnlyInRefBand` is enabled or deblending is disabled,
1042 this will only contain the reference-band catalog; otherwise,
1043 it will contain a catalog for each band.
1045 List of backgrounds (`list[~lsst.afw.math.Background]`). Will
1046 be None if detection is disabled.
1048 Multiband scarlet models produced during deblending
1049 (`~scarlet.io.LsstScarletModelData`). Will be None if
1050 deblending is disabled.
1055 mExposure, mDeconvolved, refBand, bands, catalog
1062 if idGenerator
is None:
1063 idGenerator = measBase.IdGenerator()
1067 if self.
config.doScaleVariance:
1069 for band
in mExposure.bands:
1074 catalog, backgroundList = self.
_detectSources(mExposure[refBand], idGenerator)
1076 self.log.info(
"Skipping detection; using detections from provided catalog")
1077 backgroundList =
None
1080 if self.
config.doDeblend:
1084 "Skipping deblending; proceeding with the provided catalog in the reference band"
1086 catalogs = {refBand: catalog}
1091 for band, catalog
in catalogs.items():
1092 exposure = mExposure[band]
1093 self.
_processCatalog(exposure, catalog, idGenerator, band=f
"'{band}'")
1095 return pipeBase.Struct(catalogs=catalogs, backgroundList=backgroundList, modelData=self.
modelData)
1101 refBand: str |
None,
1102 bands: list[str] |
None,
1105 """Perform validation and adjustments of inputs without heavy
1111 Multi-band data to be processed by the driver task.
1113 Multi-band deconvolved data to be processed by the driver task.
1115 Reference band to use for detection.
1117 List of bands associated with the exposures in ``mExposure``.
1119 Catalog to be extended by the driver task.
1124 Multi-band exposure to be processed by the driver task. If the
1125 input was not already a `MultibandExposure` (optionally with the
1126 relevant ``bands``), it is converted into one and returned
1127 here; otherwise, the original input is returned unchanged.
1129 Multi-band deconvolved exposure to be processed by the driver task.
1130 Same adjustments apply as for ``mExposure`` except that it is
1131 optional and may be returned as None if not provided as input.
1133 Reference band to use for detection after potential adjustments.
1134 If not provided in the input, and only one band is set to be
1135 processed, ``refBand`` will be chosen to be the only existing band
1136 in the ``bands`` list, or `mExposure.bands`, and if neither is
1137 provided, it will be set to "unknown" for single-band exposures
1138 processed by this multi-band driver.
1140 List of bands associated with the exposures in ``mExposure`` after
1141 potential adjustments. If not provided in the input, it will be set
1142 to a list containing only the provided (or inferred as "unknown")
1151 if bands
is not None:
1152 if not isinstance(bands, list):
1153 raise TypeError(f
"Expected 'bands' to be a list, got {type(bands)}")
1154 if not all(isinstance(b, str)
for b
in bands):
1155 raise TypeError(f
"All elements in 'bands' must be strings, got {[type(b) for b in bands]}")
1157 if refBand
is not None:
1158 if not isinstance(refBand, str):
1159 raise TypeError(f
"Reference band must be a string, got {type(refBand)}")
1163 if bands
is not None:
1164 if any(b
not in mExposure.bands
for b
in bands):
1166 f
"Some of the provided {bands=} are not present in {mExposure.bands=}"
1169 f
"Using {bands=} out of the available {mExposure.bands=} in the multi-band exposures"
1171 elif isinstance(mExposure, list):
1173 raise ValueError(
"The 'bands' list must be provided if 'mExposure' is a list")
1174 if len(bands) != len(mExposure):
1175 raise ValueError(
"Number of bands and exposures must match")
1177 if bands
is not None:
1180 f
"{bands=}, if provided, must only contain a single band "
1181 "if 'mExposure' is a single-band exposure"
1184 raise TypeError(f
"Unsupported 'mExposure' type: {type(mExposure)}")
1189 if bands
is not None:
1190 if any(b
not in mDeconvolved.bands
for b
in bands):
1192 f
"Some of the provided {bands=} are not present in {mDeconvolved.bands=}"
1194 elif isinstance(mDeconvolved, list):
1196 raise ValueError(
"The 'bands' list must be provided if 'mDeconvolved' is a list")
1197 if len(bands) != len(mDeconvolved):
1198 raise ValueError(
"Number of bands and deconvolved exposures must match")
1200 if bands
is not None:
1203 f
"{bands=}, if provided, must only contain a single band "
1204 "if 'mDeconvolved' is a single-band exposure"
1207 raise TypeError(f
"Unsupported {type(mDeconvolved)=}")
1210 if bands
is not None and len(bands) != 1:
1212 f
"{bands=}, if provided, must only contain a single band "
1213 "if one of 'mExposure' or 'mDeconvolved' is a single-band exposure"
1215 if bands
is None and refBand
is None:
1218 elif bands
is None and refBand
is not None:
1220 elif bands
is not None and refBand
is None:
1229 if mExposure.bands != mDeconvolved.bands:
1231 "The bands in 'mExposure' and 'mDeconvolved' must match; "
1232 f
"got {mExposure.bands} and {mDeconvolved.bands}"
1235 if len(mExposure.bands) == 1:
1240 self.log.info(f
"Running '{self._Deblender}' in single-band mode; make sure it was intended!")
1242 refBand = mExposure.bands[0]
1244 "No reference band provided for single-band data; "
1245 f
"using the only available band ('{refBand}') as the reference band"
1249 if self.
config.measureOnlyInRefBand:
1250 measInfo =
"and everything downstream of deblending"
1253 "while subtasks downstream of deblending will be run in each of "
1254 f
"the {mExposure.bands} bands"
1256 self.log.info(f
"Using '{refBand}' as the reference band for detection {measInfo}")
1260 raise ValueError(
"Reference band must be provided for multi-band data")
1262 if refBand
not in mExposure.bands:
1263 raise ValueError(f
"Requested {refBand=} is not in {mExposure.bands=}")
1265 if bands
is not None and refBand
not in bands:
1266 raise ValueError(f
"Reference {refBand=} is not in {bands=}")
1268 return mExposure, mDeconvolved, refBand, bands
1272 mExposure: afwImage.MultibandExposure,
1277 """Run multi-band deblending given a multi-band exposure and a catalog.
1282 Multi-band exposure on which to run the deblending algorithm.
1284 Multi-band deconvolved exposure to use for deblending. If None,
1285 the deblender will create it internally using the provided
1288 Catalog containing sources to be deblended.
1290 Reference band used for detection or the band to use for
1291 measurements if `measureOnlyInRefBand` is enabled.
1296 Dictionary of catalogs containing the deblended sources. If
1297 `measureOnlyInRefBand` is enabled, this will only contain the
1298 reference-band catalog; otherwise, it will contain a catalog for
1301 Multiband scarlet models produced during deblending.
1303 self.log.info(f
"Deblending using '{self._Deblender}' on {len(catalog)} detection footprints")
1305 if mDeconvolved
is None:
1307 deconvolvedCoadds = []
1308 deconvolveTask = DeconvolveExposureTask()
1309 for coadd
in mExposure:
1310 deconvolvedCoadd = deconvolveTask.run(coadd, catalog).deconvolved
1311 deconvolvedCoadds.append(deconvolvedCoadd)
1312 mDeconvolved = afwImage.MultibandExposure.fromExposures(mExposure.bands, deconvolvedCoadds)
1315 result = self.
deblend.run(mExposure, mDeconvolved, catalog)
1316 catalog = result.deblendedCatalog
1317 modelData = result.scarletModelData
1320 bands = [refBand]
if self.
config.measureOnlyInRefBand
else mExposure.bands
1322 catalogs = {band: catalog.copy(deep=
True)
for band
in bands}
1325 imageForRedistribution = mExposure[band]
if self.
config.doConserveFlux
else None
1326 scarlet.io.updateCatalogFootprints(
1327 modelData=modelData,
1328 catalog=catalogs[band],
1330 imageForRedistribution=imageForRedistribution,
1331 removeScarletData=self.
config.removeScarletData,
1332 updateFluxColumns=self.
config.updateFluxColumns,
1340 bands: list[str] |
None,
1342 """Convert a single-band exposure or a list of single-band exposures to
1343 a `MultibandExposure` if not already of that type.
1345 No conversion will be done if ``mExposureData`` is already a
1346 `MultibandExposure` except it will be subsetted to the bands provided.
1351 Input multi-band data.
1353 List of bands associated with the exposures in ``mExposure``. Only
1354 required if ``mExposure`` is a list of single-band exposures. If
1355 provided while ``mExposureData`` is a ``MultibandExposure``, it
1356 will be used to select a specific subset of bands from the
1362 Converted multi-band exposure.
1365 if bands
and not set(bands).issubset(mExposureData.bands):
1367 f
"Requested bands {bands} are not a subset of available bands: {mExposureData.bands}"
1369 return mExposureData[bands,]
if bands
and len(bands) > 1
else mExposureData
1370 elif isinstance(mExposureData, list):
1371 mExposure = afwImage.MultibandExposure.fromExposures(bands, mExposureData)
1375 mExposure = afwImage.MultibandExposure.fromExposures(bands, [mExposureData])
1381 for band, exposure
in zip(bands, mExposureData):
1382 mExposure[band].setWcs(exposure.getWcs())
1388 """Configuration for the forced measurement driver task."""
1391 target=measBase.ForcedMeasurementTask,
1392 doc=
"Measurement task for forced measurements. This should be a "
1393 "measurement task that does not perform detection.",
1397 """Set default values for the configuration.
1399 This method overrides the base class method to ensure that `doDetect`
1400 is set to `False` by default, as this task is intended for forced
1401 measurements where detection is not performed. Also, it sets some
1402 default measurement plugins by default.
1410 "base_TransformedCentroidFromCoord",
1412 "base_CircularApertureFlux",
1416 """Validate the configuration.
1418 This method overrides the base class validation to ensure that
1419 `doDetect` is set to `False`, as this task is intended for forced
1420 measurements where detection is not performed.
1425 "ForcedMeasurementDriverTask should not perform detection or "
1426 "deblending; set doDetect=False and doDeblend=False"
1429 raise ValueError(
"ForcedMeasurementDriverTask must perform measurements; set doMeasure=True")
1433 """Forced measurement driver task for single-band data.
1435 This task is the 'forced' version of the `SingleBandMeasurementDriverTask`,
1436 intended as a convenience function for performing forced photometry on an
1437 input image given a set of IDs and RA/Dec coordinates. It is designed as a
1438 public-facing interface, allowing users to measure sources without
1439 explicitly instantiating and running pipeline tasks.
1443 Here is an example of how to use this class to run forced measurements on
1444 an exposure using an Astropy table containing source IDs and RA/Dec
1447 >>> from lsst.pipe.tasks.measurementDriver import (
1448 ... ForcedMeasurementDriverConfig,
1449 ... ForcedMeasurementDriverTask,
1451 >>> import astropy.table
1452 >>> import lsst.afw.image as afwImage
1453 >>> config = ForcedMeasurementDriverConfig()
1454 >>> config.doScaleVariance = True
1455 >>> config.scaleVariance.background.binSize = 32
1456 >>> config.doApCorr = True
1457 >>> config.measurement.plugins.names = [
1458 ... "base_PixelFlags",
1459 ... "base_TransformedCentroidFromCoord",
1461 ... "base_CircularApertureFlux",
1463 >>> config.measurement.slots.psfFlux = "base_PsfFlux"
1464 >>> config.measurement.slots.centroid = "base_TransformedCentroidFromCoord"
1465 >>> config.measurement.slots.shape = None
1466 >>> config.measurement.doReplaceWithNoise = False
1467 >>> calexp = butler.get("deepCoadd_calexp", dataId=...)
1468 >>> objtable = butler.get(
1469 ... "objectTable", dataId=..., storageClass="ArrowAstropy"
1471 >>> table = objtable[:5].copy()["objectId", "coord_ra", "coord_dec"]
1472 >>> driver = ForcedMeasurementDriverTask(config=config)
1473 >>> results = driver.runFromAstropy(
1476 ... id_column_name="objectId",
1477 ... ra_column_name="coord_ra",
1478 ... dec_column_name="coord_dec",
1479 ... psf_footprint_scaling=3.0,
1481 >>> results.writeFits("forced_meas_catalog.fits")
1484 ConfigClass = ForcedMeasurementDriverConfig
1485 _DefaultName =
"forcedMeasurementDriver"
1488 """Initialize the forced measurement driver task."""
1495 table: astropy.table.Table,
1498 id_column_name: str =
"objectId",
1499 ra_column_name: str =
"coord_ra",
1500 dec_column_name: str =
"coord_dec",
1501 psf_footprint_scaling: float = 3.0,
1502 idGenerator: measBase.IdGenerator |
None =
None,
1503 ) -> astropy.table.Table:
1504 """Run forced measurements on an exposure using an Astropy table.
1509 Astropy table containing source IDs and RA/Dec coordinates.
1510 Must contain columns with names specified by `id_column_name`,
1511 `ra_column_name`, and `dec_column_name`.
1513 Exposure on which to run the forced measurements.
1515 Name of the column containing source IDs in the table.
1517 Name of the column containing RA coordinates in the table.
1519 Name of the column containing Dec coordinates in the table.
1520 psf_footprint_scaling :
1521 Scaling factor to apply to the PSF second-moments ellipse in order
1522 to determine the footprint boundary.
1524 Object that generates source IDs and provides random seeds.
1525 If not provided, a new `IdGenerator` will be created.
1530 Astropy table containing the measured sources with columns
1531 corresponding to the source IDs, RA, Dec, from the input table, and
1532 additional measurement columns defined in the configuration.
1535 coord_unit = self.
_ensureValidInputs(table, exposure, id_column_name, ra_column_name, dec_column_name)
1538 if idGenerator
is None:
1539 idGenerator = measBase.IdGenerator()
1542 refWcs = exposure.getWcs()
1552 table, columns=[id_column_name, ra_column_name, dec_column_name], coord_unit=coord_unit
1556 bbox = exposure.getBBox()
1557 for record
in refCat:
1558 localPoint = refWcs.skyToPixel(record.getCoord())
1560 assert bbox.contains(localIntPoint), (
1561 f
"Center for record {record.getId()} is not in exposure; this should be guaranteed by "
1566 if self.
config.doScaleVariance:
1573 exposure, refCat, refWcs, idFactory=idGenerator.make_table_id_factory()
1579 self.
measurement.attachPsfShapeFootprints(catalog, exposure, scaling=psf_footprint_scaling)
1583 catalog = self.
_processCatalog(exposure, catalog, idGenerator, refCat=refCat)
1586 result = catalog.asAstropy()
1594 def run(self, *args, **kwargs):
1595 raise NotImplementedError(
1596 "The run method is not implemented for `ForcedMeasurementDriverTask`. "
1597 "Use `runFromAstropy` instead."
1601 raise NotImplementedError(
1602 "The `runFromImage` method is not implemented for `ForcedMeasurementDriverTask`. "
1603 "Use `runFromAstropy` instead."
1608 table: astropy.table.Table,
1610 id_column_name: str,
1611 ra_column_name: str,
1612 dec_column_name: str,
1614 """Validate the inputs for the forced measurement task.
1619 Astropy table containing source IDs and RA/Dec coordinates.
1621 Exposure on which to run the forced measurements.
1623 Name of the column containing source IDs in the table.
1625 Name of the column containing RA coordinates in the table.
1627 Name of the column containing Dec coordinates in the table.
1632 Unit of the sky coordinates extracted from the table.
1634 if not isinstance(table, astropy.table.Table):
1635 raise TypeError(f
"Expected 'table' to be an astropy Table, got {type(table)}")
1637 if table[ra_column_name].unit == table[dec_column_name].unit:
1638 if table[ra_column_name].unit == astropy.units.deg:
1639 coord_unit =
"degrees"
1640 elif table[ra_column_name].unit == astropy.units.rad:
1641 coord_unit =
"radians"
1644 coord_unit = str(table[ra_column_name].unit)
1646 raise ValueError(
"RA and Dec columns must have the same unit")
1649 raise TypeError(f
"Expected 'exposure' to be an Exposure, got {type(exposure)}")
1651 for col
in [id_column_name, ra_column_name, dec_column_name]:
1652 if col
not in table.colnames:
1653 raise ValueError(f
"Column '{col}' not found in the input table")
1659 table: astropy.table.Table,
1660 columns: list[str] = [
"id",
"ra",
"dec"],
1661 coord_unit: str =
"degrees",
1663 """Convert an Astropy Table to a minimal LSST SourceCatalog.
1665 This is intended for use with the forced measurement subtask, which
1666 expects a `SourceCatalog` input with a minimal schema containing `id`,
1672 Astropy Table containing source IDs and sky coordinates.
1674 Names of the columns in the order [id, ra, dec], where `ra` and
1675 `dec` are in degrees by default. If the coordinates are in radians,
1676 set `coord_unit` to "radians".
1678 Unit of the sky coordinates. Can be either "degrees" or "radians".
1682 outputCatalog : `lsst.afw.table.SourceCatalog`
1683 A SourceCatalog with minimal schema populated from the input table.
1688 If `coord_unit` is not "degrees" or "radians".
1689 If `columns` does not contain exactly 3 items.
1691 If any of the specified columns are missing from the input table.
1697 if coord_unit
not in [
"degrees",
"radians"]:
1698 raise ValueError(f
"Invalid coordinate unit '{coord_unit}'; must be 'degrees' or 'radians'")
1700 if len(columns) != 3:
1701 raise ValueError(
"`columns` must contain exactly three elements for [id, ra, dec]")
1703 idCol, raCol, decCol = columns
1706 if col
not in table.colnames:
1707 raise KeyError(f
"Missing required column: '{col}'")
1710 outputCatalog.reserve(len(table))
1713 outputRecord = outputCatalog.addNew()
1714 outputRecord.setId(row[idCol])
1715 outputRecord.setCoord(
1719 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)
tuple[dict[str, afwTable.SourceCatalog], scarlet.io.LsstScarletModelData] _deblendSources(self, afwImage.MultibandExposure mExposure, afwImage.MultibandExposure|None mDeconvolved, afwTable.SourceCatalog catalog, str refBand)
afwImage.MultibandExposure _buildMultibandExposure(self, afwImage.MultibandExposure|list[afwImage.Exposure]|afwImage.Exposure mExposureData, list[str]|None bands)
scarlet.io.LsstScarletModelData modelData
__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)
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.