Coverage for python/lsst/obs/base/exposureAssembler.py: 14%
114 statements
« prev ^ index » next coverage.py v6.4.1, created at 2022-07-09 06:22 -0700
« prev ^ index » next coverage.py v6.4.1, created at 2022-07-09 06:22 -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):
37 EXPOSURE_COMPONENTS = set(("image", "variance", "mask", "wcs", "psf"))
38 EXPOSURE_INFO_COMPONENTS = set(
39 (
40 "apCorrMap",
41 "coaddInputs",
42 "photoCalib",
43 "metadata",
44 "filter",
45 "transmissionCurve",
46 "visitInfo",
47 "detector",
48 "validPolygon",
49 "summaryStats",
50 "id",
51 )
52 )
53 EXPOSURE_READ_COMPONENTS = {
54 "bbox",
55 "dimensions",
56 "xy0",
57 "filterLabel",
58 }
60 COMPONENT_MAP = {"bbox": "BBox", "xy0": "XY0"}
61 """Map component name to actual getter name."""
63 def _groupRequestedComponents(self) -> Tuple[Set[str], Set[str]]:
64 """Group requested components into top level and ExposureInfo.
66 Returns
67 -------
68 expComps : `set` [`str`]
69 Components associated with the top level Exposure.
70 expInfoComps : `set` [`str`]
71 Components associated with the ExposureInfo
73 Raises
74 ------
75 ValueError
76 There are components defined in the storage class that are not
77 expected by this assembler.
78 """
79 requested = set(self.storageClass.components.keys())
81 # Check that we are requesting something that we support
82 unknown = requested - (self.EXPOSURE_COMPONENTS | self.EXPOSURE_INFO_COMPONENTS)
83 if unknown:
84 raise ValueError("Asking for unrecognized component: {}".format(unknown))
86 expItems = requested & self.EXPOSURE_COMPONENTS
87 expInfoItems = requested & self.EXPOSURE_INFO_COMPONENTS
88 return expItems, expInfoItems
90 def getComponent(self, composite: lsst.afw.image.Exposure, componentName: str) -> Any:
91 """Get a component from an Exposure
93 Parameters
94 ----------
95 composite : `~lsst.afw.image.Exposure`
96 `Exposure` to access component.
97 componentName : `str`
98 Name of component to retrieve.
100 Returns
101 -------
102 component : `object`
103 The component. Can be None.
105 Raises
106 ------
107 AttributeError
108 The component can not be found.
109 """
110 if componentName in self.EXPOSURE_COMPONENTS or componentName in self.EXPOSURE_READ_COMPONENTS:
111 # Use getter translation if relevant or the name itself
112 return super().getComponent(composite, self.COMPONENT_MAP.get(componentName, componentName))
113 elif componentName in self.EXPOSURE_INFO_COMPONENTS:
114 if hasattr(composite, "getInfo"):
115 # it is possible for this method to be called with
116 # an ExposureInfo composite so trap for that and only get
117 # the ExposureInfo if the method is supported
118 composite = composite.getInfo()
119 return super().getComponent(composite, self.COMPONENT_MAP.get(componentName, componentName))
120 else:
121 raise AttributeError(
122 "Do not know how to retrieve component {} from {}".format(componentName, type(composite))
123 )
125 def getValidComponents(self, composite: Exposure) -> Dict[str, Any]:
126 """Extract all non-None components from a composite.
128 Parameters
129 ----------
130 composite : `object`
131 Composite from which to extract components.
133 Returns
134 -------
135 comps : `dict`
136 Non-None components extracted from the composite, indexed by the
137 component name as derived from the `self.storageClass`.
138 """
139 # For Exposure we call the generic version twice: once for top level
140 # components, and again for ExposureInfo.
141 expItems, expInfoItems = self._groupRequestedComponents()
143 components = super().getValidComponents(composite)
144 infoComps = super().getValidComponents(composite.getInfo())
145 components.update(infoComps)
146 return components
148 def disassemble(
149 self, composite: Any, subset: Optional[Iterable] = None, override: Optional[Any] = None
150 ) -> Dict[str, DatasetComponent]:
151 """Disassemble an afw Exposure.
153 This implementation attempts to extract components from the parent
154 by looking for attributes of the same name or getter methods derived
155 from the component name.
157 Parameters
158 ----------
159 composite : `~lsst.afw.image.Exposure`
160 `Exposure` composite object consisting of components to be
161 extracted.
162 subset : iterable, optional
163 Not supported by this assembler.
164 override : `object`, optional
165 Not supported by this assembler.
167 Returns
168 -------
169 components : `dict`
170 `dict` with keys matching the components defined in
171 `self.storageClass` and values being `DatasetComponent` instances
172 describing the component.
174 Raises
175 ------
176 ValueError
177 A requested component can not be found in the parent using generic
178 lookups.
179 TypeError
180 The parent object does not match the supplied `self.storageClass`.
182 Notes
183 -----
184 If a PSF is present but is not persistable, the PSF will not be
185 included in the returned components.
186 """
187 if subset is not None:
188 raise NotImplementedError(
189 "ExposureAssembler does not support the 'subset' argument to disassemble."
190 )
191 if override is not None:
192 raise NotImplementedError(
193 "ExposureAssembler does not support the 'override' argument to disassemble."
194 )
195 if not self.storageClass.validateInstance(composite):
196 raise TypeError(
197 "Unexpected type mismatch between parent and StorageClass"
198 " ({} != {})".format(type(composite), self.storageClass.pytype)
199 )
201 # Only look for components that are defined by the StorageClass
202 components: Dict[str, DatasetComponent] = {}
203 expItems, expInfoItems = self._groupRequestedComponents()
205 fromExposure = super().disassemble(composite, subset=expItems)
206 assert fromExposure is not None, "Base class implementation guarantees this, but ABC does not."
207 components.update(fromExposure)
209 fromExposureInfo = super().disassemble(composite, subset=expInfoItems, override=composite.getInfo())
210 assert fromExposureInfo is not None, "Base class implementation guarantees this, but ABC does not."
211 components.update(fromExposureInfo)
213 if "psf" in components and not components["psf"].component.isPersistable():
214 log.warning(
215 "PSF of type %s is not persistable and has been ignored.",
216 type(components["psf"].component).__name__,
217 )
218 del components["psf"]
220 return components
222 def assemble(self, components: Dict[str, Any], pytype: Optional[Type] = None) -> Exposure:
223 """Construct an Exposure from components.
225 Parameters
226 ----------
227 components : `dict`
228 All the components from which to construct the Exposure.
229 Some can be missing.
230 pytype : `type`, optional
231 Not supported by this assembler.
233 Returns
234 -------
235 exposure : `~lsst.afw.image.Exposure`
236 Assembled exposure.
238 Raises
239 ------
240 ValueError
241 Some supplied components are not recognized.
242 """
243 if pytype is not None:
244 raise NotImplementedError("ExposureAssembler does not support the 'pytype' argument to assemble.")
245 components = components.copy()
246 maskedImageComponents = {}
247 hasMaskedImage = False
248 for component in ("image", "variance", "mask"):
249 value = None
250 if component in components:
251 hasMaskedImage = True
252 value = components.pop(component)
253 maskedImageComponents[component] = value
255 wcs = None
256 if "wcs" in components:
257 wcs = components.pop("wcs")
259 pytype = self.storageClass.pytype
260 if hasMaskedImage:
261 maskedImage = makeMaskedImage(**maskedImageComponents)
262 exposure = makeExposure(maskedImage, wcs=wcs)
264 if not isinstance(exposure, pytype):
265 raise RuntimeError(
266 "Unexpected type created in assembly;"
267 " was {} expected {}".format(type(exposure), pytype)
268 )
270 else:
271 exposure = pytype()
272 if wcs is not None:
273 exposure.setWcs(wcs)
275 # Set other components
276 exposure.setPsf(components.pop("psf", None))
277 exposure.setPhotoCalib(components.pop("photoCalib", None))
279 info = exposure.getInfo()
280 if "visitInfo" in components:
281 info.setVisitInfo(components.pop("visitInfo"))
282 # Until DM-32138, "visitInfo" and "id" can both set the exposure ID.
283 # While they should always be consistent unless a component is
284 # corrupted, handle "id" second to ensure it takes precedence.
285 if "id" in components:
286 info.id = components.pop("id")
287 info.setApCorrMap(components.pop("apCorrMap", None))
288 info.setCoaddInputs(components.pop("coaddInputs", None))
289 info.setMetadata(components.pop("metadata", None))
290 info.setValidPolygon(components.pop("validPolygon", None))
291 info.setDetector(components.pop("detector", None))
292 info.setTransmissionCurve(components.pop("transmissionCurve", None))
293 info.setSummaryStats(components.pop("summaryStats", None))
295 info.setFilter(components.pop("filter", None))
297 # If we have some components left over that is a problem
298 if components:
299 raise ValueError(
300 "The following components were not understood: {}".format(list(components.keys()))
301 )
303 return exposure
305 def handleParameters(self, inMemoryDataset: Any, parameters: Optional[Mapping[str, Any]] = None) -> Any:
306 """Modify the in-memory dataset using the supplied parameters,
307 returning a possibly new object.
309 Parameters
310 ----------
311 inMemoryDataset : `object`
312 Object to modify based on the parameters.
313 parameters : `dict`, optional
314 Parameters to apply. Values are specific to the parameter.
315 Supported parameters are defined in the associated
316 `StorageClass`. If no relevant parameters are specified the
317 inMemoryDataset will be return unchanged.
319 Returns
320 -------
321 inMemoryDataset : `object`
322 Updated form of supplied in-memory dataset, after parameters
323 have been used.
324 """
325 if parameters is None:
326 return inMemoryDataset
327 # Understood by *this* subset command
328 understood = ("bbox", "origin")
329 use = self.storageClass.filterParameters(parameters, subset=understood)
330 if use:
331 inMemoryDataset = inMemoryDataset.subset(**use)
333 return inMemoryDataset
335 @classmethod
336 def selectResponsibleComponent(cls, readComponent: str, fromComponents: Set[Optional[str]]) -> str:
337 # Docstring inherited.
338 imageComponents = ["mask", "image", "variance"]
339 forwarderMap = {
340 "bbox": imageComponents,
341 "dimensions": imageComponents,
342 "xy0": imageComponents,
343 "filterLabel": ["filter"],
344 }
345 forwarder = forwarderMap.get(readComponent)
346 if forwarder is not None:
347 for c in forwarder:
348 if c in fromComponents:
349 return c
350 raise ValueError(f"Can not calculate read component {readComponent} from {fromComponents}")