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 ip_isr. 

2# 

3# Developed for the LSST Data Management System. 

4# This product includes software developed by the LSST Project 

5# (https://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 <https://www.gnu.org/licenses/>. 

21# 

22"""Brighter Fatter Kernel calibration definition.""" 

23 

24 

25__all__ = ['BrighterFatterKernel'] 

26 

27 

28import numpy as np 

29from astropy.table import Table 

30import lsst.afw.math as afwMath 

31from . import IsrCalib 

32 

33 

34class BrighterFatterKernel(IsrCalib): 

35 """Calibration of brighter-fatter kernels for an instrument. 

36 

37 ampKernels are the kernels for each amplifier in a detector, as 

38 generated by having level == 'AMP' 

39 

40 detectorKernel is the kernel generated for a detector as a 

41 whole, as generated by having level == 'DETECTOR' 

42 

43 makeDetectorKernelFromAmpwiseKernels is a method to generate the 

44 kernel for a detector, constructed by averaging together the 

45 ampwise kernels in the detector. The existing application code is 

46 only defined for kernels with level == 'DETECTOR', so this method 

47 is used if the supplied kernel was built with level == 'AMP'. 

48 

49 Parameters 

50 ---------- 

51 level : `str` 

52 Level the kernels will be generated for. 

53 log : `lsst.log.Log`, optional 

54 Log to write messages to. 

55 **kwargs : 

56 Parameters to pass to parent constructor. 

57 

58 """ 

59 _OBSTYPE = 'BFK' 

60 _SCHEMA = 'Brighter-fatter kernel' 

61 _VERSION = 1.0 

62 

63 def __init__(self, camera=None, level=None, **kwargs): 

64 self.level = level 

65 

66 # Things inherited from the PTC 

67 self.means = dict() 

68 self.variances = dict() 

69 self.rawXcorrs = dict() 

70 self.badAmps = list() 

71 self.shape = (17, 17) 

72 self.gain = dict() 

73 self.noise = dict() 

74 

75 # Things calculated from the PTC 

76 self.meanXcorrs = dict() 

77 self.valid = dict() 

78 

79 # Things that are used downstream 

80 self.ampKernels = dict() 

81 self.detKernels = dict() 

82 

83 if camera: 

84 self.initFromCamera(camera, detectorId=kwargs.get('detectorId', None)) 

85 

86 super().__init__(**kwargs) 

87 self.requiredAttributes.update(['level', 'means', 'variances', 'rawXcorrs', 

88 'badAmps', 'gain', 'noise', 'meanXcorrs', 'valid', 

89 'ampKernels', 'detKernels']) 

90 

91 def updateMetadata(self, setDate=False, **kwargs): 

92 """Update calibration metadata. 

93 

94 This calls the base class's method after ensuring the required 

95 calibration keywords will be saved. 

96 

97 Parameters 

98 ---------- 

99 setDate : `bool`, optional 

100 Update the CALIBDATE fields in the metadata to the current 

101 time. Defaults to False. 

102 kwargs : 

103 Other keyword parameters to set in the metadata. 

104 """ 

105 kwargs['LEVEL'] = self.level 

106 kwargs['KERNEL_DX'] = self.shape[0] 

107 kwargs['KERNEL_DY'] = self.shape[1] 

108 

109 super().updateMetadata(setDate=setDate, **kwargs) 

110 

111 def initFromCamera(self, camera, detectorId=None): 

112 """Initialize kernel structure from camera. 

113 

114 Parameters 

115 ---------- 

116 camera : `lsst.afw.cameraGeom.Camera` 

117 Camera to use to define geometry. 

118 detectorId : `int`, optional 

119 Index of the detector to generate. 

120 

121 Returns 

122 ------- 

123 calib : `lsst.ip.isr.BrighterFatterKernel` 

124 The initialized calibration. 

125 

126 Raises 

127 ------ 

128 RuntimeError : 

129 Raised if no detectorId is supplied for a calibration with 

130 level='AMP'. 

131 """ 

132 self._instrument = camera.getName() 

133 

134 if self.level == 'AMP': 

