Coverage for python/lsst/obs/base/exposureAssembler.py: 13%

114 statements  

« prev     ^ index     » next       coverage.py v6.5.0, created at 2023-02-23 11:15 +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 getValidComponents(self, composite: Exposure) -> Dict[str, Any]: 

124 """Extract all non-None components from a composite. 

125 

126 Parameters 

127 ---------- 

128 composite : `object` 

129 Composite from which to extract components. 

130 

131 Returns 

132 ------- 

133 comps : `dict` 

134 Non-None components extracted from the composite, indexed by the 

135 component name as derived from the `self.storageClass`. 

136 """ 

137 # For Exposure we call the generic version twice: once for top level 

138 # components, and again for ExposureInfo. 

139 expItems, expInfoItems = self._groupRequestedComponents() 

140 

141 components = super().getValidComponents(composite) 

142 infoComps = super().getValidComponents(composite.getInfo()) 

143 components.update(infoComps) 

144 return components 

145 

146 def disassemble( 

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

148 ) -> Dict[str, DatasetComponent]: 

149 """Disassemble an afw Exposure. 

150 

151 This implementation attempts to extract components from the parent 

152 by looking for attributes of the same name or getter methods derived 

153 from the component name. 

154 

155 Parameters 

156 ---------- 

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

158 `Exposure` composite object consisting of components to be 

159 extracted. 

160 subset : iterable, optional 

161 Not supported by this assembler. 

162 override : `object`, optional 

163 Not supported by this assembler. 

164 

165 Returns 

166 ------- 

167 components : `dict` 

168 `dict` with keys matching the components defined in 

169 `self.storageClass` and values being `DatasetComponent` instances 

170 describing the component. 

171 

172 Raises 

173 ------ 

174 ValueError 

175 A requested component can not be found in the parent using generic 

176 lookups. 

177 TypeError 

178 The parent object does not match the supplied `self.storageClass`. 

179 

180 Notes 

181 ----- 

182 If a PSF is present but is not persistable, the PSF will not be 

183 included in the returned components. 

184 """ 

185 if subset is not None: 

186 raise NotImplementedError( 

187 "ExposureAssembler does not support the 'subset' argument to disassemble." 

188 ) 

189 if override is not None: 

190 raise NotImplementedError( 

191 "ExposureAssembler does not support the 'override' argument to disassemble." 

192 ) 

193 if not self.storageClass.validateInstance(composite): 

194 raise TypeError( 

195 "Unexpected type mismatch between parent and StorageClass" 

196 f" ({type(composite)} != {self.storageClass.pytype})" 

197 ) 

198 

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

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

201 expItems, expInfoItems = self._groupRequestedComponents() 

202 

203 fromExposure = super().disassemble(composite, subset=expItems) 

204 assert fromExposure is not None, "Base class implementation guarantees this, but ABC does not." 

205 components.update(fromExposure) 

206 

207 fromExposureInfo = super().disassemble(composite, subset=expInfoItems, override=composite.getInfo()) 

208 assert fromExposureInfo is not None, "Base class implementation guarantees this, but ABC does not." 

209 components.update(fromExposureInfo) 

210 

211 if "psf" in components and not components["psf"].component.isPersistable(): 

212 log.warning( 

213 "PSF of type %s is not persistable and has been ignored.", 

214 type(components["psf"].component).__name__, 

215 ) 

216 del components["psf"] 

217 

218 return components 

219 

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

221 """Construct an Exposure from components. 

222 

223 Parameters 

224 ---------- 

225 components : `dict` 

226 All the components from which to construct the Exposure. 

227 Some can be missing. 

228 pytype : `type`, optional 

229 Not supported by this assembler. 

230 

231 Returns 

232 ------- 

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

234 Assembled exposure. 

235 

236 Raises 

237 ------ 

238 ValueError 

239 Some supplied components are not recognized. 

240 """ 

241 if pytype is not None: 

