Coverage for python/lsst/obs/base/exposureAssembler.py: 13%
108 statements
« prev ^ index » next coverage.py v7.2.7, created at 2023-06-15 02:57 -0700
« prev ^ index » next coverage.py v7.2.7, created at 2023-06-15 02:57 -0700
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 disassemble(
124 self, composite: Any, subset: Optional[Iterable] = None, override: Optional[Any] = None
125 ) -> Dict[str, DatasetComponent]:
126 """Disassemble an afw Exposure.
128 This implementation attempts to extract components from the parent
129 by looking for attributes of the same name or getter methods derived
130 from the component name.
132 Parameters
133 ----------
134 composite : `~lsst.afw.image.Exposure`
135 `Exposure` composite object consisting of components to be
136 extracted.
137 subset : iterable, optional
138 Not supported by this assembler.
139 override : `object`, optional
140 Not supported by this assembler.
142 Returns
143 -------
144 components : `dict`
145 `dict` with keys matching the components defined in
146 `self.storageClass` and values being `DatasetComponent` instances
147 describing the component.
149 Raises
150 ------
151 ValueError
152 A requested component can not be found in the parent using generic
153 lookups.
154 TypeError
155 The parent object does not match the supplied `self.storageClass`.
157 Notes
158 -----
159 If a PSF is present but is not persistable, the PSF will not be
160 included in the returned components.
161 """
162 if subset is not None:
163 raise NotImplementedError(
164 "ExposureAssembler does not support the 'subset' argument to disassemble."
165 )
166 if override is not None:
167 raise NotImplementedError(
168 "ExposureAssembler does not support the 'override' argument to disassemble."
169 )
170 if not self.storageClass.validateInstance(composite):
171 raise TypeError(
172 "Unexpected type mismatch between parent and StorageClass"
173 f" ({type(composite)} != {self.storageClass.pytype})"
174 )
176 # Only look for components that are defined by the StorageClass
177 components: Dict[str, DatasetComponent] = {}
178 expItems, expInfoItems = self._groupRequestedComponents()
180 fromExposure = super().disassemble(composite, subset=expItems)
181 assert fromExposure is not None, "Base class implementation guarantees this, but ABC does not."
182 components.update(fromExposure)
184 fromExposureInfo = super().disassemble(composite, subset=expInfoItems, override=composite.getInfo())
185 assert fromExposureInfo is not None, "Base class implementation guarantees this, but ABC does not."
186 components.update(fromExposureInfo)
188 if "psf" in components and not components["psf"].component.isPersistable():
189 log.warning(
190 "PSF of type %s is not persistable and has been ignored.",
191 type(components["psf"].component).__name__,
192 )
193 del components["psf"]
195 return components
197 def assemble(self, components: Dict[str, Any], pytype: Optional[Type] = None) -> Exposure:
198 """Construct an Exposure from components.
200 Parameters
201 ----------
202 components : `dict`
203 All the components from which to construct the Exposure.
204 Some can be missing.
205 pytype : `type`, optional
206 Not supported by this assembler.
208 Returns
209 -------
210 exposure : `~lsst.afw.image.Exposure`
211 Assembled exposure.
213 Raises
214 ------
215 ValueError
216 Some supplied components are not recognized.
217 """
218 if pytype is not None:
219 raise NotImplementedError("ExposureAssembler does not support the 'pytype' argument to assemble.")
220 components = components.copy()
221 maskedImageComponents = {}
222 hasMaskedImage = False
223 for component in ("image", "variance", "mask"):
224 value = None
225 if component in components:
226 hasMaskedImage = True
227 value = components.pop(component)
228 maskedImageComponents[component] = value
230 wcs = None
231 if "wcs" in components:
232 wcs = components.pop("wcs")
234 pytype = self.storageClass.pytype
235 if hasMaskedImage:
236 maskedImage = makeMaskedImage(**maskedImageComponents)
237 exposure = makeExposure(maskedImage, wcs=wcs)
239 if not isinstance(exposure, pytype):
240 raise RuntimeError(
241 f"Unexpected type created in assembly; was {type(exposure)} expected {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 info.setFilter(components.pop("filter", None))
271 # If we have some components left over that is a problem
272 if components:
273 raise ValueError(f"The following components were not understood: {list(components.keys())}")
275 return exposure
277 def handleParameters(self, inMemoryDataset: Any, parameters: Optional[Mapping[str, Any]] = None) -> Any:
278 """Modify the in-memory dataset using the supplied parameters,
279 returning a possibly new object.
281 Parameters
282 ----------
283 inMemoryDataset : `object`
284 Object to modify based on the parameters.
285 parameters : `dict`, optional
286 Parameters to apply. Values are specific to the parameter.
287 Supported parameters are defined in the associated
288 `StorageClass`. If no relevant parameters are specified the
289 inMemoryDataset will be return unchanged.
291 Returns
292 -------
293 inMemoryDataset : `object`
294 Updated form of supplied in-memory dataset, after parameters
295 have been used.
296 """
297 if parameters is None:
298 return inMemoryDataset
299 # Understood by *this* subset command
300 understood = ("bbox", "origin")
301 use = self.storageClass.filterParameters(parameters, subset=understood)
302 if use:
303 inMemoryDataset = inMemoryDataset.subset(**use)
305 return inMemoryDataset
307 @classmethod
308 def selectResponsibleComponent(cls, readComponent: str, fromComponents: Set[Optional[str]]) -> str:
309 # Docstring inherited.
310 imageComponents = ["mask", "image", "variance"]
311 forwarderMap = {
312 "bbox": imageComponents,
313 "dimensions": imageComponents,
314 "xy0": imageComponents,
315 }
316 forwarder = forwarderMap.get(readComponent)
317 if forwarder is not None:
318 for c in forwarder:
319 if c in fromComponents:
320 return c
321 raise ValueError(f"Can not calculate read component {readComponent} from {fromComponents}")