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

114 statements  

« prev     ^ index     » next       coverage.py v6.5.0, created at 2023-01-31 02:35 -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/>. 

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 

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 } 

58 

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

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

61 

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

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

64 

65 Returns 

66 ------- 

67 expComps : `set` [`str`] 

68 Components associated with the top level Exposure. 

69 expInfoComps : `set` [`str`] 

70 Components associated with the ExposureInfo 

71 

72 Raises 

73 ------ 

74 ValueError 

75 There are components defined in the storage class that are not 

76 expected by this assembler. 

77 """ 

78 requested = set(self.storageClass.components.keys()) 

79 

80 # Check that we are requesting something that we support 

81 unknown = requested - (self.EXPOSURE_COMPONENTS | self.EXPOSURE_INFO_COMPONENTS) 

82 if unknown: 

83 raise ValueError("Asking for unrecognized component: {}".format(unknown)) 

84 

85 expItems = requested & self.EXPOSURE_COMPONENTS 

86 expInfoItems = requested & self.EXPOSURE_INFO_COMPONENTS 

87 return expItems, expInfoItems 

88 

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

90 """Get a component from an Exposure 

91 

92 Parameters 

93 ---------- 

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

95 `Exposure` to access component. 

96 componentName : `str` 

97 Name of component to retrieve. 

98 

99 Returns 

100 ------- 

101 component : `object` 

102 The component. Can be None. 

103 

104 Raises 

105 ------ 

106 AttributeError 

107 The component can not be found. 

108 """ 

109 if componentName in self.EXPOSURE_COMPONENTS or componentName in self.EXPOSURE_READ_COMPONENTS: 

110 # Use getter translation if relevant or the name itself 

111 return super().getComponent(composite, self.COMPONENT_MAP.get(componentName, componentName)) 

112 elif componentName in self.EXPOSURE_INFO_COMPONENTS: 

113 if hasattr(composite, "getInfo"): 

114 # it is possible for this method to be called with 

115 # an ExposureInfo composite so trap for that and only get 

116 # the ExposureInfo if the method is supported 

117 composite = composite.getInfo() 

118 return super().getComponent(composite, self.COMPONENT_MAP.get(componentName, componentName)) 

119 else: 

120 raise AttributeError( 

121 "Do not know how to retrieve component {} from {}".format(componentName, type(composite)) 

122 ) 

123 

124 def getValidComponents(self, composite: Exposure) -> Dict[str, Any]: 

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

126 

127 Parameters 

128 ---------- 

129 composite : `object` 

130 Composite from which to extract components. 

131 

132 Returns 

133 ------- 

134 comps : `dict` 

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

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

137 """ 

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

139 # components, and again for ExposureInfo. 

140 expItems, expInfoItems = self._groupRequestedComponents() 

141 

142 components = super().getValidComponents(composite) 

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

144 components.update(infoComps) 

145 return components 

146 

147 def disassemble( 

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

149 ) -> Dict[str, DatasetComponent]: 

150 """Disassemble an afw Exposure. 

151 

152 This implementation attempts to extract components from the parent 

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

154 from the component name. 

155 

156 Parameters 

157 ---------- 

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

159 `Exposure` composite object consisting of components to be 

160 extracted. 

161 subset : iterable, optional 

162 Not supported by this assembler. 

163 override : `object`, optional 

164 Not supported by this assembler. 

165 

166 Returns 

167 ------- 

168 components : `dict` 

169 `dict` with keys matching the components defined in 

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

171 describing the component. 

172 

173 Raises 

174 ------ 

175 ValueError 

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

177 lookups. 

178 TypeError 

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

180 

181 Notes 

182 ----- 

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

184 included in the returned components. 

185 """ 

186 if subset is not None: 

187 raise NotImplementedError( 

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

189 ) 

190 if override is not None: 

191 raise NotImplementedError( 

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

193 ) 

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

195 raise TypeError( 

196 "Unexpected type mismatch between parent and StorageClass" 

197 " ({} != {})".format(type(composite), self.storageClass.pytype) 

198 ) 

199 

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

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

202 expItems, expInfoItems = self._groupRequestedComponents() 

203 

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

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

206 components.update(fromExposure) 

207 

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

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

210 components.update(fromExposureInfo) 

211 

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

213 log.warning( 

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

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

216 ) 

217 del components["psf"] 

218 

219 return components 

220 

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

222 """Construct an Exposure from components. 

223 

224 Parameters 

225 ---------- 

226 components : `dict` 

227 All the components from which to construct the Exposure. 

228 Some can be missing. 

229 pytype : `type`, optional 

230 Not supported by this assembler. 

231 

232 Returns 

233 ------- 

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

235 Assembled exposure. 

236 

237 Raises 

238 ------ 

239 ValueError 

240 Some supplied components are not recognized. 

241 """ 

242 if pytype is not None: 

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

244 components = components.copy() 

245 maskedImageComponents = {} 

246 hasMaskedImage = False 

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

248 value = None 

249 if component in components: 

250 hasMaskedImage = True 

251 value = components.pop(component) 

252 maskedImageComponents[component] = value 

253 

254 wcs = None 

255 if "wcs" in components: 

256 wcs = components.pop("wcs") 

257 

258 pytype = self.storageClass.pytype 

259 if hasMaskedImage: 

260 maskedImage = makeMaskedImage(**maskedImageComponents) 

261 exposure = makeExposure(maskedImage, wcs=wcs) 

262 

263 if not isinstance(exposure, pytype): 

264 raise RuntimeError( 

265 "Unexpected type created in assembly;" 

266 " was {} expected {}".format(type(exposure), pytype) 

267 ) 

268 

269 else: 

270 exposure = pytype() 

271 if wcs is not None: 

272 exposure.setWcs(wcs) 

273 

274 # Set other components 

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

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

277 

278 info = exposure.getInfo() 

279 if "visitInfo" in components: 

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

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

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

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

284 if "id" in components: 

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

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

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

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

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

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

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

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

293 

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

295 

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

297 if components: 

298 raise ValueError( 

299 "The following components were not understood: {}".format(list(components.keys())) 

300 ) 

301 

302 return exposure 

303 

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

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

306 returning a possibly new object. 

307 

308 Parameters 

309 ---------- 

310 inMemoryDataset : `object` 

311 Object to modify based on the parameters. 

312 parameters : `dict`, optional 

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

314 Supported parameters are defined in the associated 

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

316 inMemoryDataset will be return unchanged. 

317 

318 Returns 

319 ------- 

320 inMemoryDataset : `object` 

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

322 have been used. 

323 """ 

324 if parameters is None: 

325 return inMemoryDataset 

326 # Understood by *this* subset command 

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

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

329 if use: 

330 inMemoryDataset = inMemoryDataset.subset(**use) 

331 

332 return inMemoryDataset 

333 

334 @classmethod 

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

336 # Docstring inherited. 

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

338 forwarderMap = { 

339 "bbox": imageComponents, 

340 "dimensions": imageComponents, 

341 "xy0": imageComponents, 

342 } 

343 forwarder = forwarderMap.get(readComponent) 

344 if forwarder is not None: 

345 for c in forwarder: 

346 if c in fromComponents: 

347 return c 

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