Coverage for python/lsst/ip/isr/isrMockLSST.py: 17%

205 statements  

« prev     ^ index     » next       coverage.py v7.5.0, created at 2024-05-02 04:05 -0700

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__all__ = ["IsrMockLSSTConfig", "IsrMockLSST", "RawMockLSST", 

23 "CalibratedRawMockLSST", "ReferenceMockLSST", 

24 "BiasMockLSST", "DarkMockLSST", "FlatMockLSST", "FringeMockLSST", 

25 "BfKernelMockLSST", "DefectMockLSST", "CrosstalkCoeffMockLSST", 

26 "TransmissionMockLSST"] 

27import numpy as np 

28 

29import lsst.geom as geom 

30import lsst.pex.config as pexConfig 

31from .crosstalk import CrosstalkCalib 

32from .isrMock import IsrMockConfig, IsrMock 

33 

34 

35class IsrMockLSSTConfig(IsrMockConfig): 

36 """Configuration parameters for isrMockLSST. 

37 """ 

38 # Detector parameters and "Exposure" parameters, 

39 # mostly inherited from IsrMockConfig. 

40 isLsstLike = pexConfig.Field( 

41 dtype=bool, 

42 default=True, 

43 doc="If True, products have one raw image per amplifier, otherwise, one raw image per detector.", 

44 ) 

45 # Signal parameters. 

46 # Most of them are inherited from isrMockConfig but we update 

47 # some to LSSTcam expected values. 

48 # TODO: DM-42880 Update values to what is expected in LSSTCam. 

49 biasLevel = pexConfig.Field( 

50 dtype=float, 

51 default=30000.0, 

52 doc="Background contribution to be generated from the bias offset in ADU.", 

53 ) 

54 # Inclusion parameters are inherited from isrMock. 

55 doAddParallelOverscan = pexConfig.Field( 

56 dtype=bool, 

57 default=True, 

58 doc="Add overscan ramp to parallel overscan and data regions.", 

59 ) 

60 doAddSerialOverscan = pexConfig.Field( 

61 dtype=bool, 

62 default=True, 

63 doc="Add overscan ramp to serial overscan and data regions.", 

64 ) 

65 doApplyGain = pexConfig.Field( 

66 dtype=bool, 

67 default=True, 

68 doc="Add gain to data.", 

69 ) 

70 

71 

72class IsrMockLSST(IsrMock): 

73 """Class to generate consistent mock images for ISR testing. 

74 

75 ISR testing currently relies on one-off fake images that do not 

76 accurately mimic the full set of detector effects. This class 

77 uses the test camera/detector/amplifier structure defined in 

78 `lsst.afw.cameraGeom.testUtils` to avoid making the test data 

79 dependent on any of the actual obs package formats. 

80 """ 

81 ConfigClass = IsrMockLSSTConfig 

82 _DefaultName = "isrMockLSST" 

83 

84 def __init__(self, **kwargs): 

85 # cross-talk coeffs, bf kernel are defined in the parent class. 

86 super().__init__(**kwargs) 

87 

88 def run(self): 

89 """Generate a mock ISR product following LSSTCam ISR, and return it. 

90 

91 Returns 

92 ------- 

93 image : `lsst.afw.image.Exposure` 

94 Simulated ISR image with signals added. 

95 dataProduct : 

96 Simulated ISR data products. 

97 None : 

98 Returned if no valid configuration was found. 

99 

100 Raises 

101 ------ 

102 RuntimeError 

103 Raised if both doGenerateImage and doGenerateData are specified. 

104 """ 

105 if self.config.doGenerateImage and self.config.doGenerateData: 

106 raise RuntimeError("Only one of doGenerateImage and doGenerateData may be specified.") 

107 elif self.config.doGenerateImage: 

108 return self.makeImage() 

109 elif self.config.doGenerateData: 

110 return self.makeData() 

111 else: 

112 return None 

113 

114 def makeImage(self): 

115 """Generate a simulated ISR LSST image. 

116 

117 Returns 

118 ------- 

119 exposure : `lsst.afw.image.Exposure` or `dict` 

120 Simulated ISR image data. 

121 

122 Notes 

123 ----- 

124 This method constructs a "raw" data image. 

125 """ 

126 exposure = self.getExposure() 

127 

128 # We introduce effects as they happen from a source to the signal, 

129 # so the effects go from electrons to ADU. 

130 # The ISR steps will then correct these effects in the reverse order. 

131 for idx, amp in enumerate(exposure.getDetector()): 

132 

133 # Get image bbox and data 

134 bbox = None 

135 if self.config.isTrimmed: 

136 bbox = amp.getBBox() 

137 else: 

138 bbox = amp.getRawDataBBox().shiftedBy(amp.getRawXYOffset()) 

139 

