22 __all__ = (
"FitsExposureFormatter",
"FitsImageFormatter",
"FitsMaskFormatter",
23 "FitsMaskedImageFormatter")
25 from astro_metadata_translator
import fix_header
26 from lsst.daf.base
import PropertySet
27 from lsst.daf.butler
import Formatter
30 from lsst.afw.fits
import readMetadata
31 from lsst.afw.image
import ExposureFitsReader, ImageFitsReader, MaskFitsReader, MaskedImageFitsReader
33 from lsst.afw.math
import BoundedField
37 """Interface for reading and writing Exposures to and from FITS files.
39 This Formatter supports write recipes.
41 Each ``FitsExposureFormatter`` recipe for FITS compression should
42 define ``image``, ``mask`` and ``variance`` entries, each of which may
43 contain ``compression`` and ``scaling`` entries. Defaults will be
44 provided for any missing elements under ``compression`` and
47 The allowed entries under ``compression`` are:
49 * ``algorithm`` (`str`): compression algorithm to use
50 * ``rows`` (`int`): number of rows per tile (0 = entire dimension)
51 * ``columns`` (`int`): number of columns per tile (0 = entire dimension)
52 * ``quantizeLevel`` (`float`): cfitsio quantization level
54 The allowed entries under ``scaling`` are:
56 * ``algorithm`` (`str`): scaling algorithm to use
57 * ``bitpix`` (`int`): bits per pixel (0,8,16,32,64,-32,-64)
58 * ``fuzz`` (`bool`): fuzz the values when quantising floating-point values?
59 * ``seed`` (`int`): seed for random number generator when fuzzing
60 * ``maskPlanes`` (`list` of `str`): mask planes to ignore when doing
62 * ``quantizeLevel`` (`float`): divisor of the standard deviation for
64 * ``quantizePad`` (`float`): number of stdev to allow on the low side (for
65 ``STDEV_POSITIVE``/``NEGATIVE``)
66 * ``bscale`` (`float`): manually specified ``BSCALE``
67 (for ``MANUAL`` scaling)
68 * ``bzero`` (`float`): manually specified ``BSCALE``
69 (for ``MANUAL`` scaling)
71 A very simple example YAML recipe:
75 lsst.obs.base.fitsExposureFormatter.FitsExposureFormatter:
79 algorithm: GZIP_SHUFFLE
84 supportedExtensions = frozenset({
".fits",
".fits.gz",
".fits.fz",
".fz",
".fit"})
87 supportedWriteParameters = frozenset({
"recipe"})
88 _readerClass = ExposureFitsReader
90 unsupportedParameters = {}
91 """Support all parameters."""
95 """The metadata read from this file. It will be stripped as
96 components are extracted from it
97 (`lsst.daf.base.PropertyList`).
104 """Read all header metadata directly into a PropertyList.
108 metadata : `~lsst.daf.base.PropertyList`
116 """Remove metadata entries that are parsed into components.
118 This is only called when just the metadata is requested; stripping
119 entries there forces code that wants other components to ask for those
120 components directly rather than trying to extract them from the
121 metadata manually, which is fragile. This behavior is an intentional
126 metadata : `~lsst.daf.base.PropertyList`
127 Header metadata, to be modified in-place.
131 from lsst.afw.image
import bboxFromMetadata
132 from lsst.afw.geom
import makeSkyWcs
136 bboxFromMetadata(self.
metadatametadata)
140 makeSkyWcs(self.
metadatametadata, strip=
True)
145 """Read a component held by the Exposure.
149 component : `str`, optional
150 Component to read from the file.
151 parameters : `dict`, optional
152 If specified, a dictionary of slicing parameters that
153 overrides those in ``fileDescriptor``.
157 obj : component-dependent
158 In-memory component object.
163 Raised if the requested component cannot be handled.
167 componentMap = {
'wcs': (
'readWcs',
False),
168 'coaddInputs': (
'readCoaddInputs',
False),
169 'psf': (
'readPsf',
False),
170 'image': (
'readImage',
True),
171 'mask': (
'readMask',
True),
172 'variance': (
'readVariance',
True),
173 'photoCalib': (
'readPhotoCalib',
False),
174 'bbox': (
'readBBox',
True),
175 'dimensions': (
'readBBox',
True),
176 'xy0': (
'readXY0',
True),
178 'filter': (
'readFilter',
False),
180 'filterLabel': (
'readFilterLabel',
False),
181 'validPolygon': (
'readValidPolygon',
False),
182 'apCorrMap': (
'readApCorrMap',
False),
183 'visitInfo': (
'readVisitInfo',
False),
184 'transmissionCurve': (
'readTransmissionCurve',
False),
185 'detector': (
'readDetector',
False),
186 'extras': (
'readExtraComponents',
False),
187 'exposureInfo': (
'readExposureInfo',
False),
189 method, hasParams = componentMap.get(component, (
None,
False))
194 reader = self.
_readerClass_readerClass(self.fileDescriptor.location.path)
195 caller = getattr(reader, method,
None)
198 if parameters
is None:
199 parameters = self.fileDescriptor.parameters
200 if parameters
is None:
202 self.fileDescriptor.storageClass.validateParameters(parameters)
204 if hasParams
and parameters:
205 thisComponent = caller(**parameters)
207 thisComponent = caller()
208 if component ==
"dimensions" and thisComponent
is not None:
209 thisComponent = thisComponent.getDimensions()
212 raise KeyError(f
"Unknown component requested: {component}")
215 """Read the full Exposure object.
219 parameters : `dict`, optional
220 If specified a dictionary of slicing parameters that overrides
221 those in ``fileDescriptor``.
225 exposure : `~lsst.afw.image.Exposure`
226 Complete in-memory exposure.
228 fileDescriptor = self.fileDescriptor
229 if parameters
is None:
230 parameters = fileDescriptor.parameters
231 if parameters
is None:
233 fileDescriptor.storageClass.validateParameters(parameters)
234 reader = self.
_readerClass_readerClass(fileDescriptor.location.path)
235 return reader.read(**parameters)
237 def read(self, component=None):
238 """Read data from a file.
242 component : `str`, optional
243 Component to read from the file. Only used if the `StorageClass`
244 for reading differed from the `StorageClass` used to write the
249 inMemoryDataset : `object`
250 The requested data as a Python object. The type of object
251 is controlled by the specific formatter.
256 Component requested but this file does not seem to be a concrete
259 Raised when parameters passed with fileDescriptor are not
262 fileDescriptor = self.fileDescriptor
263 if fileDescriptor.readStorageClass != fileDescriptor.storageClass:
264 if component ==
"metadata":
267 elif component
is not None:
270 raise ValueError(
"Storage class inconsistency ({} vs {}) but no"
271 " component requested".format(fileDescriptor.readStorageClass.name,
272 fileDescriptor.storageClass.name))
276 """Write a Python object to a file.
280 inMemoryDataset : `object`
281 The Python object to store.
284 self.fileDescriptor.location.updateExtension(self.
extensionextension)
285 outputPath = self.fileDescriptor.location.path
288 recipeName = self.writeParameters.get(
"recipe")
295 inMemoryDataset.writeFitsWithOptions(outputPath, options=ps)
297 inMemoryDataset.writeFits(outputPath)
300 """Retrieve the relevant compression settings for this recipe.
305 Label associated with the collection of compression parameters
311 The selected settings.
316 if "default" not in self.writeRecipes:
318 recipeName =
"default"
320 if recipeName
not in self.writeRecipes:
321 raise RuntimeError(f
"Unrecognized recipe option given for compression: {recipeName}")
323 recipe = self.writeRecipes[recipeName]
326 seed = hash(tuple(self.dataId.items())) % 2**31
327 for plane
in (
"image",
"mask",
"variance"):
328 if plane
in recipe
and "scaling" in recipe[plane]:
329 scaling = recipe[plane][
"scaling"]
330 if "seed" in scaling
and scaling[
"seed"] == 0:
331 scaling[
"seed"] = seed
337 """Validate supplied recipes for this formatter.
339 The recipes are supplemented with default values where appropriate.
341 TODO: replace this custom validation code with Cerberus (DM-11846)
346 Recipes to validate. Can be empty dict or `None`.
351 Validated recipes. Returns what was given if there are no
357 Raised if validation fails.
361 compressionSchema = {
365 "quantizeLevel": 0.0,
370 "maskPlanes": [
"NO_DATA"],
372 "quantizeLevel": 4.0,
383 def checkUnrecognized(entry, allowed, description):
384 """Check to see if the entry contains unrecognised keywords"""
385 unrecognized = set(entry) - set(allowed)
388 f
"Unrecognized entries when parsing image compression recipe {description}: "
393 checkUnrecognized(recipes[name], [
"image",
"mask",
"variance"], name)
395 for plane
in (
"image",
"mask",
"variance"):
396 checkUnrecognized(recipes[name][plane], [
"compression",
"scaling"],
400 validated[name][plane] = np
401 for settings, schema
in ((
"compression", compressionSchema),
402 (
"scaling", scalingSchema)):
404 if settings
not in recipes[name][plane]:
406 np[settings][key] = schema[key]
408 entry = recipes[name][plane][settings]
409 checkUnrecognized(entry, schema.keys(), f
"{name}->{plane}->{settings}")
411 value = type(schema[key])(entry[key])
if key
in entry
else schema[key]
412 np[settings][key] = value
417 """Specialisation for `~lsst.afw.image.Image` reading.
420 _readerClass = ImageFitsReader
424 """Specialisation for `~lsst.afw.image.Mask` reading.
427 _readerClass = MaskFitsReader
431 """Specialisation for `~lsst.afw.image.MaskedImage` reading.
434 _readerClass = MaskedImageFitsReader