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 __eq__(self, other): 

92 """Calibration equivalence 

93 """ 

94 if not isinstance(other, self.__class__): 

95 return False 

96 

97 for attr in self._requiredAttributes: 

98 attrSelf = getattr(self, attr) 

99 attrOther = getattr(other, attr) 

100 if isinstance(attrSelf, dict) and isinstance(attrOther, dict): 

101 for ampName in attrSelf: 

102 if not np.allclose(attrSelf[ampName], attrOther[ampName], equal_nan=True): 

103 return False 

104 else: 

105 if attrSelf != attrOther: 

106 return False 

107 return True 

108 

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

110 """Update calibration metadata. 

111 

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

113 calibration keywords will be saved. 

114 

115 Parameters 

116 ---------- 

117 setDate : `bool`, optional 

118 Update the CALIBDATE fields in the metadata to the current 

119 time. Defaults to False. 

120 kwargs : 

121 Other keyword parameters to set in the metadata. 

122 """ 

123 kwargs['LEVEL'] = self.level 

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

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

126 

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

128 

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

130 """Initialize kernel structure from camera. 

131 

132 Parameters 

133 ---------- 

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

135 Camera to use to define geometry. 

136 detectorId : `int`, optional 

137 Index of the detector to generate. 

138 

139 Returns 

140 ------- 

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

142 The initialized calibration. 

143 

144 Raises 

145 ------ 

146 RuntimeError : 

147 Raised if no detectorId is supplied for a calibration with 

148 level='AMP'. 

149 """ 

150 self._instrument = camera.getName() 

151 

152 if self.level == 'AMP': 

153 if detectorId is None: 

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

155 

156 detector = camera[detectorId] 

157 self._detectorId = detectorId 

158 self._detectorName = detector.getName() 

159 self._detectorSerial = detector.getSerial() 

160 self.badAmps = [] 

161 

162 for amp in detector: 

163 ampName = amp.getName() 

164 self.means[ampName] = [] 

165 self.variances[ampName] = [] 

166 self.rawXcorrs[ampName] = [] 

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

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

169 self.meanXcorrs[ampName] = [] 

170 self.ampKernels[ampName] = [] 

171 self.valid[ampName] = [] 

172 elif self.level == 'DETECTOR': 

173 for det in camera: 

174 detName = det.getName() 

175 self.detKernels[detName] = [] 

176 

177 return self 

178 

179 def getLengths(self): 

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

181 

182 Returns 

183 ------- 

184 kernelLength : `int` 

185 Product of the elements of self.shape. 

186 smallLength : `int` 

187 Size of an untiled covariance. 

188 nObs : `int` 

189 Number of observation pairs used in the kernel. 

190 """ 

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

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

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

194 if len(nObservations) != 1: 

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

196 nObs = nObservations.pop() 

197 

198 return (kernelLength, smallLength, nObs) 

199 

200 @classmethod 

201 def fromDict(cls, dictionary): 

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

203 

204 Parameters 

205 ---------- 

206 dictionary : `dict` 

207 Dictionary of properties. 

208 

209 Returns 

210 ------- 

211 calib : `lsst.ip.isr.BrighterFatterKernel 

212 Constructed calibration. 

213 

214 Raises 

215 ------ 

216 RuntimeError : 

217 Raised if the supplied dictionary is for a different 

218 calibration. 

219 """ 

220 calib = cls() 

221 

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

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

224 f"found {found}") 

225 

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

227 calib.calibInfoFromDict(dictionary) 

228 

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

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

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

232 

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

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

235 

236 # Lengths for reshape: 

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

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

239 

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

241 smallShapeSide, 

242 smallShapeSide)) 

243 for amp in dictionary['rawXcorrs']} 

244 

245 calib.gain = dictionary['gain'] 

246 calib.noise = dictionary['noise'] 

247 

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

249 for amp in dictionary['rawXcorrs']} 

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

251 for amp in dictionary['ampKernels']} 

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

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

254 

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

256 for det in dictionary['detKernels']} 

257 

258 calib.updateMetadata() 

