Coverage for python/lsst/obs/base/exposureAssembler.py: 15%
109 statements
« prev ^ index » next coverage.py v7.5.0, created at 2024-04-27 03:06 -0700
« prev ^ index » next coverage.py v7.5.0, created at 2024-04-27 03:06 -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 collections.abc import Iterable, Mapping
26from typing import Any
28# Need to enable PSFs to be instantiated
29import lsst.afw.detection
30from lsst.afw.image import Exposure, makeExposure, makeMaskedImage
31from lsst.daf.butler import DatasetComponent, StorageClassDelegate
33log = logging.getLogger(__name__)
36class ExposureAssembler(StorageClassDelegate):
37 """Knowledge of how to assemble and disassemble an
38 `~lsst.afw.image.Exposure`.
39 """
41 EXPOSURE_COMPONENTS = {"image", "variance", "mask", "wcs", "psf"}
42 EXPOSURE_INFO_COMPONENTS = {
43 "apCorrMap",
44 "coaddInputs",
45 "photoCalib",
46 "metadata",
47 "filter",
48 "transmissionCurve",
49 "visitInfo",
50 "detector",
51 "validPolygon",
52 "summaryStats",
53 "id",
54 }
55 EXPOSURE_READ_COMPONENTS = {
56 "bbox",
57 "dimensions",
58 "xy0",
59 }
61 COMPONENT_MAP = {"bbox": "BBox", "xy0": "XY0"}
62 """Map component name to actual getter name."""
64 def _groupRequestedComponents(self) -> tuple[set[str], set[str]]:
65 """Group requested components into top level and ExposureInfo.
67 Returns
68 -------
69 expComps : `set` [`str`]
70 Components associated with the top level Exposure.
71 expInfoComps : `set` [`str`]
72 Components associated with the ExposureInfo
74 Raises
75 ------
76 ValueError
77 There are components defined in the storage class that are not
78 expected by this assembler.
79 """
80 requested = set(self.storageClass.components.keys())
82 # Check that we are requesting something that we support
83 unknown = requested - (self.EXPOSURE_COMPONENTS | self.EXPOSURE_INFO_COMPONENTS)
84 if unknown:
85 raise ValueError(f"Asking for unrecognized component: {unknown}")
87 expItems = requested & self.EXPOSURE_COMPONENTS
88 expInfoItems = requested & self.EXPOSURE_INFO_COMPONENTS
89 return expItems, expInfoItems
91 def getComponent(self, composite: lsst.afw.image.Exposure, componentName: str) -> Any:
92 """Get a component from an Exposure.
94 Parameters
95 ----------
96 composite : `~lsst.afw.image.Exposure`
97 `Exposure` to access component.
98 componentName : `str`
99 Name of component to retrieve.
101 Returns
102 -------
103 component : `object`
104 The component. Can be None.
106 Raises
107 ------
108 AttributeError
109 The component can not be found.
110 """
111 if componentName in self.EXPOSURE_COMPONENTS or componentName in self.EXPOSURE_READ_COMPONENTS:
112 # Use getter translation if relevant or the name itself
113 return super().getComponent(composite, self.COMPONENT_MAP.get(componentName, componentName))
114 elif componentName in self.EXPOSURE_INFO_COMPONENTS:
115 if hasattr(composite, "getInfo"):
116 # it is possible for this method to be called with
117 # an ExposureInfo composite so trap for that and only get
118 # the ExposureInfo if the method is supported
119 composite = composite.getInfo()
120 return super().getComponent(composite, self.COMPONENT_MAP.get(componentName, componentName))
121 else:
122 raise AttributeError(
123 f"Do not know how to retrieve component {componentName} from {type(composite)}"
124 )
126 def disassemble(
127 self, composite: Any, subset: Iterable | None = None, override: Any | None = None
128 ) -> dict[str, DatasetComponent]:
129 """Disassemble an afw Exposure.
131 This implementation attempts to extract components from the parent
132 by looking for attributes of the same name or getter methods derived
133 from the component name.
135 Parameters
136 ----------
137 composite : `~lsst.afw.image.Exposure`
138 `Exposure` composite object consisting of components to be
139 extracted.
140 subset : iterable, optional
141 Not supported by this assembler.
142 override : `object`, optional
143 Not supported by this assembler.
145 Returns
146 -------
147 components : `dict`
148 `dict` with keys matching the components defined in
149 `self.storageClass` and values being `DatasetComponent` instances
150 describing the component.
152 Raises
153 ------
154 ValueError
155 A requested component can not be found in the parent using generic
156 lookups.
157 TypeError
158 The parent object does not match the supplied `self.storageClass`.
160 Notes
161 -----
162 If a PSF is present but is not persistable, the PSF will not be
163 included in the returned components.
164 """
165 if subset is not None:
166 raise NotImplementedError(
167 "ExposureAssembler does not support the 'subset' argument to disassemble."
168 )
169 if override is not None:
170 raise NotImplementedError(
171 "ExposureAssembler does not support the 'override' argument to disassemble."
172 )
173 if not self.storageClass.validateInstance(composite):
174 raise TypeError(
175 "Unexpected type mismatch between parent and StorageClass"
176 f" ({type(composite)} != {self.storageClass.pytype})"
177 )
179 # Only look for components that are defined by the StorageClass
180 components: dict[str, DatasetComponent] = {}
181 expItems, expInfoItems = self._groupRequestedComponents()
183 fromExposure = super().disassemble(composite, subset=expItems)
184 assert fromExposure is not None, "Base class implementation guarantees this, but ABC does not."
185 components.update(fromExposure)
187 fromExposureInfo = super().disassemble(composite, subset=expInfoItems, override=composite.getInfo())
188 assert fromExposureInfo is not None, "Base class implementation guarantees this, but ABC does not."
189 components.update(fromExposureInfo)
191 if "psf" in components and not components["psf"].component.isPersistable():
192 log.warning(
193 "PSF of type %s is not persistable and has been ignored.",
194 type(components["psf"].component).__name__,
195 )
196 del components["psf"]
198 return components
200 def assemble(self, components: dict[str, Any], pytype: type | None = None) -> Exposure:
201 """Construct an Exposure from components.
203 Parameters
204 ----------
205 components : `dict`
206 All the components from which to construct the Exposure.
207 Some can be missing.
208 pytype : `type`, optional
209 Not supported by this assembler.
211 Returns
212 -------
213 exposure : `~lsst.afw.image.Exposure`
214 Assembled exposure.
216 Raises
217 ------
218 ValueError
219 Some supplied components are not recognized.
220 """
221 if pytype is not None:
222 raise NotImplementedError("ExposureAssembler does not support the 'pytype' argument to assemble.")
223 components = components.copy()
224 maskedImageComponents = {}
225 hasMaskedImage = False
226 for component in ("image", "variance", "mask"):
227 value = None
228 if component in components:
229 hasMaskedImage = True
230 value = components.pop(component)
231 maskedImageComponents[component] = value
233 wcs = None
234 if "wcs" in components:
235 wcs = components.pop("wcs")
237 pytype = self.storageClass.pytype
238 if hasMaskedImage:
239 maskedImage = makeMaskedImage(**maskedImageComponents)
240 exposure = makeExposure(maskedImage, wcs=wcs)
242 if not isinstance(exposure, pytype):
243 raise RuntimeError(
244 f"Unexpected type created in assembly; was {type(exposure)} expected {pytype}"
245 )
247 else:
248 exposure = pytype()
249 if wcs is not None:
250 exposure.setWcs(wcs)
252 # Set other components
253 exposure.setPsf(components.pop("psf", None))
254 exposure.setPhotoCalib(components.pop("photoCalib", None))
256 info = exposure.getInfo()
257 if "visitInfo" in components:
258 info.setVisitInfo(components.pop("visitInfo"))
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: Mapping[str, Any] | None = 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 `~lsst.daf.butler.StorageClass`. If no relevant parameters are
289 specified the ``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[str | None]) -> 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}")