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",
357 minValidAnnulusFraction = Field[float](
358 doc=
"Minimum number of valid pixels that must fall within the annulus for the bright star to be "
359 "included in the generation of a PSF.",
365 """Stack bright stars together to build an extended PSF model."""
367 ConfigClass = StackBrightStarsConfig
368 _DefaultName =
"stack_bright_stars"
371 """Configure stacking statistic and control from config fields."""
373 numSigmaClip=self.config.num_sigma_clip,
374 numIter=self.config.num_iter,
376 if bad_masks := self.config.bad_mask_planes:
377 and_mask = example_stamp.mask.getPlaneBitMask(bad_masks[0])
378 for bm
in bad_masks[1:]:
379 and_mask = and_mask | example_stamp.mask.getPlaneBitMask(bm)
380 stats_control.setAndMask(and_mask)
381 stats_flags = stringToStatisticsProperty(self.config.stacking_statistic)
382 return stats_control, stats_flags
385 """Remove stamps that do not have enough valid pixels in the annulus.
389 read_stars : `list` of `lsst.pipe.tasks.processBrightStars.BrightStarStamp`
390 List of bright star stamps to be stacked.
394 for stamp
in read_stars:
395 if stamp.validAnnulusFraction < self.config.minValidAnnulusFraction:
396 invalidStamps.append(stamp)
398 if len(invalidStamps):
399 for invalidStamp
in invalidStamps:
400 read_stars._stamps.remove(invalidStamp)
402 def run(self, bss_ref_list, stars_dict, region_name=None):
403 """Read input bright star stamps and stack them together.
405 The stacking is done iteratively over smaller areas of the final model
406 image to allow for a great number of bright star stamps to be used.
410 bss_ref_list : `list` of
411 `lsst.daf.butler._deferredDatasetHandle.DeferredDatasetHandle`
412 List of available bright star stamps data references.
414 Dictionary to store the number of stars used to generate the PSF.
415 region_name : `str`, optional
416 Name of the focal plane region, if applicable. Only used for
417 logging purposes, when running over multiple such regions
418 (typically from `MeasureExtendedPsfTask`)
421 region_message = f
" for region '{region_name}'."
424 if region_name
is not None:
425 stars_dict_key = region_name
427 stars_dict_key =
"all"
429 "Building extended PSF from stamps extracted from %d detector images%s",
434 example_bss = bss_ref_list[0].get()
435 example_stamp = example_bss[0].stamp_im
437 ext_psf = MaskedImageF(example_stamp.getBBox())
439 subregion_size = Extent2I(*self.config.subregion_size)
440 sub_bboxes = subBBoxIter(ext_psf.getBBox(), subregion_size)
442 n_subregions = ((ext_psf.getDimensions()[0]) // (subregion_size[0] + 1)) * (
443 (ext_psf.getDimensions()[1]) // (subregion_size[1] + 1)
446 "Stacking performed iteratively over approximately %d smaller areas of the final model image.",
452 for jbbox, bbox
in enumerate(sub_bboxes):
454 for bss_ref
in bss_ref_list:
455 read_stars = bss_ref.get(parameters={
"bbox": bbox})
460 stars_dict[stars_dict_key] += len(read_stars)
461 if self.config.do_mag_cut:
462 read_stars = read_stars.selectByMag(magMax=self.config.mag_limit)
464 all_stars.extend(read_stars)
466 all_stars = read_stars
468 coadd_sub_bbox = statisticsStack(all_stars.getMaskedImages(), stats_flags, stats_control)
469 ext_psf.assign(coadd_sub_bbox, bbox)
474 input_brightStarStamps = Input(
475 doc=
"Input list of bright star collections to be stacked.",
476 name=
"brightStarStamps",
477 storageClass=
"BrightStarStamps",
478 dimensions=(
"visit",
"detector"),
482 extended_psf = Output(
483 doc=
"Extended PSF model built by stacking bright stars.",
485 storageClass=
"ExtendedPsf",
486 dimensions=(
"band",),
491 """Configuration parameters for MeasureExtendedPsfTask."""
493 stack_bright_stars = ConfigurableField(
494 target=StackBrightStarsTask,
495 doc=
"Stack selected bright stars",
497 detectors_focal_plane_regions = ConfigDictField(
499 itemtype=DetectorsInRegion,
501 "Mapping from focal plane region names to detector IDs. "
502 "If empty, a constant extended PSF model is built from all selected bright stars. "
503 "It's possible for a single detector to be included in multiple regions if so desired."
510 """Build and save extended PSF model.
512 The model is built by stacking bright star stamps, extracted and
514 `lsst.pipe.tasks.processBrightStars.ProcessBrightStarsTask`.
516 If a mapping from detector IDs to focal plane regions is provided, a
517 different extended PSF model will be built for each focal plane region. If
518 not, a single constant extended PSF model is built with all available data.
521 ConfigClass = MeasureExtendedPsfConfig
522 _DefaultName =
"measureExtendedPsf"
524 def __init__(self, initInputs=None, *args, **kwargs):
526 self.makeSubtask(
"stack_bright_stars")
531 """Split available sets of bright star stamps according to focal plane
537 `lsst.daf.butler._deferredDatasetHandle.DeferredDatasetHandle`
538 List of available bright star stamps data references.
541 for dataset_handle
in ref_list:
542 detector_id = dataset_handle.ref.dataId[
"detector"]
549 "Bright stars were available for detector %d, but it was missing from the %s config "
550 "field, so they will not be used to build any of the extended PSF models.",
552 "'detectors_focal_plane_regions'",
556 region_ref_list[region_name].append(dataset_handle)
557 return region_ref_list
560 input_data = butlerQC.get(inputRefs)
561 bss_ref_list = input_data[
"input_brightStarStamps"]
564 self.metadata[
"psfStarCount"] = {}
565 if not self.config.detectors_focal_plane_regions:
567 "No detector groups were provided to MeasureExtendedPsfTask; computing a single, "
568 "constant extended PSF model over all available observations."
572 self.metadata[
"psfStarCount"][
"all"] = 0
574 self.stack_bright_stars.run(bss_ref_list, self.metadata[
"psfStarCount"])
579 for region_name, ref_list
in region_ref_list.items():
583 "No valid brightStarStamps reference found for region '%s'; skipping it.",
590 self.metadata[
"psfStarCount"][region_name] = 0
591 ext_psf = self.stack_bright_stars.run(ref_list, self.metadata[
"psfStarCount"], region_name)
592 output_e_psf.add_regional_extended_psf(
595 output = Struct(extended_psf=output_e_psf)
596 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, stars_dict, region_name=None)
_set_up_stacking(self, example_stamp)
removeInvalidStamps(self, read_stars)
find_region_for_detector(detector_id, detectors_focal_plane_regions)