135 if detectorId is None: 

136 raise RuntimeError("A detectorId must be supplied if level='AMP'.") 

137 

138 detector = camera[detectorId] 

139 self._detectorId = detectorId 

140 self._detectorName = detector.getName() 

141 self._detectorSerial = detector.getSerial() 

142 self.badAmps = [] 

143 

144 for amp in detector: 

145 ampName = amp.getName() 

146 self.means[ampName] = [] 

147 self.variances[ampName] = [] 

148 self.rawXcorrs[ampName] = [] 

149 self.gain[ampName] = amp.getGain() 

150 self.noise[ampName] = amp.getReadNoise() 

151 self.meanXcorrs[ampName] = [] 

152 self.ampKernels[ampName] = [] 

153 self.valid[ampName] = [] 

154 elif self.level == 'DETECTOR': 

155 for det in camera: 

156 detName = det.getName() 

157 self.detKernels[detName] = [] 

158 

159 return self 

160 

161 def getLengths(self): 

162 """Return the set of lengths needed for reshaping components. 

163 

164 Returns 

165 ------- 

166 kernelLength : `int` 

167 Product of the elements of self.shape. 

168 smallLength : `int` 

169 Size of an untiled covariance. 

170 nObs : `int` 

171 Number of observation pairs used in the kernel. 

172 """ 

173 kernelLength = self.shape[0] * self.shape[1] 

174 smallLength = int((self.shape[0] - 1)*(self.shape[1] - 1)/4) 

175 nObservations = set([len(self.means[amp]) for amp in self.means]) 

176 if len(nObservations) != 1: 

177 raise RuntimeError("Inconsistent number of observations found.") 

178 nObs = nObservations.pop() 

179 

180 return (kernelLength, smallLength, nObs) 

181 

182 @classmethod 

183 def fromDict(cls, dictionary): 

184 """Construct a calibration from a dictionary of properties. 

185 

186 Parameters 

187 ---------- 

188 dictionary : `dict` 

189 Dictionary of properties. 

190 

191 Returns 

192 ------- 

193 calib : `lsst.ip.isr.BrighterFatterKernel 

194 Constructed calibration. 

195 

196 Raises 

197 ------ 

198 RuntimeError : 

199 Raised if the supplied dictionary is for a different 

200 calibration. 

201 """ 

202 calib = cls() 

203 

204 if calib._OBSTYPE != (found := dictionary['metadata']['OBSTYPE']): 

205 raise RuntimeError(f"Incorrect brighter-fatter kernel supplied. Expected {calib._OBSTYPE}, " 

206 f"found {found}") 

207 

208 calib.setMetadata(dictionary['metadata']) 

209 calib.calibInfoFromDict(dictionary) 

210 

211 calib.level = dictionary['metadata'].get('LEVEL', 'AMP') 

212 calib.shape = (dictionary['metadata'].get('KERNEL_DX', 0), 

213 dictionary['metadata'].get('KERNEL_DY', 0)) 

214 

215 calib.means = {amp: np.array(dictionary['means'][amp]) for amp in dictionary['means']} 

216 calib.variances = {amp: np.array(dictionary['variances'][amp]) for amp in dictionary['variances']} 

217 

218 # Lengths for reshape: 

219 _, smallLength, nObs = calib.getLengths() 

220 smallShapeSide = int(np.sqrt(smallLength)) 

221 

222 calib.rawXcorrs = {amp: np.array(dictionary['rawXcorrs'][amp]).reshape((nObs, 

223 smallShapeSide, 

224 smallShapeSide)) 

225 for amp in dictionary['rawXcorrs']} 

226 

227 calib.gain = dictionary['gain'] 

228 calib.noise = dictionary['noise'] 

229 

230 calib.meanXcorrs = {amp: np.array(dictionary['meanXcorrs'][amp]).reshape(calib.shape) 

231 for amp in dictionary['rawXcorrs']} 

232 calib.ampKernels = {amp: np.array(dictionary['ampKernels'][amp]).reshape(calib.shape) 

233 for amp in dictionary['ampKernels']} 

