LSST Applications  21.0.0+75b29a8a7f,21.0.0+e70536a077,21.0.0-1-ga51b5d4+62c747d40b,21.0.0-10-gbfb87ad6+3307648ee3,21.0.0-15-gedb9d5423+47cba9fc36,21.0.0-2-g103fe59+fdf0863a2a,21.0.0-2-g1367e85+d38a93257c,21.0.0-2-g45278ab+e70536a077,21.0.0-2-g5242d73+d38a93257c,21.0.0-2-g7f82c8f+e682ffb718,21.0.0-2-g8dde007+d179fbfa6a,21.0.0-2-g8f08a60+9402881886,21.0.0-2-ga326454+e682ffb718,21.0.0-2-ga63a54e+08647d4b1b,21.0.0-2-gde069b7+26c92b3210,21.0.0-2-gecfae73+0445ed2f95,21.0.0-2-gfc62afb+d38a93257c,21.0.0-27-gbbd0d29+ae871e0f33,21.0.0-28-g5fc5e037+feb0e9397b,21.0.0-3-g21c7a62+f4b9c0ff5c,21.0.0-3-g357aad2+57b0bddf0b,21.0.0-3-g4be5c26+d38a93257c,21.0.0-3-g65f322c+3f454acf5d,21.0.0-3-g7d9da8d+75b29a8a7f,21.0.0-3-gaa929c8+9e4ef6332c,21.0.0-3-ge02ed75+4b120a55c4,21.0.0-4-g3300ddd+e70536a077,21.0.0-4-g591bb35+4b120a55c4,21.0.0-4-gc004bbf+4911b9cd27,21.0.0-4-gccdca77+f94adcd104,21.0.0-4-ge8fba5a+2b3a696ff9,21.0.0-5-gb155db7+2c5429117a,21.0.0-5-gdf36809+637e4641ee,21.0.0-6-g00874e7+c9fd7f7160,21.0.0-6-g4e60332+4b120a55c4,21.0.0-7-gc8ca178+40eb9cf840,21.0.0-8-gfbe0b4b+9e4ef6332c,21.0.0-9-g2fd488a+d83b7cd606,w.2021.05
LSST Data Management Base Package
fitsExposure.py
Go to the documentation of this file.
1 # This file is part of obs_base.
2 #
3 # Developed for the LSST Data Management System.
4 # This product includes software developed by the LSST Project
5 # (http://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 <http://www.gnu.org/licenses/>.
21 
22 __all__ = ("FitsExposureFormatter", "FitsImageFormatter", "FitsMaskFormatter",
23  "FitsMaskedImageFormatter")
24 
25 from astro_metadata_translator import fix_header
26 from lsst.daf.base import PropertySet
27 from lsst.daf.butler import Formatter
28 # Do not use ExposureFitsReader.readMetadata because that strips
29 # out lots of headers and there is no way to recover them
30 from lsst.afw.fits import readMetadata
31 from lsst.afw.image import ExposureFitsReader, ImageFitsReader, MaskFitsReader, MaskedImageFitsReader
32 from lsst.afw.image import ExposureInfo
33 # Needed for ApCorrMap to resolve properly
34 from lsst.afw.math import BoundedField # noqa: F401
35 
36 
37 class FitsExposureFormatter(Formatter):
38  """Interface for reading and writing Exposures to and from FITS files.
39 
40  This Formatter supports write recipes.
41 
42  Each ``FitsExposureFormatter`` recipe for FITS compression should
43  define ``image``, ``mask`` and ``variance`` entries, each of which may
44  contain ``compression`` and ``scaling`` entries. Defaults will be
45  provided for any missing elements under ``compression`` and
46  ``scaling``.
47 
48  The allowed entries under ``compression`` are:
49 
50  * ``algorithm`` (`str`): compression algorithm to use
51  * ``rows`` (`int`): number of rows per tile (0 = entire dimension)
52  * ``columns`` (`int`): number of columns per tile (0 = entire dimension)
53  * ``quantizeLevel`` (`float`): cfitsio quantization level
54 
55  The allowed entries under ``scaling`` are:
56 
57  * ``algorithm`` (`str`): scaling algorithm to use
58  * ``bitpix`` (`int`): bits per pixel (0,8,16,32,64,-32,-64)
59  * ``fuzz`` (`bool`): fuzz the values when quantising floating-point values?
60  * ``seed`` (`int`): seed for random number generator when fuzzing
61  * ``maskPlanes`` (`list` of `str`): mask planes to ignore when doing
62  statistics
63  * ``quantizeLevel`` (`float`): divisor of the standard deviation for
64  ``STDEV_*`` scaling
65  * ``quantizePad`` (`float`): number of stdev to allow on the low side (for
66  ``STDEV_POSITIVE``/``NEGATIVE``)
67  * ``bscale`` (`float`): manually specified ``BSCALE``
68  (for ``MANUAL`` scaling)
69  * ``bzero`` (`float`): manually specified ``BSCALE``
70  (for ``MANUAL`` scaling)
71 
72  A very simple example YAML recipe:
73 
74  .. code-block:: yaml
75 
76  lsst.obs.base.fitsExposureFormatter.FitsExposureFormatter:
77  default:
78  image: &default
79  compression:
80  algorithm: GZIP_SHUFFLE
81  mask: *default
82  variance: *default
83 
84  """
85  supportedExtensions = frozenset({".fits", ".fits.gz", ".fits.fz", ".fz", ".fit"})
86  extension = ".fits"
87  _metadata = None
88  supportedWriteParameters = frozenset({"recipe"})
89  _readerClass = ExposureFitsReader
90 
91  unsupportedParameters = {}
92  """Support all parameters."""
93 
94  @property
95  def metadata(self):
96  """The metadata read from this file. It will be stripped as
97  components are extracted from it
98  (`lsst.daf.base.PropertyList`).
99  """
100  if self._metadata_metadata is None:
101  self._metadata_metadata = self.readMetadatareadMetadata()
102  return self._metadata_metadata
103 
104  def readMetadata(self):
105  """Read all header metadata directly into a PropertyList.
106 
107  Returns
108  -------
109  metadata : `~lsst.daf.base.PropertyList`
110  Header metadata.
111  """
112  md = readMetadata(self.fileDescriptor.location.path)
113  fix_header(md)
114  return md
115 
116  def stripMetadata(self):
117  """Remove metadata entries that are parsed into components.
118 
119  This is only called when just the metadata is requested; stripping
120  entries there forces code that wants other components to ask for those
121  components directly rather than trying to extract them from the
122  metadata manually, which is fragile. This behavior is an intentional
123  change from Gen2.
124 
125  Parameters
126  ----------
127  metadata : `~lsst.daf.base.PropertyList`
128  Header metadata, to be modified in-place.
129  """
130  # TODO: make sure this covers everything, by delegating to something
131  # that doesn't yet exist in afw.image.ExposureInfo.
132  from lsst.afw.image import bboxFromMetadata
133  from lsst.afw.geom import makeSkyWcs
134 
135  # Protect against the metadata being missing
136  try:
137  bboxFromMetadata(self.metadatametadata) # always strips
138  except LookupError:
139  pass
140  try:
141  makeSkyWcs(self.metadatametadata, strip=True)
142  except Exception:
143  pass
144 
145  def readComponent(self, component, parameters=None):
146  """Read a component held by the Exposure.
147 
148  Parameters
149  ----------
150  component : `str`, optional
151  Component to read from the file.
152  parameters : `dict`, optional
153  If specified, a dictionary of slicing parameters that
154  overrides those in ``fileDescriptor``.
155 
156  Returns
157  -------
158  obj : component-dependent
159  In-memory component object.
160 
161  Raises
162  ------
163  KeyError
164  Raised if the requested component cannot be handled.
165  """
166 
167  # Metadata is handled explicitly elsewhere
168  componentMap = {'wcs': ('readWcs', False, None),
169  'coaddInputs': ('readCoaddInputs', False, None),
170  'psf': ('readPsf', False, None),
171  'image': ('readImage', True, None),
172  'mask': ('readMask', True, None),
173  'variance': ('readVariance', True, None),
174  'photoCalib': ('readPhotoCalib', False, None),
175  'bbox': ('readBBox', True, None),
176  'dimensions': ('readBBox', True, None),
177  'xy0': ('readXY0', True, None),
178  # TODO: deprecate in DM-27170, remove in DM-27177
179  'filter': ('readFilter', False, None),
180  # TODO: deprecate in DM-27177, remove in DM-27811
181  'filterLabel': ('readFilterLabel', False, None),
182  'validPolygon': ('readValidPolygon', False, None),
183  'apCorrMap': ('readApCorrMap', False, None),
184  'visitInfo': ('readVisitInfo', False, None),
185  'transmissionCurve': ('readTransmissionCurve', False, None),
186  'detector': ('readDetector', False, None),
187  'exposureInfo': ('readExposureInfo', False, None),
188  'summaryStats': ('readComponent', False, ExposureInfo.KEY_SUMMARY_STATS),
189  }
190  method, hasParams, componentName = componentMap.get(component, (None, False, None))
191 
192  if method:
193  # This reader can read standalone Image/Mask files as well
194  # when dealing with components.
195  reader = self._readerClass_readerClass(self.fileDescriptor.location.path)
196  caller = getattr(reader, method, None)
197 
198  if caller:
199  if parameters is None:
200  parameters = self.fileDescriptor.parameters
201  if parameters is None:
202  parameters = {}
203  self.fileDescriptor.storageClass.validateParameters(parameters)
204 
205  if componentName is None:
206  if hasParams and parameters:
207  thisComponent = caller(**parameters)
208  else:
209  thisComponent = caller()
210  else:
211  thisComponent = caller(componentName)
212 
213  if component == "dimensions" and thisComponent is not None:
214  thisComponent = thisComponent.getDimensions()
215  return thisComponent
216  else:
217  raise KeyError(f"Unknown component requested: {component}")
218 
219  def readFull(self, parameters=None):
220  """Read the full Exposure object.
221 
222  Parameters
223  ----------
224  parameters : `dict`, optional
225  If specified a dictionary of slicing parameters that overrides
226  those in ``fileDescriptor``.
227 
228  Returns
229  -------
230  exposure : `~lsst.afw.image.Exposure`
231  Complete in-memory exposure.
232  """
233  fileDescriptor = self.fileDescriptor
234  if parameters is None:
235  parameters = fileDescriptor.parameters
236  if parameters is None:
237  parameters = {}
238  fileDescriptor.storageClass.validateParameters(parameters)
239  reader = self._readerClass_readerClass(fileDescriptor.location.path)
240  return reader.read(**parameters)
241 
242  def read(self, component=None):
243  """Read data from a file.
244 
245  Parameters
246  ----------
247  component : `str`, optional
248  Component to read from the file. Only used if the `StorageClass`
249  for reading differed from the `StorageClass` used to write the
250  file.
251 
252  Returns
253  -------
254  inMemoryDataset : `object`
255  The requested data as a Python object. The type of object
256  is controlled by the specific formatter.
257 
258  Raises
259  ------
260  ValueError
261  Component requested but this file does not seem to be a concrete
262  composite.
263  KeyError
264  Raised when parameters passed with fileDescriptor are not
265  supported.
266  """
267  fileDescriptor = self.fileDescriptor
268  if fileDescriptor.readStorageClass != fileDescriptor.storageClass:
269  if component == "metadata":
270  self.stripMetadatastripMetadata()
271  return self.metadatametadata
272  elif component is not None:
273  return self.readComponentreadComponent(component)
274  else:
275  raise ValueError("Storage class inconsistency ({} vs {}) but no"
276  " component requested".format(fileDescriptor.readStorageClass.name,
277  fileDescriptor.storageClass.name))
278  return self.readFullreadFull()
279 
280  def write(self, inMemoryDataset):
281  """Write a Python object to a file.
282 
283  Parameters
284  ----------
285  inMemoryDataset : `object`
286  The Python object to store.
287  """
288  # Update the location with the formatter-preferred file extension
289  self.fileDescriptor.location.updateExtension(self.extensionextension)
290  outputPath = self.fileDescriptor.location.path
291 
292  # check to see if we have a recipe requested
293  recipeName = self.writeParameters.get("recipe")
294  recipe = self.getImageCompressionSettingsgetImageCompressionSettings(recipeName)
295  if recipe:
296  # Can not construct a PropertySet from a hierarchical
297  # dict but can update one.
298  ps = PropertySet()
299  ps.update(recipe)
300  inMemoryDataset.writeFitsWithOptions(outputPath, options=ps)
301  else:
302  inMemoryDataset.writeFits(outputPath)
303 
304  def getImageCompressionSettings(self, recipeName):
305  """Retrieve the relevant compression settings for this recipe.
306 
307  Parameters
308  ----------
309  recipeName : `str`
310  Label associated with the collection of compression parameters
311  to select.
312 
313  Returns
314  -------
315  settings : `dict`
316  The selected settings.
317  """
318  # if no recipe has been provided and there is no default
319  # return immediately
320  if not recipeName:
321  if "default" not in self.writeRecipes:
322  return {}
323  recipeName = "default"
324 
325  if recipeName not in self.writeRecipes:
326  raise RuntimeError(f"Unrecognized recipe option given for compression: {recipeName}")
327 
328  recipe = self.writeRecipes[recipeName]
329 
330  # Set the seed based on dataId
331  seed = hash(tuple(self.dataId.items())) % 2**31
332  for plane in ("image", "mask", "variance"):
333  if plane in recipe and "scaling" in recipe[plane]:
334  scaling = recipe[plane]["scaling"]
335  if "seed" in scaling and scaling["seed"] == 0:
336  scaling["seed"] = seed
337 
338  return recipe
339 
340  @classmethod
341  def validateWriteRecipes(cls, recipes):
342  """Validate supplied recipes for this formatter.
343 
344  The recipes are supplemented with default values where appropriate.
345 
346  TODO: replace this custom validation code with Cerberus (DM-11846)
347 
348  Parameters
349  ----------
350  recipes : `dict`
351  Recipes to validate. Can be empty dict or `None`.
352 
353  Returns
354  -------
355  validated : `dict`
356  Validated recipes. Returns what was given if there are no
357  recipes listed.
358 
359  Raises
360  ------
361  RuntimeError
362  Raised if validation fails.
363  """
364  # Schemas define what should be there, and the default values (and by
365  # the default value, the expected type).
366  compressionSchema = {
367  "algorithm": "NONE",
368  "rows": 1,
369  "columns": 0,
370  "quantizeLevel": 0.0,
371  }
372  scalingSchema = {
373  "algorithm": "NONE",
374  "bitpix": 0,
375  "maskPlanes": ["NO_DATA"],
376  "seed": 0,
377  "quantizeLevel": 4.0,
378  "quantizePad": 5.0,
379  "fuzz": True,
380  "bscale": 1.0,
381  "bzero": 0.0,
382  }
383 
384  if not recipes:
385  # We can not insist on recipes being specified
386  return recipes
387 
388  def checkUnrecognized(entry, allowed, description):
389  """Check to see if the entry contains unrecognised keywords"""
390  unrecognized = set(entry) - set(allowed)
391  if unrecognized:
392  raise RuntimeError(
393  f"Unrecognized entries when parsing image compression recipe {description}: "
394  f"{unrecognized}")
395 
396  validated = {}
397  for name in recipes:
398  checkUnrecognized(recipes[name], ["image", "mask", "variance"], name)
399  validated[name] = {}
400  for plane in ("image", "mask", "variance"):
401  checkUnrecognized(recipes[name][plane], ["compression", "scaling"],
402  f"{name}->{plane}")
403 
404  np = {}
405  validated[name][plane] = np
406  for settings, schema in (("compression", compressionSchema),
407  ("scaling", scalingSchema)):
408  np[settings] = {}
409  if settings not in recipes[name][plane]:
410  for key in schema:
411  np[settings][key] = schema[key]
412  continue
413  entry = recipes[name][plane][settings]
414  checkUnrecognized(entry, schema.keys(), f"{name}->{plane}->{settings}")
415  for key in schema:
416  value = type(schema[key])(entry[key]) if key in entry else schema[key]
417  np[settings][key] = value
418  return validated
419 
420 
422  """Specialisation for `~lsst.afw.image.Image` reading.
423  """
424 
425  _readerClass = ImageFitsReader
426 
427 
429  """Specialisation for `~lsst.afw.image.Mask` reading.
430  """
431 
432  _readerClass = MaskFitsReader
433 
434 
436  """Specialisation for `~lsst.afw.image.MaskedImage` reading.
437  """
438 
439  _readerClass = MaskedImageFitsReader
std::vector< SchemaItem< Flag > > * items
Class for storing generic metadata.
Definition: PropertySet.h:67
def readComponent(self, component, parameters=None)
std::shared_ptr< SkyWcs > makeSkyWcs(daf::base::PropertySet &metadata, bool strip=false)
Construct a SkyWcs from FITS keywords.
Definition: SkyWcs.cc:526
Backwards-compatibility support for depersisting the old Calib (FluxMag0/FluxMag0Err) objects.
lsst::geom::Box2I bboxFromMetadata(daf::base::PropertySet &metadata)
Determine the image bounding box from its metadata (FITS header)
Definition: Image.cc:688
def format(config, name=None, writeSourceLine=True, prefix="", verbose=False)
Definition: history.py:174
table::Key< int > type
Definition: Detector.cc:163
daf::base::PropertySet * set
Definition: fits.cc:912