Coverage for python / lsst / obs / base / exposureAssembler.py: 14%
114 statements
« prev ^ index » next coverage.py v7.13.1, created at 2026-01-22 02:06 -0800
« prev ^ index » next coverage.py v7.13.1, created at 2026-01-22 02:06 -0800
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 contextlib
25import logging
26from collections.abc import Iterable, Mapping
27from typing import Any
29# Need to enable PSFs to be instantiated
30import lsst.afw.detection
31from lsst.afw.image import Exposure, makeExposure, makeMaskedImage
32from lsst.daf.butler import DatasetComponent, DatasetProvenance, DatasetRef, StorageClassDelegate
34from .utils import add_provenance_to_fits_header
36log = logging.getLogger(__name__)
39class ExposureAssembler(StorageClassDelegate):
40 """Knowledge of how to assemble and disassemble an
41 `~lsst.afw.image.Exposure`.
42 """
44 EXPOSURE_COMPONENTS = {"image", "variance", "mask", "wcs", "psf"}
45 EXPOSURE_INFO_COMPONENTS = {
46 "apCorrMap",
47 "coaddInputs",
48 "photoCalib",
49 "metadata",
50 "filter",
51 "transmissionCurve",
52 "visitInfo",
53 "detector",
54 "validPolygon",
55 "summaryStats",
56 "id",
57 }
58 EXPOSURE_READ_COMPONENTS = {
59 "bbox",
60 "dimensions",
61 "xy0",
62 }
64 COMPONENT_MAP = {"bbox": "BBox", "xy0": "XY0"}
65 """Map component name to actual getter name."""
67 def _groupRequestedComponents(self) -> tuple[set[str], set[str]]:
68 """Group requested components into top level and ExposureInfo.
70 Returns
71 -------
72 expComps : `set` [`str`]
73 Components associated with the top level Exposure.
74 expInfoComps : `set` [`str`]
75 Components associated with the ExposureInfo
77 Raises
78 ------
79 ValueError
80 There are components defined in the storage class that are not
81 expected by this assembler.
82 """
83 requested = set(self.storageClass.components.keys())
85 # Check that we are requesting something that we support
86 unknown = requested - (self.EXPOSURE_COMPONENTS | self.EXPOSURE_INFO_COMPONENTS)
87 if unknown:
88 raise ValueError(f"Asking for unrecognized component: {unknown}")
90 expItems = requested & self.EXPOSURE_COMPONENTS
91 expInfoItems = requested & self.EXPOSURE_INFO_COMPONENTS
92 return expItems, expInfoItems
94 def getComponent(self, composite: lsst.afw.image.Exposure, componentName: str) -> Any:
95 """Get a component from an Exposure.
97 Parameters
98 ----------
99 composite : `~lsst.afw.image.Exposure`
100 `Exposure` to access component.
101 componentName : `str`
102 Name of component to retrieve.
104 Returns
105 -------
106 component : `object`
107 The component. Can be None.
109 Raises
110 ------
111 AttributeError
112 The component can not be found.
113 """
114 if componentName in self.EXPOSURE_COMPONENTS or componentName in self.EXPOSURE_READ_COMPONENTS:
115 # Use getter translation if relevant or the name itself
116 return super().getComponent(composite, self.COMPONENT_MAP.get(componentName, componentName))
117 elif componentName in self.EXPOSURE_INFO_COMPONENTS:
118 if hasattr(composite, "getInfo"):
119 # it is possible for this method to be called with
120 # an ExposureInfo composite so trap for that and only get
121 # the ExposureInfo if the method is supported
122 composite = composite.getInfo()
123 return super().getComponent(composite, self.COMPONENT_MAP.get(componentName, componentName))
124 else:
125 raise AttributeError(
126 f"Do not know how to retrieve component {componentName} from {type(composite)}"
127 )
129 def disassemble(
130 self, composite: Any, subset: Iterable | None = None, override: Any | None = None
131 ) -> dict[str, DatasetComponent]:
132 """Disassemble an afw Exposure.
134 This implementation attempts to extract components from the parent
135 by looking for attributes of the same name or getter methods derived
136 from the component name.
138 Parameters
139 ----------
140 composite : `~lsst.afw.image.Exposure`
141 `Exposure` composite object consisting of components to be
142 extracted.
143 subset : iterable, optional
144 Not supported by this assembler.
145 override : `object`, optional
146 Not supported by this assembler.
148 Returns
149 -------
150 components : `dict`
151 `dict` with keys matching the components defined in
152 `self.storageClass` and values being `DatasetComponent` instances
153 describing the component.
155 Raises
156 ------
157 ValueError
158 A requested component can not be found in the parent using generic
159 lookups.
160 TypeError
161 The parent object does not match the supplied `self.storageClass`.
163 Notes
164 -----
165 If a PSF is present but is not persistable, the PSF will not be
166 included in the returned components.
167 """
168 if subset is not None:
169 raise NotImplementedError(
170 "ExposureAssembler does not support the 'subset' argument to disassemble."
171 )
172 if override is not None:
173 raise NotImplementedError(
174 "ExposureAssembler does not support the 'override' argument to disassemble."
175 )
176 if not self.storageClass.validateInstance(composite):
177 raise TypeError(
178 "Unexpected type mismatch between parent and StorageClass"
179 f" ({type(composite)} != {self.storageClass.pytype})"
180 )
182 # Only look for components that are defined by the StorageClass
183 components: dict[str, DatasetComponent] = {}
184 expItems, expInfoItems = self._groupRequestedComponents()
186 fromExposure = super().disassemble(composite, subset=expItems)
187 assert fromExposure is not None, "Base class implementation guarantees this, but ABC does not."
188 components.update(fromExposure)
190 fromExposureInfo = super().disassemble(composite, subset=expInfoItems, override=composite.getInfo())
191 assert fromExposureInfo is not None, "Base class implementation guarantees this, but ABC does not."
192 components.update(fromExposureInfo)
194 if "psf" in components and not components["psf"].component.isPersistable():
195 log.warning(
196 "PSF of type %s is not persistable and has been ignored.",
197 type(components["psf"].component).__name__,
198 )
199 del components["psf"]
201 return components
203 def assemble(self, components: dict[str, Any], pytype: type | None = None) -> Exposure:
204 """Construct an Exposure from components.
206 Parameters
207 ----------
208 components : `dict`
209 All the components from which to construct the Exposure.
210 Some can be missing.
211 pytype : `type`, optional
212 Not supported by this assembler.
214 Returns
215 -------
216 exposure : `~lsst.afw.image.Exposure`
217 Assembled exposure.
219 Raises
220 ------
221 ValueError
222 Some supplied components are not recognized.
223 """
224 if pytype is not None:
225 raise NotImplementedError("ExposureAssembler does not support the 'pytype' argument to assemble.")
226 components = components.copy()
227 maskedImageComponents = {}
228 hasMaskedImage = False
229 for component in ("image", "variance", "mask"):
230 value = None
231 if component in components:
232 hasMaskedImage = True
233 value = components.pop(component)
234 maskedImageComponents[component] = value
236 wcs = None
237 if "wcs" in components:
238 wcs = components.pop("wcs")
240 pytype = self.storageClass.pytype
241 if hasMaskedImage:
242 maskedImage = makeMaskedImage(**maskedImageComponents)
243 exposure = makeExposure(maskedImage, wcs=wcs)
245 if not isinstance(exposure, pytype):
246 raise RuntimeError(
247 f"Unexpected type created in assembly; was {type(exposure)} expected {pytype}"
248 )
250 else:
251 exposure = pytype()
252 if wcs is not None:
253 exposure.setWcs(wcs)
255 # Set other components
256 exposure.setPsf(components.pop("psf", None))
257 exposure.setPhotoCalib(components.pop("photoCalib", None))
259 info = exposure.getInfo()
260 if "visitInfo" in components:
261 info.setVisitInfo(components.pop("visitInfo"))
262 if "id" in components:
263 info.id = components.pop("id")
264 info.setApCorrMap(components.pop("apCorrMap", None))
265 info.setCoaddInputs(components.pop("coaddInputs", None))
266 info.setMetadata(components.pop("metadata", None))
267 info.setValidPolygon(components.pop("validPolygon", None))
268 info.setDetector(components.pop("detector", None))
269 info.setTransmissionCurve(components.pop("transmissionCurve", None))
270 info.setSummaryStats(components.pop("summaryStats", None))
272 info.setFilter(components.pop("filter", None))
274 # If we have some components left over that is a problem
275 if components:
276 raise ValueError(f"The following components were not understood: {list(components.keys())}")
278 return exposure
280 def handleParameters(self, inMemoryDataset: Any, parameters: Mapping[str, Any] | None = None) -> Any:
281 """Modify the in-memory dataset using the supplied parameters,
282 returning a possibly new object.
284 Parameters
285 ----------
286 inMemoryDataset : `object`
287 Object to modify based on the parameters.
288 parameters : `dict`, optional
289 Parameters to apply. Values are specific to the parameter.
290 Supported parameters are defined in the associated
291 `~lsst.daf.butler.StorageClass`. If no relevant parameters are
292 specified the ``inMemoryDataset`` will be return unchanged.
294 Returns
295 -------
296 inMemoryDataset : `object`
297 Updated form of supplied in-memory dataset, after parameters
298 have been used.
299 """
300 if parameters is None:
301 return inMemoryDataset
302 # Understood by *this* subset command
303 understood = ("bbox", "origin")
304 use = self.storageClass.filterParameters(parameters, subset=understood)
305 if use:
306 inMemoryDataset = inMemoryDataset.subset(**use)
308 return inMemoryDataset
310 @classmethod
311 def selectResponsibleComponent(cls, readComponent: str, fromComponents: set[str | None]) -> str:
312 # Docstring inherited.
313 imageComponents = ["mask", "image", "variance"]
314 forwarderMap = {
315 "bbox": imageComponents,
316 "dimensions": imageComponents,
317 "xy0": imageComponents,
318 }
319 forwarder = forwarderMap.get(readComponent)
320 if forwarder is not None:
321 for c in forwarder:
322 if c in fromComponents:
323 return c
324 raise ValueError(f"Can not calculate read component {readComponent} from {fromComponents}")
326 def add_provenance(
327 self, inMemoryDataset: Any, ref: DatasetRef, provenance: DatasetProvenance | None = None
328 ) -> Any:
329 # Add provenance via FITS headers. This delegate is reused by
330 # MaskedImage as well as Exposure so no guarantee that metadata
331 # is present.
332 with contextlib.suppress(AttributeError):
333 add_provenance_to_fits_header(inMemoryDataset.metadata, ref, provenance)
334 return inMemoryDataset