234 calib.valid = {amp: bool(value) for amp, value in dictionary['valid'].items()} 

235 calib.badAmps = [amp for amp, valid in dictionary['valid'].items() if valid is False] 

236 

237 calib.detKernels = {det: np.array(dictionary['detKernels'][det]).reshape(calib.shape) 

238 for det in dictionary['detKernels']} 

239 

240 calib.updateMetadata() 

241 return calib 

242 

243 def toDict(self): 

244 """Return a dictionary containing the calibration properties. 

245 

246 The dictionary should be able to be round-tripped through 

247 `fromDict`. 

248 

249 Returns 

250 ------- 

251 dictionary : `dict` 

252 Dictionary of properties. 

253 """ 

254 self.updateMetadata() 

255 

256 outDict = {} 

257 metadata = self.getMetadata() 

258 outDict['metadata'] = metadata 

259 

260 # Lengths for ravel: 

261 kernelLength, smallLength, nObs = self.getLengths() 

262 

263 outDict['means'] = {amp: np.array(self.means[amp]).tolist() for amp in self.means} 

264 outDict['variances'] = {amp: np.array(self.variances[amp]).tolist() for amp in self.variances} 

265 outDict['rawXcorrs'] = {amp: np.array(self.rawXcorrs[amp]).reshape(nObs*smallLength).tolist() 

266 for amp in self.rawXcorrs} 

267 outDict['badAmps'] = self.badAmps 

268 outDict['gain'] = self.gain 

269 outDict['noise'] = self.noise 

270 

271 outDict['meanXcorrs'] = {amp: self.meanXcorrs[amp].reshape(kernelLength).tolist() 

272 for amp in self.meanXcorrs} 

273 outDict['ampKernels'] = {amp: self.ampKernels[amp].reshape(kernelLength).tolist() 

274 for amp in self.ampKernels} 

275 outDict['valid'] = self.valid 

276 outDict['detKernels'] = {det: self.detKernels[det].reshape(kernelLength).tolist() 

277 for det in self.detKernels} 

278 return outDict 

279 

280 @classmethod 

281 def fromTable(cls, tableList): 

282 """Construct calibration from a list of tables. 

283 

284 This method uses the `fromDict` method to create the 

285 calibration, after constructing an appropriate dictionary from 

286 the input tables. 

287 

288 Parameters 

289 ---------- 

290 tableList : `list` [`astropy.table.Table`] 

291 List of tables to use to construct the brighter-fatter 

292 calibration. 

293 

294 Returns 

295 ------- 

296 calib : `lsst.ip.isr.BrighterFatterKernel` 

297 The calibration defined in the tables. 

298 """ 

299 ampTable = tableList[0] 

300 

301 metadata = ampTable.meta 

302 inDict = dict() 

303 inDict['metadata'] = metadata 

304 

305 amps = ampTable['AMPLIFIER'] 

306 

307 meanList = ampTable['MEANS'] 

308 varianceList = ampTable['VARIANCES'] 

309 

310 rawXcorrs = ampTable['RAW_XCORRS'] 

311 gainList = ampTable['GAIN'] 

312 noiseList = ampTable['NOISE'] 

313 

314 meanXcorrs = ampTable['MEAN_XCORRS'] 

315 ampKernels = ampTable['KERNEL'] 

316 validList = ampTable['VALID'] 

317 

318 inDict['means'] = {amp: mean for amp, mean in zip(amps, meanList)} 

319 inDict['variances'] = {amp: var for amp, var in zip(amps, varianceList)} 

320 inDict['rawXcorrs'] = {amp: kernel for amp, kernel in zip(amps, rawXcorrs)} 

321 inDict['gain'] = {amp: gain for amp, gain in zip(amps, gainList)} 

322 inDict['noise'] = {amp: noise for amp, noise in zip(amps, noiseList)} 

323 inDict['meanXcorrs'] = {amp: kernel for amp, kernel in zip(amps, meanXcorrs)} 

324 inDict['ampKernels'] = {amp: kernel for amp, kernel in zip(amps, ampKernels)} 

325 inDict['valid'] = {amp: bool(valid) for amp, valid in zip(amps, validList)} 

