Coverage for python/lsst/obs/base/exposureAssembler.py: 13%
114 statements
« prev ^ index » next coverage.py v6.5.0, created at 2023-04-14 09:29 +0000
« prev ^ index » next coverage.py v6.5.0, created at 2023-04-14 09:29 +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(f"Asking for unrecognized component: {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 f"Do not know how to retrieve component {componentName} from {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 f" ({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 f"Unexpected type created in assembly; was {type(exposure)} expected {pytype}"
265 )
267 else:
268 exposure = pytype()
269 if wcs is not None:
270 exposure.setWcs(wcs)
272 # Set other components
273 exposure.setPsf(components.pop("psf", None))
274 exposure.setPhotoCalib(components.pop("photoCalib", None))
276 info = exposure.getInfo()
277 if "visitInfo" in components:
278 info.setVisitInfo(components.pop("visitInfo"))
279 # Until DM-32138, "visitInfo" and "id" can both set the exposure ID.
280 # While they should always be consistent unless a component is
281 # corrupted, handle "id" second to ensure it takes precedence.
282 if "id" in components:
283 info.id = components.pop("id")
284 info.setApCorrMap(components.pop("apCorrMap", None))
285 info.setCoaddInputs(components.pop("coaddInputs", None))
286 info.setMetadata(components.pop("metadata", None))
287 info.setValidPolygon(components.pop("validPolygon", None))
288 info.setDetector(components.pop("detector", None))
289 info.setTransmissionCurve(components.pop("transmissionCurve", None))
290 info.setSummaryStats(components.pop("summaryStats", None))
292 info.setFilter(components.pop("filter", None))
294 # If we have some components left over that is a problem
295 if components:
296 raise ValueError(f"The following components were not understood: {list(components.keys())}")
298 return exposure
300 def handleParameters(self, inMemoryDataset: Any, parameters: Optional[Mapping[str, Any]] = None) -> Any:
301 """Modify the in-memory dataset using the supplied parameters,
302 returning a possibly new object.
304 Parameters
305 ----------
306 inMemoryDataset : `object`
307 Object to modify based on the parameters.
308 parameters : `dict`, optional
309 Parameters to apply. Values are specific to the parameter.
310 Supported parameters are defined in the associated
311 `StorageClass`. If no relevant parameters are specified the
312 inMemoryDataset will be return unchanged.
314 Returns
315 -------
316 inMemoryDataset : `object`
317 Updated form of supplied in-memory dataset, after parameters
318 have been used.
319 """
320 if parameters is None:
321 return inMemoryDataset
322 # Understood by *this* subset command
323 understood = ("bbox", "origin")
324 use = self.storageClass.filterParameters(parameters, subset=understood)
325 if use:
326 inMemoryDataset = inMemoryDataset.subset(**use)
328 return inMemoryDataset
330 @classmethod
331 def selectResponsibleComponent(cls, readComponent: str, fromComponents: Set[Optional[str]]) -> str:
332 # Docstring inherited.
333 imageComponents = ["mask", "image", "variance"]
334 forwarderMap = {
335 "bbox": imageComponents,
336 "dimensions": imageComponents,
337 "xy0": imageComponents,
338 }
339 forwarder = forwarderMap.get(readComponent)
340 if forwarder is not None:
341 for c in forwarder:
342 if c in fromComponents:
343 return c
344 raise ValueError(f"Can not calculate read component {readComponent} from {fromComponents}")