140 ampData = exposure.image[bbox] 

141 

142 # Sky effects in e- 

143 if self.config.doAddSky: 

144 self.amplifierAddNoise(ampData, self.config.skyLevel, np.sqrt(self.config.skyLevel)) 

145 

146 if self.config.doAddSource: 

147 for sourceAmp, sourceFlux, sourceX, sourceY in zip(self.config.sourceAmp, 

148 self.config.sourceFlux, 

149 self.config.sourceX, 

150 self.config.sourceY): 

151 if idx == sourceAmp: 

152 self.amplifierAddSource(ampData, sourceFlux, sourceX, sourceY) 

153 

154 # Other effects in e- 

155 if self.config.doAddFringe: 

156 self.amplifierAddFringe(amp, ampData, np.array(self.config.fringeScale), 

157 x0=np.array(self.config.fringeX0), 

158 y0=np.array(self.config.fringeY0)) 

159 

160 if self.config.doAddFlat: 

161 if ampData.getArray().sum() == 0.0: 

162 self.amplifierAddNoise(ampData, 1.0, 0.0) 

163 u0 = exposure.getDimensions().getX() 

164 v0 = exposure.getDimensions().getY() 

165 self.amplifierMultiplyFlat(amp, ampData, self.config.flatDrop, u0=u0, v0=v0) 

166 

167 # ISR effects 

168 # 1. Add dark in e- (different from isrMock which does it in ADU) 

169 if self.config.doAddDark: 

170 self.amplifierAddNoise(ampData, 

171 self.config.darkRate * self.config.darkTime, 

172 np.sqrt(self.config.darkRate * self.config.darkTime)) 

173 

174 # 2. Gain normalize (from e- to ADU) 

175 # TODO: DM-43601 gain from PTC per amplifier 

176 # TODO: DM-36639 gain with temperature dependence 

177 if self.config.doApplyGain: 

178 self.applyGain(ampData, self.config.gain) 

179 

180 # 3. Add read noise with or without a bias level 

181 # to the image region in ADU. 

182 self.amplifierAddNoise(ampData, self.config.biasLevel if self.config.doAddBias else 0.0, 

183 self.config.readNoise / self.config.gain) 

184 

185 # 4. Apply cross-talk in ADU 

186 if self.config.doAddCrosstalk: 

187 ctCalib = CrosstalkCalib() 

188 for idxS, ampS in enumerate(exposure.getDetector()): 

189 for idxT, ampT in enumerate(exposure.getDetector()): 

190 ampDataT = exposure.image[ampT.getBBox() if self.config.isTrimmed 

191 else ampT.getRawDataBBox().shiftedBy(ampT.getRawXYOffset())] 

192 outAmp = ctCalib.extractAmp(exposure.getImage(), ampS, ampT, 

193 isTrimmed=self.config.isTrimmed) 

194 self.amplifierAddCT(outAmp, ampDataT, self.crosstalkCoeffs[idxS][idxT]) 

195 

196 # We now apply parallel and serial overscans 

197 for amp in exposure.getDetector(): 

198 # Get image bbox and data 

199 bbox = None 

200 if self.config.isTrimmed: 

201 bbox = amp.getBBox() 

202 else: 

203 bbox = amp.getRawDataBBox().shiftedBy(amp.getRawXYOffset()) 

204 ampData = exposure.image[bbox] 

205 

206 # Get overscan bbox and data 

207 if not self.config.isTrimmed: 

208 parallelOscanBBox = amp.getRawParallelOverscanBBox().shiftedBy(amp.getRawXYOffset()) 

209 parallelOscanData = exposure.image[parallelOscanBBox] 

210 

211 serialOscanBBox = amp.getRawSerialOverscanBBox().shiftedBy(amp.getRawXYOffset()) 

212 

213 # 5. Apply parallel overscan in ADU 

214 if self.config.doAddParallelOverscan: 

215 if not self.config.isTrimmed: 

216 # Add read noise with or without a bias level 

217 # to the parallel overscan region. 

218 self.amplifierAddNoise(parallelOscanData, self.config.biasLevel 

219 if self.config.doAddBias else 0.0, 

220 self.config.readNoise / self.config.gain) 

221 # Apply gradient along the Y axis 

222 # to the parallel overscan region. 

223 self.amplifierAddYGradient(parallelOscanData, -1.0 * self.config.overscanScale, 

224 1.0 * self.config.overscanScale) 

225 

226 # Apply gradient along the Y axis to the image region 

227 self.amplifierAddYGradient(ampData, -1.0 * self.config.overscanScale, 

228 1.0 * self.config.overscanScale) 

229 

230 # 6. Add Parallel overscan xtalk. 

231 # TODO: DM-43286 

232 

233 if self.config.doAddSerialOverscan: 

234 if not self.config.isTrimmed: 