326 

327 inDict['badAmps'] = [amp for amp, valid in inDict['valid'].items() if valid is False] 

328 

329 if len(tableList) > 1: 

330 detTable = tableList[1] 

331 inDict['detKernels'] = {det: kernel for det, kernel 

332 in zip(detTable['DETECTOR'], detTable['KERNEL'])} 

333 else: 

334 inDict['detKernels'] = {} 

335 

336 return cls.fromDict(inDict) 

337 

338 def toTable(self): 

339 """Construct a list of tables containing the information in this calibration. 

340 

341 The list of tables should create an identical calibration 

342 after being passed to this class's fromTable method. 

343 

344 Returns 

345 ------- 

346 tableList : `list` [`lsst.afw.table.Table`] 

347 List of tables containing the crosstalk calibration 

348 information. 

349 

350 """ 

351 tableList = [] 

352 self.updateMetadata() 

353 

354 # Lengths 

355 kernelLength, smallLength, nObs = self.getLengths() 

356 

357 ampList = [] 

358 meanList = [] 

359 varianceList = [] 

360 rawXcorrs = [] 

361 gainList = [] 

362 noiseList = [] 

363 

364 meanXcorrsList = [] 

365 kernelList = [] 

366 validList = [] 

367 

368 for amp in self.means.keys(): 

369 ampList.append(amp) 

370 meanList.append(self.means[amp]) 

371 varianceList.append(self.variances[amp]) 

372 rawXcorrs.append(np.array(self.rawXcorrs[amp]).reshape(nObs*smallLength).tolist()) 

373 gainList.append(self.gain[amp]) 

374 noiseList.append(self.noise[amp]) 

375 

376 meanXcorrsList.append(self.meanXcorrs[amp].reshape(kernelLength).tolist()) 

377 kernelList.append(self.ampKernels[amp].reshape(kernelLength).tolist()) 

378 validList.append(int(self.valid[amp] and not (amp in self.badAmps))) 

379 

380 ampTable = Table({'AMPLIFIER': ampList, 

381 'MEANS': meanList, 

382 'VARIANCES': varianceList, 

383 'RAW_XCORRS': rawXcorrs, 

384 'GAIN': gainList, 

385 'NOISE': noiseList, 

386 'MEAN_XCORRS': meanXcorrsList, 

387 'KERNEL': kernelList, 

388 'VALID': validList, 

389 }) 

390 

391 ampTable.meta = self.getMetadata().toDict() 

392 tableList.append(ampTable) 

393 

394 if len(self.detKernels): 

395 detList = [] 

396 kernelList = [] 

397 for det in self.detKernels.keys(): 

398 detList.append(det) 

399 kernelList.append(self.detKernels[det].reshape(kernelLength).tolist()) 

400 

401 detTable = Table({'DETECTOR': detList, 

402 'KERNEL': kernelList}) 

403 detTable.meta = self.getMetadata().toDict() 

404 tableList.append(detTable) 

405 

406 return tableList 

407 

408 # Implementation methods 

409 def makeDetectorKernelFromAmpwiseKernels(self, detectorName, ampsToExclude=[]): 

410 """Average the amplifier level kernels to create a detector level kernel. 

411 """ 

412 inKernels = np.array([self.ampKernels[amp] for amp in 

413 self.ampKernels if amp not in ampsToExclude]) 

414 averagingList = np.transpose(inKernels) 

415 avgKernel = np.zeros_like(inKernels[0]) 

416 sctrl = afwMath.StatisticsControl() 

417 sctrl.setNumSigmaClip(5.0) 

418 for i in range(np.shape(avgKernel)[0]): 

419 for j in range(np.shape(avgKernel)[1]): 

420 avgKernel[i, j] = afwMath.makeStatistics(averagingList[i, j], 

421 afwMath.MEANCLIP, sctrl).getValue() 

422 

423 self.detKernels[detectorName] = avgKernel 

424 

425 def replaceDetectorKernelWithAmpKernel(self, ampName, detectorName): 

426 self.detKernel[detectorName] = self.ampKernel[ampName]