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

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/>. 

21 

22"""Support for assembling and disassembling afw Exposures.""" 

23 

24import logging 

25from typing import Any, Dict, Iterable, Mapping, Optional, Set, Tuple, Type 

26 

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 

31 

32log = logging.getLogger(__name__) 

33 

34 

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 } 

57 

58 COMPONENT_MAP = {"bbox": "BBox", "xy0": "XY0"} 

59 """Map component name to actual getter name.""" 

60 

61 def _groupRequestedComponents(self) -> Tuple[Set[str], Set[str]]: 

62 """Group requested components into top level and ExposureInfo. 

63 

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 

70 

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()) 

78 

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}") 

83 

84 expItems = requested & self.EXPOSURE_COMPONENTS 

85 expInfoItems = requested & self.EXPOSURE_INFO_COMPONENTS 

86 return expItems, expInfoItems 

87 

88 def getComponent(self, composite: lsst.afw.image.Exposure, componentName: str) -> Any: 

89 """Get a component from an Exposure 

90 

91 Parameters 

92 ---------- 

93 composite : `~lsst.afw.image.Exposure` 

94 `Exposure` to access component. 

95 componentName : `str` 

96 Name of component to retrieve. 

97 

98 Returns 

99 ------- 

100 component : `object` 

101 The component. Can be None. 

102 

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 ) 

122 

123 def disassemble( 

124 self, composite: Any, subset: Optional[Iterable] = None, override: Optional[Any] = None 

125 ) -> Dict[str, DatasetComponent]: 

126 """Disassemble an afw Exposure. 

127 

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. 

131 

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. 

141 

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. 

148 

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`. 

156 

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 ) 

175 

176 # Only look for components that are defined by the StorageClass 

177 components: Dict[str, DatasetComponent] = {} 

178 expItems, expInfoItems = self._groupRequestedComponents() 

179 

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) 

183 

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) 

187 

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"] 

194 

195 return components 

196 

197 def assemble(self, components: Dict[str, Any], pytype: Optional[Type] = None) -> Exposure: 

198 """Construct an Exposure from components. 

199 

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. 

207 

208 Returns 

209 ------- 

210 exposure : `~lsst.afw.image.Exposure` 

211 Assembled exposure. 

212 

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 

229 

230 wcs = None 

231 if "wcs" in components: 

232 wcs = components.pop("wcs") 

233 

234 pytype = self.storageClass.pytype 

235 if hasMaskedImage: 

236 maskedImage = makeMaskedImage(**maskedImageComponents) 

237 exposure = makeExposure(maskedImage, wcs=wcs) 

238 

239 if not isinstance(exposure, pytype): 

240 raise RuntimeError( 

241 f"Unexpected type created in assembly; was {type(exposure)} expected {pytype}" 

242 ) 

243 

244 else: 

245 exposure = pytype() 

246 if wcs is not None: 

247 exposure.setWcs(wcs) 

248 

249 # Set other components 

250 exposure.setPsf(components.pop("psf", None)) 

251 exposure.setPhotoCalib(components.pop("photoCalib", None)) 

252 

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)) 

265 

266 info.setFilter(components.pop("filter", None)) 

267 

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())}") 

271 

272 return exposure 

273 

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. 

277 

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. 

287 

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) 

301 

302 return inMemoryDataset 

303 

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}")