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

220 statements  

« prev     ^ index     » next       coverage.py v7.5.1, created at 2024-05-16 03:55 -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 # Change bias level to LSSTCam expected values. 

46 biasLevel = pexConfig.Field( 

47 dtype=float, 

48 default=25000.0, 

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

50 ) 

51 calibMode = pexConfig.Field( 

52 dtype=bool, 

53 default=False, 

54 doc="Set to true to produce mock calibration products, e.g. combined bias, dark, flat, etc.", 

55 ) 

56 doAddParallelOverscan = pexConfig.Field( 

57 dtype=bool, 

58 default=True, 

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

60 ) 

61 doAddSerialOverscan = pexConfig.Field( 

62 dtype=bool, 

63 default=True, 

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

65 ) 

66 doApplyGain = pexConfig.Field( 

67 dtype=bool, 

68 default=True, 

69 doc="Add gain to data.", 

70 ) 

71 

72 

73class IsrMockLSST(IsrMock): 

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

75 """ 

76 ConfigClass = IsrMockLSSTConfig 

77 _DefaultName = "isrMockLSST" 

78 

79 def __init__(self, **kwargs): 

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

81 super().__init__(**kwargs) 

82 

83 def run(self): 

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

85 

86 Returns 

87 ------- 

88 image : `lsst.afw.image.Exposure` 

89 Simulated ISR image with signals added. 

90 dataProduct : 

91 Simulated ISR data products. 

92 None : 

93 Returned if no valid configuration was found. 

94 

95 Raises 

96 ------ 

97 RuntimeError 

98 Raised if both doGenerateImage and doGenerateData are specified. 

99 """ 

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

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

102 elif self.config.doGenerateImage: 

103 return self.makeImage() 

104 elif self.config.doGenerateData: 

105 return self.makeData() 

106 else: 

107 return None 

108 

109 def makeImage(self): 

110 """Generate a simulated ISR LSST image. 

111 

112 Returns 

113 ------- 

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

115 Simulated ISR image data. 

116 

117 Notes 

118 ----- 

119 This method constructs a "raw" data image. 

120 """ 

121 exposure = self.getExposure() 

122 

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

124 # so the effects go from electrons to ADU. 

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

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

127 

128 # Get image bbox and data 

129 bbox = None 

130 if self.config.isTrimmed: 

131 bbox = amp.getBBox() 

132 else: 

133 bbox = amp.getRawDataBBox() 

134 

135 ampData = exposure.image[bbox] 

136 

137 # Sky effects in e- 

138 if self.config.doAddSky: 

139 # The sky effects are in electrons, 

140 # but the skyLevel is configured in ADU 

141 # TODO: DM-42880 to set configs to correct units 

142 self.amplifierAddNoise(ampData, self.config.skyLevel * self.config.gain, 

143 np.sqrt(self.config.skyLevel * self.config.gain)) 

144 

145 if self.config.doAddSource: 

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

147 self.config.sourceFlux, 

148 self.config.sourceX, 

149 self.config.sourceY): 

150 if idx == sourceAmp: 

151 # The source flux is in electrons, 

152 # but the sourceFlux is configured in ADU 

153 # TODO: DM-42880 to set configs to correct units 

154 self.amplifierAddSource(ampData, sourceFlux * self.config.gain, sourceX, sourceY) 

155 

156 # Other effects in e- 

157 if self.config.doAddFringe: 

158 # Fringes are added in electrons, 

159 # but the fringeScale is configured in ADU 

