22"""Read preprocessed bright stars and stack to build an extended PSF model."""
25 "FocalPlaneRegionExtendedPsf",
27 "StackBrightStarsConfig",
28 "StackBrightStarsTask",
29 "MeasureExtendedPsfConfig",
30 "MeasureExtendedPsfTask",
34from dataclasses
import dataclass
38from lsst.afw.math import StatisticsControl, statisticsStack, stringToStatisticsProperty
41from lsst.pex.config import ChoiceField, Config, ConfigDictField, ConfigurableField, Field, ListField
42from lsst.pipe.base
import PipelineTaskConfig, PipelineTaskConnections, Struct, Task
43from lsst.pipe.base.connectionTypes
import Input, Output
48 """Find the focal plane region that contains a given detector.
55 detectors_focal_plane_regions :
56 `dict` [`str`, `lsst.pipe.tasks.extended_psf.DetectorsInRegion`]
57 A dictionary containing focal plane region names as keys, and the
58 corresponding detector IDs encoded within the values.
63 The name of the region to which the given detector belongs.
68 Raised if the given detector is not included in any focal plane region.
70 for region_id, detectors_in_region
in detectors_focal_plane_regions.items():
71 if detector_id
in detectors_in_region.detectors:
74 "Detector %d is not included in any focal plane region.",
80 """Provides a list of detectors that define a region."""
82 detectors = ListField[int](
83 doc=
"A list containing the detectors IDs.",
90 """Single extended PSF over a focal plane region.
92 The focal plane region is defined through a list of detectors.
96 extended_psf_image : `lsst.afw.image.MaskedImageF`
97 Image of the extended PSF model.
98 region_detectors : `lsst.pipe.tasks.extended_psf.DetectorsInRegion`
99 List of detector IDs that define the focal plane region over which this
100 extended PSF model has been built (and can be used).
103 extended_psf_image: MaskedImageF
104 region_detectors: DetectorsInRegion
108 """Extended PSF model.
110 Each instance may contain a default extended PSF, a set of extended PSFs
111 that correspond to different focal plane regions, or both. At this time,
112 focal plane regions are always defined as a subset of detectors.
116 default_extended_psf : `lsst.afw.image.MaskedImageF`
117 Extended PSF model to be used as default (or only) extended PSF model.
126 """Add a new focal plane region, along with its extended PSF, to the
127 ExtendedPsf instance.
131 extended_psf_image : `lsst.afw.image.MaskedImageF`
132 Extended PSF model for the region.
134 Name of the focal plane region. Will be converted to all-uppercase.
135 region_detectors : `lsst.pipe.tasks.extended_psf.DetectorsInRegion`
136 List of detector IDs for the detectors that define a region on the
139 region_name = region_name.upper()
141 raise ValueError(f
"Region name {region_name} is already used by this ExtendedPsf instance.")
143 extended_psf_image=extended_psf_image, region_detectors=region_detectors
148 """Return the appropriate extended PSF.
150 If the instance contains no extended PSF defined over focal plane
151 regions, the default extended PSF will be returned regardless of
152 whether a detector ID was passed as argument.
156 detector : `int`, optional
157 Detector ID. If focal plane region PSFs are defined, is used to
158 determine which model to return.
162 extendedPsfImage : `lsst.afw.image.MaskedImageF`
163 The extended PSF model. If this instance contains extended PSFs
164 defined over focal plane regions, the extended PSF model for the
165 region that contains ``detector`` is returned. If not, the default
166 extended PSF is returned.
170 raise ValueError(
"No default extended PSF available; please provide detector number.")
177 """Returns the number of extended PSF models present in the instance.
179 Note that if the instance contains both a default model and a set of
180 focal plane region models, the length of the instance will be the
181 number of regional models, plus one (the default). This is true even
182 in the case where the default model is one of the focal plane
183 region-specific models.
191 """Returns the extended PSF for a focal plane region or detector.
193 This method takes either a region name or a detector ID as input. If
194 the input is a `str` type, it is assumed to be the region name and if
195 the input is a `int` type it is assumed to be the detector ID.
199 region_name : `str` or `int`
200 Name of the region (str) or detector (int) for which the extended
201 PSF should be retrieved.
205 extended_psf_image: `lsst.afw.image.MaskedImageF`
206 The extended PSF model for the requested region or detector.
211 Raised if the input is not in the correct type.
213 if isinstance(region_name, str):
215 elif isinstance(region_name, int):
219 raise ValueError(
"A region name with `str` type or detector number with `int` must be provided")
222 """Write this object to a file.
227 Name of file to write.
233 metadata[
"HAS_REGIONS"] =
True
236 metadata[region] = e_psf_region.region_detectors.detectors
238 metadata[
"HAS_REGIONS"] =
False
239 fits_primary =
Fits(filename,
"w")
240 fits_primary.createEmpty()
241 fits_primary.writeMetadata(metadata)
242 fits_primary.closeFile()
246 default_hdu_metadata.update({
"REGION":
"DEFAULT",
"EXTNAME":
"IMAGE"})
248 default_hdu_metadata.update({
"REGION":
"DEFAULT",
"EXTNAME":
"MASK"})
253 metadata.update({
"REGION": region,
"EXTNAME":
"IMAGE"})
254 e_psf_region.extended_psf_image.image.writeFits(filename, metadata=metadata, mode=
"a")
255 metadata.update({
"REGION": region,
"EXTNAME":
"MASK"})
256 e_psf_region.extended_psf_image.mask.writeFits(filename, metadata=metadata, mode=
"a")
259 """Alias for ``write_fits``; for compatibility with the Butler."""
264 """Build an instance of this class from a file.
269 Name of the file to read.
272 global_metadata = readMetadata(filename, hdu=0)
273 has_default = global_metadata.getBool(
"HAS_DEFAULT")
274 if global_metadata.getBool(
"HAS_REGIONS"):
275 focal_plane_region_names = global_metadata.getArray(
"REGION_NAMES")
277 focal_plane_region_names = []
278 f =
Fits(filename,
"r")
279 n_extensions = f.countHdus()
280 extended_psf_parts = {}
281 for j
in range(1, n_extensions):
282 md = readMetadata(filename, hdu=j)
283 if has_default
and md[
"REGION"] ==
"DEFAULT":
284 if md[
"EXTNAME"] ==
"IMAGE":
285 default_image = ImageF(filename, hdu=j)
286 elif md[
"EXTNAME"] ==
"MASK":
287 default_mask = MaskX(filename, hdu=j)
289 if md[
"EXTNAME"] ==
"IMAGE":
290 extended_psf_part = ImageF(filename, hdu=j)
291 elif md[
"EXTNAME"] ==
"MASK":
292 extended_psf_part = MaskX(filename, hdu=j)
293 extended_psf_parts.setdefault(md[
"REGION"], {})[md[
"EXTNAME"].lower()] = extended_psf_part
296 extended_psf = cls(MaskedImageF(default_image, default_mask))
300 if len(extended_psf_parts) != len(focal_plane_region_names):
302 f
"Number of per-region extended PSFs read ({len(extended_psf_parts)}) does not "
303 "match with the number of regions recorded in the metadata "
304 f
"({len(focal_plane_region_names)})."
307 for r_name
in focal_plane_region_names:
308 extended_psf_image = MaskedImageF(**extended_psf_parts[r_name])
310 region_detectors.detectors = global_metadata.getArray(r_name)
311 extended_psf.add_regional_extended_psf(extended_psf_image, r_name, region_detectors)
317 """Alias for ``readFits``; exists for compatibility with the Butler."""
322 """Configuration parameters for StackBrightStarsTask."""
324 subregion_size = ListField[int](
325 doc=
"Size, in pixels, of the subregions over which the stacking will be " "iteratively performed.",
328 stacking_statistic = ChoiceField[str](
329 doc=
"Type of statistic to use for stacking.",
334 "MEANCLIP":
"clipped mean",
337 num_sigma_clip = Field[float](
338 doc=
"Sigma for outlier rejection; ignored if stacking_statistic != 'MEANCLIP'.",
341 num_iter = Field[int](
342 doc=
"Number of iterations of outlier rejection; ignored if stackingStatistic != 'MEANCLIP'.",
345 bad_mask_planes = ListField[str](
346 doc=
"Mask planes that define pixels to be excluded from the stacking of the bright star stamps.",
347 default=(
"BAD",
"CR",
"CROSSTALK",
"EDGE",
"NO_DATA",
"SAT",
"SUSPECT",
"UNMASKEDNAN"),
349 do_mag_cut = Field[bool](
350 doc=
"Apply magnitude cut before stacking?",
353 mag_limit = Field[float](
354 doc=
"Magnitude limit, in Gaia G; all stars brighter than this value will be stacked",
360 """Stack bright stars together to build an extended PSF model."""
362 ConfigClass = StackBrightStarsConfig
363 _DefaultName =
"stack_bright_stars"
366 """Configure stacking statistic and control from config fields."""
368 numSigmaClip=self.config.num_sigma_clip,
369 numIter=self.config.num_iter,
371 if bad_masks := self.config.bad_mask_planes:
372 and_mask = example_stamp.mask.getPlaneBitMask(bad_masks[0])
373 for bm
in bad_masks[1:]:
374 and_mask = and_mask | example_stamp.mask.getPlaneBitMask(bm)
375 stats_control.setAndMask(and_mask)
376 stats_flags = stringToStatisticsProperty(self.config.stacking_statistic)
377 return stats_control, stats_flags
379 def run(self, bss_ref_list, region_name=None):
380 """Read input bright star stamps and stack them together.
382 The stacking is done iteratively over smaller areas of the final model
383 image to allow for a great number of bright star stamps to be used.
387 bss_ref_list : `list` of
388 `lsst.daf.butler._deferredDatasetHandle.DeferredDatasetHandle`
389 List of available bright star stamps data references.
390 region_name : `str`, optional
391 Name of the focal plane region, if applicable. Only used for
392 logging purposes, when running over multiple such regions
393 (typically from `MeasureExtendedPsfTask`)
396 region_message = f
" for region '{region_name}'."
400 "Building extended PSF from stamps extracted from %d detector images%s",
405 example_bss = bss_ref_list[0].get()
406 example_stamp = example_bss[0].stamp_im
408 ext_psf = MaskedImageF(example_stamp.getBBox())
410 subregion_size = Extent2I(*self.config.subregion_size)
411 sub_bboxes = subBBoxIter(ext_psf.getBBox(), subregion_size)
413 n_subregions = ((ext_psf.getDimensions()[0]) // (subregion_size[0] + 1)) * (
414 (ext_psf.getDimensions()[1]) // (subregion_size[1] + 1)
417 "Stacking performed iteratively over approximately %d smaller areas of the final model image.",
423 for jbbox, bbox
in enumerate(sub_bboxes):
425 for bss_ref
in bss_ref_list:
426 read_stars = bss_ref.get(parameters={
"bbox": bbox})
427 if self.config.do_mag_cut:
428 read_stars = read_stars.selectByMag(magMax=self.config.mag_limit)
430 all_stars.extend(read_stars)
432 all_stars = read_stars
434 coadd_sub_bbox = statisticsStack(all_stars.getMaskedImages(), stats_flags, stats_control)
435 ext_psf.assign(coadd_sub_bbox, bbox)
440 input_brightStarStamps = Input(
441 doc=
"Input list of bright star collections to be stacked.",
442 name=
"brightStarStamps",
443 storageClass=
"BrightStarStamps",
444 dimensions=(
"visit",
"detector"),
448 extended_psf = Output(
449 doc=
"Extended PSF model built by stacking bright stars.",
451 storageClass=
"ExtendedPsf",
452 dimensions=(
"band",),
457 """Configuration parameters for MeasureExtendedPsfTask."""
459 stack_bright_stars = ConfigurableField(
460 target=StackBrightStarsTask,
461 doc=
"Stack selected bright stars",
463 detectors_focal_plane_regions = ConfigDictField(
465 itemtype=DetectorsInRegion,
467 "Mapping from focal plane region names to detector IDs. "
468 "If empty, a constant extended PSF model is built from all selected bright stars. "
469 "It's possible for a single detector to be included in multiple regions if so desired."
476 """Build and save extended PSF model.
478 The model is built by stacking bright star stamps, extracted and
480 `lsst.pipe.tasks.processBrightStars.ProcessBrightStarsTask`.
482 If a mapping from detector IDs to focal plane regions is provided, a
483 different extended PSF model will be built for each focal plane region. If
484 not, a single constant extended PSF model is built with all available data.
487 ConfigClass = MeasureExtendedPsfConfig
488 _DefaultName =
"measureExtendedPsf"
490 def __init__(self, initInputs=None, *args, **kwargs):
492 self.makeSubtask(
"stack_bright_stars")
497 """Split available sets of bright star stamps according to focal plane
503 `lsst.daf.butler._deferredDatasetHandle.DeferredDatasetHandle`
504 List of available bright star stamps data references.
507 for dataset_handle
in ref_list:
508 detector_id = dataset_handle.ref.dataId[
"detector"]
515 "Bright stars were available for detector %d, but it was missing from the %s config "
516 "field, so they will not be used to build any of the extended PSF models.",
518 "'detectors_focal_plane_regions'",
522 region_ref_list[region_name].append(dataset_handle)
523 return region_ref_list
526 input_data = butlerQC.get(inputRefs)
527 bss_ref_list = input_data[
"input_brightStarStamps"]
528 if not self.config.detectors_focal_plane_regions:
530 "No detector groups were provided to MeasureExtendedPsfTask; computing a single, "
531 "constant extended PSF model over all available observations."
533 output_e_psf =
ExtendedPsf(self.stack_bright_stars.run(bss_ref_list))
537 for region_name, ref_list
in region_ref_list.items():
541 "No valid brightStarStamps reference found for region '%s'; skipping it.",
545 ext_psf = self.stack_bright_stars.run(ref_list, region_name)
546 output_e_psf.add_regional_extended_psf(
549 output = Struct(extended_psf=output_e_psf)
550 butlerQC.put(output, outputRefs)
writeFits(self, filename)
__init__(self, default_extended_psf=None)
detectors_focal_plane_regions
add_regional_extended_psf(self, extended_psf_image, region_name, region_detectors)
write_fits(self, filename)
get_extended_psf(self, region_name)
__call__(self, detector=None)
__init__(self, initInputs=None, *args, **kwargs)
runQuantum(self, butlerQC, inputRefs, outputRefs)
detectors_focal_plane_regions
select_detector_refs(self, ref_list)
run(self, bss_ref_list, region_name=None)
_set_up_stacking(self, example_stamp)
find_region_for_detector(detector_id, detectors_focal_plane_regions)