28 from astro_metadata_translator
import fix_header
30 from .
import ImageMapping, ExposureMapping, CalibrationMapping, DatasetMapping
39 from .exposureIdInfo
import ExposureIdInfo
40 from .makeRawVisitInfo
import MakeRawVisitInfo
41 from .utils
import createInitialSkyWcs, InitialSkyWcsError
44 __all__ = [
"CameraMapper",
"exposureFromImage"]
49 """CameraMapper is a base class for mappers that handle images from a
50 camera and products derived from them. This provides an abstraction layer
51 between the data on disk and the code.
53 Public methods: keys, queryMetadata, getDatasetTypes, map,
54 canStandardize, standardize
56 Mappers for specific data sources (e.g., CFHT Megacam, LSST
57 simulations, etc.) should inherit this class.
59 The CameraMapper manages datasets within a "root" directory. Note that
60 writing to a dataset present in the input root will hide the existing
61 dataset but not overwrite it. See #2160 for design discussion.
63 A camera is assumed to consist of one or more rafts, each composed of
64 multiple CCDs. Each CCD is in turn composed of one or more amplifiers
65 (amps). A camera is also assumed to have a camera geometry description
66 (CameraGeom object) as a policy file, a filter description (Filter class
67 static configuration) as another policy file.
69 Information from the camera geometry and defects are inserted into all
70 Exposure objects returned.
72 The mapper uses one or two registries to retrieve metadata about the
73 images. The first is a registry of all raw exposures. This must contain
74 the time of the observation. One or more tables (or the equivalent)
75 within the registry are used to look up data identifier components that
76 are not specified by the user (e.g. filter) and to return results for
77 metadata queries. The second is an optional registry of all calibration
78 data. This should contain validity start and end entries for each
79 calibration dataset in the same timescale as the observation time.
81 Subclasses will typically set MakeRawVisitInfoClass and optionally the
82 metadata translator class:
84 MakeRawVisitInfoClass: a class variable that points to a subclass of
85 MakeRawVisitInfo, a functor that creates an
86 lsst.afw.image.VisitInfo from the FITS metadata of a raw image.
88 translatorClass: The `~astro_metadata_translator.MetadataTranslator`
89 class to use for fixing metadata values. If it is not set an attempt
90 will be made to infer the class from ``MakeRawVisitInfoClass``, failing
91 that the metadata fixup will try to infer the translator class from the
94 Subclasses must provide the following methods:
96 _extractDetectorName(self, dataId): returns the detector name for a CCD
97 (e.g., "CFHT 21", "R:1,2 S:3,4") as used in the AFW CameraGeom class given
98 a dataset identifier referring to that CCD or a subcomponent of it.
100 _computeCcdExposureId(self, dataId): see below
102 _computeCoaddExposureId(self, dataId, singleFilter): see below
104 Subclasses may also need to override the following methods:
106 _transformId(self, dataId): transformation of a data identifier
107 from colloquial usage (e.g., "ccdname") to proper/actual usage
108 (e.g., "ccd"), including making suitable for path expansion (e.g. removing
109 commas). The default implementation does nothing. Note that this
110 method should not modify its input parameter.
112 getShortCcdName(self, ccdName): a static method that returns a shortened
113 name suitable for use as a filename. The default version converts spaces
116 _mapActualToPath(self, template, actualId): convert a template path to an
117 actual path, using the actual dataset identifier.
119 The mapper's behaviors are largely specified by the policy file.
120 See the MapperDictionary.paf for descriptions of the available items.
122 The 'exposures', 'calibrations', and 'datasets' subpolicies configure
123 mappings (see Mappings class).
125 Common default mappings for all subclasses can be specified in the
126 "policy/{images,exposures,calibrations,datasets}.yaml" files. This
127 provides a simple way to add a product to all camera mappers.
129 Functions to map (provide a path to the data given a dataset
130 identifier dictionary) and standardize (convert data into some standard
131 format or type) may be provided in the subclass as "map_{dataset type}"
132 and "std_{dataset type}", respectively.
134 If non-Exposure datasets cannot be retrieved using standard
135 daf_persistence methods alone, a "bypass_{dataset type}" function may be
136 provided in the subclass to return the dataset instead of using the
137 "datasets" subpolicy.
139 Implementations of map_camera and bypass_camera that should typically be
140 sufficient are provided in this base class.
146 Instead of auto-loading the camera at construction time, load it from
147 the calibration registry
151 policy : daf_persistence.Policy,
152 Policy with per-camera defaults already merged.
153 repositoryDir : string
154 Policy repository for the subclassing module (obtained with
155 getRepositoryPath() on the per-camera default dictionary).
156 root : string, optional
157 Path to the root directory for data.
158 registry : string, optional
159 Path to registry with data's metadata.
160 calibRoot : string, optional
161 Root directory for calibrations.
162 calibRegistry : string, optional
163 Path to registry with calibrations' metadata.
164 provided : list of string, optional
165 Keys provided by the mapper.
166 parentRegistry : Registry subclass, optional
167 Registry from a parent repository that may be used to look up
169 repositoryCfg : daf_persistence.RepositoryCfg or None, optional
170 The configuration information for the repository this mapper is
177 MakeRawVisitInfoClass = MakeRawVisitInfo
180 PupilFactoryClass = afwCameraGeom.PupilFactory
183 translatorClass =
None
186 root=None, registry=None, calibRoot=None, calibRegistry=None,
187 provided=None, parentRegistry=None, repositoryCfg=None):
189 dafPersist.Mapper.__init__(self)
191 self.
log = lsstLog.Log.getLogger(
"CameraMapper")
196 self.
root = repositoryCfg.root
200 repoPolicy = repositoryCfg.policy
if repositoryCfg
else None
201 if repoPolicy
is not None:
202 policy.update(repoPolicy)
206 if 'levels' in policy:
207 levelsPolicy = policy[
'levels']
208 for key
in levelsPolicy.names(
True):
212 if 'defaultSubLevels' in policy:
220 self.
rootStorage = dafPersist.Storage.makeFromURI(uri=root)
228 if calibRoot
is not None:
229 calibRoot = dafPersist.Storage.absolutePath(root, calibRoot)
230 calibStorage = dafPersist.Storage.makeFromURI(uri=calibRoot,
233 calibRoot = policy.get(
'calibRoot',
None)
235 calibStorage = dafPersist.Storage.makeFromURI(uri=calibRoot,
237 if calibStorage
is None:
245 posixIfNoSql=(
not parentRegistry))
248 needCalibRegistry = policy.get(
'needCalibRegistry',
None)
249 if needCalibRegistry:
252 "calibRegistryPath", calibStorage,
256 "'needCalibRegistry' is true in Policy, but was unable to locate a repo at "
257 f
"calibRoot ivar:{calibRoot} or policy['calibRoot']:{policy.get('calibRoot', None)}")
277 raise ValueError(
'class variable packageName must not be None')
287 def _initMappings(self, policy, rootStorage=None, calibStorage=None, provided=None):
288 """Initialize mappings
290 For each of the dataset types that we want to be able to read, there
291 are methods that can be created to support them:
292 * map_<dataset> : determine the path for dataset
293 * std_<dataset> : standardize the retrieved dataset
294 * bypass_<dataset> : retrieve the dataset (bypassing the usual
296 * query_<dataset> : query the registry
298 Besides the dataset types explicitly listed in the policy, we create
299 additional, derived datasets for additional conveniences,
300 e.g., reading the header of an image, retrieving only the size of a
305 policy : `lsst.daf.persistence.Policy`
306 Policy with per-camera defaults already merged
307 rootStorage : `Storage subclass instance`
308 Interface to persisted repository data.
309 calibRoot : `Storage subclass instance`
310 Interface to persisted calib repository data
311 provided : `list` of `str`
312 Keys provided by the mapper
316 "obs_base",
"ImageMappingDefaults.yaml",
"policy"))
318 "obs_base",
"ExposureMappingDefaults.yaml",
"policy"))
320 "obs_base",
"CalibrationMappingDefaults.yaml",
"policy"))
325 (
"images", imgMappingPolicy, ImageMapping),
326 (
"exposures", expMappingPolicy, ExposureMapping),
327 (
"calibrations", calMappingPolicy, CalibrationMapping),
328 (
"datasets", dsMappingPolicy, DatasetMapping)
331 for name, defPolicy, cls
in mappingList:
333 datasets = policy[name]
336 defaultsPath = os.path.join(
getPackageDir(
"obs_base"),
"policy", name +
".yaml")
337 if os.path.exists(defaultsPath):
341 setattr(self, name, mappings)
342 for datasetType
in datasets.names(
True):
343 subPolicy = datasets[datasetType]
344 subPolicy.merge(defPolicy)
346 if not hasattr(self,
"map_" + datasetType)
and 'composite' in subPolicy:
347 def compositeClosure(dataId, write=False, mapper=None, mapping=None,
348 subPolicy=subPolicy):
349 components = subPolicy.get(
'composite')
350 assembler = subPolicy[
'assembler']
if 'assembler' in subPolicy
else None
351 disassembler = subPolicy[
'disassembler']
if 'disassembler' in subPolicy
else None
352 python = subPolicy[
'python']
354 disassembler=disassembler,
358 for name, component
in components.items():
359 butlerComposite.add(id=name,
360 datasetType=component.get(
'datasetType'),
361 setter=component.get(
'setter',
None),
362 getter=component.get(
'getter',
None),
363 subset=component.get(
'subset',
False),
364 inputOnly=component.get(
'inputOnly',
False))
365 return butlerComposite
366 setattr(self,
"map_" + datasetType, compositeClosure)
370 if name ==
"calibrations":
372 provided=provided, dataRoot=rootStorage)
374 mapping =
cls(datasetType, subPolicy, self.
registry, rootStorage, provided=provided)
377 raise ValueError(f
"Duplicate mapping policy for dataset type {datasetType}")
378 self.
keyDict.update(mapping.keys())
379 mappings[datasetType] = mapping
380 self.
mappings[datasetType] = mapping
381 if not hasattr(self,
"map_" + datasetType):
382 def mapClosure(dataId, write=False, mapper=weakref.proxy(self), mapping=mapping):
383 return mapping.map(mapper, dataId, write)
384 setattr(self,
"map_" + datasetType, mapClosure)
385 if not hasattr(self,
"query_" + datasetType):
386 def queryClosure(format, dataId, mapping=mapping):
387 return mapping.lookup(format, dataId)
388 setattr(self,
"query_" + datasetType, queryClosure)
389 if hasattr(mapping,
"standardize")
and not hasattr(self,
"std_" + datasetType):
390 def stdClosure(item, dataId, mapper=weakref.proxy(self), mapping=mapping):
391 return mapping.standardize(mapper, item, dataId)
392 setattr(self,
"std_" + datasetType, stdClosure)
394 def setMethods(suffix, mapImpl=None, bypassImpl=None, queryImpl=None):
395 """Set convenience methods on CameraMapper"""
396 mapName =
"map_" + datasetType +
"_" + suffix
397 bypassName =
"bypass_" + datasetType +
"_" + suffix
398 queryName =
"query_" + datasetType +
"_" + suffix
399 if not hasattr(self, mapName):
400 setattr(self, mapName, mapImpl
or getattr(self,
"map_" + datasetType))
401 if not hasattr(self, bypassName):
402 if bypassImpl
is None and hasattr(self,
"bypass_" + datasetType):
403 bypassImpl = getattr(self,
"bypass_" + datasetType)
404 if bypassImpl
is not None:
405 setattr(self, bypassName, bypassImpl)
406 if not hasattr(self, queryName):
407 setattr(self, queryName, queryImpl
or getattr(self,
"query_" + datasetType))
410 setMethods(
"filename", bypassImpl=
lambda datasetType, pythonType, location, dataId:
411 [os.path.join(location.getStorage().root, p)
for p
in location.getLocations()])
413 if subPolicy[
"storage"] ==
"FitsStorage":
414 def getMetadata(datasetType, pythonType, location, dataId):
419 setMethods(
"md", bypassImpl=getMetadata)
422 addName =
"add_" + datasetType
423 if not hasattr(self, addName):
426 if name ==
"exposures":
427 def getSkyWcs(datasetType, pythonType, location, dataId):
429 return fitsReader.readWcs()
431 setMethods(
"wcs", bypassImpl=getSkyWcs)
433 def getRawHeaderWcs(datasetType, pythonType, location, dataId):
434 """Create a SkyWcs from the un-modified raw FITS WCS header keys."""
435 if datasetType[:3] !=
"raw":
440 setMethods(
"header_wcs", bypassImpl=getRawHeaderWcs)
442 def getPhotoCalib(datasetType, pythonType, location, dataId):
444 return fitsReader.readPhotoCalib()
446 setMethods(
"photoCalib", bypassImpl=getPhotoCalib)
448 def getVisitInfo(datasetType, pythonType, location, dataId):
450 return fitsReader.readVisitInfo()
452 setMethods(
"visitInfo", bypassImpl=getVisitInfo)
454 def getFilter(datasetType, pythonType, location, dataId):
456 return fitsReader.readFilter()
458 setMethods(
"filter", bypassImpl=getFilter)
460 setMethods(
"detector",
461 mapImpl=
lambda dataId, write=
False:
463 pythonType=
"lsst.afw.cameraGeom.CameraConfig",
465 storageName=
"Internal",
466 locationList=
"ignored",
471 bypassImpl=
lambda datasetType, pythonType, location, dataId:
475 def getBBox(datasetType, pythonType, location, dataId):
476 md =
readMetadata(location.getLocationsWithRoot()[0], hdu=1)
480 setMethods(
"bbox", bypassImpl=getBBox)
482 elif name ==
"images":
483 def getBBox(datasetType, pythonType, location, dataId):
487 setMethods(
"bbox", bypassImpl=getBBox)
489 if subPolicy[
"storage"] ==
"FitsCatalogStorage":
491 def getMetadata(datasetType, pythonType, location, dataId):
492 md =
readMetadata(os.path.join(location.getStorage().root,
493 location.getLocations()[0]), hdu=1)
497 setMethods(
"md", bypassImpl=getMetadata)
500 if subPolicy[
"storage"] ==
"FitsStorage":
501 def mapSubClosure(dataId, write=False, mapper=weakref.proxy(self), mapping=mapping):
502 subId = dataId.copy()
504 loc = mapping.map(mapper, subId, write)
505 bbox = dataId[
'bbox']
506 llcX = bbox.getMinX()
507 llcY = bbox.getMinY()
508 width = bbox.getWidth()
509 height = bbox.getHeight()
510 loc.additionalData.set(
'llcX', llcX)
511 loc.additionalData.set(
'llcY', llcY)
512 loc.additionalData.set(
'width', width)
513 loc.additionalData.set(
'height', height)
514 if 'imageOrigin' in dataId:
515 loc.additionalData.set(
'imageOrigin',
516 dataId[
'imageOrigin'])
519 def querySubClosure(key, format, dataId, mapping=mapping):
520 subId = dataId.copy()
522 return mapping.lookup(format, subId)
523 setMethods(
"sub", mapImpl=mapSubClosure, queryImpl=querySubClosure)
525 if subPolicy[
"storage"] ==
"FitsCatalogStorage":
528 def getLen(datasetType, pythonType, location, dataId):
529 md =
readMetadata(os.path.join(location.getStorage().root,
530 location.getLocations()[0]), hdu=1)
534 setMethods(
"len", bypassImpl=getLen)
537 if not datasetType.endswith(
"_schema")
and datasetType +
"_schema" not in datasets:
538 setMethods(
"schema", bypassImpl=
lambda datasetType, pythonType, location, dataId:
539 afwTable.Schema.readFits(os.path.join(location.getStorage().root,
540 location.getLocations()[0])))
542 def _computeCcdExposureId(self, dataId):
543 """Compute the 64-bit (long) identifier for a CCD exposure.
545 Subclasses must override
550 Data identifier with visit, ccd.
552 raise NotImplementedError()
554 def _computeCoaddExposureId(self, dataId, singleFilter):
555 """Compute the 64-bit (long) identifier for a coadd.
557 Subclasses must override
562 Data identifier with tract and patch.
563 singleFilter : `bool`
564 True means the desired ID is for a single-filter coadd, in which
565 case dataIdmust contain filter.
567 raise NotImplementedError()
569 def _search(self, path):
570 """Search for path in the associated repository's storage.
575 Path that describes an object in the repository associated with
577 Path may contain an HDU indicator, e.g. 'foo.fits[1]'. The
578 indicator will be stripped when searching and so will match
579 filenames without the HDU indicator, e.g. 'foo.fits'. The path
580 returned WILL contain the indicator though, e.g. ['foo.fits[1]'].
585 The path for this object in the repository. Will return None if the
586 object can't be found. If the input argument path contained an HDU
587 indicator, the returned path will also contain the HDU indicator.
592 """Rename any existing object with the given type and dataId.
594 The CameraMapper implementation saves objects in a sequence of e.g.:
600 All of the backups will be placed in the output repo, however, and will
601 not be removed if they are found elsewhere in the _parent chain. This
602 means that the same file will be stored twice if the previous version
603 was found in an input repo.
612 def firstElement(list):
613 """Get the first element in the list, or None if that can't be
616 return list[0]
if list
is not None and len(list)
else None
619 newLocation = self.
map(datasetType, dataId, write=
True)
620 newPath = newLocation.getLocations()[0]
621 path = dafPersist.PosixStorage.search(self.
root, newPath, searchParents=
True)
622 path = firstElement(path)
624 while path
is not None:
626 oldPaths.append((n, path))
627 path = dafPersist.PosixStorage.search(self.
root,
"%s~%d" % (newPath, n), searchParents=
True)
628 path = firstElement(path)
629 for n, oldPath
in reversed(oldPaths):
630 self.
rootStorage.copyFile(oldPath,
"%s~%d" % (newPath, n))
633 """Return supported keys.
638 List of keys usable in a dataset identifier
643 """Return a dict of supported keys and their value types for a given
644 dataset type at a given level of the key hierarchy.
649 Dataset type or None for all dataset types.
650 level : `str` or None
651 Level or None for all levels or '' for the default level for the
657 Keys are strings usable in a dataset identifier, values are their
665 if datasetType
is None:
666 keyDict = copy.copy(self.
keyDict)
669 if level
is not None and level
in self.
levels:
670 keyDict = copy.copy(keyDict)
671 for lev
in self.
levels[level]:
686 """Return the name of the camera that this CameraMapper is for."""
688 className = className[className.find(
'.'):-1]
689 m = re.search(
r'(\w+)Mapper', className)
691 m = re.search(
r"class '[\w.]*?(\w+)'", className)
693 return name[:1].lower() + name[1:]
if name
else ''
697 """Return the name of the package containing this CameraMapper."""
699 raise ValueError(
'class variable packageName must not be None')
704 """Return the base directory of this package"""
708 """Map a camera dataset."""
710 raise RuntimeError(
"No camera dataset available.")
713 pythonType=
"lsst.afw.cameraGeom.CameraConfig",
715 storageName=
"ConfigStorage",
723 """Return the (preloaded) camera object.
726 raise RuntimeError(
"No camera dataset available.")
731 pythonType=
"lsst.obs.base.ExposureIdInfo",
733 storageName=
"Internal",
734 locationList=
"ignored",
741 """Hook to retrieve an lsst.obs.base.ExposureIdInfo for an exposure"""
742 expId = self.bypass_ccdExposureId(datasetType, pythonType, location, dataId)
743 expBits = self.bypass_ccdExposureId_bits(datasetType, pythonType, location, dataId)
747 """Disable standardization for bfKernel
749 bfKernel is a calibration product that is numpy array,
750 unlike other calibration products that are all images;
751 all calibration images are sent through _standardizeExposure
752 due to CalibrationMapping, but we don't want that to happen to bfKernel
757 """Standardize a raw dataset by converting it to an Exposure instead
760 trimmed=
False, setVisitInfo=
True)
763 """Map a sky policy."""
765 "Internal",
None,
None, self,
769 """Standardize a sky policy by returning the one we use."""
770 return self.skypolicy
778 def _setupRegistry(self, name, description, path, policy, policyKey, storage, searchParents=True,
780 """Set up a registry (usually SQLite3), trying a number of possible
788 Description of registry (for log messages)
792 Policy that contains the registry name, used if path is None.
794 Key in policy for registry path.
795 storage : Storage subclass
796 Repository Storage to look in.
797 searchParents : bool, optional
798 True if the search for a registry should follow any Butler v1
800 posixIfNoSql : bool, optional
801 If an sqlite registry is not found, will create a posix registry if
806 lsst.daf.persistence.Registry
809 if path
is None and policyKey
in policy:
811 if os.path.isabs(path):
812 raise RuntimeError(
"Policy should not indicate an absolute path for registry.")
813 if not storage.exists(path):
814 newPath = storage.instanceSearch(path)
816 newPath = newPath[0]
if newPath
is not None and len(newPath)
else None
818 self.
log.
warn(
"Unable to locate registry at policy path (also looked in root): %s",
822 self.
log.
warn(
"Unable to locate registry at policy path: %s", path)
830 if path
and (path.startswith(root)):
831 path = path[len(root +
'/'):]
832 except AttributeError:
838 def search(filename, description):
839 """Search for file in storage
844 Filename to search for
846 Description of file, for error message.
850 path : `str` or `None`
851 Path to file, or None
853 result = storage.instanceSearch(filename)
856 self.
log.
debug(
"Unable to locate %s: %s", description, filename)
861 path = search(
"%s.pgsql" % name,
"%s in root" % description)
863 path = search(
"%s.sqlite3" % name,
"%s in root" % description)
865 path = search(os.path.join(
".",
"%s.sqlite3" % name),
"%s in current dir" % description)
868 if not storage.exists(path):
869 newPath = storage.instanceSearch(path)
870 newPath = newPath[0]
if newPath
is not None and len(newPath)
else None
871 if newPath
is not None:
873 localFileObj = storage.getLocalFile(path)
874 self.
log.
info(
"Loading %s registry from %s", description, localFileObj.name)
875 registry = dafPersist.Registry.create(localFileObj.name)
877 elif not registry
and posixIfNoSql:
879 self.
log.
info(
"Loading Posix %s registry from %s", description, storage.root)
886 def _transformId(self, dataId):
887 """Generate a standard ID dict from a camera-specific ID dict.
889 Canonical keys include:
890 - amp: amplifier name
891 - ccd: CCD name (in LSST this is a combination of raft and sensor)
892 The default implementation returns a copy of its input.
897 Dataset identifier; this must not be modified
902 Transformed dataset identifier.
907 def _mapActualToPath(self, template, actualId):
908 """Convert a template path to an actual path, using the actual data
909 identifier. This implementation is usually sufficient but can be
910 overridden by the subclass.
927 return template % transformedId
928 except Exception
as e:
929 raise RuntimeError(
"Failed to format %r with data %r: %s" % (template, transformedId, e))
933 """Convert a CCD name to a form useful as a filename
935 The default implementation converts spaces to underscores.
937 return ccdName.replace(
" ",
"_")
939 def _extractDetectorName(self, dataId):
940 """Extract the detector (CCD) name from the dataset identifier.
942 The name in question is the detector name used by lsst.afw.cameraGeom.
954 raise NotImplementedError(
"No _extractDetectorName() function specified")
956 def _setAmpDetector(self, item, dataId, trimmed=True):
957 """Set the detector object in an Exposure for an amplifier.
959 Defects are also added to the Exposure based on the detector object.
963 item : `lsst.afw.image.Exposure`
964 Exposure to set the detector in.
968 Should detector be marked as trimmed? (ignored)
973 def _setCcdDetector(self, item, dataId, trimmed=True):
974 """Set the detector object in an Exposure for a CCD.
978 item : `lsst.afw.image.Exposure`
979 Exposure to set the detector in.
983 Should detector be marked as trimmed? (ignored)
985 if item.getDetector()
is not None:
989 detector = self.
camera[detectorName]
990 item.setDetector(detector)
992 def _setFilter(self, mapping, item, dataId):
993 """Set the filter object in an Exposure. If the Exposure had a FILTER
994 keyword, this was already processed during load. But if it didn't,
995 use the filter from the registry.
999 mapping : `lsst.obs.base.Mapping`
1000 Where to get the filter from.
1001 item : `lsst.afw.image.Exposure`
1002 Exposure to set the filter in.
1007 if not (isinstance(item, afwImage.ExposureU)
or isinstance(item, afwImage.ExposureI)
1008 or isinstance(item, afwImage.ExposureF)
or isinstance(item, afwImage.ExposureD)):
1011 if item.getFilter().getId() != afwImage.Filter.UNKNOWN:
1014 actualId = mapping.need([
'filter'], dataId)
1015 filterName = actualId[
'filter']
1017 filterName = self.
filters[filterName]
1021 self.
log.
warn(
"Filter %s not defined. Set to UNKNOWN." % (filterName))
1023 def _standardizeExposure(self, mapping, item, dataId, filter=True,
1024 trimmed=True, setVisitInfo=True):
1025 """Default standardization function for images.
1027 This sets the Detector from the camera geometry
1028 and optionally set the Filter. In both cases this saves
1029 having to persist some data in each exposure (or image).
1033 mapping : `lsst.obs.base.Mapping`
1034 Where to get the values from.
1035 item : image-like object
1036 Can be any of lsst.afw.image.Exposure,
1037 lsst.afw.image.DecoratedImage, lsst.afw.image.Image
1038 or lsst.afw.image.MaskedImage
1043 Set filter? Ignored if item is already an exposure
1045 Should detector be marked as trimmed?
1046 setVisitInfo : `bool`
1047 Should Exposure have its VisitInfo filled out from the metadata?
1051 `lsst.afw.image.Exposure`
1052 The standardized Exposure.
1056 setVisitInfo=setVisitInfo)
1057 except Exception
as e:
1058 self.
log.
error(
"Could not turn item=%r into an exposure: %s" % (repr(item), e))
1061 if mapping.level.lower() ==
"amp":
1063 elif mapping.level.lower() ==
"ccd":
1069 if mapping.level.lower() !=
"amp" and exposure.getWcs()
is None and \
1070 (exposure.getInfo().getVisitInfo()
is not None or exposure.getMetadata().toDict()):
1078 def _createSkyWcsFromMetadata(self, exposure):
1079 """Create a SkyWcs from the FITS header metadata in an Exposure.
1083 exposure : `lsst.afw.image.Exposure`
1084 The exposure to get metadata from, and attach the SkyWcs to.
1086 metadata = exposure.getMetadata()
1090 exposure.setWcs(wcs)
1093 self.
log.
debug(
"wcs set to None; missing information found in metadata to create a valid wcs:"
1096 exposure.setMetadata(metadata)
1098 def _createInitialSkyWcs(self, exposure):
1099 """Create a SkyWcs from the boresight and camera geometry.
1101 If the boresight or camera geometry do not support this method of
1102 WCS creation, this falls back on the header metadata-based version
1103 (typically a purely linear FITS crval/crpix/cdmatrix WCS).
1107 exposure : `lsst.afw.image.Exposure`
1108 The exposure to get data from, and attach the SkyWcs to.
1113 if exposure.getInfo().getVisitInfo()
is None:
1114 msg =
"No VisitInfo; cannot access boresight information. Defaulting to metadata-based SkyWcs."
1118 newSkyWcs =
createInitialSkyWcs(exposure.getInfo().getVisitInfo(), exposure.getDetector())
1119 exposure.setWcs(newSkyWcs)
1120 except InitialSkyWcsError
as e:
1121 msg =
"Cannot create SkyWcs using VisitInfo and Detector, using metadata-based SkyWcs: %s"
1123 self.
log.
debug(
"Exception was: %s", traceback.TracebackException.from_exception(e))
1124 if e.__context__
is not None:
1125 self.
log.
debug(
"Root-cause Exception was: %s",
1126 traceback.TracebackException.from_exception(e.__context__))
1128 def _makeCamera(self, policy, repositoryDir):
1129 """Make a camera (instance of lsst.afw.cameraGeom.Camera) describing
1132 Also set self.cameraDataLocation, if relevant (else it can be left
1135 This implementation assumes that policy contains an entry "camera"
1136 that points to the subdirectory in this package of camera data;
1137 specifically, that subdirectory must contain:
1138 - a file named `camera.py` that contains persisted camera config
1139 - ampInfo table FITS files, as required by
1140 lsst.afw.cameraGeom.makeCameraFromPath
1144 policy : `lsst.daf.persistence.Policy`
1145 Policy with per-camera defaults already merged
1146 (PexPolicy only for backward compatibility).
1147 repositoryDir : `str`
1148 Policy repository for the subclassing module (obtained with
1149 getRepositoryPath() on the per-camera default dictionary).
1151 if 'camera' not in policy:
1152 raise RuntimeError(
"Cannot find 'camera' in policy; cannot construct a camera")
1153 cameraDataSubdir = policy[
'camera']
1155 os.path.join(repositoryDir, cameraDataSubdir,
"camera.py"))
1156 cameraConfig = afwCameraGeom.CameraConfig()
1159 return afwCameraGeom.makeCameraFromPath(
1160 cameraConfig=cameraConfig,
1161 ampInfoPath=ampInfoPath,
1167 """Get the registry used by this mapper.
1172 The registry used by this mapper for this mapper's repository.
1177 """Stuff image compression settings into a daf.base.PropertySet
1179 This goes into the ButlerLocation's "additionalData", which gets
1180 passed into the boost::persistence framework.
1185 Type of dataset for which to get the image compression settings.
1191 additionalData : `lsst.daf.base.PropertySet`
1192 Image compression settings.
1194 mapping = self.
mappings[datasetType]
1195 recipeName = mapping.recipe
1196 storageType = mapping.storage
1200 raise RuntimeError(
"Unrecognized write recipe for datasetType %s (storage type %s): %s" %
1201 (datasetType, storageType, recipeName))
1202 recipe = self.
_writeRecipes[storageType][recipeName].deepCopy()
1203 seed = hash(tuple(dataId.items())) % 2**31
1204 for plane
in (
"image",
"mask",
"variance"):
1205 if recipe.exists(plane +
".scaling.seed")
and recipe.getScalar(plane +
".scaling.seed") == 0:
1206 recipe.set(plane +
".scaling.seed", seed)
1209 def _initWriteRecipes(self):
1210 """Read the recipes for writing files
1212 These recipes are currently used for configuring FITS compression,
1213 but they could have wider uses for configuring different flavors
1214 of the storage types. A recipe is referred to by a symbolic name,
1215 which has associated settings. These settings are stored as a
1216 `PropertySet` so they can easily be passed down to the
1217 boost::persistence framework as the "additionalData" parameter.
1219 The list of recipes is written in YAML. A default recipe and
1220 some other convenient recipes are in obs_base/policy/writeRecipes.yaml
1221 and these may be overridden or supplemented by the individual obs_*
1222 packages' own policy/writeRecipes.yaml files.
1224 Recipes are grouped by the storage type. Currently, only the
1225 ``FitsStorage`` storage type uses recipes, which uses it to
1226 configure FITS image compression.
1228 Each ``FitsStorage`` recipe for FITS compression should define
1229 "image", "mask" and "variance" entries, each of which may contain
1230 "compression" and "scaling" entries. Defaults will be provided for
1231 any missing elements under "compression" and "scaling".
1233 The allowed entries under "compression" are:
1235 * algorithm (string): compression algorithm to use
1236 * rows (int): number of rows per tile (0 = entire dimension)
1237 * columns (int): number of columns per tile (0 = entire dimension)
1238 * quantizeLevel (float): cfitsio quantization level
1240 The allowed entries under "scaling" are:
1242 * algorithm (string): scaling algorithm to use
1243 * bitpix (int): bits per pixel (0,8,16,32,64,-32,-64)
1244 * fuzz (bool): fuzz the values when quantising floating-point values?
1245 * seed (long): seed for random number generator when fuzzing
1246 * maskPlanes (list of string): mask planes to ignore when doing
1248 * quantizeLevel: divisor of the standard deviation for STDEV_* scaling
1249 * quantizePad: number of stdev to allow on the low side (for
1250 STDEV_POSITIVE/NEGATIVE)
1251 * bscale: manually specified BSCALE (for MANUAL scaling)
1252 * bzero: manually specified BSCALE (for MANUAL scaling)
1254 A very simple example YAML recipe:
1260 algorithm: GZIP_SHUFFLE
1264 recipesFile = os.path.join(
getPackageDir(
"obs_base"),
"policy",
"writeRecipes.yaml")
1266 supplementsFile = os.path.join(self.
getPackageDir(),
"policy",
"writeRecipes.yaml")
1267 validationMenu = {
'FitsStorage': validateRecipeFitsStorage, }
1268 if os.path.exists(supplementsFile)
and supplementsFile != recipesFile:
1271 for entry
in validationMenu:
1272 intersection =
set(recipes[entry].names()).intersection(
set(supplements.names()))
1274 raise RuntimeError(
"Recipes provided in %s section %s may not override those in %s: %s" %
1275 (supplementsFile, entry, recipesFile, intersection))
1276 recipes.update(supplements)
1279 for storageType
in recipes.names(
True):
1280 if "default" not in recipes[storageType]:
1281 raise RuntimeError(
"No 'default' recipe defined for storage type %s in %s" %
1282 (storageType, recipesFile))
1283 self.
_writeRecipes[storageType] = validationMenu[storageType](recipes[storageType])
1287 """Generate an Exposure from an image-like object
1289 If the image is a DecoratedImage then also set its WCS and metadata
1290 (Image and MaskedImage are missing the necessary metadata
1291 and Exposure already has those set)
1295 image : Image-like object
1296 Can be one of lsst.afw.image.DecoratedImage, Image, MaskedImage or
1301 `lsst.afw.image.Exposure`
1302 Exposure containing input image.
1304 translatorClass =
None
1305 if mapper
is not None:
1306 translatorClass = mapper.translatorClass
1313 metadata = image.getMetadata()
1314 fix_header(metadata, translator_class=translatorClass)
1315 exposure.setMetadata(metadata)
1318 metadata = exposure.getMetadata()
1319 fix_header(metadata, translator_class=translatorClass)
1324 if setVisitInfo
and exposure.getInfo().getVisitInfo()
is None:
1325 if metadata
is not None:
1328 logger = lsstLog.Log.getLogger(
"CameraMapper")
1329 logger.warn(
"I can only set the VisitInfo if you provide a mapper")
1331 exposureId = mapper._computeCcdExposureId(dataId)
1332 visitInfo = mapper.makeRawVisitInfo(md=metadata, exposureId=exposureId)
1334 exposure.getInfo().setVisitInfo(visitInfo)
1340 """Validate recipes for FitsStorage
1342 The recipes are supplemented with default values where appropriate.
1344 TODO: replace this custom validation code with Cerberus (DM-11846)
1348 recipes : `lsst.daf.persistence.Policy`
1349 FitsStorage recipes to validate.
1353 validated : `lsst.daf.base.PropertySet`
1354 Validated FitsStorage recipe.
1359 If validation fails.
1363 compressionSchema = {
1364 "algorithm":
"NONE",
1367 "quantizeLevel": 0.0,
1370 "algorithm":
"NONE",
1372 "maskPlanes": [
"NO_DATA"],
1374 "quantizeLevel": 4.0,
1381 def checkUnrecognized(entry, allowed, description):
1382 """Check to see if the entry contains unrecognised keywords"""
1383 unrecognized =
set(entry.keys()) -
set(allowed)
1386 "Unrecognized entries when parsing image compression recipe %s: %s" %
1387 (description, unrecognized))
1390 for name
in recipes.names(
True):
1391 checkUnrecognized(recipes[name], [
"image",
"mask",
"variance"], name)
1393 validated[name] = rr
1394 for plane
in (
"image",
"mask",
"variance"):
1395 checkUnrecognized(recipes[name][plane], [
"compression",
"scaling"],
1396 name +
"->" + plane)
1398 for settings, schema
in ((
"compression", compressionSchema),
1399 (
"scaling", scalingSchema)):
1400 prefix = plane +
"." + settings
1401 if settings
not in recipes[name][plane]:
1403 rr.set(prefix +
"." + key, schema[key])
1405 entry = recipes[name][plane][settings]
1406 checkUnrecognized(entry, schema.keys(), name +
"->" + plane +
"->" + settings)
1408 value =
type(schema[key])(entry[key])
if key
in entry
else schema[key]
1409 rr.set(prefix +
"." + key, value)