259 return calib 

260 

261 def toDict(self): 

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

263 

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

265 `fromDict`. 

266 

267 Returns 

268 ------- 

269 dictionary : `dict` 

270 Dictionary of properties. 

271 """ 

272 self.updateMetadata() 

273 

274 outDict = {} 

275 metadata = self.getMetadata() 

276 outDict['metadata'] = metadata 

277 

278 # Lengths for ravel: 

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

280 

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

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

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

284 for amp in self.rawXcorrs} 

285 outDict['badAmps'] = self.badAmps 

286 outDict['gain'] = self.gain 

287 outDict['noise'] = self.noise 

288 

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

290 for amp in self.meanXcorrs} 

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

292 for amp in self.ampKernels} 

293 outDict['valid'] = self.valid 

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

295 for det in self.detKernels} 

296 return outDict 

297 

298 @classmethod 

299 def fromTable(cls, tableList): 

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

301 

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

303 calibration, after constructing an appropriate dictionary from 

304 the input tables. 

305 

306 Parameters 

307 ---------- 

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

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

310 calibration. 

311 

312 Returns 

313 ------- 

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

315 The calibration defined in the tables. 

316 """ 

317 ampTable = tableList[0] 

318 

319 metadata = ampTable.meta 

320 inDict = dict() 

321 inDict['metadata'] = metadata 

322 

323 amps = ampTable['AMPLIFIER'] 

324 

325 meanList = ampTable['MEANS'] 

326 varianceList = ampTable['VARIANCES'] 

327 

328 rawXcorrs = ampTable['RAW_XCORRS'] 

329 gainList = ampTable['GAIN'] 

330 noiseList = ampTable['NOISE'] 

331 

332 meanXcorrs = ampTable['MEAN_XCORRS'] 

333 ampKernels = ampTable['KERNEL'] 

334 validList = ampTable['VALID'] 

335 

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

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

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

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

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

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

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

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

344 

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

346 

347 if len(tableList) > 1: 

348 detTable = tableList[1] 

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

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

351 else: 

352 inDict['detKernels'] = {} 

353 

354 return cls.fromDict(inDict) 

355 

356 def toTable(self): 

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

358 

359 The list of tables should create an identical calibration 

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

361 

362 Returns 

363 ------- 

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

365 List of tables containing the crosstalk calibration 

366 information. 

367 

368 """ 

369 tableList = [] 

370 self.updateMetadata() 

371 

372 # Lengths 

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

374 

375 ampList = [] 

376 meanList = [] 

377 varianceList = [] 

378 rawXcorrs = [] 

379 gainList = [] 

380 noiseList = [] 

381 

382 meanXcorrsList = [] 

383 kernelList = [] 

384 validList = [] 

385 

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

387 ampList.append(amp) 

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

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

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

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

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

393 

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

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

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

397 

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

399 'MEANS': meanList, 

400 'VARIANCES': varianceList, 

401 'RAW_XCORRS': rawXcorrs, 

402 'GAIN': gainList, 

403 'NOISE': noiseList, 

404 'MEAN_XCORRS': meanXcorrsList, 

405 'KERNEL': kernelList, 

406 'VALID': validList, 

407 }) 

408 

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

410 tableList.append(ampTable) 

411 

412 if len(self.detKernels): 

413 detList = [] 

414 kernelList = [] 

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

416 detList.append(det) 

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

418 

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

420 'KERNEL': kernelList}) 

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

422 tableList.append(detTable) 

423 

424 return tableList 

425 

426 # Implementation methods 

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

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

429 """ 

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

431 self.ampKernels if amp not in ampsToExclude]) 

432 averagingList = np.transpose(inKernels) 

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

434 sctrl = afwMath.StatisticsControl() 

435 sctrl.setNumSigmaClip(5.0) 

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

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

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

439 afwMath.MEANCLIP, sctrl).getValue() 

440 

441 self.detKernels[detectorName] = avgKernel 

442 

443 def replaceDetectorKernelWithAmpKernel(self, ampName, detectorName): 

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