Coverage for python/lsst/obs/base/exposureAssembler.py: 15%
Shortcuts 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
Shortcuts 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."""
24import logging
26# Need to enable PSFs to be instantiated
27import lsst.afw.detection # noqa: F401
28from lsst.afw.image import makeExposure, makeMaskedImage
29from lsst.daf.butler import StorageClassDelegate
31log = logging.getLogger(__name__)
34class ExposureAssembler(StorageClassDelegate):
36 EXPOSURE_COMPONENTS = set(("image", "variance", "mask", "wcs", "psf"))
37 EXPOSURE_INFO_COMPONENTS = set(
38 (
39 "apCorrMap",
40 "coaddInputs",
41 "photoCalib",
42 "metadata",
43 "filterLabel",
44 "transmissionCurve",
45 "visitInfo",
46 "detector",
47 "validPolygon",
48 "summaryStats",
49 "id",
50 )
51 )
52 EXPOSURE_READ_COMPONENTS = {"bbox", "dimensions", "xy0", "filter"}
54 COMPONENT_MAP = {"bbox": "BBox", "xy0": "XY0"}
55 """Map component name to actual getter name."""
57 def _groupRequestedComponents(self):
58 """Group requested components into top level and ExposureInfo.
60 Returns
61 -------
62 expComps : `set` [`str`]
63 Components associated with the top level Exposure.
64 expInfoComps : `set` [`str`]
65 Components associated with the ExposureInfo
67 Raises
68 ------
69 ValueError
70 There are components defined in the storage class that are not
71 expected by this assembler.
72 """
73 requested = set(self.storageClass.components.keys())
75 # Check that we are requesting something that we support
76 unknown = requested - (self.EXPOSURE_COMPONENTS | self.EXPOSURE_INFO_COMPONENTS)
77 if unknown:
78 raise ValueError("Asking for unrecognized component: {}".format(unknown))
80 expItems = requested & self.EXPOSURE_COMPONENTS
81 expInfoItems = requested & self.EXPOSURE_INFO_COMPONENTS
82 return expItems, expInfoItems
84 def getComponent(self, composite, componentName):
85 """Get a component from an Exposure
87 Parameters
88 ----------
89 composite : `~lsst.afw.image.Exposure`
90 `Exposure` to access component.
91 componentName : `str`
92 Name of component to retrieve.
94 Returns
95 -------
96 component : `object`
97 The component. Can be None.
99 Raises
100 ------
101 AttributeError
102 The component can not be found.
103 """
104 if componentName in self.EXPOSURE_COMPONENTS or componentName in self.EXPOSURE_READ_COMPONENTS:
105 # Use getter translation if relevant or the name itself
106 return super().getComponent(composite, self.COMPONENT_MAP.get(componentName, componentName))
107 elif componentName in self.EXPOSURE_INFO_COMPONENTS:
108 if hasattr(composite, "getInfo"):
109 # it is possible for this method to be called with
110 # an ExposureInfo composite so trap for that and only get
111 # the ExposureInfo if the method is supported
112 composite = composite.getInfo()
113 return super().getComponent(composite, self.COMPONENT_MAP.get(componentName, componentName))
114 else:
115 raise AttributeError(
116 "Do not know how to retrieve component {} from {}".format(componentName, type(composite))
117 )
119 def getValidComponents(self, composite):
120 """Extract all non-None components from a composite.
122 Parameters
123 ----------
124 composite : `object`
125 Composite from which to extract components.
127 Returns
128 -------
129 comps : `dict`
130 Non-None components extracted from the composite, indexed by the
131 component name as derived from the `self.storageClass`.
132 """
133 # For Exposure we call the generic version twice: once for top level
134 # components, and again for ExposureInfo.
135 expItems, expInfoItems = self._groupRequestedComponents()
137 components = super().getValidComponents(composite)
138 infoComps = super().getValidComponents(composite.getInfo())
139 components.update(infoComps)
140 return components
142 def disassemble(self, composite):
143 """Disassemble an afw Exposure.
145 This implementation attempts to extract components from the parent
146 by looking for attributes of the same name or getter methods derived
147 from the component name.
149 Parameters
150 ----------
151 composite : `~lsst.afw.image.Exposure`
152 `Exposure` composite object consisting of components to be
153 extracted.
155 Returns
156 -------
157 components : `dict`
158 `dict` with keys matching the components defined in
159 `self.storageClass` and values being `DatasetComponent` instances
160 describing the component.
162 Raises
163 ------
164 ValueError
165 A requested component can not be found in the parent using generic
166 lookups.
167 TypeError
168 The parent object does not match the supplied `self.storageClass`.
170 Notes
171 -----
172 If a PSF is present but is not persistable, the PSF will not be
173 included in the returned components.
174 """
175 if not self.storageClass.validateInstance(composite):
176 raise TypeError(
177 "Unexpected type mismatch between parent and StorageClass"
178 " ({} != {})".format(type(composite), self.storageClass.pytype)
179 )
181 # Only look for components that are defined by the StorageClass
182 components = {}
183 expItems, expInfoItems = self._groupRequestedComponents()
185 fromExposure = super().disassemble(composite, subset=expItems)
186 components.update(fromExposure)
188 fromExposureInfo = super().disassemble(composite, subset=expInfoItems, override=composite.getInfo())
189 components.update(fromExposureInfo)
191 if "psf" in components and not components["psf"].component.isPersistable():
192 log.warning(
193 "PSF of type %s is not persistable and has been ignored.",
194 type(components["psf"].component).__name__,
195 )
196 del components["psf"]
198 return components
200 def assemble(self, components):
201 """Construct an Exposure from components.
203 Parameters
204 ----------
205 components : `dict`
206 All the components from which to construct the Exposure.
207 Some can be missing.
209 Returns
210 -------
211 exposure : `~lsst.afw.image.Exposure`
212 Assembled exposure.
214 Raises
215 ------
216 ValueError
217 Some supplied components are not recognized.
218 """
219 components = components.copy()
220 maskedImageComponents = {}
221 hasMaskedImage = False
222 for component in ("image", "variance", "mask"):
223 value = None
224 if component in components:
225 hasMaskedImage = True
226 value = components.pop(component)
227 maskedImageComponents[component] = value
229 wcs = None
230 if "wcs" in components:
231 wcs = components.pop("wcs")
233 pytype = self.storageClass.pytype
234 if hasMaskedImage:
235 maskedImage = makeMaskedImage(**maskedImageComponents)
236 exposure = makeExposure(maskedImage, wcs=wcs)
238 if not isinstance(exposure, pytype):
239 raise RuntimeError(
240 "Unexpected type created in assembly;"
241 " was {} expected {}".format(type(exposure), pytype)
242 )
244 else:
245 exposure = pytype()
246 if wcs is not None:
247 exposure.setWcs(wcs)
249 # Set other components
250 exposure.setPsf(components.pop("psf", None))
251 exposure.setPhotoCalib(components.pop("photoCalib", None))
253 info = exposure.getInfo()
254 if "visitInfo" in components:
255 info.setVisitInfo(components.pop("visitInfo"))
256 # Until DM-32138, "visitInfo" and "id" can both set the exposure ID.
257 # While they should always be consistent unless a component is
258 # corrupted, handle "id" second to ensure it takes precedence.
259 if "id" in components:
260 info.id = components.pop("id")
261 info.setApCorrMap(components.pop("apCorrMap", None))
262 info.setCoaddInputs(components.pop("coaddInputs", None))
263 info.setMetadata(components.pop("metadata", None))
264 info.setValidPolygon(components.pop("validPolygon", None))
265 info.setDetector(components.pop("detector", None))
266 info.setTransmissionCurve(components.pop("transmissionCurve", None))
267 info.setSummaryStats(components.pop("summaryStats", None))
269 # TODO: switch back to "filter" as primary component in DM-27177
270 info.setFilterLabel(components.pop("filterLabel", None))
272 # If we have some components left over that is a problem
273 if components:
274 raise ValueError(
275 "The following components were not understood: {}".format(list(components.keys()))
276 )
278 return exposure
280 def handleParameters(self, inMemoryDataset, parameters=None):
281 """Modify the in-memory dataset using the supplied parameters,
282 returning a possibly new object.
284 Parameters
285 ----------
286 inMemoryDataset : `object`
287 Object to modify based on the parameters.
288 parameters : `dict`, optional
289 Parameters to apply. Values are specific to the parameter.
290 Supported parameters are defined in the associated
291 `StorageClass`. If no relevant parameters are specified the
292 inMemoryDataset will be return unchanged.
294 Returns
295 -------
296 inMemoryDataset : `object`
297 Updated form of supplied in-memory dataset, after parameters
298 have been used.
299 """
300 # Understood by *this* subset command
301 understood = ("bbox", "origin")
302 use = self.storageClass.filterParameters(parameters, subset=understood)
303 if use:
304 inMemoryDataset = inMemoryDataset.subset(**use)
306 return inMemoryDataset
308 @classmethod
309 def selectResponsibleComponent(cls, readComponent: str, fromComponents) -> str:
310 imageComponents = ["mask", "image", "variance"]
311 forwarderMap = {
312 "bbox": imageComponents,
313 "dimensions": imageComponents,
314 "xy0": imageComponents,
315 "filter": ["filterLabel"],
316 }
317 forwarder = forwarderMap.get(readComponent)
318 if forwarder is not None:
319 for c in forwarder:
320 if c in fromComponents:
321 return c
322 raise ValueError(f"Can not calculate read component {readComponent} from {fromComponents}")