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

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 

29 

30from lsst.daf.butler import 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(("apCorrMap", "coaddInputs", "photoCalib", "metadata", 

39 "filterLabel", "transmissionCurve", "visitInfo", 

40 "detector", "validPolygon", "summaryStats", "id")) 

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

42 

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

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

45 

46 def _groupRequestedComponents(self): 

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

48 

49 Returns 

50 ------- 

51 expComps : `set` [`str`] 

52 Components associated with the top level Exposure. 

53 expInfoComps : `set` [`str`] 

54 Components associated with the ExposureInfo 

55 

56 Raises 

57 ------ 

58 ValueError 

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

60 expected by this assembler. 

61 """ 

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

63 

64 # Check that we are requesting something that we support 

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

66 if unknown: 

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

68 

69 expItems = requested & self.EXPOSURE_COMPONENTS 

70 expInfoItems = requested & self.EXPOSURE_INFO_COMPONENTS 

71 return expItems, expInfoItems 

72 

73 def getComponent(self, composite, componentName): 

74 """Get a component from an Exposure 

75 

76 Parameters 

77 ---------- 

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

79 `Exposure` to access component. 

80 componentName : `str` 

81 Name of component to retrieve. 

82 

83 Returns 

84 ------- 

85 component : `object` 

86 The component. Can be None. 

87 

88 Raises 

89 ------ 

90 AttributeError 

91 The component can not be found. 

92 """ 

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

94 # Use getter translation if relevant or the name itself 

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

96 elif componentName in self.EXPOSURE_INFO_COMPONENTS: 

97 if hasattr(composite, "getInfo"): 

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

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

100 # the ExposureInfo if the method is supported 

101 composite = composite.getInfo() 

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

103 else: 

104 raise AttributeError("Do not know how to retrieve component {} from {}".format(componentName, 

105 type(composite))) 

106 

107 def getValidComponents(self, composite): 

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

109 

110 Parameters 

111 ---------- 

112 composite : `object` 

113 Composite from which to extract components. 

114 

115 Returns 

116 ------- 

117 comps : `dict` 

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

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

120 """ 

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

122 # components, and again for ExposureInfo. 

123 expItems, expInfoItems = self._groupRequestedComponents() 

124 

125 components = super().getValidComponents(composite) 

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

127 components.update(infoComps) 

128 return components 

129 

130 def disassemble(self, composite): 

131 """Disassemble an afw Exposure. 

132 

133 This implementation attempts to extract components from the parent 

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

135 from the component name. 

136 

137 Parameters 

138 ---------- 

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

140 `Exposure` composite object consisting of components to be 

141 extracted. 

142 

143 Returns 

144 ------- 

145 components : `dict` 

146 `dict` with keys matching the components defined in 

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

148 describing the component. 

149 

150 Raises 

151 ------ 

152 ValueError 

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

154 lookups. 

155 TypeError 

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

157 

158 Notes 

159 ----- 

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

161 included in the returned components. 

162 """ 

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

164 raise TypeError("Unexpected type mismatch between parent and StorageClass" 

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

166 

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

168 components = {} 

169 expItems, expInfoItems = self._groupRequestedComponents() 

170 

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

172 components.update(fromExposure) 

173 

174 fromExposureInfo = super().disassemble(composite, 

175 subset=expInfoItems, override=composite.getInfo()) 

176 components.update(fromExposureInfo) 

177 

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

179 log.warning("PSF of type %s is not persistable and has been ignored.", 

180 type(components["psf"].component).__name__) 

181 del components["psf"] 

182 

183 return components 

184 

185 def assemble(self, components): 

186 """Construct an Exposure from components. 

187 

188 Parameters 

189 ---------- 

190 components : `dict` 

191 All the components from which to construct the Exposure. 

192 Some can be missing. 

193 

194 Returns 

195 ------- 

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

197 Assembled exposure. 

198 

199 Raises 

200 ------ 

201 ValueError 

202 Some supplied components are not recognized. 

203 """ 

204 components = components.copy() 

205 maskedImageComponents = {} 

206 hasMaskedImage = False 

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

208 value = None 

209 if component in components: 

210 hasMaskedImage = True 

211 value = components.pop(component) 

212 maskedImageComponents[component] = value 

213 

214 wcs = None 

215 if "wcs" in components: 

216 wcs = components.pop("wcs") 

217 

218 pytype = self.storageClass.pytype 

219 if hasMaskedImage: 

220 maskedImage = makeMaskedImage(**maskedImageComponents) 

221 exposure = makeExposure(maskedImage, wcs=wcs) 

222 

223 if not isinstance(exposure, pytype): 

224 raise RuntimeError("Unexpected type created in assembly;" 

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

226 

227 else: 

228 exposure = pytype() 

229 if wcs is not None: 

230 exposure.setWcs(wcs) 

231 

232 # Set other components 

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

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

235 

236 info = exposure.getInfo() 

237 if "visitInfo" in components: 

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

239 # Override ID set in visitInfo, if necessary 

240 if "id" in components: 

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

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

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

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

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

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

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

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

249 

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

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

252 

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

254 if components: 

255 raise ValueError("The following components were not understood:" 

256 " {}".format(list(components.keys()))) 

257 

258 return exposure 

259 

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

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

262 returning a possibly new object. 

263 

264 Parameters 

265 ---------- 

266 inMemoryDataset : `object` 

267 Object to modify based on the parameters. 

268 parameters : `dict`, optional 

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

270 Supported parameters are defined in the associated 

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

272 inMemoryDataset will be return unchanged. 

273 

274 Returns 

275 ------- 

276 inMemoryDataset : `object` 

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

278 have been used. 

279 """ 

280 # Understood by *this* subset command 

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

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

283 if use: 

284 inMemoryDataset = inMemoryDataset.subset(**use) 

285 

286 return inMemoryDataset 

287 

288 @classmethod 

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

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

291 forwarderMap = { 

292 "bbox": imageComponents, 

293 "dimensions": imageComponents, 

294 "xy0": imageComponents, 

295 "filter": ["filterLabel"], 

296 "id": ["metadata"], 

297 } 

298 forwarder = forwarderMap.get(readComponent) 

299 if forwarder is not None: 

300 for c in forwarder: 

301 if c in fromComponents: 

302 return c 

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