Coverage for python/lsst/obs/base/exposureAssembler.py : 12%

Hot-keys on this page
r m x p toggle line displays
j k next/prev highlighted chunk
0 (zero) top of page
1 (one) first highlighted chunk
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/>.
22"""Support for assembling and disassembling afw Exposures."""
24# Need to enable PSFs to be instantiated
25import lsst.afw.detection # noqa: F401
26from lsst.afw.image import makeExposure, makeMaskedImage, Filter, stripFilterKeywords
28from lsst.daf.butler import CompositeAssembler
31class ExposureAssembler(CompositeAssembler):
33 EXPOSURE_COMPONENTS = set(("image", "variance", "mask", "wcs", "psf"))
34 EXPOSURE_INFO_COMPONENTS = set(("apCorrMap", "coaddInputs", "photoCalib", "metadata",
35 "filter", "transmissionCurve", "visitInfo",
36 "detector", "validPolygon"))
37 EXPOSURE_READ_COMPONENTS = {"bbox", "dimensions", "xy0"}
39 COMPONENT_MAP = {"bbox": "BBox", "xy0": "XY0"}
40 """Map component name to actual getter name."""
42 def _groupRequestedComponents(self):
43 """Group requested components into top level and ExposureInfo.
45 Returns
46 -------
47 expComps : `dict`
48 Components associated with the top level Exposure.
49 expInfoComps : `dict`
50 Components associated with the ExposureInfo
52 Raises
53 ------
54 ValueError
55 There are components defined in the storage class that are not
56 expected by this assembler.
57 """
58 requested = set(self.storageClass.components.keys())
60 # Check that we are requesting something that we support
61 unknown = requested - (self.EXPOSURE_COMPONENTS | self.EXPOSURE_INFO_COMPONENTS)
62 if unknown:
63 raise ValueError("Asking for unrecognized component: {}".format(unknown))
65 expItems = requested & self.EXPOSURE_COMPONENTS
66 expInfoItems = requested & self.EXPOSURE_INFO_COMPONENTS
67 return expItems, expInfoItems
69 def getComponent(self, composite, componentName):
70 """Get a component from an Exposure
72 Parameters
73 ----------
74 composite : `~lsst.afw.image.Exposure`
75 `Exposure` to access component.
76 componentName : `str`
77 Name of component to retrieve.
79 Returns
80 -------
81 component : `object`
82 The component. Can be None.
84 Raises
85 ------
86 AttributeError
87 The component can not be found.
88 """
89 if componentName in self.EXPOSURE_COMPONENTS or componentName in self.EXPOSURE_READ_COMPONENTS:
90 # Use getter translation if relevant or the name itself
91 return super().getComponent(composite, self.COMPONENT_MAP.get(componentName, componentName))
92 elif componentName in self.EXPOSURE_INFO_COMPONENTS:
93 if hasattr(composite, "getInfo"):
94 # it is possible for this method to be called with
95 # an ExposureInfo composite so trap for that and only get
96 # the ExposureInfo if the method is supported
97 composite = composite.getInfo()
98 return super().getComponent(composite, componentName)
99 else:
100 raise AttributeError("Do not know how to retrieve component {} from {}".format(componentName,
101 type(composite)))
103 def getValidComponents(self, composite):
104 """Extract all non-None components from a composite.
106 Parameters
107 ----------
108 composite : `object`
109 Composite from which to extract components.
111 Returns
112 -------
113 comps : `dict`
114 Non-None components extracted from the composite, indexed by the
115 component name as derived from the `self.storageClass`.
116 """
117 # For Exposure we call the generic version twice: once for top level
118 # components, and again for ExposureInfo.
119 expItems, expInfoItems = self._groupRequestedComponents()
121 components = super().getValidComponents(composite)
122 infoComps = super().getValidComponents(composite.getInfo())
123 components.update(infoComps)
124 return components
126 def disassemble(self, composite):
127 """Disassemble an afw Exposure.
129 This implementation attempts to extract components from the parent
130 by looking for attributes of the same name or getter methods derived
131 from the component name.
133 Parameters
134 ----------
135 composite : `~lsst.afw.image.Exposure`
136 `Exposure` composite object consisting of components to be
137 extracted.
139 Returns
140 -------
141 components : `dict`
142 `dict` with keys matching the components defined in
143 `self.storageClass` and values being `DatasetComponent` instances
144 describing the component.
146 Raises
147 ------
148 ValueError
149 A requested component can not be found in the parent using generic
150 lookups.
151 TypeError
152 The parent object does not match the supplied `self.storageClass`.
153 """
154 if not self.storageClass.validateInstance(composite):
155 raise TypeError("Unexpected type mismatch between parent and StorageClass"
156 " ({} != {})".format(type(composite), self.storageClass.pytype))
158 # Only look for components that are defined by the StorageClass
159 components = {}
160 expItems, expInfoItems = self._groupRequestedComponents()
162 fromExposure = super().disassemble(composite, subset=expItems)
163 components.update(fromExposure)
165 fromExposureInfo = super().disassemble(composite,
166 subset=expInfoItems, override=composite.getInfo())
167 components.update(fromExposureInfo)
169 # We must reproduce some of the metadata manipulation that occurs
170 # in ExposureInfo::FitsWriteData.
172 # Force the FILTER header to be overwritten
173 if "filter" in components and "metadata" in components:
174 md = components["metadata"].component
175 md["FILTER"] = components["filter"].component.getName()
177 return components
179 def assemble(self, components):
180 """Construct an Exposure from components.
182 Parameters
183 ----------
184 components : `dict`
185 All the components from which to construct the Exposure.
186 Some can be missing.
188 Returns
189 -------
190 exposure : `~lsst.afw.image.Exposure`
191 Assembled exposure.
193 Raises
194 ------
195 ValueError
196 Some supplied components are not recognized.
197 """
198 components = components.copy()
199 maskedImageComponents = {}
200 hasMaskedImage = False
201 for component in ("image", "variance", "mask"):
202 value = None
203 if component in components:
204 hasMaskedImage = True
205 value = components.pop(component)
206 maskedImageComponents[component] = value
208 wcs = None
209 if "wcs" in components:
210 wcs = components.pop("wcs")
212 pytype = self.storageClass.pytype
213 if hasMaskedImage:
214 maskedImage = makeMaskedImage(**maskedImageComponents)
215 exposure = makeExposure(maskedImage, wcs=wcs)
217 if not isinstance(exposure, pytype):
218 raise RuntimeError("Unexpected type created in assembly;"
219 " was {} expected {}".format(type(exposure), pytype))
221 else:
222 exposure = pytype()
223 if wcs is not None:
224 exposure.setWcs(wcs)
226 # Set other components
227 exposure.setPsf(components.pop("psf", None))
228 exposure.setPhotoCalib(components.pop("photoCalib", None))
230 info = exposure.getInfo()
231 if "visitInfo" in components:
232 info.setVisitInfo(components.pop("visitInfo"))
233 info.setApCorrMap(components.pop("apCorrMap", None))
234 info.setCoaddInputs(components.pop("coaddInputs", None))
235 info.setMetadata(components.pop("metadata", None))
236 info.setValidPolygon(components.pop("validPolygon", None))
237 info.setDetector(components.pop("detector", None))
238 info.setTransmissionCurve(components.pop("transmissionCurve", None))
240 # Filter needs to be updated specially to match Exposure behavior
241 # from ExposureFitsReader::MetadataReader
243 # Override the serialized filter knowing that we are using FILTER
244 md = info.getMetadata()
245 if "filter" in components and "FILTER" in md:
246 filter = Filter(md, True)
247 stripFilterKeywords(md)
248 if filter.getName() != components["filter"].getName():
249 components["filter"] = filter
251 info.setFilter(components.pop("filter", None))
253 # If we have some components left over that is a problem
254 if components:
255 raise ValueError("The following components were not understood:"
256 " {}".format(list(components.keys())))
258 return exposure
260 def handleParameters(self, inMemoryDataset, parameters=None):
261 """Modify the in-memory dataset using the supplied parameters,
262 returning a possibly new object.
264 Parameters
265 ----------
266 inMemoryDataset : `object`
267 Object to modify based on the parameters.
268 parameters : `dict`, optional
269 Parameters to apply. Values are specific to the parameter.
270 Supported parameters are defined in the associated
271 `StorageClass`. If no relevant parameters are specified the
272 inMemoryDataset will be return unchanged.
274 Returns
275 -------
276 inMemoryDataset : `object`
277 Updated form of supplied in-memory dataset, after parameters
278 have been used.
279 """
280 # Understood by *this* subset command
281 understood = ("bbox", "origin")
282 use = self.storageClass.filterParameters(parameters, subset=understood)
283 if use:
284 inMemoryDataset = inMemoryDataset.subset(**use)
286 return inMemoryDataset
288 @classmethod
289 def selectResponsibleComponent(cls, readComponent: str, fromComponents) -> str:
290 imageComponents = ["mask", "image", "variance"]
291 forwarderMap = {
292 "bbox": imageComponents,
293 "dimensions": imageComponents,
294 "xy0": imageComponents,
295 }
296 forwarder = forwarderMap.get(readComponent)
297 if forwarder is not None:
298 for c in forwarder:
299 if c in fromComponents:
300 return c
301 raise ValueError(f"Can not calculate read component {readComponent} from {fromComponents}")