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