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

114 statements  

« prev     ^ index     » next       coverage.py v6.4.1, created at 2022-06-28 02:15 -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/>. 

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 "filterLabel", 

58 } 

59 

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

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

62 

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

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

65 

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 

72 

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

80 

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

85 

86 expItems = requested & self.EXPOSURE_COMPONENTS 

87 expInfoItems = requested & self.EXPOSURE_INFO_COMPONENTS 

88 return expItems, expInfoItems 

89 

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

91 """Get a component from an Exposure 

92 

93 Parameters 

94 ---------- 

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

96 `Exposure` to access component. 

97 componentName : `str` 

98 Name of component to retrieve. 

99 

100 Returns 

101 ------- 

102 component : `object` 

103 The component. Can be None. 

104 

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 ) 

124 

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

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

127 

128 Parameters 

129 ---------- 

130 composite : `object` 

131 Composite from which to extract components. 

132 

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

142 

143 components = super().getValidComponents(composite) 

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

145 components.update(infoComps) 

146 return components 

147 

148 def disassemble( 

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

150 ) -> Dict[str, DatasetComponent]: 

151 """Disassemble an afw Exposure. 

152 

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. 

156 

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. 

166 

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. 

173 

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

181 

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 ) 

200 

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

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

203 expItems, expInfoItems = self._groupRequestedComponents() 

204 

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) 

208 

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) 

212 

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

219 

220 return components 

221 

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

223 """Construct an Exposure from components. 

224 

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. 

232 

233 Returns 

234 ------- 

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

236 Assembled exposure. 

237 

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 

254 

255 wcs = None 

256 if "wcs" in components: 

257 wcs = components.pop("wcs") 

258 

259 pytype = self.storageClass.pytype 

260 if hasMaskedImage: 

261 maskedImage = makeMaskedImage(**maskedImageComponents) 

262 exposure = makeExposure(maskedImage, wcs=wcs) 

263 

264 if not isinstance(exposure, pytype): 

265 raise RuntimeError( 

266 "Unexpected type created in assembly;" 

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

268 ) 

269 

270 else: 

271 exposure = pytype() 

272 if wcs is not None: 

273 exposure.setWcs(wcs) 

274 

275 # Set other components 

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

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

278 

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

294 

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

296 

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 ) 

302 

303 return exposure 

304 

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. 

308 

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. 

318 

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) 

332 

333 return inMemoryDataset 

334 

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