Hide keyboard shortcuts

Hot-keys 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

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 

24# Need to enable PSFs to be instantiated 

25import lsst.afw.detection # noqa: F401 

26from lsst.afw.image import makeExposure, makeMaskedImage, Filter, stripFilterKeywords 

27 

28from lsst.daf.butler import StorageClassDelegate 

29 

30 

31class ExposureAssembler(StorageClassDelegate): 

32 

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

34 EXPOSURE_INFO_COMPONENTS = set(("apCorrMap", "coaddInputs", "photoCalib", "metadata", 

35 "filter", "transmissionCurve", "visitInfo", 

36 "detector", "validPolygon")) 

37 EXPOSURE_READ_COMPONENTS = {"bbox", "dimensions", "xy0"} 

38 

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

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

41 

42 def _groupRequestedComponents(self): 

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

44 

45 Returns 

46 ------- 

47 expComps : `dict` 

48 Components associated with the top level Exposure. 

49 expInfoComps : `dict` 

50 Components associated with the ExposureInfo 

51 

52 Raises 

53 ------ 

54 ValueError 

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

56 expected by this assembler. 

57 """ 

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

59 

60 # Check that we are requesting something that we support 

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

62 if unknown: 

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

64 

65 expItems = requested & self.EXPOSURE_COMPONENTS 

66 expInfoItems = requested & self.EXPOSURE_INFO_COMPONENTS 

67 return expItems, expInfoItems 

68 

69 def getComponent(self, composite, componentName): 

70 """Get a component from an Exposure 

71 

72 Parameters 

73 ---------- 

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

75 `Exposure` to access component. 

76 componentName : `str` 

77 Name of component to retrieve. 

78 

79 Returns 

80 ------- 

81 component : `object` 

82 The component. Can be None. 

83 

84 Raises 

85 ------ 

86 AttributeError 

87 The component can not be found. 

88 """ 

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

90 # Use getter translation if relevant or the name itself 

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

92 elif componentName in self.EXPOSURE_INFO_COMPONENTS: 

93 if hasattr(composite, "getInfo"): 

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

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

96 # the ExposureInfo if the method is supported 

97 composite = composite.getInfo() 

98 return super().getComponent(composite, componentName) 

99 else: 

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

101 type(composite))) 

102 

103 def getValidComponents(self, composite): 

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

105 

106 Parameters 

107 ---------- 

108 composite : `object` 

109 Composite from which to extract components. 

110 

111 Returns 

112 ------- 

113 comps : `dict` 

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

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

116 """ 

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

118 # components, and again for ExposureInfo. 

119 expItems, expInfoItems = self._groupRequestedComponents() 

120 

121 components = super().getValidComponents(composite) 

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

123 components.update(infoComps) 

124 return components 

125 

126 def disassemble(self, composite): 

127 """Disassemble an afw Exposure. 

128 

129 This implementation attempts to extract components from the parent 

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

131 from the component name. 

132 

133 Parameters 

134 ---------- 

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

136 `Exposure` composite object consisting of components to be 

137 extracted. 

138 

139 Returns 

140 ------- 

141 components : `dict` 

142 `dict` with keys matching the components defined in 

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

144 describing the component. 

145 

146 Raises 

147 ------ 

148 ValueError 

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

150 lookups. 

151 TypeError 

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

153 """ 

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

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

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

157 

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

159 components = {} 

160 expItems, expInfoItems = self._groupRequestedComponents() 

161 

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

163 components.update(fromExposure) 

164 

165 fromExposureInfo = super().disassemble(composite, 

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

167 components.update(fromExposureInfo) 

168 

169 # We must reproduce some of the metadata manipulation that occurs 

170 # in ExposureInfo::FitsWriteData. 

171 

172 # Force the FILTER header to be overwritten 

173 if "filter" in components and "metadata" in components: 

174 md = components["metadata"].component 

175 md["FILTER"] = components["filter"].component.getName() 

176 

177 return components 

178 

179 def assemble(self, components): 

180 """Construct an Exposure from components. 

181 

182 Parameters 

183 ---------- 

184 components : `dict` 

185 All the components from which to construct the Exposure. 

186 Some can be missing. 

187 

188 Returns 

189 ------- 

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

191 Assembled exposure. 

192 

193 Raises 

194 ------ 

195 ValueError 

196 Some supplied components are not recognized. 

197 """ 

198 components = components.copy() 

199 maskedImageComponents = {} 

200 hasMaskedImage = False 

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

202 value = None 

203 if component in components: 

204 hasMaskedImage = True 

205 value = components.pop(component) 

206 maskedImageComponents[component] = value 

207 

208 wcs = None 

209 if "wcs" in components: 

210 wcs = components.pop("wcs") 

211 

212 pytype = self.storageClass.pytype 

213 if hasMaskedImage: 

214 maskedImage = makeMaskedImage(**maskedImageComponents) 

215 exposure = makeExposure(maskedImage, wcs=wcs) 

216 

217 if not isinstance(exposure, pytype): 

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

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

220 

221 else: 

222 exposure = pytype() 

223 if wcs is not None: 

224 exposure.setWcs(wcs) 

225 

226 # Set other components 

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

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

229 

230 info = exposure.getInfo() 

231 if "visitInfo" in components: 

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

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

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

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

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

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

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

239 

240 # Filter needs to be updated specially to match Exposure behavior 

241 # from ExposureFitsReader::MetadataReader 

242 

243 # Override the serialized filter knowing that we are using FILTER 

244 md = info.getMetadata() 

245 if "filter" in components and "FILTER" in md: 

246 filter = Filter(md, True) 

247 stripFilterKeywords(md) 

248 if filter.getName() != components["filter"].getName(): 

249 components["filter"] = filter 

250 

251 info.setFilter(components.pop("filter", 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 } 

296 forwarder = forwarderMap.get(readComponent) 

297 if forwarder is not None: 

298 for c in forwarder: 

299 if c in fromComponents: 

300 return c 

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