160 self.amplifierAddFringe(amp, ampData, np.array(self.config.fringeScale) * self.config.gain, 

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

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

163 

164 if self.config.doAddFlat: 

165 if self.config.calibMode: 

166 # In case we are making a combined flat, 

167 # add a non-zero signal so the mock flat can be multiplied 

168 self.amplifierAddNoise(ampData, 1.0, 0.0) 

169 # Multiply each amplifier by a Gaussian centered on u0 and v0 

170 u0 = exposure.getDetector().getBBox().getDimensions().getX()/2. 

171 v0 = exposure.getDetector().getBBox().getDimensions().getY()/2. 

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

173 

174 # ISR effects 

175 # 1. Add dark in e- (darkRate is configured in e-/s) 

176 # TODO: DM-42880 to set configs to correct units 

177 if self.config.doAddDark: 

178 self.amplifierAddNoise(ampData, 

179 self.config.darkRate * self.config.darkTime, 

180 0. if self.config.calibMode 

181 else np.sqrt(self.config.darkRate * self.config.darkTime)) 

182 

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

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

185 # TODO: DM-36639 gain with temperature dependence 

186 if self.config.doApplyGain: 

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

188 

189 # 3. Add read noise to the image region in ADU. 

190 if not self.config.calibMode: 

191 self.amplifierAddNoise(ampData, 0.0, 

192 self.config.readNoise / self.config.gain) 

193 

194 # 4. Apply cross-talk in ADU 

195 if self.config.doAddCrosstalk: 

196 ctCalib = CrosstalkCalib() 

197 exposureClean = exposure.clone() 

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

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

200 ampDataTarget = exposure.image[ampT.getBBox() if self.config.isTrimmed 

201 else ampT.getRawDataBBox()] 

202 ampDataSource = ctCalib.extractAmp(exposureClean.image, ampS, ampT, 

203 isTrimmed=self.config.isTrimmed) 

204 self.amplifierAddCT(ampDataSource, ampDataTarget, self.crosstalkCoeffs[idxS][idxT]) 

205 

206 # We now apply parallel and serial overscans 

207 for amp in exposure.getDetector(): 

208 # Get image bbox and data 

209 bbox = None 

210 if self.config.isTrimmed: 

211 bbox = amp.getBBox() 

212 else: 

213 bbox = amp.getRawDataBBox() 

214 ampData = exposure.image[bbox] 

215 

216 if self.config.doAddParallelOverscan or self.config.doAddSerialOverscan or self.config.doAddBias: 

217 

218 allData = ampData 

219 

220 if self.config.doAddParallelOverscan or self.config.doAddSerialOverscan: 

221 # 5. Apply parallel overscan in ADU 

222 # First get the parallel and serial overscan bbox 

223 # and corresponding data 

224 parallelOscanBBox = amp.getRawParallelOverscanBBox() 

225 parallelOscanData = exposure.image[parallelOscanBBox] 

226 

227 grownImageBBox = bbox.expandedTo(parallelOscanBBox) 

228 

229 serialOscanBBox = amp.getRawSerialOverscanBBox() 

230 # Extend the serial overscan bbox to include corners 

231 serialOscanBBox = geom.Box2I( 

232 geom.Point2I(serialOscanBBox.getMinX(), 

233 grownImageBBox.getMinY()), 

234 geom.Extent2I(serialOscanBBox.getWidth(), 

235 grownImageBBox.getHeight())) 

236 serialOscanData = exposure.image[serialOscanBBox] 

237 

238 # Add read noise of mean 0 

239 # to the parallel and serial overscan regions 

240 self.amplifierAddNoise(parallelOscanData, 0.0, 

241 self.config.readNoise / self.config.gain) 

242 

243 self.amplifierAddNoise(serialOscanData, 0.0, 

244 self.config.readNoise / self.config.gain) 

245 

246 grownImageBBoxAll = grownImageBBox.expandedTo(serialOscanBBox) 

247 allData = exposure.image[grownImageBBoxAll] 

248 

249 if self.config.doAddParallelOverscan: 

250 # Apply gradient along the Y axis 

251 self.amplifierAddXGradient(allData, -1.0 * self.config.overscanScale, 

252 1.0 * self.config.overscanScale) 

253 

254 # 6. Add Parallel overscan xtalk. 

255 # TODO: DM-43286 

256 

257 # Add bias level to the whole image 

258 # (science and overscan regions if any) 

259 self.addBiasLevel(allData, self.config.biasLevel if self.config.doAddBias else 0.0) 

260 

261 if self.config.doAddSerialOverscan: 

262 # Apply gradient along the Y axis 

263 self.amplifierAddYGradient(allData, -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 addBiasLevel(self, ampData, biasLevel): 

275 """Add bias level to an amplifier's image data. 

276 

277 Parameters 

278 ---------- 

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

280 Amplifier image to operate on. 

281 biasLevel : `float` 

282 Bias level to be added to the image. 

283 """ 

284 ampArr = ampData.array 

285 ampArr[:] = ampArr[:] + biasLevel 

286 

287 def amplifierMultiplyFlat(self, amp, ampData, fracDrop, u0=100.0, v0=100.0): 

288 """Multiply an amplifier's image data by a flat-like pattern. 

289 

290 Parameters 

291 ---------- 

292 amp : `lsst.afw.ampInfo.AmpInfoRecord` 

293 Amplifier to operate on. Needed for amp<->exp coordinate 

294 transforms. 

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

296 Amplifier image to operate on. 

297 fracDrop : `float` 

298 Fractional drop from center to edge of detector along x-axis. 

299 u0 : `float` 

300 Peak location in detector coordinates. 

301 v0 : `float` 

302 Peak location in detector coordinates. 

303 """ 

304 if fracDrop >= 1.0: 

305 raise RuntimeError("Flat fractional drop cannot be greater than 1.0") 

306 

307 sigma = u0 / np.sqrt(2.0 * fracDrop) 

308 

309 for x in range(0, ampData.getDimensions().getX()): 

310 for y in range(0, ampData.getDimensions().getY()): 

311 (u, v) = self.localCoordToExpCoord(amp, x, y) 

312 f = np.exp(-0.5 * ((u - u0)**2 + (v - v0)**2) / sigma**2) 

313 ampData.array[y][x] = (ampData.array[y][x] * f) 

314 

315 def applyGain(self, ampData, gain): 

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

317 This method divides the data by the gain 

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

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

320 

321 Parameters 

322 ---------- 

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

324 Amplifier image to operate on. 

325 gain : `float` 

326 Gain value in e^-/DN. 

327 """ 

328 ampArr = ampData.array 

329 ampArr[:] = ampArr[:] / gain 

330 

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

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

333 

334 This method operates in the amplifier coordinate frame. 

335 

336 Parameters 

337 ---------- 

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

339 Amplifier image to operate on. 

340 start : `float` 

341 Start value of the gradient (at x=0). 

342 end : `float` 

343 End value of the gradient (at x=xmax). 

344 """ 

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

346 ampArr = ampData.array 

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

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

349 

350 

351class RawMockLSST(IsrMockLSST): 

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

353 """ 

354 def __init__(self, **kwargs): 

355 super().__init__(**kwargs) 

356 self.config.isTrimmed = False 

357 self.config.doGenerateImage = True 

358 self.config.doGenerateAmpDict = False 

359 

360 # Add astro effects 

361 self.config.doAddSky = True 

362 self.config.doAddSource = True 

363 

364 # Add optical effects 

365 self.config.doAddFringe = True 

366 

367 # Add instru effects 

368 self.config.doAddParallelOverscan = True 

369 self.config.doAddSerialOverscan = True 

370 self.config.doAddCrosstalk = False 

371 self.config.doAddBias = True 

372 self.config.doAddDark = True 

373 

374 self.config.doAddFlat = True 

375 

376 

377class TrimmedRawMockLSST(RawMockLSST): 

378 """Generate a trimmed raw exposure. 

379 """ 

380 def __init__(self, **kwargs): 

381 super().__init__(**kwargs) 

382 self.config.isTrimmed = True 

383 self.config.doAddParallelOverscan = False 

384 self.config.doAddSerialOverscan = False 

385 

386 

387class CalibratedRawMockLSST(RawMockLSST): 

388 """Generate a trimmed raw exposure. 

389 """ 

390 def __init__(self, **kwargs): 

391 super().__init__(**kwargs) 

392 self.config.isTrimmed = True 

393 self.config.doGenerateImage = True 

394 

395 self.config.doAddSky = True 

396 self.config.doAddSource = True 

397 

398 self.config.doAddFringe = True 

399 

400 self.config.doAddParallelOverscan = False 

401 self.config.doAddSerialOverscan = False 

402 self.config.doAddCrosstalk = False 

403 self.config.doAddBias = False 

404 self.config.doAddDark = False 

405 self.config.doApplyGain = False 

406 self.config.doAddFlat = False 

407 

408 self.config.biasLevel = 0.0 

409 # Assume combined calibrations are made with 16 inputs. 

410 self.config.readNoise *= 0.25 

411 

412 

413class ReferenceMockLSST(IsrMockLSST): 

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

415 """ 

416 def __init__(self, **kwargs): 

417 super().__init__(**kwargs) 

418 self.config.isTrimmed = True 

419 self.config.doGenerateImage = True 

420 

421 self.config.calibMode = True 

422 

423 self.config.doAddSky = False 

424 self.config.doAddSource = False 

425 

426 self.config.doAddFringe = False 

427 

428 self.config.doAddParallelOverscan = False 

429 self.config.doAddSerialOverscan = False 

430 self.config.doAddCrosstalk = False 

431 self.config.doAddBias = False 

432 self.config.doAddDark = False 

433 self.config.doApplyGain = False 

434 self.config.doAddFlat = False 

435 

436 

437# Classes to generate calibration products mocks. 

438class DarkMockLSST(ReferenceMockLSST): 

439 """Simulated reference dark calibration. 

440 """ 

441 def __init__(self, **kwargs): 

442 super().__init__(**kwargs) 

443 self.config.doAddDark = True 

444 self.config.darkTime = 1.0 

445 

446 

447class BiasMockLSST(ReferenceMockLSST): 

448 """Simulated combined bias calibration. 

449 """ 

450 def __init__(self, **kwargs): 

451 super().__init__(**kwargs) 

452 # We assume a perfect noiseless bias frame. 

453 # A combined bias has mean 0 

454 # so we set its bias level to 0. 

455 # This is equivalent to doAddBias = False 

456 # but we do the following instead to be consistent 

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

458 self.config.doAddBias = True 

459 self.config.biasLevel = 0.0 

460 self.config.doApplyGain = True 

461 

462 

463class FlatMockLSST(ReferenceMockLSST): 

464 """Simulated reference flat calibration. 

465 """ 

466 def __init__(self, **kwargs): 

467 super().__init__(**kwargs) 

468 self.config.doAddFlat = True 

469 

470 

471class FringeMockLSST(ReferenceMockLSST): 

472 """Simulated reference fringe calibration. 

473 """ 

474 def __init__(self, **kwargs): 

475 super().__init__(**kwargs) 

476 self.config.doAddFringe = True 

477 

478 

479class BfKernelMockLSST(IsrMockLSST): 

480 """Simulated brighter-fatter kernel. 

481 """ 

482 def __init__(self, **kwargs): 

483 super().__init__(**kwargs) 

484 self.config.doGenerateImage = False 

485 self.config.doGenerateData = True 

486 

487 self.config.doBrighterFatter = True 

488 self.config.doDefects = False 

489 self.config.doCrosstalkCoeffs = False 

490 self.config.doTransmissionCurve = False 

491 

492 

493class DefectMockLSST(IsrMockLSST): 

494 """Simulated defect list. 

495 """ 

496 def __init__(self, **kwargs): 

497 super().__init__(**kwargs) 

498 self.config.doGenerateImage = False 

499 self.config.doGenerateData = True 

500 

501 self.config.doBrighterFatter = False 

502 self.config.doDefects = True 

503 self.config.doCrosstalkCoeffs = False 

504 self.config.doTransmissionCurve = False 

505 

506 

507class CrosstalkCoeffMockLSST(IsrMockLSST): 

508 """Simulated crosstalk coefficient matrix. 

509 """ 

510 def __init__(self, **kwargs): 

511 super().__init__(**kwargs) 

512 self.config.doGenerateImage = False 

513 self.config.doGenerateData = True 

514 

515 self.config.doBrighterFatter = False 

516 self.config.doDefects = False 

517 self.config.doCrosstalkCoeffs = True 

518 self.config.doTransmissionCurve = False 

519 

520 

521class TransmissionMockLSST(IsrMockLSST): 

522 """Simulated transmission curve. 

523 """ 

524 def __init__(self, **kwargs): 

525 super().__init__(**kwargs) 

526 self.config.doGenerateImage = False 

527 self.config.doGenerateData = True 

528 

529 self.config.doBrighterFatter = False 

530 self.config.doDefects = False 

531 self.config.doCrosstalkCoeffs = False 

532 self.config.doTransmissionCurve = True