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

109 statements  

« prev     ^ index     » next       coverage.py v7.4.4, created at 2024-04-04 10:09 +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 collections.abc import Iterable, Mapping 

26from typing import Any 

27 

28# Need to enable PSFs to be instantiated 

29import lsst.afw.detection 

30from lsst.afw.image import Exposure, makeExposure, makeMaskedImage 

31from lsst.daf.butler import DatasetComponent, StorageClassDelegate 

32 

33log = logging.getLogger(__name__) 

34 

35 

36class ExposureAssembler(StorageClassDelegate): 

37 """Knowledge of how to assemble and disassemble an 

38 `~lsst.afw.image.Exposure`. 

39 """ 

40 

41 EXPOSURE_COMPONENTS = {"image", "variance", "mask", "wcs", "psf"} 

42 EXPOSURE_INFO_COMPONENTS = { 

43 "apCorrMap", 

44 "coaddInputs", 

45 "photoCalib", 

46 "metadata", 

47 "filter", 

48 "transmissionCurve", 

49 "visitInfo", 

50 "detector", 

51 "validPolygon", 

52 "summaryStats", 

53 "id", 

54 } 

55 EXPOSURE_READ_COMPONENTS = { 

56 "bbox", 

57 "dimensions", 

58 "xy0", 

59 } 

60 

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

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

63 

64 def _groupRequestedComponents(self) -> tuple[set[str], set[str]]: 

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

66 

67 Returns 

68 ------- 

69 expComps : `set` [`str`] 

70 Components associated with the top level Exposure. 

71 expInfoComps : `set` [`str`] 

72 Components associated with the ExposureInfo 

73 

74 Raises 

75 ------ 

76 ValueError 

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

78 expected by this assembler. 

79 """ 

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

81 

82 # Check that we are requesting something that we support 

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

84 if unknown: 

85 raise ValueError(f"Asking for unrecognized component: {unknown}") 

86 

87 expItems = requested & self.EXPOSURE_COMPONENTS 

88 expInfoItems = requested & self.EXPOSURE_INFO_COMPONENTS 

89 return expItems, expInfoItems 

90 

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

92 """Get a component from an Exposure. 

93 

94 Parameters 

95 ---------- 

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

97 `Exposure` to access component. 

98 componentName : `str` 

99 Name of component to retrieve. 

100 

101 Returns 

102 ------- 

103 component : `object` 

104 The component. Can be None. 

105 

106 Raises 

107 ------ 

108 AttributeError 

109 The component can not be found. 

110 """ 

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

112 # Use getter translation if relevant or the name itself 

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

114 elif componentName in self.EXPOSURE_INFO_COMPONENTS: 

115 if hasattr(composite, "getInfo"): 

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

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

118 # the ExposureInfo if the method is supported 

119 composite = composite.getInfo() 

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

121 else: 

122 raise AttributeError( 

123 f"Do not know how to retrieve component {componentName} from {type(composite)}" 

124 ) 

125 

126 def disassemble( 

127 self, composite: Any, subset: Iterable | None = None, override: Any | None = None 

128 ) -> dict[str, DatasetComponent]: 

129 """Disassemble an afw Exposure. 

130 

131 This implementation attempts to extract components from the parent 

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

133 from the component name. 

134 

135 Parameters 

136 ---------- 

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

138 `Exposure` composite object consisting of components to be 

139 extracted. 

140 subset : iterable, optional 

141 Not supported by this assembler. 

142 override : `object`, optional 

143 Not supported by this assembler. 

144 

145 Returns 

146 ------- 

147 components : `dict` 

148 `dict` with keys matching the components defined in 

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

150 describing the component. 

151 

152 Raises 

153 ------ 

154 ValueError 

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

156 lookups. 

157 TypeError 

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

159 

160 Notes 

161 ----- 

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

163 included in the returned components. 

164 """ 

165 if subset is not None: 

166 raise NotImplementedError( 

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

168 ) 

169 if override is not None: 

170 raise NotImplementedError( 

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

172 ) 

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

174 raise TypeError( 

175 "Unexpected type mismatch between parent and StorageClass" 

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

177 ) 

178 

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

180 components: dict[str, DatasetComponent] = {} 

181 expItems, expInfoItems = self._groupRequestedComponents() 

182 

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

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

185 components.update(fromExposure) 

186 

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

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

189 components.update(fromExposureInfo) 

190 

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

192 log.warning( 

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

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

195 ) 

196 del components["psf"] 

197 

198 return components 

199 

200 def assemble(self, components: dict[str, Any], pytype: type | None = None) -> Exposure: 

201 """Construct an Exposure from components. 

202 

203 Parameters 

204 ---------- 

205 components : `dict` 

206 All the components from which to construct the Exposure. 

207 Some can be missing. 

208 pytype : `type`, optional 

209 Not supported by this assembler. 

210 

211 Returns 

212 ------- 

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

214 Assembled exposure. 

215 

216 Raises 

217 ------ 

218 ValueError 

219 Some supplied components are not recognized. 

220 """ 

221 if pytype is not None: 

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

223 components = components.copy() 

224 maskedImageComponents = {} 

225 hasMaskedImage = False 

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

227 value = None 

228 if component in components: 

229 hasMaskedImage = True 

230 value = components.pop(component) 

231 maskedImageComponents[component] = value 

232 

233 wcs = None 

234 if "wcs" in components: 

235 wcs = components.pop("wcs") 

236 

237 pytype = self.storageClass.pytype 

238 if hasMaskedImage: 

239 maskedImage = makeMaskedImage(**maskedImageComponents) 

240 exposure = makeExposure(maskedImage, wcs=wcs) 

241 

242 if not isinstance(exposure, pytype): 

243 raise RuntimeError( 

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

245 ) 

246 

247 else: 

248 exposure = pytype() 

249 if wcs is not None: 

250 exposure.setWcs(wcs) 

251 

252 # Set other components 

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

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

255 

256 info = exposure.getInfo() 

257 if "visitInfo" in components: 

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

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: Mapping[str, Any] | None = 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 `~lsst.daf.butler.StorageClass`. If no relevant parameters are 

289 specified the ``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[str | None]) -> 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}")