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

Shortcuts on this page

r m x p   toggle line displays

j k   next/prev highlighted chunk

0   (zero) top of page

1   (one) first highlighted chunk

102 statements  

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 

25 

26# Need to enable PSFs to be instantiated 

27import lsst.afw.detection # noqa: F401 

28from lsst.afw.image import makeExposure, makeMaskedImage 

29from lsst.daf.butler import StorageClassDelegate 

30 

31log = logging.getLogger(__name__) 

32 

33 

34class ExposureAssembler(StorageClassDelegate): 

35 

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

37 EXPOSURE_INFO_COMPONENTS = set( 

38 ( 

39 "apCorrMap", 

40 "coaddInputs", 

41 "photoCalib", 

42 "metadata", 

43 "filterLabel", 

44 "transmissionCurve", 

45 "visitInfo", 

46 "detector", 

47 "validPolygon", 

48 "summaryStats", 

49 "id", 

50 ) 

51 ) 

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

53 

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

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

56 

57 def _groupRequestedComponents(self): 

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

59 

60 Returns 

61 ------- 

62 expComps : `set` [`str`] 

63 Components associated with the top level Exposure. 

64 expInfoComps : `set` [`str`] 

65 Components associated with the ExposureInfo 

66 

67 Raises 

68 ------ 

69 ValueError 

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

71 expected by this assembler. 

72 """ 

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

74 

75 # Check that we are requesting something that we support 

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

77 if unknown: 

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

79 

80 expItems = requested & self.EXPOSURE_COMPONENTS 

81 expInfoItems = requested & self.EXPOSURE_INFO_COMPONENTS 

82 return expItems, expInfoItems 

83 

84 def getComponent(self, composite, componentName): 

85 """Get a component from an Exposure 

86 

87 Parameters 

88 ---------- 

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

90 `Exposure` to access component. 

91 componentName : `str` 

92 Name of component to retrieve. 

93 

94 Returns 

95 ------- 

96 component : `object` 

97 The component. Can be None. 

98 

99 Raises 

100 ------ 

101 AttributeError 

102 The component can not be found. 

103 """ 

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

105 # Use getter translation if relevant or the name itself 

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

107 elif componentName in self.EXPOSURE_INFO_COMPONENTS: 

108 if hasattr(composite, "getInfo"): 

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

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

111 # the ExposureInfo if the method is supported 

112 composite = composite.getInfo() 

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

114 else: 

115 raise AttributeError( 

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

117 ) 

118 

119 def getValidComponents(self, composite): 

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

121 

122 Parameters 

123 ---------- 

124 composite : `object` 

125 Composite from which to extract components. 

126 

127 Returns 

128 ------- 

129 comps : `dict` 

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

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

132 """ 

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

134 # components, and again for ExposureInfo. 

135 expItems, expInfoItems = self._groupRequestedComponents() 

136 

137 components = super().getValidComponents(composite) 

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

139 components.update(infoComps) 

140 return components 

141 

142 def disassemble(self, composite): 

143 """Disassemble an afw Exposure. 

144 

145 This implementation attempts to extract components from the parent 

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

147 from the component name. 

148 

149 Parameters 

150 ---------- 

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

152 `Exposure` composite object consisting of components to be 

153 extracted. 

154 

155 Returns 

156 ------- 

157 components : `dict` 

158 `dict` with keys matching the components defined in 

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

160 describing the component. 

161 

162 Raises 

163 ------ 

164 ValueError 

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

166 lookups. 

167 TypeError 

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

169 

170 Notes 

171 ----- 

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

173 included in the returned components. 

174 """ 

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

176 raise TypeError( 

177 "Unexpected type mismatch between parent and StorageClass" 

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

179 ) 

180 

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

182 components = {} 

183 expItems, expInfoItems = self._groupRequestedComponents() 

184 

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

186 components.update(fromExposure) 

187 

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

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

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 

209 Returns 

210 ------- 

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

212 Assembled exposure. 

213 

214 Raises 

215 ------ 

216 ValueError 

217 Some supplied components are not recognized. 

218 """ 

219 components = components.copy() 

220 maskedImageComponents = {} 

221 hasMaskedImage = False 

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

223 value = None 

224 if component in components: 

225 hasMaskedImage = True 

226 value = components.pop(component) 

227 maskedImageComponents[component] = value 

228 

229 wcs = None 

230 if "wcs" in components: 

231 wcs = components.pop("wcs") 

232 

233 pytype = self.storageClass.pytype 

234 if hasMaskedImage: 

235 maskedImage = makeMaskedImage(**maskedImageComponents) 

236 exposure = makeExposure(maskedImage, wcs=wcs) 

237 

238 if not isinstance(exposure, pytype): 

239 raise RuntimeError( 

240 "Unexpected type created in assembly;" 

241 " was {} expected {}".format(type(exposure), 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 # TODO: switch back to "filter" as primary component in DM-27177 

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

271 

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

273 if components: 

274 raise ValueError( 

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

276 ) 

277 

278 return exposure 

279 

280 def handleParameters(self, inMemoryDataset, parameters=None): 

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

282 returning a possibly new object. 

283 

284 Parameters 

285 ---------- 

286 inMemoryDataset : `object` 

287 Object to modify based on the parameters. 

288 parameters : `dict`, optional 

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

290 Supported parameters are defined in the associated 

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

292 inMemoryDataset will be return unchanged. 

293 

294 Returns 

295 ------- 

296 inMemoryDataset : `object` 

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

298 have been used. 

299 """ 

300 # Understood by *this* subset command 

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

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

303 if use: 

304 inMemoryDataset = inMemoryDataset.subset(**use) 

305 

306 return inMemoryDataset 

307 

308 @classmethod 

309 def selectResponsibleComponent(cls, readComponent: str, fromComponents) -> str: 

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

311 forwarderMap = { 

312 "bbox": imageComponents, 

313 "dimensions": imageComponents, 

314 "xy0": imageComponents, 

315 "filter": ["filterLabel"], 

316 } 

317 forwarder = forwarderMap.get(readComponent) 

318 if forwarder is not None: 

319 for c in forwarder: 

320 if c in fromComponents: 

321 return c 

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