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
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}")
131 """Base class for the mid-level driver running variance scaling, detection,
132 deblending, measurement, apperture correction, and catalog calculation in
135 Users don't need to Butlerize their input data, which is a significant
136 advantage for quick data exploration and testing. This driver simplifies
137 the process of applying measurement algorithms to images by abstracting
138 away low-level implementation details such as Schema and table boilerplate.
139 It's a convenient way to process images into catalogs with a user-friendly
140 interface for non-developers while allowing extensive configuration and
141 integration into unit tests for developers. It also considerably improves
142 how demos and workflows are showcased in Jupyter notebooks.
147 Schema used to create the output `~lsst.afw.table.SourceCatalog`,
148 modified in place with fields that will be written by this task.
150 Schema of Footprint Peaks that will be passed to the deblender.
152 Additional kwargs to pass to lsst.pipe.base.Task.__init__()
156 Subclasses (e.g., single-band vs. multi-band) share most methods and config
157 options but differ in handling and validating inputs by overriding the base
158 config class and any methods that require their own logic.
161 ConfigClass = MeasurementDriverBaseConfig
162 _DefaultName =
"measurementDriverBase"
178 self.
deblend: measDeblender.SourceDeblendTask | scarlet.ScarletDeblendTask
179 self.
measurement: measBase.SingleFrameMeasurementTask | measBase.ForcedMeasurementTask
188 super().
__setattr__(
"initSchema", copy.deepcopy(schema))
191 """Prevent external modifications of the initial Schema."""
192 if name ==
"initSchema":
193 raise AttributeError(f
"Cannot modify {name} directly")
197 def run(self, *args, **kwargs) -> pipeBase.Struct:
198 """Run the measurement driver task. Subclasses must implement this
199 method using their own logic to handle single-band or multi-band data.
201 raise NotImplementedError(
"This is not implemented on the base class")
205 catalog: afwTable.SourceCatalog |
None,
207 """Perform validation and adjustments of inputs without heavy
213 Catalog to be extended by the driver task.
219 if catalog
is not None:
221 "An input catalog was given to bypass detection, but 'doDetect' is still on"
225 raise RuntimeError(
"Cannot run without detection if no 'catalog' is provided")
228 """Initialize the Schema to be used for constructing the subtasks.
230 Though it may seem clunky, this workaround is necessary to ensure
231 Schema consistency across all subtasks.
236 Catalog from which to extract the Schema. If not provided, the
237 user-provided Schema and if that is also not provided during
238 initialization, a minimal Schema will be used.
250 self.
schema = afwTable.SourceTable.makeMinimalSchema()
255 if self.
schema is not None:
257 "Both a catalog and a Schema were provided; using the Schema from the catalog only"
261 catalogSchema = catalog.schema
271 self.
mapper.addMinimalSchema(catalogSchema,
True)
277 if isinstance(self, ForcedMeasurementDriverTask):
281 self.
schema.addField(
"deblend_nChild",
"I",
"Needed for minimal forced photometry schema")
284 """Add coordinate error fields to the schema in-place if they are not
290 Schema to be checked for coordinate error fields.
293 errorField
in schema.getNames()
294 for errorField
in (
"coord_raErr",
"coord_decErr",
"coord_ra_dec_Cov")
296 afwTable.CoordKey.addErrorFields(schema)
299 """Construct subtasks based on the configuration and the Schema."""
300 if self.
schema is None and any(
301 getattr(self.
config, attr)
for attr
in self.
config.doOptions
if attr !=
"doScaleVariance"
304 "Cannot create requested subtasks without a Schema; "
305 "ensure one is provided explicitly or via a catalog"
308 if self.
config.doScaleVariance:
309 self.makeSubtask(
"scaleVariance")
311 if isinstance(self, ForcedMeasurementDriverTask):
314 self.makeSubtask(
"measurement", refSchema=self.
schema)
326 self.makeSubtask(
"applyApCorr", schema=self.
measurement.schema)
330 if self.
config.doRunCatalogCalculation:
331 self.makeSubtask(
"catalogCalculation", schema=self.
measurement.schema)
334 self.makeSubtask(
"detection", schema=self.
schema)
340 self.makeSubtask(
"measurement", schema=self.
schema)
343 self.makeSubtask(
"applyApCorr", schema=self.
measurement.schema)
345 if self.
config.doRunCatalogCalculation:
346 self.makeSubtask(
"catalogCalculation", schema=self.
schema)
349 self, catalog: afwTable.SourceCatalog |
None
351 """Ensure subtasks are properly initialized according to the
352 configuration and the provided catalog.
357 Optional catalog to be used for initializing the Schema and the
363 Updated catalog to be passed to the subtasks, if it was provided.
373 self.
schema.checkUnits(parse_strict=self.
config.checkUnitsParseStrict)
382 """Scale the variance plane of an exposure to match the observed
388 Exposure on which to run the variance scaling algorithm.
390 Band associated with the exposure. Used for logging.
392 self.log.info(f
"Scaling variance plane for {band} band")
394 exposure.getMetadata().add(
"VARIANCE_SCALE", varScale)
399 """Make a catalog or catalogs contiguous if they are not already.
404 Catalog or dictionary of catalogs with bands as keys to be made
410 Contiguous catalog or dictionary of contiguous catalogs.
412 if isinstance(catalog, dict):
413 for band, cat
in catalog.items():
414 if not cat.isContiguous():
415 self.log.info(f
"{band}-band catalog is not contiguous; making it contiguous")
416 catalog[band] = cat.copy(deep=
True)
418 if not catalog.isContiguous():
419 self.log.info(
"Catalog is not contiguous; making it contiguous")
420 catalog = catalog.copy(deep=
True)
424 """Update the Schema of the provided catalog to incorporate changes
425 made by the configured subtasks.
430 Catalog to be updated with the Schema changes.
435 Catalog with the updated Schema.
443 updatedCatalog.extend(catalog, mapper=self.
mapper)
447 return updatedCatalog
452 """Run the detection subtask to identify sources in the image.
457 Exposure on which to run the detection algorithm.
459 Generator for unique source IDs.
464 A catalog containing detected sources.
466 A list of background models obtained from the detection process,
469 self.log.info(f
"Running detection on a {exposure.width}x{exposure.height} pixel exposure")
473 table = afwTable.SourceTable.make(self.
schema, idGenerator.make_table_id_factory())
476 detections = self.
detection.run(table, exposure)
477 catalog = detections.sources
481 if hasattr(detections,
"background")
and detections.background:
482 for bg
in detections.background:
483 backgroundList.append(bg)
485 return catalog, backgroundList
489 """Run the deblending subtask to separate blended sources. Subclasses
490 must implement this method to handle task-specific deblending logic.
492 raise NotImplementedError(
"This is not implemented on the base class")
496 exposure: afwImage.Exposure,
498 idGenerator: measBase.IdGenerator,
501 """Run the measurement subtask to compute properties of sources.
506 Exposure on which to run the measurement algorithm.
508 Catalog containing sources on which to run the measurement subtask.
510 Generator for unique source IDs.
512 Reference catalog to be used for forced measurements, if any.
513 If not provided, the measurement will be run on the sources in the
514 catalog in a standard manner without reference.
519 refWcs = exposure.getWcs()
526 exposureId=idGenerator.catalog_id,
530 self.
measurement.run(measCat=catalog, exposure=exposure, exposureId=idGenerator.catalog_id)
535 """Apply aperture corrections to the catalog.
540 Exposure on which to apply aperture corrections.
542 Catalog to be corrected using the aperture correction map from
545 Generator for unique source IDs.
547 apCorrMap = exposure.getInfo().getApCorrMap()
548 if apCorrMap
is None:
550 "Image does not have valid aperture correction map for catalog id "
551 f
"{idGenerator.catalog_id}; skipping aperture correction"
554 self.
applyApCorr.run(catalog=catalog, apCorrMap=apCorrMap)
557 """Run the catalog calculation plugins on the catalog.
562 Catalog to be processed by the catalog calculation subtask.
568 exposure: afwImage.Exposure,
570 idGenerator: measBase.IdGenerator,
571 band: str =
"a single",
574 """Process a catalog through measurement, aperture correction, and
575 catalog calculation subtasks.
580 Exposure associated with the catalog.
582 Catalog to be processed by the subtasks.
584 Generator for unique source IDs.
586 Band associated with the exposure and catalog. Used for logging.
588 Reference catalog for forced measurements. If not provided, the
589 measurement will be run on the sources in the catalog in a standard
590 manner without reference.
595 Catalog after processing through the configured subtasks.
599 if self.
config.psfCache > 0:
601 exposure.psf.setCacheCapacity(self.
config.psfCache)
609 exposure.psf.setCacheCapacity(2 * len(self.
config.measurement.plugins.names))
614 f
"Measuring {len(catalog)} sources in {band} band "
615 f
"using '{self.measurement.__class__.__name__}'"
624 self.log.info(f
"Applying aperture corrections to {band} band")
628 if self.
config.doRunCatalogCalculation:
629 self.log.info(f
"Running catalog calculation on {band} band")
633 f
"Finished processing for {band} band; output catalog has {catalog.schema.getFieldCount()} "
634 f
"fields and {len(catalog)} records"
641 """Configuration for the single-band measurement driver task."""
643 deblend =
ConfigurableField(target=measDeblender.SourceDeblendTask, doc=
"Deblender for single-band data.")
647 """Mid-level driver for processing single-band data.
649 Offers a helper method for direct handling of raw image data in addition to
650 the standard single-band exposure.
654 Here is an example of how to use this class to run variance scaling,
655 detection, deblending, and measurement on a single-band exposure:
657 >>> from lsst.pipe.tasks.measurementDriver import (
658 ... SingleBandMeasurementDriverConfig,
659 ... SingleBandMeasurementDriverTask,
661 >>> import lsst.meas.extensions.shapeHSM # To register its plugins
662 >>> config = SingleBandMeasurementDriverConfig()
663 >>> config.doScaleVariance = True
664 >>> config.doDetect = True
665 >>> config.doDeblend = True
666 >>> config.doMeasure = True
667 >>> config.scaleVariance.background.binSize = 64
668 >>> config.detection.thresholdValue = 5.5
669 >>> config.deblend.tinyFootprintSize = 3
670 >>> config.measurement.plugins.names |= [
671 ... "base_SdssCentroid",
672 ... "base_SdssShape",
673 ... "ext_shapeHSM_HsmSourceMoments",
675 >>> config.measurement.slots.psfFlux = None
676 >>> config.measurement.doReplaceWithNoise = False
677 >>> exposure = butler.get("deepCoadd", dataId=...)
678 >>> driver = SingleBandMeasurementDriverTask(config=config)
679 >>> results = driver.run(exposure)
680 >>> results.catalog.writeFits("meas_catalog.fits")
682 Alternatively, if an exposure is not available, the driver can also process
691 >>> results = driver.runFromImage(
692 ... image, mask, variance, wcs, psf, photoCalib
694 >>> results.catalog.writeFits("meas_catalog.fits")
697 ConfigClass = SingleBandMeasurementDriverConfig
698 _DefaultName =
"singleBandMeasurementDriver"
699 _Deblender =
"meas_deblender"
709 exposure: afwImage.Exposure,
711 idGenerator: measBase.IdGenerator |
None =
None,
712 ) -> pipeBase.Struct:
713 """Process a single-band exposure through the configured subtasks and
714 return the results as a struct.
719 The exposure on which to run the driver task.
721 Catalog to be extended by the driver task. If not provided, an
722 empty catalog will be created and populated.
724 Object that generates source IDs and provides random seeds.
729 Results as a struct with attributes:
732 Catalog containing the measured sources
733 (`~lsst.afw.table.SourceCatalog`).
735 List of backgrounds (`list[~lsst.afw.math.Background]`). Only
736 populated if detection is enabled.
746 if idGenerator
is None:
747 idGenerator = measBase.IdGenerator()
751 if self.
config.doScaleVariance:
756 catalog, backgroundList = self.
_detectSources(exposure, idGenerator)
758 self.log.info(
"Skipping detection; using detections from the provided catalog")
759 backgroundList =
None
765 self.log.info(
"Skipping deblending")
771 return pipeBase.Struct(catalog=catalog, backgroundList=backgroundList)
779 psf: afwDetection.Psf | np.ndarray =
None,
782 idGenerator: measBase.IdGenerator =
None,
783 ) -> pipeBase.Struct:
784 """Convert image data to an `Exposure`, then run it through the
790 Input image data. Will be converted into an `Exposure` before
793 Mask data for the image. Used if ``image`` is a bare `array` or
796 Variance plane data for the image.
798 World Coordinate System to associate with the exposure that will
799 be created from ``image``.
801 PSF model for the exposure.
803 Photometric calibration model for the exposure.
805 Catalog to be extended by the driver task. If not provided, a new
806 catalog will be created during detection and populated.
808 Generator for unique source IDs.
813 Results as a struct with attributes:
816 Catalog containing the measured sources
817 (`~lsst.afw.table.SourceCatalog`).
819 List of backgrounds (`list[~lsst.afw.math.Background]`).
822 if isinstance(image, np.ndarray):
823 image = afwImage.makeImageFromArray(image)
824 if isinstance(mask, np.ndarray):
825 mask = afwImage.makeMaskFromArray(mask)
826 if isinstance(variance, np.ndarray):
827 variance = afwImage.makeImageFromArray(variance)
836 raise TypeError(f
"Unsupported 'image' type: {type(image)}")
839 if isinstance(psf, np.ndarray):
844 psf = afwDetection.KernelPsf(kernel)
845 elif not isinstance(psf, afwDetection.Psf):
846 raise TypeError(f
"Unsupported 'psf' type: {type(psf)}")
849 if photoCalib
is not None:
850 exposure.setPhotoCalib(photoCalib)
852 return self.
run(exposure, catalog=catalog, idGenerator=idGenerator)
857 """Run single-band deblending given an exposure and a catalog.
862 Exposure on which to run the deblending algorithm.
864 Catalog containing sources to be deblended.
869 Catalog after deblending, with sources separated into their
870 individual components if they were deblended.
872 self.log.info(f
"Deblending using '{self._Deblender}' on {len(catalog)} detection footprints")
873 self.
deblend.run(exposure=exposure, sources=catalog)
880 """Configuration for the multi-band measurement driver task."""
883 target=scarlet.ScarletDeblendTask, doc=
"Scarlet deblender for multi-band data"
886 doConserveFlux = Field[bool](
887 doc=
"Whether to use the deblender models as templates to re-distribute the flux from "
888 "the 'exposure' (True), or to perform measurements on the deblender model footprints.",
892 measureOnlyInRefBand = Field[bool](
893 doc=
"If True, all measurements downstream of deblending run only in the reference band that "
894 "was used for detection; otherwise, they are performed in all available bands, generating a "
895 "catalog for each. Regardless of this setting, deblending still uses all available bands.",
899 removeScarletData = Field[bool](
900 doc=
"Whether or not to remove `ScarletBlendData` for each blend in order to save memory. "
901 "If set to True, some sources may end up with missing footprints in catalogs other than the "
902 "reference-band catalog, leading to failures in subsequent measurements that require footprints. "
903 "For example, keep this False if `measureOnlyInRefBand` is set to False and "
904 "`measurement.doReplaceWithNoise` to True, in order to make the footprints available in "
905 "non-reference bands in addition to the reference band.",
909 updateFluxColumns = Field[bool](
910 doc=
"Whether or not to update the `deblend_*` columns in the catalog. This should only be "
911 "True when the input catalog schema already contains those columns.",
917 """Mid-level driver for processing multi-band data.
919 The default behavior is to run detection on the reference band, use all
920 available bands for deblending, and then process everything downstream
921 separately for each band making per-band catalogs unless configured
922 otherwise. This subclass provides functionality for handling a singe-band
923 exposure and a list of single-band exposures in addition to a standard
928 Here is an example of how to use this class to run variance scaling,
929 detection, deblending, measurement, and aperture correction on a multi-band
932 >>> from lsst.afw.image import MultibandExposure
933 >>> from lsst.pipe.tasks.measurementDriver import (
934 ... MultiBandMeasurementDriverConfig,
935 ... MultiBandMeasurementDriverTask,
937 >>> import lsst.meas.extensions.shapeHSM # To register its plugins
938 >>> config = MultiBandMeasurementDriverConfig()
939 >>> config.doScaleVariance = True
940 >>> config.doDetect = True
941 >>> config.doDeblend = True
942 >>> config.doMeasure = True
943 >>> config.doApCorr = True
944 >>> config.scaleVariance.background.binSize = 64
945 >>> config.detection.thresholdValue = 5.5
946 >>> config.deblend.minSNR = 42.0
947 >>> config.deblend.maxIter = 20
948 >>> config.measurement.plugins.names |= [
949 ... "base_SdssCentroid",
950 ... "base_SdssShape",
951 ... "ext_shapeHSM_HsmSourceMoments",
953 >>> config.measurement.slots.psfFlux = None
954 >>> config.measurement.doReplaceWithNoise = False
955 >>> config.applyApCorr.doFlagApCorrFailures = False
956 >>> mExposure = MultibandExposure.fromButler(
957 ... butler, ["g", "r", "i"], "deepCoadd_calexp", ...
959 >>> driver = MultiBandMeasurementDriverTask(config=config)
960 >>> results = driver.run(mExposure, "r")
961 >>> for band, catalog in results.catalogs.items():
962 ... catalog.writeFits(f"meas_catalog_{band}.fits")
965 ConfigClass = MultiBandMeasurementDriverConfig
966 _DefaultName =
"multiBandMeasurementDriver"
967 _Deblender =
"scarlet"
981 refBand: str |
None =
None,
982 bands: list[str] |
None =
None,
984 idGenerator: measBase.IdGenerator =
None,
985 ) -> pipeBase.Struct:
986 """Process an exposure through the configured subtasks while using
987 multi-band information for deblending.
992 Multi-band data. May be a `MultibandExposure`, a single-band
993 exposure (i.e., `Exposure`), or a list of single-band exposures
994 associated with different bands in which case ``bands`` must be
995 provided. If a single-band exposure is given, it will be treated as
996 a `MultibandExposure` that contains only that one band.
998 Reference band to use for detection. Not required for single-band
999 exposures. If `measureOnlyInRefBand` is enabled while detection is
1000 disabled and a catalog of detected sources is provided, this
1001 should specify the band the sources were detected on (or the band
1002 you want to use to perform measurements on exclusively). If
1003 `measureOnlyInRefBand` is disabled instead in the latter scenario,
1004 ``refBand`` does not need to be provided.
1006 List of bands associated with the exposures in ``mExposure``. Only
1007 required if ``mExposure`` is a list of single-band exposures. If
1008 provided for a multi-band exposure, it will be used to only process
1009 that subset of bands from the available ones in the exposure.
1011 Catalog to be extended by the driver task. If not provided, a new
1012 catalog will be created and populated.
1014 Generator for unique source IDs.
1019 Results as a struct with attributes:
1022 Dictionary of catalogs containing the measured sources with
1023 bands as keys (`dict[str, ~lsst.afw.table.SourceCatalog]`). If
1024 `measureOnlyInRefBand` is enabled or deblending is disabled,
1025 this will only contain the reference-band catalog; otherwise,
1026 it will contain a catalog for each band.
1028 List of backgrounds (`list[~lsst.afw.math.Background]`). Will
1029 be None if detection is disabled.
1031 Multiband scarlet models produced during deblending
1032 (`~lsst.scarlet.lite.io.ScarletModelData`). Will be None if
1033 deblending is disabled.
1037 mExposure, refBand, bands = self.
_ensureValidInputs(mExposure, refBand, bands, catalog)
1043 if idGenerator
is None:
1044 idGenerator = measBase.IdGenerator()
1048 if self.
config.doScaleVariance:
1050 for band
in mExposure.bands:
1055 catalog, backgroundList = self.
_detectSources(mExposure[refBand], idGenerator)
1057 self.log.info(
"Skipping detection; using detections from provided catalog")
1058 backgroundList =
None
1061 if self.
config.doDeblend:
1065 "Skipping deblending; proceeding with the provided catalog in the reference band"
1067 catalogs = {refBand: catalog}
1072 for band, catalog
in catalogs.items():
1073 exposure = mExposure[band]
1074 self.
_processCatalog(exposure, catalog, idGenerator, band=f
"'{band}'")
1076 return pipeBase.Struct(catalogs=catalogs, backgroundList=backgroundList, modelData=self.
modelData)
1081 refBand: str |
None,
1082 bands: list[str] |
None,
1085 """Perform validation and adjustments of inputs without heavy
1091 Multi-band data to be processed by the driver task.
1093 Reference band to use for detection or measurements.
1095 List of bands associated with the exposures in ``mExposure``.
1097 Catalog to be extended by the driver task.
1102 Multi-band exposure to be processed by the driver task.
1104 Reference band to use for detection or measurements.
1106 List of bands associated with the exposures in ``mExposure``.
1114 if bands
is not None:
1115 if any(b
not in mExposure.bands
for b
in bands):
1117 "Some bands in the 'bands' list are not present in the input multi-band exposure"
1120 f
"Using bands {bands} out of the available {mExposure.bands} in the multi-band exposure"
1122 elif isinstance(mExposure, list):
1124 raise ValueError(
"The 'bands' list must be provided if 'mExposure' is a list")
1125 if len(bands) != len(mExposure):
1126 raise ValueError(
"Number of bands and exposures must match")
1128 if bands
is not None and len(bands) != 1:
1130 "The 'bands' list, if provided, must only contain a single band "
1131 "if a single-band exposure is given"
1133 if bands
is None and refBand
is None:
1136 elif bands
is None and refBand
is not None:
1138 elif bands
is not None and refBand
is None:
1141 raise TypeError(f
"Unsupported 'mExposure' type: {type(mExposure)}")
1147 if len(mExposure.bands) == 1:
1152 self.log.info(f
"Running '{self._Deblender}' in single-band mode; make sure it was intended!")
1154 refBand = mExposure.bands[0]
1156 "No reference band provided for single-band data; "
1157 f
"using the only available band ('{refBand}') as the reference band"
1161 if self.
config.measureOnlyInRefBand:
1162 measInfo =
"and everything downstream of deblending"
1165 "while subtasks downstream of deblending will be run in each of "
1166 f
"the {mExposure.bands} bands"
1168 self.log.info(f
"Using '{refBand}' as the reference band for detection {measInfo}")
1172 raise ValueError(
"Reference band must be provided for multi-band data")
1174 if refBand
not in mExposure.bands:
1175 raise ValueError(f
"Requested band '{refBand}' is not present in the multi-band exposure")
1177 if bands
is not None and refBand
not in bands:
1178 raise ValueError(f
"Reference band '{refBand}' is not in the list of 'bands' provided: {bands}")
1180 return mExposure, refBand, bands
1185 """Run multi-band deblending given a multi-band exposure and a catalog.
1190 Multi-band exposure on which to run the deblending algorithm.
1192 Catalog containing sources to be deblended.
1194 Reference band used for detection or the band to use for
1195 measurements if `measureOnlyInRefBand` is enabled.
1200 Dictionary of catalogs containing the deblended sources. If
1201 `measureOnlyInRefBand` is enabled, this will only contain the
1202 reference-band catalog; otherwise, it will contain a catalog for
1205 Multiband scarlet models produced during deblending.
1207 self.log.info(f
"Deblending using '{self._Deblender}' on {len(catalog)} detection footprints")
1210 catalog, modelData = self.
deblend.run(mExposure, catalog)
1213 bands = [refBand]
if self.
config.measureOnlyInRefBand
else mExposure.bands
1215 catalogs = {band: catalog.copy(deep=
True)
for band
in bands}
1218 imageForRedistribution = mExposure[band]
if self.
config.doConserveFlux
else None
1219 scarlet.io.updateCatalogFootprints(
1220 modelData=modelData,
1221 catalog=catalogs[band],
1223 imageForRedistribution=imageForRedistribution,
1224 removeScarletData=self.
config.removeScarletData,
1225 updateFluxColumns=self.
config.updateFluxColumns,
1233 bands: list[str] |
None,
1235 """Convert a single-band exposure or a list of single-band exposures to
1236 a `MultibandExposure` if not already of that type.
1238 No conversion will be done if ``mExposureData`` is already a
1239 `MultibandExposure` except it will be subsetted to the bands provided.
1244 Input multi-band data.
1246 List of bands associated with the exposures in ``mExposure``. Only
1247 required if ``mExposure`` is a list of single-band exposures. If
1248 provided while ``mExposureData`` is a ``MultibandExposure``, it
1249 will be used to select a specific subset of bands from the
1255 Converted multi-band exposure.
1258 if bands
and not set(bands).issubset(mExposureData.bands):
1260 f
"Requested bands {bands} are not a subset of available bands: {mExposureData.bands}"
1262 return mExposureData[bands,]
if bands
and len(bands) > 1
else mExposureData
1263 elif isinstance(mExposureData, list):
1264 mExposure = afwImage.MultibandExposure.fromExposures(bands, mExposureData)
1268 mExposure = afwImage.MultibandExposure.fromExposures(bands, [mExposureData])
1274 for band, exposure
in zip(bands, mExposureData):
1275 mExposure[band].setWcs(exposure.getWcs())
1281 """Configuration for the forced measurement driver task."""
1284 target=measBase.ForcedMeasurementTask,
1285 doc=
"Measurement task for forced measurements. This should be a "
1286 "measurement task that does not perform detection.",
1290 """Set default values for the configuration.
1292 This method overrides the base class method to ensure that `doDetect`
1293 is set to `False` by default, as this task is intended for forced
1294 measurements where detection is not performed. Also, it sets some
1295 default measurement plugins by default.
1303 "base_TransformedCentroidFromCoord",
1305 "base_CircularApertureFlux",
1309 """Validate the configuration.
1311 This method overrides the base class validation to ensure that
1312 `doDetect` is set to `False`, as this task is intended for forced
1313 measurements where detection is not performed.
1318 "ForcedMeasurementDriverTask should not perform detection or "
1319 "deblending; set doDetect=False and doDeblend=False"
1323 "ForcedMeasurementDriverTask must perform measurements; "
1324 "set doMeasure=True"
1329 """Forced measurement driver task for single-band data.
1331 This task is the 'forced' version of the `SingleBandMeasurementDriverTask`,
1332 intended as a convenience function for performing forced photometry on an
1333 input image given a set of IDs and RA/Dec coordinates. It is designed as a
1334 public-facing interface, allowing users to measure sources without
1335 explicitly instantiating and running pipeline tasks.
1339 Here is an example of how to use this class to run forced measurements on
1340 an exposure using an Astropy table containing source IDs and RA/Dec
1343 >>> from lsst.pipe.tasks.measurementDriver import (
1344 ... ForcedMeasurementDriverConfig,
1345 ... ForcedMeasurementDriverTask,
1347 >>> import astropy.table
1348 >>> import lsst.afw.image as afwImage
1349 >>> config = ForcedMeasurementDriverConfig()
1350 >>> config.doScaleVariance = True
1351 >>> config.scaleVariance.background.binSize = 32
1352 >>> config.doApCorr = True
1353 >>> config.measurement.plugins.names = [
1354 ... "base_PixelFlags",
1355 ... "base_TransformedCentroidFromCoord",
1357 ... "base_CircularApertureFlux",
1359 >>> config.measurement.slots.psfFlux = "base_PsfFlux"
1360 >>> config.measurement.slots.centroid = "base_TransformedCentroidFromCoord"
1361 >>> config.measurement.slots.shape = None
1362 >>> config.measurement.doReplaceWithNoise = False
1363 >>> calexp = butler.get("deepCoadd_calexp", dataId=...)
1364 >>> objtable = butler.get(
1365 ... "objectTable", dataId=..., storageClass="ArrowAstropy"
1367 >>> table = objtable[:5].copy()["objectId", "coord_ra", "coord_dec"]
1368 >>> driver = ForcedMeasurementDriverTask(config=config)
1369 >>> results = driver.runFromAstropy(
1372 ... id_column_name="objectId",
1373 ... ra_column_name="coord_ra",
1374 ... dec_column_name="coord_dec",
1375 ... psf_footprint_scaling=3.0,
1377 >>> results.writeFits("forced_meas_catalog.fits")
1380 ConfigClass = ForcedMeasurementDriverConfig
1381 _DefaultName =
"forcedMeasurementDriver"
1384 """Initialize the forced measurement driver task."""
1391 table: astropy.table.Table,
1394 id_column_name: str =
"objectId",
1395 ra_column_name: str =
"coord_ra",
1396 dec_column_name: str =
"coord_dec",
1397 psf_footprint_scaling: float = 3.0,
1398 idGenerator: measBase.IdGenerator |
None =
None,
1399 ) -> astropy.table.Table:
1400 """Run forced measurements on an exposure using an Astropy table.
1405 Astropy table containing source IDs and RA/Dec coordinates.
1406 Must contain columns with names specified by `id_column_name`,
1407 `ra_column_name`, and `dec_column_name`.
1409 Exposure on which to run the forced measurements.
1411 Name of the column containing source IDs in the table.
1413 Name of the column containing RA coordinates in the table.
1415 Name of the column containing Dec coordinates in the table.
1416 psf_footprint_scaling :
1417 Scaling factor to apply to the PSF second-moments ellipse in order
1418 to determine the footprint boundary.
1420 Object that generates source IDs and provides random seeds.
1421 If not provided, a new `IdGenerator` will be created.
1426 Astropy table containing the measured sources with columns
1427 corresponding to the source IDs, RA, Dec, from the input table, and
1428 additional measurement columns defined in the configuration.
1431 self.
_ensureValidInputs(table, exposure, id_column_name, ra_column_name, dec_column_name)
1434 if idGenerator
is None:
1435 idGenerator = measBase.IdGenerator()
1438 refWcs = exposure.getWcs()
1448 table, columns=[id_column_name, ra_column_name, dec_column_name]
1452 bbox = exposure.getBBox()
1453 for record
in refCat:
1454 localPoint = refWcs.skyToPixel(record.getCoord())
1456 assert bbox.contains(localIntPoint), (
1457 f
"Center for record {record.getId()} is not in exposure; this should be guaranteed by "
1462 if self.
config.doScaleVariance:
1469 exposure, refCat, refWcs, idFactory=idGenerator.make_table_id_factory()
1475 self.
measurement.attachPsfShapeFootprints(catalog, exposure, scaling=psf_footprint_scaling)
1479 catalog = self.
_processCatalog(exposure, catalog, idGenerator, refCat=refCat)
1482 result = catalog.asAstropy()
1490 def run(self, *args, **kwargs):
1491 raise NotImplementedError(
1492 "The run method is not implemented for `ForcedMeasurementDriverTask`. "
1493 "Use `runFromAstropy` instead."
1497 raise NotImplementedError(
1498 "The `runFromImage` method is not implemented for `ForcedMeasurementDriverTask`. "
1499 "Use `runFromAstropy` instead."
1504 table: astropy.table.Table,
1506 id_column_name: str,
1507 ra_column_name: str,
1508 dec_column_name: str,
1510 """Validate the inputs for the forced measurement task.
1515 Astropy table containing source IDs and RA/Dec coordinates.
1517 Exposure on which to run the forced measurements.
1519 Name of the column containing source IDs in the table.
1521 Name of the column containing RA coordinates in the table.
1523 Name of the column containing Dec coordinates in the table.
1525 if not isinstance(table, astropy.table.Table):
1526 raise TypeError(f
"Expected 'table' to be an astropy Table, got {type(table)}")
1529 raise TypeError(f
"Expected 'exposure' to be an Exposure, got {type(exposure)}")
1531 for col
in [id_column_name, ra_column_name, dec_column_name]:
1532 if col
not in table.colnames:
1533 raise ValueError(f
"Column '{col}' not found in the input table")
1536 self, table: astropy.table.Table, columns: list[str] = [
"id",
"ra",
"dec"]
1538 """Convert an Astropy Table to a minimal LSST SourceCatalog.
1540 This is intended for use with the forced measurement subtask, which
1541 expects a `SourceCatalog` input with a minimal schema containing `id`,
1547 Astropy Table containing source IDs and sky coordinates.
1549 Names of the columns in the order [id, ra, dec], where `ra` and
1550 `dec` are in degrees.
1554 outputCatalog : `lsst.afw.table.SourceCatalog`
1555 A SourceCatalog with minimal schema populated from the input table.
1560 If `columns` does not contain exactly 3 items.
1562 If any of the specified columns are missing from the input table.
1568 if len(columns) != 3:
1569 raise ValueError(
"`columns` must contain exactly three elements for [id, ra, dec]")
1571 idCol, raCol, decCol = columns
1574 if col
not in table.colnames:
1575 raise KeyError(f
"Missing required column: '{col}'")
1578 outputCatalog.reserve(len(table))
1581 outputRecord = outputCatalog.addNew()
1582 outputRecord.setId(row[idCol])
1585 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)
_makeMinimalSourceCatalogFromAstropy(self, astropy.table.Table table, list[str] columns=["id", "ra", "dec"])
run(self, *args, **kwargs)
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, str|None refBand=None, list[str]|None bands=None, afwTable.SourceCatalog catalog=None, measBase.IdGenerator idGenerator=None)
tuple[afwImage.MultibandExposure, str, list[str]|None] _ensureValidInputs(self, afwImage.MultibandExposure|list[afwImage.Exposure]|afwImage.Exposure mExposure, 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, afwTable.SourceCatalog catalog, str refBand)
afwImage.MultibandExposure _buildMultibandExposure(self, afwImage.MultibandExposure|list[afwImage.Exposure]|afwImage.Exposure mExposureData, list[str]|None bands)
__init__(self, *args, **kwargs)
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.