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

108 statements  

« prev     ^ index     » next       coverage.py v7.2.7, created at 2023-06-22 09:59 +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 # Until DM-32138, "visitInfo" and "id" can both set the exposure ID. 

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

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

259 if "id" in components: 

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

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

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

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

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

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

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

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

268 

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

270 

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

272 if components: 

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

274 

275 return exposure 

276 

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

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

279 returning a possibly new object. 

280 

281 Parameters 

282 ---------- 

283 inMemoryDataset : `object` 

284 Object to modify based on the parameters. 

285 parameters : `dict`, optional 

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

287 Supported parameters are defined in the associated 

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

289 inMemoryDataset will be return unchanged. 

290 

291 Returns 

292 ------- 

293 inMemoryDataset : `object` 

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

295 have been used. 

296 """ 

297 if parameters is None: 

298 return inMemoryDataset 

299 # Understood by *this* subset command 

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

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

302 if use: 

303 inMemoryDataset = inMemoryDataset.subset(**use) 

304 

305 return inMemoryDataset 

306 

307 @classmethod 

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

309 # Docstring inherited. 

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

311 forwarderMap = { 

312 "bbox": imageComponents, 

313 "dimensions": imageComponents, 

314 "xy0": imageComponents, 

315 } 

316 forwarder = forwarderMap.get(readComponent) 

317 if forwarder is not None: 

318 for c in forwarder: 

319 if c in fromComponents: 

320 return c 

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