235 # We grow the image to the parallel overscan region 

236 # (we do this instead of using the whole raw region 

237 # in case there are prescan regions) 

238 grownImageBBox = bbox.expandedTo(parallelOscanBBox) 

239 # Now we grow the serial overscan region 

240 # to include the corners 

241 serialOscanBBox = geom.Box2I( 

242 geom.Point2I(serialOscanBBox.getMinX(), 

243 grownImageBBox.getMinY()), 

244 geom.Extent2I(serialOscanBBox.getWidth(), 

245 grownImageBBox.getHeight()), 

246 ) 

247 serialOscanData = exposure.image[serialOscanBBox] 

248 

249 # Add read noise with or without a bias level 

250 # to the serial overscan region. 

251 self.amplifierAddNoise(serialOscanData, self.config.biasLevel 

252 if self.config.doAddBias else 0.0, 

253 self.config.readNoise / self.config.gain) 

254 

255 # 7. Apply serial overscan in ADU 

256 # Apply gradient along the X axis to both overscan regions. 

257 self.amplifierAddXGradient(serialOscanData, -1.0 * self.config.overscanScale, 

258 1.0 * self.config.overscanScale) 

259 self.amplifierAddXGradient(parallelOscanData, -1.0 * self.config.overscanScale, 

260 1.0 * self.config.overscanScale) 

261 

262 # Apply gradient along the X axis to the image region. 

263 self.amplifierAddXGradient(ampData, -1.0 * self.config.overscanScale, 

264 1.0 * self.config.overscanScale) 

265 

266 if self.config.doGenerateAmpDict: 

267 expDict = dict() 

268 for amp in exposure.getDetector(): 

269 expDict[amp.getName()] = exposure 

270 return expDict 

271 else: 

272 return exposure 

273 

274 def applyGain(self, ampData, gain): 

275 """Apply gain to the amplifier's data. 

276 This method divides the data by the gain 

277 because the mocks need to convert the data in electron to ADU, 

278 so it does the inverse operation to applyGains in isrFunctions. 

279 

280 Parameters 

281 ---------- 

282 ampData : `lsst.afw.image.ImageF` 

283 Amplifier image to operate on. 

284 gain : `float` 

285 Gain value in e^-/DN. 

286 """ 

287 ampArr = ampData.array 

288 ampArr[:] = ampArr[:] / gain 

289 

290 def amplifierAddXGradient(self, ampData, start, end): 

291 """Add a x-axis linear gradient to an amplifier's image data. 

292 

293 This method operates in the amplifier coordinate frame. 

294 

295 Parameters 

296 ---------- 

297 ampData : `lsst.afw.image.ImageF` 

298 Amplifier image to operate on. 

299 start : `float` 

300 Start value of the gradient (at y=0). 

301 end : `float` 

302 End value of the gradient (at y=ymax). 

303 """ 

304 nPixX = ampData.getDimensions().getX() 

305 ampArr = ampData.array 

306 ampArr[:] = ampArr[:] + (np.interp(range(nPixX), (0, nPixX - 1), (start, end)).reshape(1, nPixX) 

307 + np.zeros(ampData.getDimensions()).transpose()) 

308 

309 

310class RawMockLSST(IsrMockLSST): 

311 """Generate a raw exposure suitable for ISR. 

312 """ 

313 def __init__(self, **kwargs): 

314 super().__init__(**kwargs) 

315 self.config.isTrimmed = False 

316 self.config.doGenerateImage = True 

317 self.config.doGenerateAmpDict = False 

318 

319 # Add astro effects 

320 self.config.doAddSky = True 

321 self.config.doAddSource = True 

322 

323 # Add optical effects 

324 self.config.doAddFringe = True 

325 

326 # Add instru effects 

327 self.config.doAddParallelOverscan = True 

328 self.config.doAddSerialOverscan = True 

329 self.config.doAddCrosstalk = False 

330 self.config.doAddBias = True 

331 self.config.doAddDark = True 

332 

333 self.config.doAddFlat = True 

334 

335 

336class TrimmedRawMockLSST(RawMockLSST): 

337 """Generate a trimmed raw exposure. 

338 """ 

339 def __init__(self, **kwargs): 

340 super().__init__(**kwargs) 

341 self.config.isTrimmed = True 

342 self.config.doAddParallelOverscan = False 

343 self.config.doAddSerialOverscan = False 

344 

345 

346class CalibratedRawMockLSST(RawMockLSST): 

347 """Generate a trimmed raw exposure. 

348 """ 

349 def __init__(self, **kwargs): 

350 super().__init__(**kwargs) 

351 self.config.isTrimmed = True 

352 self.config.doGenerateImage = True 

353 

354 self.config.doAddSky = True 

355 self.config.doAddSource = True 

356 

