Coverage for python/lsst/obs/base/exposureAssembler.py: 14%
108 statements
« prev ^ index » next coverage.py v7.2.7, created at 2023-07-14 20:02 +0000
« prev ^ index » next coverage.py v7.2.7, created at 2023-07-14 20:02 +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 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 if "id" in components:
257 info.id = components.pop("id")
258 info.setApCorrMap(components.pop("apCorrMap", None))
259 info.setCoaddInputs(components.pop("coaddInputs", None))
260 info.setMetadata(components.pop("metadata", None))
261 info.setValidPolygon(components.pop("validPolygon", None))
262 info.setDetector(components.pop("detector", None))
263 info.setTransmissionCurve(components.pop("transmissionCurve", None))
264 info.setSummaryStats(components.pop("summaryStats", None))
266 info.setFilter(components.pop("filter", None))
268 # If we have some components left over that is a problem
269 if components:
270 raise ValueError(f"The following components were not understood: {list(components.keys())}")
272 return exposure
274 def handleParameters(self, inMemoryDataset: Any, parameters: Optional[Mapping[str, Any]] = None) -> Any:
275 """Modify the in-memory dataset using the supplied parameters,
276 returning a possibly new object.
278 Parameters
279 ----------
280 inMemoryDataset : `object`
281 Object to modify based on the parameters.
282 parameters : `dict`, optional
283 Parameters to apply. Values are specific to the parameter.
284 Supported parameters are defined in the associated
285 `StorageClass`. If no relevant parameters are specified the
286 inMemoryDataset will be return unchanged.
288 Returns
289 -------
290 inMemoryDataset : `object`
291 Updated form of supplied in-memory dataset, after parameters
292 have been used.
293 """
294 if parameters is None:
295 return inMemoryDataset
296 # Understood by *this* subset command
297 understood = ("bbox", "origin")
298 use = self.storageClass.filterParameters(parameters, subset=understood)
299 if use:
300 inMemoryDataset = inMemoryDataset.subset(**use)
302 return inMemoryDataset
304 @classmethod
305 def selectResponsibleComponent(cls, readComponent: str, fromComponents: Set[Optional[str]]) -> str:
306 # Docstring inherited.
307 imageComponents = ["mask", "image", "variance"]
308 forwarderMap = {
309 "bbox": imageComponents,
310 "dimensions": imageComponents,
311 "xy0": imageComponents,
312 }
313 forwarder = forwarderMap.get(readComponent)
314 if forwarder is not None:
315 for c in forwarder:
316 if c in fromComponents:
317 return c
318 raise ValueError(f"Can not calculate read component {readComponent} from {fromComponents}")