242 raise NotImplementedError("ExposureAssembler does not support the 'pytype' argument to assemble.") 

243 components = components.copy() 

244 maskedImageComponents = {} 

245 hasMaskedImage = False 

246 for component in ("image", "variance", "mask"): 

247 value = None 

248 if component in components: 

249 hasMaskedImage = True 

250 value = components.pop(component) 

251 maskedImageComponents[component] = value 

252 

253 wcs = None 

254 if "wcs" in components: 

255 wcs = components.pop("wcs") 

256 

257 pytype = self.storageClass.pytype 

258 if hasMaskedImage: 

259 maskedImage = makeMaskedImage(**maskedImageComponents) 

260 exposure = makeExposure(maskedImage, wcs=wcs) 

261 

262 if not isinstance(exposure, pytype): 

263 raise RuntimeError( 

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

265 ) 

266 

267 else: 

268 exposure = pytype() 

269 if wcs is not None: 

270 exposure.setWcs(wcs) 

271 

272 # Set other components 

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

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

275 

276 info = exposure.getInfo() 

277 if "visitInfo" in components: 

278 info.setVisitInfo(components.pop("visitInfo")) 

279 # Until DM-32138, "visitInfo" and "id" can both set the exposure ID. 

280 # While they should always be consistent unless a component is 

281 # corrupted, handle "id" second to ensure it takes precedence. 

282 if "id" in components: 

283 info.id = components.pop("id") 

284 info.setApCorrMap(components.pop("apCorrMap", None)) 

285 info.setCoaddInputs(components.pop("coaddInputs", None)) 

286 info.setMetadata(components.pop("metadata", None)) 

287 info.setValidPolygon(components.pop("validPolygon", None)) 

288 info.setDetector(components.pop("detector", None)) 

289 info.setTransmissionCurve(components.pop("transmissionCurve", None)) 

290 info.setSummaryStats(components.pop("summaryStats", None)) 

291 

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

293 

294 # If we have some components left over that is a problem 

295 if components: 

296 raise ValueError(f"The following components were not understood: {list(components.keys())}") 

297 

298 return exposure 

299 

300 def handleParameters(self, inMemoryDataset: Any, parameters: Optional[Mapping[str, Any]] = None) -> Any: 

301 """Modify the in-memory dataset using the supplied parameters, 

302 returning a possibly new object. 

303 

304 Parameters 

305 ---------- 

306 inMemoryDataset : `object` 

307 Object to modify based on the parameters. 

308 parameters : `dict`, optional 

309 Parameters to apply. Values are specific to the parameter. 

310 Supported parameters are defined in the associated 

311 `StorageClass`. If no relevant parameters are specified the 

312 inMemoryDataset will be return unchanged. 

313 

314 Returns 

315 ------- 

316 inMemoryDataset : `object` 

317 Updated form of supplied in-memory dataset, after parameters 

318 have been used. 

319 """ 

320 if parameters is None: 

321 return inMemoryDataset 

322 # Understood by *this* subset command 

323 understood = ("bbox", "origin") 

324 use = self.storageClass.filterParameters(parameters, subset=understood) 

325 if use: 

326 inMemoryDataset = inMemoryDataset.subset(**use) 

327 

328 return inMemoryDataset 

329 

330 @classmethod 

331 def selectResponsibleComponent(cls, readComponent: str, fromComponents: Set[Optional[str]]) -> str: 

332 # Docstring inherited. 

333 imageComponents = ["mask", "image", "variance"] 

334 forwarderMap = { 

335 "bbox": imageComponents, 

336 "dimensions": imageComponents, 

337 "xy0": imageComponents, 

338 } 

339 forwarder = forwarderMap.get(readComponent) 

340 if forwarder is not None: 

341 for c in forwarder: 

342 if c in fromComponents: 

343 return c 

344 raise ValueError(f"Can not calculate read component {readComponent} from {fromComponents}")