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