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

114 statements  

« prev     ^ index     » next       coverage.py v6.4, created at 2022-05-27 11:22 +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 

37 EXPOSURE_COMPONENTS = set(("image", "variance", "mask", "wcs", "psf")) 

38 EXPOSURE_INFO_COMPONENTS = set( 

39 ( 

40 "apCorrMap", 

41 "coaddInputs", 

42 "photoCalib", 

43 "metadata", 

44 "filterLabel", 

45 "transmissionCurve", 

46 "visitInfo", 

47 "detector", 

48 "validPolygon", 

49 "summaryStats", 

50 "id", 

51 ) 

52 ) 

53 EXPOSURE_READ_COMPONENTS = {"bbox", "dimensions", "xy0", "filter"} 

54 

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

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

57 

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

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

60 

61 Returns 

62 ------- 

63 expComps : `set` [`str`] 

64 Components associated with the top level Exposure. 

65 expInfoComps : `set` [`str`] 

66 Components associated with the ExposureInfo 

67 

68 Raises 

69 ------ 

70 ValueError 

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

72 expected by this assembler. 

73 """ 

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

75 

76 # Check that we are requesting something that we support 

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

78 if unknown: 

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

80 

81 expItems = requested & self.EXPOSURE_COMPONENTS 

82 expInfoItems = requested & self.EXPOSURE_INFO_COMPONENTS 

83 return expItems, expInfoItems 

84 

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

86 """Get a component from an Exposure 

87 

88 Parameters 

89 ---------- 

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

91 `Exposure` to access component. 

92 componentName : `str` 

93 Name of component to retrieve. 

94 

95 Returns 

96 ------- 

97 component : `object` 

98 The component. Can be None. 

99 

100 Raises 

101 ------ 

102 AttributeError 

103 The component can not be found. 

104 """ 

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

106 # Use getter translation if relevant or the name itself 

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

108 elif componentName in self.EXPOSURE_INFO_COMPONENTS: 

109 if hasattr(composite, "getInfo"): 

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

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

112 # the ExposureInfo if the method is supported 

113 composite = composite.getInfo() 

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

115 else: 

116 raise AttributeError( 

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

118 ) 

119 

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

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

122 

123 Parameters 

124 ---------- 

125 composite : `object` 

126 Composite from which to extract components. 

127 

128 Returns 

129 ------- 

130 comps : `dict` 

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

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

133 """ 

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

135 # components, and again for ExposureInfo. 

136 expItems, expInfoItems = self._groupRequestedComponents() 

137 

138 components = super().getValidComponents(composite) 

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

140 components.update(infoComps) 

141 return components 

142 

143 def disassemble( 

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

145 ) -> Dict[str, DatasetComponent]: 

146 """Disassemble an afw Exposure. 

147 

148 This implementation attempts to extract components from the parent 

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

150 from the component name. 

151 

152 Parameters 

153 ---------- 

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

155 `Exposure` composite object consisting of components to be 

156 extracted. 

157 subset : iterable, optional 

158 Not supported by this assembler. 

159 override : `object`, optional 

160 Not supported by this assembler. 

161 

162 Returns 

163 ------- 

164 components : `dict` 

165 `dict` with keys matching the components defined in 

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

167 describing the component. 

168 

169 Raises 

170 ------ 

171 ValueError 

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

173 lookups. 

174 TypeError 

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

176 

177 Notes 

178 ----- 

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

180 included in the returned components. 

181 """ 

182 if subset is not None: 

183 raise NotImplementedError( 

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

185 ) 

186 if override is not None: 

187 raise NotImplementedError( 

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

189 ) 

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

191 raise TypeError( 

192 "Unexpected type mismatch between parent and StorageClass" 

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

194 ) 

195 

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

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

198 expItems, expInfoItems = self._groupRequestedComponents() 

199 

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

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

202 components.update(fromExposure) 

203 

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

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

206 components.update(fromExposureInfo) 

207 

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

209 log.warning( 

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

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

212 ) 

213 del components["psf"] 

214 

215 return components 

216 

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

218 """Construct an Exposure from components. 

219 

220 Parameters 

221 ---------- 

222 components : `dict` 

223 All the components from which to construct the Exposure. 

224 Some can be missing. 

225 pytype : `type`, optional 

226 Not supported by this assembler. 

227 

228 Returns 

229 ------- 

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

231 Assembled exposure. 

232 

233 Raises 

234 ------ 

235 ValueError 

236 Some supplied components are not recognized. 

237 """ 

238 if pytype is not None: 

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

240 components = components.copy() 

241 maskedImageComponents = {} 

242 hasMaskedImage = False 

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

244 value = None 

245 if component in components: 

246 hasMaskedImage = True 

247 value = components.pop(component) 

248 maskedImageComponents[component] = value 

249 

250 wcs = None 

251 if "wcs" in components: 

252 wcs = components.pop("wcs") 

253 

254 pytype = self.storageClass.pytype 

255 if hasMaskedImage: 

256 maskedImage = makeMaskedImage(**maskedImageComponents) 

257 exposure = makeExposure(maskedImage, wcs=wcs) 

258 

259 if not isinstance(exposure, pytype): 

260 raise RuntimeError( 

261 "Unexpected type created in assembly;" 

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

263 ) 

264 

265 else: 

266 exposure = pytype() 

267 if wcs is not None: 

268 exposure.setWcs(wcs) 

269 

270 # Set other components 

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

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

273 

274 info = exposure.getInfo() 

275 if "visitInfo" in components: 

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

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

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

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

280 if "id" in components: 

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

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

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

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

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

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

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

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

289 

290 # TODO: switch back to "filter" as primary component in DM-27177 

291 info.setFilterLabel(components.pop("filterLabel", None)) 

292 

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

294 if components: 

295 raise ValueError( 

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

297 ) 

298 

299 return exposure 

300 

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

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

303 returning a possibly new object. 

304 

305 Parameters 

306 ---------- 

307 inMemoryDataset : `object` 

308 Object to modify based on the parameters. 

309 parameters : `dict`, optional 

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

311 Supported parameters are defined in the associated 

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

313 inMemoryDataset will be return unchanged. 

314 

315 Returns 

316 ------- 

317 inMemoryDataset : `object` 

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

319 have been used. 

320 """ 

321 if parameters is None: 

322 return inMemoryDataset 

323 # Understood by *this* subset command 

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

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

326 if use: 

327 inMemoryDataset = inMemoryDataset.subset(**use) 

328 

329 return inMemoryDataset 

330 

331 @classmethod 

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

333 # Docstring inherited. 

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

335 forwarderMap = { 

336 "bbox": imageComponents, 

337 "dimensions": imageComponents, 

338 "xy0": imageComponents, 

339 "filter": ["filterLabel"], 

340 } 

341 forwarder = forwarderMap.get(readComponent) 

342 if forwarder is not None: 

343 for c in forwarder: 

344 if c in fromComponents: 

345 return c 

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