lsst.pipe.tasks  21.0.0-72-gffa78901+792ce60a79
extended_psf.py
Go to the documentation of this file.
1 # This file is part of pipe_tasks.
2 #
3 # Developed for the LSST Data Management System.
4 # This product includes software developed by the LSST Project
5 # (https://www.lsst.org).
6 # See the COPYRIGHT file at the top-level directory of this distribution
7 # for details of code ownership.
8 #
9 # This program is free software: you can redistribute it and/or modify
10 # it under the terms of the GNU General Public License as published by
11 # the Free Software Foundation, either version 3 of the License, or
12 # (at your option) any later version.
13 #
14 # This program is distributed in the hope that it will be useful,
15 # but WITHOUT ANY WARRANTY; without even the implied warranty of
16 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
17 # GNU General Public License for more details.
18 #
19 # You should have received a copy of the GNU General Public License
20 # along with this program. If not, see <https://www.gnu.org/licenses/>.
21 #
22 """Read preprocessed bright stars and stack them to build an extended
23 PSF model.
24 """
25 
26 from dataclasses import dataclass
27 from typing import List
28 
29 from lsst.afw import image as afwImage
30 from lsst.afw import fits as afwFits
31 from lsst.daf.base import PropertyList
32 
33 
34 @dataclass
36  """Single extended PSF over a focal plane region.
37 
38  The focal plane region is defined through a list
39  of detectors.
40 
41  Parameters
42  ----------
43  extended_psf_image : `lsst.afw.image.MaskedImageF`
44  Image of the extended PSF model.
45  detector_list : `list` [`int`]
46  List of detector IDs that define the focal plane region over which this
47  extended PSF model has been built (and can be used).
48  """
49  extended_psf_image: afwImage.MaskedImageF
50  detector_list: List[int]
51 
52 
54  """Extended PSF model.
55 
56  Each instance may contain a default extended PSF, a set of extended PSFs
57  that correspond to different focal plane regions, or both. At this time,
58  focal plane regions are always defined as a subset of detectors.
59 
60  Parameters
61  ----------
62  default_extended_psf : `lsst.afw.image.MaskedImageF`
63  Extended PSF model to be used as default (or only) extended PSF model.
64  """
65  def __init__(self, default_extended_psf=None):
66  self.default_extended_psfdefault_extended_psf = default_extended_psf
67  self.focal_plane_regionsfocal_plane_regions = {}
68  self.detectors_focal_plane_regionsdetectors_focal_plane_regions = {}
69 
70  def add_regional_extended_psf(self, extended_psf_image, region_name, detector_list):
71  """Add a new focal plane region, along wit hits extended PSF, to the
72  ExtendedPsf instance.
73 
74  Parameters
75  ----------
76  extended_psf_image : `lsst.afw.image.MaskedImageF`
77  Extended PSF model for the region.
78  region_name : `str`
79  Name of the focal plane region. Will be converted to all-uppercase.
80  detector_list : `list` [`int`]
81  List of IDs for the detectors that define the focal plane region.
82  """
83  region_name = region_name.upper()
84  if region_name in self.focal_plane_regionsfocal_plane_regions:
85  raise ValueError(f"Region name {region_name} is already used by this ExtendedPsf instance.")
86  self.focal_plane_regionsfocal_plane_regions[region_name] = FocalPlaneRegionExtendedPsf(
87  extended_psf_image=extended_psf_image, detector_list=detector_list)
88  for det in detector_list:
89  self.detectors_focal_plane_regionsdetectors_focal_plane_regions[det] = region_name
90 
91  def __call__(self, detector=None):
92  """Return the appropriate extended PSF.
93 
94  If the instance contains no extended PSF defined over focal plane
95  regions, the default extended PSF will be returned regardless of
96  whether a detector ID was passed as argument.
97 
98  Parameters
99  ----------
100  detector : `int`, optional
101  Detector ID. If focal plane region PSFs are defined, is used to
102  determine which model to return.
103 
104  Returns
105  -------
106  extendedPsfImage : `lsst.afw.image.MaskedImageF`
107  The extended PSF model. If this instance contains extended PSFs
108  defined over focal plane regions, the extended PSF model for the
109  region that contains ``detector`` is returned. If not, the default
110  extended PSF is returned.
111  """
112  if detector is None:
113  if self.default_extended_psfdefault_extended_psf is None:
114  raise ValueError("No default extended PSF available; please provide detector number.")
115  return self.default_extended_psfdefault_extended_psf
116  elif not self.focal_plane_regionsfocal_plane_regions:
117  return self.default_extended_psfdefault_extended_psf
118  return self.get_regional_extended_psfget_regional_extended_psf(detector=detector)
119 
120  def __len__(self):
121  """Returns the number of extended PSF models present in the instance.
122 
123  Note that if the instance contains both a default model and a set of
124  focal plane region models, the length of the instance will be the
125  number of regional models, plus one (the default). This is true even
126  in the case where the default model is one of the focal plane
127  region-specific models.
128  """
129  n_regions = len(self.focal_plane_regionsfocal_plane_regions)
130  if self.default_extended_psfdefault_extended_psf is not None:
131  n_regions += 1
132  return n_regions
133 
134  def get_regional_extended_psf(self, region_name=None, detector=None):
135  """Returns the extended PSF for a focal plane region.
136 
137  The region can be identified either by name, or through a detector ID.
138 
139  Parameters
140  ----------
141  region_name : `str` or `None`, optional
142  Name of the region for which the extended PSF should be retrieved.
143  Ignored if ``detector`` is provided. Must be provided if
144  ``detector`` is None.
145  detector : `int` or `None`, optional
146  If provided, returns the extended PSF for the focal plane region
147  that includes this detector.
148 
149  Raises
150  ------
151  ValueError
152  Raised if neither ``detector`` nor ``regionName`` is provided.
153  """
154  if detector is None:
155  if region_name is None:
156  raise ValueError("One of either a regionName or a detector number must be provided.")
157  return self.focal_plane_regionsfocal_plane_regions[region_name].extended_psf_image
158  return self.focal_plane_regionsfocal_plane_regions[self.detectors_focal_plane_regionsdetectors_focal_plane_regions[detector]].extended_psf_image
159 
160  def write_fits(self, filename):
161  """Write this object to a file.
162 
163  Parameters
164  ----------
165  filename : `str`
166  Name of file to write.
167  """
168  # Create primary HDU with global metadata.
169  metadata = PropertyList()
170  metadata["HAS_DEFAULT"] = self.default_extended_psfdefault_extended_psf is not None
171  if self.focal_plane_regionsfocal_plane_regions:
172  metadata["HAS_REGIONS"] = True
173  metadata["REGION_NAMES"] = list(self.focal_plane_regionsfocal_plane_regions.keys())
174  for region, e_psf_region in self.focal_plane_regionsfocal_plane_regions.items():
175  metadata[region] = e_psf_region.detector_list
176  else:
177  metadata["HAS_REGIONS"] = False
178  fits_primary = afwFits.Fits(filename, "w")
179  fits_primary.createEmpty()
180  fits_primary.writeMetadata(metadata)
181  fits_primary.closeFile()
182  # Write default extended PSF.
183  if self.default_extended_psfdefault_extended_psf is not None:
184  default_hdu_metadata = PropertyList()
185  default_hdu_metadata.update({"REGION": "DEFAULT", "EXTNAME": "IMAGE"})
186  self.default_extended_psfdefault_extended_psf.image.writeFits(filename, metadata=default_hdu_metadata, mode="a")
187  default_hdu_metadata.update({"REGION": "DEFAULT", "EXTNAME": "MASK"})
188  self.default_extended_psfdefault_extended_psf.mask.writeFits(filename, metadata=default_hdu_metadata, mode="a")
189  # Write extended PSF for each focal plane region.
190  for j, (region, e_psf_region) in enumerate(self.focal_plane_regionsfocal_plane_regions.items()):
191  metadata = PropertyList()
192  metadata.update({"REGION": region, "EXTNAME": "IMAGE"})
193  e_psf_region.extended_psf_image.image.writeFits(filename, metadata=metadata, mode="a")
194  metadata.update({"REGION": region, "EXTNAME": "MASK"})
195  e_psf_region.extended_psf_image.mask.writeFits(filename, metadata=metadata, mode="a")
196 
197  def writeFits(self, filename):
198  """Alias for ``write_fits``; exists for compatibility with the Butler.
199  """
200  self.write_fitswrite_fits(filename)
201 
202  @classmethod
203  def read_fits(cls, filename):
204  """Build an instance of this class from a file.
205 
206  Parameters
207  ----------
208  filename : `str`
209  Name of the file to read.
210  """
211  # Extract info from metadata.
212  global_metadata = afwFits.readMetadata(filename, hdu=0)
213  has_default = global_metadata.getBool("HAS_DEFAULT")
214  if global_metadata.getBool("HAS_REGIONS"):
215  focal_plane_region_names = global_metadata.getArray("REGION_NAMES")
216  else:
217  focal_plane_region_names = []
218  f = afwFits.Fits(filename, "r")
219  n_extensions = f.countHdus()
220  extended_psf_parts = {}
221  for j in range(1, n_extensions):
222  md = afwFits.readMetadata(filename, hdu=j)
223  if has_default and md["REGION"] == "DEFAULT":
224  if md["EXTNAME"] == "IMAGE":
225  default_image = afwImage.ImageF(filename, hdu=j)
226  elif md["EXTNAME"] == "MASK":
227  default_mask = afwImage.MaskX(filename, hdu=j)
228  continue
229  if md["EXTNAME"] == "IMAGE":
230  extended_psf_part = afwImage.ImageF(filename, hdu=j)
231  elif md["EXTNAME"] == "MASK":
232  extended_psf_part = afwImage.MaskX(filename, hdu=j)
233  extended_psf_parts.setdefault(md["REGION"], {})[md["EXTNAME"].lower()] = extended_psf_part
234  # Handle default if present.
235  if has_default:
236  extended_psf = cls(afwImage.MaskedImageF(default_image, default_mask))
237  else:
238  extended_psf = cls()
239  # Ensure we recovered an extended PSF for all focal plane regions.
240  if len(extended_psf_parts) != len(focal_plane_region_names):
241  raise ValueError(f"Number of per-region extended PSFs read ({len(extended_psf_parts)}) does not "
242  "match with the number of regions recorded in the metadata "
243  f"({len(focal_plane_region_names)}).")
244  # Generate extended PSF regions mappings.
245  for r_name in focal_plane_region_names:
246  extended_psf_image = afwImage.MaskedImageF(**extended_psf_parts[r_name])
247  detector_list = global_metadata.getArray(r_name)
248  extended_psf.add_regional_extended_psf(extended_psf_image, r_name, detector_list)
249  # Instantiate ExtendedPsf.
250  return extended_psf
251 
252  @classmethod
253  def readFits(cls, filename):
254  """Alias for ``readFits``; exists for compatibility with the Butler.
255  """
256  return cls.read_fitsread_fits(filename)
def __call__(self, detector=None)
Definition: extended_psf.py:91
def get_regional_extended_psf(self, region_name=None, detector=None)
def add_regional_extended_psf(self, extended_psf_image, region_name, detector_list)
Definition: extended_psf.py:70
def __init__(self, default_extended_psf=None)
Definition: extended_psf.py:65