357 self.config.doAddFringe = True 

358 

359 self.config.doAddParallelOverscan = False 

360 self.config.doAddSerialOverscan = False 

361 self.config.doAddCrosstalk = False 

362 self.config.doAddBias = False 

363 self.config.doAddDark = False 

364 self.config.doApplyGain = False 

365 self.config.doAddFlat = False 

366 

367 self.config.biasLevel = 0.0 

368 # Assume combined calibrations are made with 16 inputs. 

369 self.config.readNoise *= 0.25 

370 

371 

372class ReferenceMockLSST(IsrMockLSST): 

373 """Parent class for those that make reference calibrations. 

374 """ 

375 def __init__(self, **kwargs): 

376 super().__init__(**kwargs) 

377 self.config.isTrimmed = True 

378 self.config.doGenerateImage = True 

379 

380 self.config.doAddSky = False 

381 self.config.doAddSource = False 

382 

383 self.config.doAddFringe = False 

384 

385 self.config.doAddParallelOverscan = False 

386 self.config.doAddSerialOverscan = False 

387 self.config.doAddCrosstalk = False 

388 self.config.doAddBias = False 

389 self.config.doAddDark = False 

390 self.config.doApplyGain = False 

391 self.config.doAddFlat = False 

392 

393 

394# Classes to generate calibration products mocks. 

395class DarkMockLSST(ReferenceMockLSST): 

396 """Simulated reference dark calibration. 

397 """ 

398 def __init__(self, **kwargs): 

399 super().__init__(**kwargs) 

400 self.config.doAddDark = True 

401 self.config.darkTime = 1.0 

402 

403 

404class BiasMockLSST(ReferenceMockLSST): 

405 """Simulated combined bias calibration. 

406 """ 

407 def __init__(self, **kwargs): 

408 super().__init__(**kwargs) 

409 # A combined bias has mean 0 

410 # so we set its bias level to 0. 

411 # This is equivalent to doAddBias = False 

412 # but we do the following instead to be consistent 

413 # with any other bias products we might want to produce. 

414 self.config.doAddBias = True 

415 self.config.biasLevel - 0.0 

416 self.config.doApplyGain = True 

417 # Assume combined calibrations are made with 16 inputs. 

418 self.config.readNoise = 10.0*0.25 

419 

420 

421class FlatMockLSST(ReferenceMockLSST): 

422 """Simulated reference flat calibration. 

423 """ 

424 def __init__(self, **kwargs): 

425 super().__init__(**kwargs) 

426 self.config.doAddFlat = True 

427 

428 

429class FringeMockLSST(ReferenceMockLSST): 

430 """Simulated reference fringe calibration. 

431 """ 

432 def __init__(self, **kwargs): 

433 super().__init__(**kwargs) 

434 self.config.doAddFringe = True 

435 

436 

437class BfKernelMockLSST(IsrMockLSST): 

438 """Simulated brighter-fatter kernel. 

439 """ 

440 def __init__(self, **kwargs): 

441 super().__init__(**kwargs) 

442 self.config.doGenerateImage = False 

443 self.config.doGenerateData = True 

444 

445 # calibration products configs 

446 self.config.doBrighterFatter = True 

447 self.config.doDefects = False 

448 self.config.doCrosstalkCoeffs = False 

449 self.config.doTransmissionCurve = False 

450 

451 

452class DefectMockLSST(IsrMockLSST): 

453 """Simulated defect list. 

454 """ 

455 def __init__(self, **kwargs): 

456 super().__init__(**kwargs) 

457 self.config.doGenerateImage = False 

458 self.config.doGenerateData = True 

459 

460 self.config.doBrighterFatter = False 

461 self.config.doDefects = True 

462 self.config.doCrosstalkCoeffs = False 

463 self.config.doTransmissionCurve = False 

464 

465 

466class CrosstalkCoeffMockLSST(IsrMockLSST): 

467 """Simulated crosstalk coefficient matrix. 

468 """ 

469 def __init__(self, **kwargs): 

470 super().__init__(**kwargs) 

471 self.config.doGenerateImage = False 

472 self.config.doGenerateData = True 

473 

474 self.config.doBrighterFatter = False 

475 self.config.doDefects = False 

476 self.config.doCrosstalkCoeffs = True 

477 self.config.doTransmissionCurve = False 

478 

479 

480class TransmissionMockLSST(IsrMockLSST): 

481 """Simulated transmission curve. 

482 """ 

483 def __init__(self, **kwargs): 

484 super().__init__(**kwargs) 

485 self.config.doGenerateImage = False 

486 self.config.doGenerateData = True 

487 

488 self.config.doBrighterFatter = False 

489 self.config.doDefects = False 

490 self.config.doCrosstalkCoeffs = False 

491 self.config.doTransmissionCurve = True