Coverage for python/lsst/obs/base/exposureAssembler.py: 14%
114 statements
« prev ^ index » next coverage.py v6.4, created at 2022-05-27 11:22 +0000
« prev ^ index » next coverage.py v6.4, created at 2022-05-27 11:22 +0000
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
25from typing import Any, Dict, Iterable, Mapping, Optional, Set, Tuple, Type
27# Need to enable PSFs to be instantiated
28import lsst.afw.detection # noqa: F401
29from lsst.afw.image import Exposure, makeExposure, makeMaskedImage
30from lsst.daf.butler import DatasetComponent, StorageClassDelegate
32log = logging.getLogger(__name__)
35class ExposureAssembler(StorageClassDelegate):
37 EXPOSURE_COMPONENTS = set(("image", "variance", "mask", "wcs", "psf"))
38 EXPOSURE_INFO_COMPONENTS = set(
39 (
40 "apCorrMap",
41 "coaddInputs",
42 "photoCalib",
43 "metadata",
44 "filterLabel",
45 "transmissionCurve",
46 "visitInfo",
47 "detector",
48 "validPolygon",
49 "summaryStats",
50 "id",
51 )
52 )
53 EXPOSURE_READ_COMPONENTS = {"bbox", "dimensions", "xy0", "filter"}
55 COMPONENT_MAP = {"bbox": "BBox", "xy0": "XY0"}
56 """Map component name to actual getter name."""
58 def _groupRequestedComponents(self) -> Tuple[Set[str], Set[str]]:
59 """Group requested components into top level and ExposureInfo.
61 Returns
62 -------
63 expComps : `set` [`str`]
64 Components associated with the top level Exposure.
65 expInfoComps : `set` [`str`]
66 Components associated with the ExposureInfo
68 Raises
69 ------
70 ValueError
71 There are components defined in the storage class that are not
72 expected by this assembler.
73 """
74 requested = set(self.storageClass.components.keys())
76 # Check that we are requesting something that we support
77 unknown = requested - (self.EXPOSURE_COMPONENTS | self.EXPOSURE_INFO_COMPONENTS)
78 if unknown:
79 raise ValueError("Asking for unrecognized component: {}".format(unknown))
81 expItems = requested & self.EXPOSURE_COMPONENTS
82 expInfoItems = requested & self.EXPOSURE_INFO_COMPONENTS
83 return expItems, expInfoItems
85 def getComponent(self, composite: lsst.afw.image.Exposure, componentName: str) -> Any:
86 """Get a component from an Exposure
88 Parameters
89 ----------
90 composite : `~lsst.afw.image.Exposure`
91 `Exposure` to access component.
92 componentName : `str`
93 Name of component to retrieve.
95 Returns
96 -------
97 component : `object`
98 The component. Can be None.
100 Raises
101 ------
102 AttributeError
103 The component can not be found.
104 """
105 if componentName in self.EXPOSURE_COMPONENTS or componentName in self.EXPOSURE_READ_COMPONENTS:
106 # Use getter translation if relevant or the name itself
107 return super().getComponent(composite, self.COMPONENT_MAP.get(componentName, componentName))
108 elif componentName in self.EXPOSURE_INFO_COMPONENTS:
109 if hasattr(composite, "getInfo"):
110 # it is possible for this method to be called with
111 # an ExposureInfo composite so trap for that and only get
112 # the ExposureInfo if the method is supported
113 composite = composite.getInfo()
114 return super().getComponent(composite, self.COMPONENT_MAP.get(componentName, componentName))
115 else:
116 raise AttributeError(
117 "Do not know how to retrieve component {} from {}".format(componentName, type(composite))
118 )
120 def getValidComponents(self, composite: Exposure) -> Dict[str, Any]:
121 """Extract all non-None components from a composite.
123 Parameters
124 ----------
125 composite : `object`
126 Composite from which to extract components.
128 Returns
129 -------
130 comps : `dict`
131 Non-None components extracted from the composite, indexed by the
132 component name as derived from the `self.storageClass`.
133 """
134 # For Exposure we call the generic version twice: once for top level
135 # components, and again for ExposureInfo.
136 expItems, expInfoItems = self._groupRequestedComponents()
138 components = super().getValidComponents(composite)
139 infoComps = super().getValidComponents(composite.getInfo())
140 components.update(infoComps)
141 return components
143 def disassemble(
144 self, composite: Any, subset: Optional[Iterable] = None, override: Optional[Any] = None
145 ) -> Dict[str, DatasetComponent]:
146 """Disassemble an afw Exposure.
148 This implementation attempts to extract components from the parent
149 by looking for attributes of the same name or getter methods derived
150 from the component name.
152 Parameters
153 ----------
154 composite : `~lsst.afw.image.Exposure`
155 `Exposure` composite object consisting of components to be
156 extracted.
157 subset : iterable, optional
158 Not supported by this assembler.
159 override : `object`, optional
160 Not supported by this assembler.
162 Returns
163 -------
164 components : `dict`
165 `dict` with keys matching the components defined in
166 `self.storageClass` and values being `DatasetComponent` instances
167 describing the component.
169 Raises
170 ------
171 ValueError
172 A requested component can not be found in the parent using generic
173 lookups.
174 TypeError
175 The parent object does not match the supplied `self.storageClass`.
177 Notes
178 -----
179 If a PSF is present but is not persistable, the PSF will not be
180 included in the returned components.
181 """
182 if subset is not None:
183 raise NotImplementedError(
184 "ExposureAssembler does not support the 'subset' argument to disassemble."
185 )
186 if override is not None:
187 raise NotImplementedError(
188 "ExposureAssembler does not support the 'override' argument to disassemble."
189 )
190 if not self.storageClass.validateInstance(composite):
191 raise TypeError(
192 "Unexpected type mismatch between parent and StorageClass"
193 " ({} != {})".format(type(composite), self.storageClass.pytype)
194 )
196 # Only look for components that are defined by the StorageClass
197 components: Dict[str, DatasetComponent] = {}
198 expItems, expInfoItems = self._groupRequestedComponents()
200 fromExposure = super().disassemble(composite, subset=expItems)
201 assert fromExposure is not None, "Base class implementation guarantees this, but ABC does not."
202 components.update(fromExposure)
204 fromExposureInfo = super().disassemble(composite, subset=expInfoItems, override=composite.getInfo())
205 assert fromExposureInfo is not None, "Base class implementation guarantees this, but ABC does not."
206 components.update(fromExposureInfo)
208 if "psf" in components and not components["psf"].component.isPersistable():
209 log.warning(
210 "PSF of type %s is not persistable and has been ignored.",
211 type(components["psf"].component).__name__,
212 )
213 del components["psf"]
215 return components
217 def assemble(self, components: Dict[str, Any], pytype: Optional[Type] = None) -> Exposure:
218 """Construct an Exposure from components.
220 Parameters
221 ----------
222 components : `dict`
223 All the components from which to construct the Exposure.
224 Some can be missing.
225 pytype : `type`, optional
226 Not supported by this assembler.
228 Returns
229 -------
230 exposure : `~lsst.afw.image.Exposure`
231 Assembled exposure.
233 Raises
234 ------
235 ValueError
236 Some supplied components are not recognized.
237 """
238 if pytype is not None:
239 raise NotImplementedError("ExposureAssembler does not support the 'pytype' argument to assemble.")
240 components = components.copy()
241 maskedImageComponents = {}
242 hasMaskedImage = False
243 for component in ("image", "variance", "mask"):
244 value = None
245 if component in components:
246 hasMaskedImage = True
247 value = components.pop(component)
248 maskedImageComponents[component] = value
250 wcs = None
251 if "wcs" in components:
252 wcs = components.pop("wcs")
254 pytype = self.storageClass.pytype
255 if hasMaskedImage:
256 maskedImage = makeMaskedImage(**maskedImageComponents)
257 exposure = makeExposure(maskedImage, wcs=wcs)
259 if not isinstance(exposure, pytype):
260 raise RuntimeError(
261 "Unexpected type created in assembly;"
262 " was {} expected {}".format(type(exposure), pytype)
263 )
265 else:
266 exposure = pytype()
267 if wcs is not None:
268 exposure.setWcs(wcs)
270 # Set other components
271 exposure.setPsf(components.pop("psf", None))
272 exposure.setPhotoCalib(components.pop("photoCalib", None))
274 info = exposure.getInfo()
275 if "visitInfo" in components:
276 info.setVisitInfo(components.pop("visitInfo"))
277 # Until DM-32138, "visitInfo" and "id" can both set the exposure ID.
278 # While they should always be consistent unless a component is
279 # corrupted, handle "id" second to ensure it takes precedence.
280 if "id" in components:
281 info.id = components.pop("id")
282 info.setApCorrMap(components.pop("apCorrMap", None))
283 info.setCoaddInputs(components.pop("coaddInputs", None))
284 info.setMetadata(components.pop("metadata", None))
285 info.setValidPolygon(components.pop("validPolygon", None))
286 info.setDetector(components.pop("detector", None))
287 info.setTransmissionCurve(components.pop("transmissionCurve", None))
288 info.setSummaryStats(components.pop("summaryStats", None))
290 # TODO: switch back to "filter" as primary component in DM-27177
291 info.setFilterLabel(components.pop("filterLabel", None))
293 # If we have some components left over that is a problem
294 if components:
295 raise ValueError(
296 "The following components were not understood: {}".format(list(components.keys()))
297 )
299 return exposure
301 def handleParameters(self, inMemoryDataset: Any, parameters: Optional[Mapping[str, Any]] = None) -> Any:
302 """Modify the in-memory dataset using the supplied parameters,
303 returning a possibly new object.
305 Parameters
306 ----------
307 inMemoryDataset : `object`
308 Object to modify based on the parameters.
309 parameters : `dict`, optional
310 Parameters to apply. Values are specific to the parameter.
311 Supported parameters are defined in the associated
312 `StorageClass`. If no relevant parameters are specified the
313 inMemoryDataset will be return unchanged.
315 Returns
316 -------
317 inMemoryDataset : `object`
318 Updated form of supplied in-memory dataset, after parameters
319 have been used.
320 """
321 if parameters is None:
322 return inMemoryDataset
323 # Understood by *this* subset command
324 understood = ("bbox", "origin")
325 use = self.storageClass.filterParameters(parameters, subset=understood)
326 if use:
327 inMemoryDataset = inMemoryDataset.subset(**use)
329 return inMemoryDataset
331 @classmethod
332 def selectResponsibleComponent(cls, readComponent: str, fromComponents: Set[Optional[str]]) -> str:
333 # Docstring inherited.
334 imageComponents = ["mask", "image", "variance"]
335 forwarderMap = {
336 "bbox": imageComponents,
337 "dimensions": imageComponents,
338 "xy0": imageComponents,
339 "filter": ["filterLabel"],
340 }
341 forwarder = forwarderMap.get(readComponent)
342 if forwarder is not None:
343 for c in forwarder:
344 if c in fromComponents:
345 return c
346 raise ValueError(f"Can not calculate read component {readComponent} from {fromComponents}")