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

465 statements  

« prev     ^ index     » next       coverage.py v7.11.0, created at 2025-11-14 01:33 -0800

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"] 

27 

28import numpy as np 

29import galsim 

30 

31import lsst.geom as geom 

32import lsst.pex.config as pexConfig 

33import lsst.afw.detection as afwDetection 

34import lsst.afw.math as afwMath 

35from lsst.afw.cameraGeom import ReadoutCorner 

36from lsst.daf.base import PropertyList 

37from .crosstalk import CrosstalkCalib 

38from .isrMock import IsrMockConfig, IsrMock 

39from .defects import Defects 

40from .assembleCcdTask import AssembleCcdTask 

41from .linearize import Linearizer 

42from .brighterFatterKernel import BrighterFatterKernel 

43from .deferredCharge import ( 

44 SegmentSimulator, 

45 FloatingOutputAmplifier, 

46 DeferredChargeCalib 

47) 

48 

49 

50class IsrMockLSSTConfig(IsrMockConfig): 

51 """Configuration parameters for isrMockLSST. 

52 """ 

53 # Detector parameters and "Exposure" parameters, 

54 # mostly inherited from IsrMockConfig. 

55 isLsstLike = pexConfig.Field( 

56 dtype=bool, 

57 default=True, 

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

59 ) 

60 calibMode = pexConfig.Field( 

61 dtype=bool, 

62 default=False, 

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

64 ) 

65 doAdd2DBias = pexConfig.Field( 

66 dtype=bool, 

67 default=True, 

68 doc="Add 2D bias residual frame to data.", 

69 ) 

70 doAddBrightDefects = pexConfig.Field( 

71 dtype=bool, 

72 default=True, 

73 doc="Add bright defects (bad column) to data.", 

74 ) 

75 brightDefectLevel = pexConfig.Field( 

76 dtype=float, 

77 default=30000.0, 

78 doc="Bright defect level (electron).", 

79 ) 

80 doAddBadParallelOverscanColumn = pexConfig.Field( 

81 dtype=bool, 

82 default=True, 

83 doc="Add a bad column to the parallel overscan.", 

84 ) 

85 badParallelOverscanColumnLevel = pexConfig.Field( 

86 dtype=float, 

87 default=300000., 

88 doc="Bright parallel overscan column level (electron). Should be above saturation.", 

89 ) 

90 doAddBadParallelOverscanColumnNeighbors = pexConfig.Field( 

91 dtype=bool, 

92 default=True, 

93 doc="Add low-level bad columns next to parallel overscan bad column.", 

94 ) 

95 badParallelOverscanColumnNeighborsLevel = pexConfig.Field( 

96 dtype=float, 

97 default=50.0, 

98 doc="Bright parallel overscan column neighbors level (electron).", 

99 ) 

100 doAddBrighterFatter = pexConfig.Field( 

101 dtype=bool, 

102 default=False, 

103 doc="Add brighter fatter and/or diffusion effects to image.", 

104 ) 

105 doAddDeferredCharge = pexConfig.Field( 

106 dtype=bool, 

107 default=False, 

108 doc="Add serial CTI at the amp level?", 

109 ) 

110 bfStrength = pexConfig.Field( 

111 dtype=float, 

112 default=2.0, 

113 doc="The brighter fatter effect scaling parameter (cannot be zero)." 

114 "Nominally = 1, but = 2 is more realistic." 

115 ) 

116 nRecalc = pexConfig.Field( 

117 dtype=int, 

118 default=10000, 

119 doc="Number of electrons to accumulate before recalculating pixel shapes.", 

120 ) 

121 doAddClockInjectedOffset = pexConfig.Field( 

122 dtype=bool, 

123 default=True, 

124 doc="Add clock-injected offset to data (on-chip bias level).", 

125 ) 

126 clockInjectedOffsetLevel = pexConfig.Field( 

127 dtype=float, 

128 default=8500.0, 

129 doc="Clock-injected offset (on-chip bias level), in electron.", 

130 ) 

131 noise2DBias = pexConfig.Field( 

132 dtype=float, 

133 default=2.0, 

134 doc="Noise (in electron) to generate a 2D bias residual frame.", 

135 ) 

136 doAddDarkNoiseOnly = pexConfig.Field( 

137 dtype=bool, 

138 default=False, 

139 doc="Add only dark current noise, for testing consistency.", 

140 ) 

141 doAddParallelOverscanRamp = pexConfig.Field( 

142 dtype=bool, 

143 default=True, 

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

145 ) 

146 doAddSerialOverscanRamp = pexConfig.Field( 

147 dtype=bool, 

148 default=True, 

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

150 ) 

151 doAddHighSignalNonlinearity = pexConfig.Field( 

152 dtype=bool, 

153 default=True, 

154 doc="Add high signal non-linearity to overscan and data regions?", 

155 ) 

156 doAddLowSignalNonlinearity = pexConfig.Field( 

157 dtype=bool, 

158 default=False, 

159 doc="Add low signal non-linearity to overscan and data regions? (Not supported yet.)", 

160 ) 

161 highSignalNonlinearityThreshold = pexConfig.Field( 

162 dtype=float, 

163 default=40_000., 

164 doc="Threshold (in adu) for the non-linearity to be considered ``high signal``.", 

165 ) 

166 doApplyGain = pexConfig.Field( 

167 dtype=bool, 

168 default=True, 

169 doc="Add gain to data.", 

170 ) 

171 doRoundAdu = pexConfig.Field( 

172 dtype=bool, 

173 default=True, 

174 doc="Round adu values to nearest integer.", 

175 ) 

176 gainDict = pexConfig.DictField( 

177 keytype=str, 

178 itemtype=float, 

179 doc="Dictionary of amp name to gain; any amps not listed will use " 

180 "config.gain as the value. Units are electron/adu.", 

181 default={ 

182 "C:0,0": 1.65, 

183 "C:0,1": 1.60, 

184 "C:0,2": 1.55, 

185 "C:0,3": 1.70, 

186 "C:1,0": 1.75, 

187 "C:1,1": 1.80, 

188 "C:1,2": 1.85, 

189 "C:1,3": 1.70, 

190 }, 

191 ) 

192 assembleCcd = pexConfig.ConfigurableField( 

193 target=AssembleCcdTask, 

194 doc="CCD assembly task; used for defect box conversions.", 

195 ) 

196 

197 def validate(self): 

198 super().validate() 

199 

200 if self.doAddLowSignalNonlinearity: 

201 raise NotImplementedError("Low signal non-linearity is not implemented.") 

202 

203 if self.doAddDeferredCharge and self.isTrimmed: 

204 raise NotImplementedError("Must be untrimmed for mock serial CTI to" 

205 "realistically add charge into overscan regions.") 

206 

207 def setDefaults(self): 

208 super().setDefaults() 

209 

210 self.gain = 1.7 # Default value. 

211 self.skyLevel = 1700.0 # electron 

212 self.sourceFlux = [50_000.0] # electron 

213 self.sourceX = [35.0] # pixel 

214 self.sourceY = [37.0] # pixel 

215 self.overscanScale = 170.0 # electron 

216 self.biasLevel = 20_000.0 # adu 

217 self.doAddCrosstalk = True 

218 

219 

220class IsrMockLSST(IsrMock): 

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

222 """ 

223 ConfigClass = IsrMockLSSTConfig 

224 _DefaultName = "isrMockLSST" 

225 

226 def __init__(self, **kwargs): 

227 super().__init__(**kwargs) 

228 

229 # Get kernel derived from imSim generated flats with BFE. The kernel 

230 # was used for Ops Rehearsal 3 for LSSTCam-type sensors 

231 # See https://rubinobs.atlassian.net/browse/DM-43059 for more details. 

232 self.bfKernel = np.array([[4.83499829e-01, 8.10171823e-01, 5.31096720e-01, 

233 3.54369868e-02, -8.44782871e-01, -1.64614462e+00, 

234 -3.83933101e+00, -5.60243416e+00, -6.51691578e+00, 

235 -5.60243416e+00, -3.83933101e+00, -1.64614462e+00, 

236 -8.44782871e-01, 3.54369868e-02, 5.31096720e-01, 

237 8.10171823e-01, 4.83499829e-01], 

238 [1.12382749e+00, 2.22609074e+00, 1.27877807e+00, 

239 4.55434098e-01, -1.76842385e+00, -1.90046460e+00, 

240 -8.10874526e+00, -1.20534899e+01, -1.48627948e+01, 

241 -1.20534899e+01, -8.10874526e+00, -1.90046460e+00, 

242 -1.76842385e+00, 4.55434098e-01, 1.27877807e+00, 

243 2.22609074e+00, 1.12382749e+00], 

244 [1.78571940e+00, 4.38918110e+00, 3.95098587e+00, 

245 3.70961649e-01, -3.48151981e+00, -9.61567736e+00, 

246 -1.78621172e+01, -2.32278872e+01, -2.31833727e+01, 

247 -2.32278872e+01, -1.78621172e+01, -9.61567736e+00, 

248 -3.48151981e+00, 3.70961649e-01, 3.95098587e+00, 

249 4.38918110e+00, 1.78571940e+00], 

250 [1.62986900e+00, 3.67851228e+00, 5.68645252e+00, 

251 2.15342566e-01, -8.89937202e+00, -1.44739813e+01, 

252 -2.98952660e+01, -4.37420817e+01, -4.83160958e+01, 

253 -4.37420817e+01, -2.98952660e+01, -1.44739813e+01, 

254 -8.89937202e+00, 2.15342566e-01, 5.68645252e+00, 

255 3.67851228e+00, 1.62986900e+00], 

256 [1.05524430e+00, 1.71917897e+00, 1.73105590e+00, 

257 -2.10088420e+00, -1.15118208e+01, -2.55007598e+01, 

258 -4.73056159e+01, -6.97257685e+01, -8.09264433e+01, 

259 -6.97257685e+01, -4.73056159e+01, -2.55007598e+01, 

260 -1.15118208e+01, -2.10088420e+00, 1.73105590e+00, 

261 1.71917897e+00, 1.05524430e+00], 

262 [8.71929228e-01, 5.41025574e-01, 9.47560771e-01, 

263 -5.75314708e-01, -7.46104027e+00, -4.42314481e+01, 

264 -9.54126971e+01, -1.61603201e+02, -2.07520692e+02, 

265 -1.61603201e+02, -9.54126971e+01, -4.42314481e+01, 

266 -7.46104027e+00, -5.75314708e-01, 9.47560771e-01, 

267 5.41025574e-01, 8.71929228e-01], 

268 [1.89144704e+00, 3.57543979e+00, -6.91419168e-02, 

269 -3.37950835e+00, -1.46695089e+01, -7.22850746e+01, 

270 -1.65563055e+02, -3.10820425e+02, -4.70026655e+02, 

271 -3.10820425e+02, -1.65563055e+02, -7.22850746e+01, 

272 -1.46695089e+01, -3.37950835e+00, -6.91419168e-02, 

273 3.57543979e+00, 1.89144704e+00], 

274 [3.11841913e+00, 7.84024994e+00, 1.88495248e+00, 

275 -7.69011009e+00, -2.71782400e+01, -1.04343326e+02, 

276 -2.47561370e+02, -5.32959841e+02, -1.16529012e+03, 

277 -5.32959841e+02, -2.47561370e+02, -1.04343326e+02, 

278 -2.71782400e+01, -7.69011009e+00, 1.88495248e+00, 

279 7.84024994e+00, 3.11841913e+00], 

280 [2.74197956e+00, 4.73107997e+00, -9.48352966e-01, 

281 -9.44822832e+00, -3.06477671e+01, -1.26788739e+02, 

282 -3.22828411e+02, -8.47943472e+02, -3.87702420e+03, 

283 -8.47943472e+02, -3.22828411e+02, -1.26788739e+02, 

284 -3.06477671e+01, -9.44822832e+00, -9.48352966e-01, 

285 4.73107997e+00, 2.74197956e+00], 

286 [3.11841913e+00, 7.84024994e+00, 1.88495248e+00, 

287 -7.69011009e+00, -2.71782400e+01, -1.04343326e+02, 

288 -2.47561370e+02, -5.32959841e+02, -1.16529012e+03, 

289 -5.32959841e+02, -2.47561370e+02, -1.04343326e+02, 

290 -2.71782400e+01, -7.69011009e+00, 1.88495248e+00, 

291 7.84024994e+00, 3.11841913e+00], 

292 [1.89144704e+00, 3.57543979e+00, -6.91419168e-02, 

293 -3.37950835e+00, -1.46695089e+01, -7.22850746e+01, 

294 -1.65563055e+02, -3.10820425e+02, -4.70026655e+02, 

295 -3.10820425e+02, -1.65563055e+02, -7.22850746e+01, 

296 -1.46695089e+01, -3.37950835e+00, -6.91419168e-02, 

297 3.57543979e+00, 1.89144704e+00], 

298 [8.71929228e-01, 5.41025574e-01, 9.47560771e-01, 

299 -5.75314708e-01, -7.46104027e+00, -4.42314481e+01, 

300 -9.54126971e+01, -1.61603201e+02, -2.07520692e+02, 

301 -1.61603201e+02, -9.54126971e+01, -4.42314481e+01, 

302 -7.46104027e+00, -5.75314708e-01, 9.47560771e-01, 

303 5.41025574e-01, 8.71929228e-01], 

304 [1.05524430e+00, 1.71917897e+00, 1.73105590e+00, 

305 -2.10088420e+00, -1.15118208e+01, -2.55007598e+01, 

306 -4.73056159e+01, -6.97257685e+01, -8.09264433e+01, 

307 -6.97257685e+01, -4.73056159e+01, -2.55007598e+01, 

308 -1.15118208e+01, -2.10088420e+00, 1.73105590e+00, 

309 1.71917897e+00, 1.05524430e+00], 

310 [1.62986900e+00, 3.67851228e+00, 5.68645252e+00, 

311 2.15342566e-01, -8.89937202e+00, -1.44739813e+01, 

312 -2.98952660e+01, -4.37420817e+01, -4.83160958e+01, 

313 -4.37420817e+01, -2.98952660e+01, -1.44739813e+01, 

314 -8.89937202e+00, 2.15342566e-01, 5.68645252e+00, 

315 3.67851228e+00, 1.62986900e+00], 

316 [1.78571940e+00, 4.38918110e+00, 3.95098587e+00, 

317 3.70961649e-01, -3.48151981e+00, -9.61567736e+00, 

318 -1.78621172e+01, -2.32278872e+01, -2.31833727e+01, 

319 -2.32278872e+01, -1.78621172e+01, -9.61567736e+00, 

320 -3.48151981e+00, 3.70961649e-01, 3.95098587e+00, 

321 4.38918110e+00, 1.78571940e+00], 

322 [1.12382749e+00, 2.22609074e+00, 1.27877807e+00, 

323 4.55434098e-01, -1.76842385e+00, -1.90046460e+00, 

324 -8.10874526e+00, -1.20534899e+01, -1.48627948e+01, 

325 -1.20534899e+01, -8.10874526e+00, -1.90046460e+00, 

326 -1.76842385e+00, 4.55434098e-01, 1.27877807e+00, 

327 2.22609074e+00, 1.12382749e+00], 

328 [4.83499829e-01, 8.10171823e-01, 5.31096720e-01, 

329 3.54369868e-02, -8.44782871e-01, -1.64614462+00, 

330 -3.83933101e+00, -5.60243416e+00, -6.51691578e+00, 

331 -5.60243416e+00, -3.83933101e+00, -1.64614462e+00, 

332 -8.44782871e-01, 3.54369868e-02, 5.31096720e-01, 

333 8.10171823e-01, 4.83499829e-01]]) * 1e-10 

334 

335 # Spline trap coefficients and the ctiCalibDict are all taken from a 

336 # cti calibration measured from LSSTCam sensor R03_S12 during Run 5 

337 # EO testing. These are the coefficients for the spline trap model 

338 # used in the deferred charge calibration. The collection can be 

339 # found in /repo/ir2: u/abrought/13144/cti (processed 3/4/2024). 

340 self.splineTrapCoeffs = np.array([0.0, 28.1, 47.4, 56.4, 66.6, 78.6, 92.4, 109.4, 

341 129.0, 151.9, 179.4, 211.9, 250.5, 296.2, 350.0, 

342 413.5, 488.0, 576.0, 680.4, 753.0, 888.2, 1040.5, 

343 1254.1, 1478.9, 1747.0, 2055.7, 2416.9, 2855.2, 

344 3361.9, 3969.4, 4665.9, 5405.3, 6380.0, 7516.7, 

345 8875.9, 10488.6, 12681.9, 14974.2, 17257.6, 20366.5, 

346 24026.7, 28372.1, 33451.7, 39550.4, 46624.8, 55042.9, 

347 64862.7, 76503.1, 90265.6, 106384.2, 0.0, 0.0, 0.0, 

348 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.1, 

349 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.1, 0.0, 

350 0.1, 0.2, 0.6, 0.1, 0.0, 0.1, 0.0, 0.6, 0.3, 0.5, 0.8, 

351 0.8, 1.5, 2.0, 1.8, 2.4, 2.6, 3.7, 5.0, 6.4, 8.4, 10.9, 

352 14.5, 21.1, 28.9]) 

353 

354 self.ctiCalibDict = {'inputGain': {'C:0,0': 1.5, 

355 'C:0,1': 1.5, 

356 'C:0,2': 1.5, 

357 'C:0,3': 1.5, 

358 'C:1,0': 1.5, 

359 'C:1,1': 1.5, 

360 'C:1,2': 1.5, 

361 'C:1,3': 1.5}, 

362 'driftScale': {'C:0,0': 0.000127, 

363 'C:0,1': 0.000137, 

364 'C:0,2': 0.000138, 

365 'C:0,3': 0.000147, 

366 'C:1,0': 0.000147, 

367 'C:1,1': 0.000122, 

368 'C:1,2': 0.000123, 

369 'C:1,3': 0.000116}, 

370 'decayTime': {'C:0,0': 2.30, 

371 'C:0,1': 2.21, 

372 'C:0,2': 2.28, 

373 'C:0,3': 2.34, 

374 'C:1,0': 2.30, 

375 'C:1,1': 2.40, 

376 'C:1,2': 2.51, 

377 'C:1,3': 2.21}, 

378 'globalCti': {'C:0,0': 5.25e-07, 

379 'C:0,1': 5.38e-07, 

380 'C:0,2': 5.80e-07, 

381 'C:0,3': 5.91e-07, 

382 'C:1,0': 6.24e-07, 

383 'C:1,1': 5.72e-07, 

384 'C:1,2': 5.60e-07, 

385 'C:1,3': 4.40e-07}, 

386 'signals': {'C:0,0': np.linspace(1.0e2, 1.0e5, 10), 

387 'C:0,1': np.linspace(1.0e2, 1.0e5, 10), 

388 'C:0,2': np.linspace(1.0e2, 1.0e5, 10), 

389 'C:0,3': np.linspace(1.0e2, 1.0e5, 10), 

390 'C:1,0': np.linspace(1.0e2, 1.0e5, 10), 

391 'C:1,1': np.linspace(1.0e2, 1.0e5, 10), 

392 'C:1,2': np.linspace(1.0e2, 1.0e5, 10), 

393 'C:1,3': np.linspace(1.0e2, 1.0e5, 10)}, 

394 'serialEper': {'C:0,0': np.full(10, 5.25e-07), 

395 'C:0,1': np.full(10, 5.38e-07), 

396 'C:0,2': np.full(10, 5.80e-07), 

397 'C:0,3': np.full(10, 5.91e-07), 

398 'C:1,0': np.full(10, 6.24e-07), 

399 'C:1,1': np.full(10, 5.72e-07), 

400 'C:1,2': np.full(10, 5.60e-07), 

401 'C:1,3': np.full(10, 4.40e-07)}, 

402 'parallelEper': {'C:0,0': np.full(10, 5.25e-07), 

403 'C:0,1': np.full(10, 5.38e-07), 

404 'C:0,2': np.full(10, 5.80e-07), 

405 'C:0,3': np.full(10, 5.91e-07), 

406 'C:1,0': np.full(10, 6.24e-07), 

407 'C:1,1': np.full(10, 5.72e-07), 

408 'C:1,2': np.full(10, 5.60e-07), 

409 'C:1,3': np.full(10, 4.40e-07)}, 

410 'serialCtiTurnoff': {'C:0,0': 1.0e5, 

411 'C:0,1': 1.0e5, 

412 'C:0,2': 1.0e5, 

413 'C:0,3': 1.0e5, 

414 'C:1,0': 1.0e5, 

415 'C:1,1': 1.0e5, 

416 'C:1,2': 1.0e5, 

417 'C:1,3': 1.0e5}, 

418 'parallelCtiTurnoff': {'C:0,0': 1.0e5, 

419 'C:0,1': 1.0e5, 

420 'C:0,2': 1.0e5, 

421 'C:0,3': 1.0e5, 

422 'C:1,0': 1.0e5, 

423 'C:1,1': 1.0e5, 

424 'C:1,2': 1.0e5, 

425 'C:1,3': 1.0e5}, 

426 'serialCtiTurnoffSamplingErr': {'C:0,0': 1.0e3, 

427 'C:0,1': 1.0e3, 

428 'C:0,2': 1.0e3, 

429 'C:0,3': 1.0e3, 

430 'C:1,0': 1.0e3, 

431 'C:1,1': 1.0e3, 

432 'C:1,2': 1.0e3, 

433 'C:1,3': 1.0e3}, 

434 'parallelCtiTurnoffSamplingErr': {'C:0,0': 1.0e3, 

435 'C:0,1': 1.0e3, 

436 'C:0,2': 1.0e3, 

437 'C:0,3': 1.0e3, 

438 'C:1,0': 1.0e3, 

439 'C:1,1': 1.0e3, 

440 'C:1,2': 1.0e3, 

441 'C:1,3': 1.0e3}, 

442 'serialTraps': {'C:0,0': {'size': 20000.0, 

443 'emissionTime': 0.4, 

444 'pixel': 1, 

445 'trap_type': 'spline', 

446 'coeffs': self.splineTrapCoeffs}, 

447 'C:0,1': {'size': 20000.0, 

448 'emissionTime': 0.4, 

449 'pixel': 1, 

450 'trap_type': 'spline', 

451 'coeffs': self.splineTrapCoeffs}, 

452 'C:0,2': {'size': 20000.0, 

453 'emissionTime': 0.4, 

454 'pixel': 1, 

455 'trap_type': 'spline', 

456 'coeffs': self.splineTrapCoeffs}, 

457 'C:0,3': {'size': 20000.0, 

458 'emissionTime': 0.4, 

459 'pixel': 1, 

460 'trap_type': 'spline', 

461 'coeffs': self.splineTrapCoeffs}, 

462 'C:1,0': {'size': 20000.0, 

463 'emissionTime': 0.4, 

464 'pixel': 1, 

465 'trap_type': 'spline', 

466 'coeffs': self.splineTrapCoeffs}, 

467 'C:1,1': {'size': 20000.0, 

468 'emissionTime': 0.4, 

469 'pixel': 1, 

470 'trap_type': 'spline', 

471 'coeffs': self.splineTrapCoeffs}, 

472 'C:1,2': {'size': 20000.0, 

473 'emissionTime': 0.4, 

474 'pixel': 1, 

475 'trap_type': 'spline', 

476 'coeffs': self.splineTrapCoeffs}, 

477 'C:1,3': {'size': 20000.0, 

478 'emissionTime': 0.4, 

479 'pixel': 1, 

480 'trap_type': 'spline', 

481 'coeffs': self.splineTrapCoeffs}}} 

482 

483 self.deferredChargeCalib = self.makeDeferredChargeCalib() 

484 

485 # Cross-talk coeffs are defined in the parent class. 

486 

487 self.makeSubtask("assembleCcd") 

488 

489 def run(self): 

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

491 

492 Returns 

493 ------- 

494 image : `lsst.afw.image.Exposure` 

495 Simulated ISR image with signals added. 

496 dataProduct : 

497 Simulated ISR data products. 

498 None : 

499 Returned if no valid configuration was found. 

500 

501 Raises 

502 ------ 

503 RuntimeError 

504 Raised if both doGenerateImage and doGenerateData are specified. 

505 """ 

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

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

508 elif self.config.doGenerateImage: 

509 return self.makeImage() 

510 elif self.config.doGenerateData: 

511 return self.makeData() 

512 else: 

513 return None 

514 

515 def makeImage(self): 

516 """Generate a simulated ISR LSST image. 

517 

518 Returns 

519 ------- 

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

521 Simulated ISR image data. 

522 

523 Notes 

524 ----- 

525 This method constructs a "raw" data image. 

526 """ 

527 exposure = self.getExposure() 

528 

529 # Set up random number generators for consistency of components, 

530 # no matter the group that are configured. 

531 rngSky = np.random.RandomState(seed=self.config.rngSeed + 1) 

532 rngDark = np.random.RandomState(seed=self.config.rngSeed + 2) 

533 rng2DBias = np.random.RandomState(seed=self.config.rngSeed + 3) 

534 rngOverscan = np.random.RandomState(seed=self.config.rngSeed + 4) 

535 rngReadNoise = np.random.RandomState(seed=self.config.rngSeed + 5) 

536 rngBrighterFatter = galsim.BaseDeviate(self.config.rngSeed + 6) 

537 

538 # Create the linearizer if we will need it. 

539 if self.config.doAddHighSignalNonlinearity: 

540 linearizer = LinearizerMockLSST().run() 

541 

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

543 # so the effects go from electron to adu. 

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

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

546 # Get image bbox and data 

547 bbox = None 

548 if self.config.isTrimmed: 

549 bbox = amp.getBBox() 

550 bboxFull = bbox 

551 else: 

552 bbox = amp.getRawDataBBox() 

553 bboxFull = amp.getRawBBox() 

554 

555 # This is the image data (excluding pre/overscans). 

556 ampImageData = exposure.image[bbox] 

557 # This is the full data (including pre/overscans if untrimmed). 

558 ampFullData = exposure.image[bboxFull] 

559 

560 # Astrophysical signals are all in electron (e-). 

561 # These are only applied to the imaging portion of the 

562 # amplifier (ampImageData) 

563 

564 if self.config.doAddSky: 

565 # The sky effects are in electron. 

566 self.amplifierAddNoise( 

567 ampImageData, 

568 self.config.skyLevel, 

569 np.sqrt(self.config.skyLevel), 

570 rng=rngSky, 

571 ) 

572 

573 if self.config.doAddSource: 

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

575 self.config.sourceFlux, 

576 self.config.sourceX, 

577 self.config.sourceY): 

578 if idx == sourceAmp: 

579 # The source flux is in electron. 

580 self.amplifierAddSource(ampImageData, sourceFlux, sourceX, sourceY) 

581 

582 if self.config.doAddFringe: 

583 # Fringes are added in electron. 

584 self.amplifierAddFringe(amp, 

585 ampImageData, 

586 np.array(self.config.fringeScale), 

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

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

589 

590 if self.config.doAddFlat: 

591 if self.config.calibMode: 

592 # In case we are making a combined flat, 

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

594 self.amplifierAddNoise(ampImageData, 1.0, 0.0) 

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

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

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

598 self.amplifierMultiplyFlat(amp, ampImageData, self.config.flatDrop, u0=u0, v0=v0) 

599 

600 # On-chip electronic effects. 

601 

602 # 1. Add bright defect(s). 

603 if self.config.doAddBrightDefects: 

604 defectList = self.makeDefectList(isTrimmed=self.config.isTrimmed) 

605 

606 for defect in defectList: 

607 exposure.image[defect.getBBox()] = self.config.brightDefectLevel 

608 

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

610 # Get image bbox and data 

611 bbox = None 

612 if self.config.isTrimmed: 

613 bbox = amp.getBBox() 

614 bboxFull = bbox 

615 else: 

616 bbox = amp.getRawDataBBox() 

617 bboxFull = amp.getRawBBox() 

618 

619 # This is the image data (excluding pre/overscans). 

620 ampImageData = exposure.image[bbox] 

621 # This is the full data (including pre/overscans if untrimmed). 

622 ampFullData = exposure.image[bboxFull] 

623 

624 # 2. Add dark current (electron) to imaging portion of the amp. 

625 if self.config.doAddDark or self.config.doAddDarkNoiseOnly: 

626 if self.config.doAddDarkNoiseOnly: 

627 darkLevel = 0.0 

628 else: 

629 darkLevel = self.config.darkRate * self.config.darkTime 

630 if self.config.calibMode: 

631 darkNoise = 0.0 

632 else: 

633 darkNoise = np.sqrt(self.config.darkRate * self.config.darkTime) 

634 

635 self.amplifierAddNoise(ampImageData, darkLevel, darkNoise, rng=rngDark) 

636 

637 # 3. Add BF effect (electron) to imaging portion of the amp. 

638 if self.config.doAddBrighterFatter is True: 

639 self.amplifierAddBrighterFatter( 

640 ampImageData, 

641 rngBrighterFatter, 

642 self.config.bfStrength, 

643 self.config.nRecalc, 

644 ) 

645 

646 # 4. Add serial CTI (electron) to amplifier (imaging + overscan). 

647 if self.config.doAddDeferredCharge: 

648 # Get the free charge area for the amplifier. 

649 self.amplifierAddDeferredCharge(exposure, amp) 

650 

651 # 5. Add 2D bias residual (electron) to imaging portion of the amp. 

652 if self.config.doAdd2DBias: 

653 # For now we use an unstructured noise field to add some 

654 # consistent 2D bias residual that can be subtracted. In 

655 # the future this can be made into a warm corner (for example). 

656 self.amplifierAddNoise( 

657 ampImageData, 

658 0.0, 

659 self.config.noise2DBias, 

660 rng=rng2DBias, 

661 ) 

662 

663 # 6. Add clock-injected offset (electron) to amplifer 

664 # (imaging + overscan). 

665 # This is just an offset that will be crosstalked and modified by 

666 # the gain, and does not have a noise associated with it. 

667 if self.config.doAddClockInjectedOffset: 

668 self.amplifierAddNoise( 

669 ampFullData, 

670 self.config.clockInjectedOffsetLevel, 

671 0.0, 

672 ) 

673 

674 # 7./8. Add serial and parallel overscan slopes (electron) 

675 # (imaging + overscan) 

676 if (self.config.doAddParallelOverscanRamp or self.config.doAddSerialOverscanRamp) and \ 

677 not self.config.isTrimmed: 

678 

679 if self.config.doAddParallelOverscanRamp: 

680 # Apply gradient along the X axis. 

681 self.amplifierAddXGradient(ampFullData, -1.0 * self.config.overscanScale, 

682 1.0 * self.config.overscanScale) 

683 

684 if self.config.doAddSerialOverscanRamp: 

685 # Apply the gradient along the Y axis. 

686 self.amplifierAddYGradient(ampFullData, -1.0 * self.config.overscanScale, 

687 1.0 * self.config.overscanScale) 

688 

689 # 9. Add non-linearity (electron) to amplifier 

690 # (imaging + overscan). 

691 if self.config.doAddHighSignalNonlinearity: 

692 # The linearizer coefficients come from makeLinearizer(). 

693 if linearizer.linearityType[amp.getName()] != "Spline": 

694 raise RuntimeError("IsrMockLSST only supports spline non-linearity.") 

695 

696 coeffs = linearizer.linearityCoeffs[amp.getName()] 

697 centers, values = np.split(coeffs, 2) 

698 

699 # This is an application of high signal non-linearity, so we 

700 # set the lower values to 0.0 (this cut is arbitrary). 

701 values[centers < self.config.highSignalNonlinearityThreshold] = 0.0 

702 

703 # The linearizer is units of adu, so convert to electron 

704 centers *= self.config.gainDict[amp.getName()] 

705 values *= self.config.gainDict[amp.getName()] 

706 

707 # Note that the linearity spline is in "overscan subtracted" 

708 # units so needs to be applied without the clock-injected 

709 # offset. 

710 self.amplifierAddNonlinearity( 

711 ampFullData, 

712 centers, 

713 values, 

714 self.config.clockInjectedOffsetLevel if self.config.doAddClockInjectedOffset else 0.0, 

715 ) 

716 

717 # 10. Add read noise (electron) to the amplifier 

718 # (imaging + overscan). 

719 # Unsure if this should be before or after crosstalk. 

720 # Probably some of both; hopefully doesn't matter. 

721 if not self.config.calibMode: 

722 # Add read noise to the imaging region. 

723 self.amplifierAddNoise( 

724 ampImageData, 

725 0.0, 

726 self.config.readNoise, 

727 rng=rngReadNoise, 

728 ) 

729 

730 # If not trimmed, add to the overscan regions. 

731 if not self.config.isTrimmed: 

732 parallelOverscanBBox = amp.getRawParallelOverscanBBox() 

733 parallelOverscanData = exposure.image[parallelOverscanBBox] 

734 

735 serialOverscanBBox = self.getFullSerialOverscanBBox(amp) 

736 serialOverscanData = exposure.image[serialOverscanBBox] 

737 

738 # Add read noise of mean 0 

739 # to the parallel and serial overscan regions. 

740 self.amplifierAddNoise( 

741 parallelOverscanData, 

742 0.0, 

743 self.config.readNoise, 

744 rng=rngOverscan, 

745 ) 

746 self.amplifierAddNoise( 

747 serialOverscanData, 

748 0.0, 

749 self.config.readNoise, 

750 rng=rngOverscan, 

751 ) 

752 

753 # 7b. Add bad column to the parallel overscan region. 

754 if self.config.doAddBadParallelOverscanColumn and not self.config.isTrimmed: 

755 # We want to place this right above the defect, to simulate 

756 # bleeding into the parallel overscan region. 

757 amp = exposure.getDetector()[2] 

758 parBBox = amp.getRawParallelOverscanBBox() 

759 bboxBad = geom.Box2I( 

760 corner=geom.Point2I(50, parBBox.getMinY()), 

761 dimensions=geom.Extent2I(1, parBBox.getHeight()), 

762 ) 

763 exposure[bboxBad].image.array[:, :] += self.config.badParallelOverscanColumnLevel 

764 

765 if self.config.doAddBadParallelOverscanColumnNeighbors: 

766 for neighbor in [49, 51]: 

767 bboxBad = geom.Box2I( 

768 corner=geom.Point2I(neighbor, parBBox.getMinY()), 

769 dimensions=geom.Extent2I(1, parBBox.getHeight()), 

770 ) 

771 exposure[bboxBad].image.array[:, :] += self.config.badParallelOverscanColumnNeighborsLevel 

772 

773 # 11. Add crosstalk (electron) to all the amplifiers 

774 # (imaging + overscan). 

775 if self.config.doAddCrosstalk: 

776 ctCalib = CrosstalkCalib() 

777 # We use the regular subtractCrosstalk code but with a negative 

778 # sign on the crosstalk coefficients so it adds instead of 

779 # subtracts. We only apply the signal plane (ignoreVariance, 

780 # subtrahendMasking) with a very large pixel to mask to ensure 

781 # no crosstalk mask bits are set. 

782 ctCalib.subtractCrosstalk( 

783 exposure, 

784 crosstalkCoeffs=-1*self.crosstalkCoeffs, 

785 doSubtrahendMasking=True, 

786 minPixelToMask=np.inf, 

787 ignoreVariance=True, 

788 fullAmplifier=True, 

789 ) 

790 

791 for amp in exposure.getDetector(): 

792 # Get image bbox and data (again). 

793 bbox = None 

794 if self.config.isTrimmed: 

795 bbox = amp.getBBox() 

796 bboxFull = bbox 

797 else: 

798 bbox = amp.getRawDataBBox() 

799 bboxFull = amp.getRawBBox() 

800 

801 # This is the image data (excluding pre/overscans). 

802 ampImageData = exposure.image[bbox] 

803 # This is the full data (including pre/overscans if untrimmed). 

804 ampFullData = exposure.image[bboxFull] 

805 

806 # 12. Gain un-normalize (from electron to floating point adu) 

807 if self.config.doApplyGain: 

808 gain = self.config.gainDict.get(amp.getName(), self.config.gain) 

809 self.applyGain(ampFullData, gain) 

810 

811 # 13. Add overall bias level (adu) to the amplifier 

812 # (imaging + overscan) 

813 if self.config.doAddBias: 

814 self.addBiasLevel(ampFullData, self.config.biasLevel) 

815 

816 # 14. Round/Truncate to integers (adu) 

817 if self.config.doRoundAdu: 

818 self.roundADU(ampFullData) 

819 

820 # Add units metadata to calibrations. 

821 if self.config.calibMode: 

822 if self.config.doApplyGain: 

823 exposure.metadata["LSST ISR UNITS"] = "adu" 

824 else: 

825 exposure.metadata["LSST ISR UNITS"] = "electron" 

826 

827 # Add a variance plane appropriate for a calibration frame. 

828 # We take the absolute value for biases which have no signal. 

829 exposure.variance.array[:, :] = np.abs(np.median(exposure.image.array)/10.) 

830 

831 exposure.metadata["BSSVBS"] = 50.0 

832 exposure.metadata["HVBIAS"] = "ON" 

833 

834 if self.config.doGenerateAmpDict: 

835 expDict = dict() 

836 for amp in exposure.getDetector(): 

837 expDict[amp.getName()] = exposure 

838 return expDict 

839 else: 

840 return exposure 

841 

842 def addBiasLevel(self, ampData, biasLevel): 

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

844 

845 Parameters 

846 ---------- 

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

848 Amplifier image to operate on. 

849 biasLevel : `float` 

850 Bias level to be added to the image. 

851 """ 

852 ampArr = ampData.array 

853 ampArr[:] = ampArr[:] + biasLevel 

854 

855 def makeDefectList(self, isTrimmed=True): 

856 """Generate a simple defect list. 

857 

858 Parameters 

859 ---------- 

860 isTrimmed : `bool`, optional 

861 Return defects in trimmed coordinates? 

862 

863 Returns 

864 ------- 

865 defectList : `lsst.meas.algorithms.Defects` 

866 Simulated defect list 

867 """ 

868 defectBoxesUntrimmed = [ 

869 geom.Box2I( 

870 geom.Point2I(50, 118), 

871 geom.Extent2I(1, 51), 

872 ), 

873 ] 

874 

875 if not isTrimmed: 

876 return Defects(defectBoxesUntrimmed) 

877 

878 # If trimmed, we need to convert. 

879 tempExp = self.getExposure(isTrimmed=False) 

880 tempExp.image.array[:, :] = 0.0 

881 for bbox in defectBoxesUntrimmed: 

882 tempExp.image[bbox] = 1.0 

883 

884 assembledExp = self.assembleCcd.assembleCcd(tempExp) 

885 

886 # Use thresholding code to find defect footprints/boxes. 

887 threshold = afwDetection.createThreshold(1.0, "value", polarity=True) 

888 footprintSet = afwDetection.FootprintSet(assembledExp.image, threshold) 

889 

890 return Defects.fromFootprintList(footprintSet.getFootprints()) 

891 

892 def makeBfKernel(self): 

893 """Generate a simple simulated brighter-fatter kernel. 

894 Returns 

895 ------- 

896 kernel : `lsst.ip.isr.BrighterFatterKernel` 

897 Simulated brighter-fatter kernel. 

898 """ 

899 bfkArray = super().makeBfKernel() 

900 bfKernelObject = BrighterFatterKernel() 

901 bfKernelObject.level = 'AMP' 

902 bfKernelObject.gain = self.config.gainDict 

903 

904 for amp in self.getExposure().getDetector(): 

905 # Kernel must be in (y,x) orientation 

906 bfKernelObject.ampKernels[amp.getName()] = bfkArray.T 

907 bfKernelObject.valid[amp.getName()] = True 

908 

909 return bfKernelObject 

910 

911 def makeDeferredChargeCalib(self): 

912 """Generate a CTI calibration. 

913 

914 Returns 

915 ------- 

916 cti : `lsst.ip.isr.deferredCharge.DeferredChargeCalib` 

917 Simulated deferred charge calibration. 

918 """ 

919 

920 metadataDict = {'metadata': PropertyList()} 

921 metadataDict['metadata'].add(name="OBSTYPE", value="CTI") 

922 metadataDict['metadata'].add(name="CALIBCLS", 

923 value="lsst.ip.isr.deferredCharge.DeferredChargeCalib") 

924 self.ctiCalibDict = {**metadataDict, **self.ctiCalibDict} 

925 deferredChargeCalib = DeferredChargeCalib() 

926 self.cti = deferredChargeCalib.fromDict(self.ctiCalibDict) 

927 

928 return self.cti 

929 

930 def amplifierAddBrighterFatter(self, ampImageData, rng, bfStrength, nRecalc): 

931 """Add brighter fatter effect and/or diffusion to the image. 

932 Parameters 

933 ---------- 

934 ampImageData : `lsst.afw.image.ImageF` 

935 Amplifier image to operate on. 

936 rng : `galsim.BaseDeviate` 

937 Random number generator. 

938 bfStrength : `float` 

939 Scaling parameter of the brighter fatter effect (nominally = 1) 

940 nRecalc: 'int' 

941 The number of electrons to accumulate before recalculating the 

942 distortion of the pixel shapes. 

943 """ 

944 

945 incidentImage = galsim.Image(ampImageData.array, scale=1) 

946 measuredImage = galsim.ImageF( 

947 ampImageData.array.shape[1], 

948 ampImageData.array.shape[0], 

949 scale=1, 

950 ) 

951 photons = galsim.PhotonArray.makeFromImage(incidentImage) 

952 

953 sensorModel = galsim.SiliconSensor( 

954 strength=bfStrength, 

955 rng=rng, 

956 diffusion_factor=0.0, 

957 nrecalc=nRecalc, 

958 ) 

959 

960 totalFluxAdded = sensorModel.accumulate(photons, measuredImage) 

961 ampImageData.array = measuredImage.array 

962 

963 return totalFluxAdded 

964 

965 def amplifierAddDeferredCharge(self, exposure, amp): 

966 """Add serial CTI to the amplifier data. 

967 

968 Parameters 

969 ---------- 

970 exposure : `lsst.afw.image.ExposureF` 

971 The exposure object containing the amplifier 

972 to apply deferred charge to. 

973 amp : `lsst.afw.image.Amplifier` 

974 The amplifier object (contains geometry info). 

975 """ 

976 # Get the amplifier's geometry parameters. 

977 # When adding deferred charge, we have already assured that 

978 # isTrimmed is False. Therefore we want to make sure that we 

979 # get the RawDataBBox. 

980 readoutCorner = amp.getReadoutCorner() 

981 prescanWidth = amp.getRawHorizontalPrescanBBox().getWidth() 

982 serialOverscanWidth = amp.getRawHorizontalOverscanBBox().getWidth() 

983 parallelOverscanWidth = amp.getRawVerticalOverscanBBox().getHeight() 

984 bboxFreeCharge = amp.getRawDataBBox() 

985 bboxFreeCharge = bboxFreeCharge.expandedTo(amp.getRawHorizontalOverscanBBox()) 

986 bboxFreeCharge = bboxFreeCharge.expandedTo(amp.getRawHorizontalPrescanBBox()) 

987 bboxFreeCharge = bboxFreeCharge.expandedTo(amp.getRawVerticalOverscanBBox()) 

988 

989 ampFreeChargeData = exposure.image[bboxFreeCharge] 

990 ampImageData = exposure.image[amp.getRawDataBBox()] 

991 

992 # Get the deferred charge parameters for this amplifier. 

993 cti = self.deferredChargeCalib.globalCti[amp.getName()] 

994 traps = self.deferredChargeCalib.serialTraps[amp.getName()] 

995 driftScale = self.deferredChargeCalib.driftScale[amp.getName()] 

996 decayTime = self.deferredChargeCalib.decayTime[amp.getName()] 

997 

998 # Create a fake amplifier object that contains some deferred charge 

999 # paramters. 

1000 floatingOutputAmplifier = FloatingOutputAmplifier( 

1001 gain=1.0, # Everything is already in electrons. 

1002 scale=driftScale, 

1003 decay_time=decayTime, 

1004 noise=0.0, 

1005 offset=0.0, 

1006 ) 

1007 

1008 def flipImage(arr, readoutCorner): 

1009 # Flip an array so that the readout corner is in 

1010 # the lower left. 

1011 if readoutCorner == ReadoutCorner.LR: 

1012 return np.fliplr(arr) 

1013 elif readoutCorner == ReadoutCorner.UR: 

1014 return np.fliplr(np.flipud(arr)) 

1015 elif readoutCorner == ReadoutCorner.UL: 

1016 return np.flipud(arr) 

1017 else: 

1018 pass 

1019 

1020 return arr 

1021 

1022 # The algorithm expects that the readout corner is in 

1023 # the lower left corner. Flip it to be so: 

1024 imageData = ampImageData.array 

1025 imageData = flipImage(imageData, readoutCorner) 

1026 

1027 # Simulate the amplifier. 

1028 ampSim = SegmentSimulator( 

1029 imarr=imageData, 

1030 prescan_width=prescanWidth, 

1031 output_amplifier=floatingOutputAmplifier, 

1032 cti=cti, 

1033 traps=traps, 

1034 ) 

1035 

1036 # Simulate deferred charge! 

1037 # Note that the readout() method uses the image region data and the 

1038 # overscan dimensions provided as input parameters. It then creates 

1039 # overscan nd adds it to the image data to create the raw image. 

1040 result = ampSim.readout( 

1041 serial_overscan_width=serialOverscanWidth, 

1042 parallel_overscan_width=parallelOverscanWidth, 

1043 ) 

1044 

1045 # Flip the image back to the original orientation. 

1046 result = flipImage(result, readoutCorner) 

1047 

1048 # Set the image with the deferred charge added. 

1049 ampFreeChargeData.array[:, :] = result 

1050 

1051 def makeLinearizer(self): 

1052 # docstring inherited. 

1053 

1054 # The linearizer has units of adu. 

1055 nNodes = 10 

1056 # Set this to just above the mock saturation (adu) 

1057 maxADU = 101_000 

1058 nonLinSplineNodes = np.linspace(0, maxADU, nNodes) 

1059 # These values come from cp_pipe/tests/test_linearity.py and 

1060 # are based on a test fit to LSSTCam data, run 7193D, detector 22, 

1061 # amp C00. 

1062 nonLinSplineValues = np.array( 

1063 [0.0, -8.87, 1.46, 1.69, -6.92, -68.23, -78.01, -11.56, 80.26, 185.01] 

1064 ) 

1065 

1066 if self.config.doAddHighSignalNonlinearity and not self.config.doAddLowSignalNonlinearity: 

1067 nonLinSplineValues[nonLinSplineNodes < self.config.highSignalNonlinearityThreshold] = 0.0 

1068 elif self.config.doAddLowSignalNonlinearity: 

1069 raise NotImplementedError("Low signal non-linearity is not implemented.") 

1070 

1071 exp = self.getExposure() 

1072 detector = exp.getDetector() 

1073 

1074 linearizer = Linearizer(detector=detector) 

1075 linearizer.updateMetadataFromExposures([exp]) 

1076 

1077 # We need to set override by hand because we are constructing a 

1078 # linearizer manually and not from a serialized object. 

1079 linearizer.override = True 

1080 linearizer.hasLinearity = True 

1081 linearizer.validate() 

1082 linearizer.updateMetadata(camera=self.getCamera(), detector=detector, filterName='NONE') 

1083 linearizer.updateMetadata(setDate=True, setCalibId=True) 

1084 

1085 for amp in detector: 

1086 ampName = amp.getName() 

1087 linearizer.linearityType[ampName] = "Spline" 

1088 linearizer.linearityCoeffs[ampName] = np.concatenate([nonLinSplineNodes, nonLinSplineValues]) 

1089 # We need to specify the raw bbox here. 

1090 linearizer.linearityBBox[ampName] = amp.getRawBBox() 

1091 

1092 return linearizer 

1093 

1094 def amplifierAddNonlinearity(self, ampData, centers, values, offset): 

1095 """Add non-linearity to amplifier data. 

1096 

1097 Parameters 

1098 ---------- 

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

1100 Amplifier image to operate on. 

1101 centers : `np.ndarray` 

1102 Spline nodes. 

1103 values : `np.ndarray` 

1104 Spline values. 

1105 offset : `float` 

1106 Offset zero-point between linearizer (internal vs external). 

1107 """ 

1108 # I'm not sure what to do about negative values... 

1109 

1110 # Note that we are using the afw AKIMA_SPLINE to offset the 

1111 # data but using the equivalent but faster scipy Akima1DInterpolator to 

1112 # correct the data. 

1113 spl = afwMath.makeInterpolate( 

1114 centers, 

1115 values, 

1116 afwMath.stringToInterpStyle("AKIMA_SPLINE"), 

1117 ) 

1118 

1119 delta = np.asarray(spl.interpolate(ampData.array.ravel() - offset)) 

1120 

1121 ampData.array[:, :] += delta.reshape(ampData.array.shape) 

1122 

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

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

1125 

1126 Parameters 

1127 ---------- 

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

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

1130 transforms. 

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

1132 Amplifier image to operate on. 

1133 fracDrop : `float` 

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

1135 u0 : `float` 

1136 Peak location in detector coordinates. 

1137 v0 : `float` 

1138 Peak location in detector coordinates. 

1139 """ 

1140 if fracDrop >= 1.0: 

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

1142 

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

1144 

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

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

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

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

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

1150 

1151 def applyGain(self, ampData, gain): 

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

1153 This method divides the data by the gain 

1154 because the mocks need to convert the data in electron to adu, 

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

1156 

1157 Parameters 

1158 ---------- 

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

1160 Amplifier image to operate on. 

1161 gain : `float` 

1162 Gain value in electron/adu. 

1163 """ 

1164 ampArr = ampData.array 

1165 ampArr[:] = ampArr[:] / gain 

1166 

1167 def roundADU(self, ampData): 

1168 """Round adu to nearest integer. 

1169 

1170 Parameters 

1171 ---------- 

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

1173 Amplifier image to operate on. 

1174 """ 

1175 ampArr = ampData.array 

1176 ampArr[:] = np.around(ampArr) 

1177 

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

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

1180 

1181 This method operates in the amplifier coordinate frame. 

1182 

1183 Parameters 

1184 ---------- 

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

1186 Amplifier image to operate on. 

1187 start : `float` 

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

1189 end : `float` 

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

1191 """ 

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

1193 ampArr = ampData.array 

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

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

1196 

1197 def getFullSerialOverscanBBox(self, amp): 

1198 """Get the full serial overscan bounding box from an amplifier. 

1199 

1200 This includes the serial/parallel overscan region. 

1201 

1202 Parameters 

1203 ---------- 

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

1205 Amplifier to operate on. 

1206 

1207 Returns 

1208 ------- 

1209 bbox : `lsst.geom.Box2I` 

1210 """ 

1211 # This only works for untrimmed data. 

1212 bbox = amp.getRawDataBBox() 

1213 

1214 parallelOverscanBBox = amp.getRawParallelOverscanBBox() 

1215 grownImageBBox = bbox.expandedTo(parallelOverscanBBox) 

1216 

1217 serialOverscanBBox = amp.getRawSerialOverscanBBox() 

1218 # Extend the serial overscan bbox to include corners 

1219 serialOverscanBBox = geom.Box2I( 

1220 geom.Point2I(serialOverscanBBox.getMinX(), 

1221 grownImageBBox.getMinY()), 

1222 geom.Extent2I(serialOverscanBBox.getWidth(), 

1223 grownImageBBox.getHeight())) 

1224 

1225 return serialOverscanBBox 

1226 

1227 

1228class RawMockLSST(IsrMockLSST): 

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

1230 """ 

1231 def __init__(self, **kwargs): 

1232 super().__init__(**kwargs) 

1233 self.config.isTrimmed = False 

1234 self.config.doGenerateImage = True 

1235 self.config.doGenerateAmpDict = False 

1236 

1237 # Add astro effects 

1238 self.config.doAddSky = True 

1239 self.config.doAddSource = True 

1240 

1241 # Add optical effects 

1242 self.config.doAddFringe = True 

1243 

1244 # Add instrument effects 

1245 self.config.doAddParallelOverscanRamp = True 

1246 self.config.doAddSerialOverscanRamp = True 

1247 self.config.doAddCrosstalk = True 

1248 self.config.doAddBias = True 

1249 self.config.doAddDark = True 

1250 

1251 self.config.doAddFlat = True 

1252 

1253 

1254class TrimmedRawMockLSST(RawMockLSST): 

1255 """Generate a trimmed raw exposure. 

1256 """ 

1257 def __init__(self, **kwargs): 

1258 super().__init__(**kwargs) 

1259 self.config.isTrimmed = True 

1260 self.config.doAddParallelOverscanRamp = False 

1261 self.config.doAddSerialOverscanRamp = False 

1262 

1263 

1264class CalibratedRawMockLSST(RawMockLSST): 

1265 """Generate a trimmed raw exposure. 

1266 

1267 This represents a "truth" image that can be compared to a 

1268 post-ISR cleaned image. 

1269 """ 

1270 def __init__(self, **kwargs): 

1271 super().__init__(**kwargs) 

1272 self.config.isTrimmed = True 

1273 self.config.doGenerateImage = True 

1274 

1275 self.config.doAddSky = True 

1276 self.config.doAddSource = True 

1277 

1278 self.config.doAddFringe = True 

1279 

1280 self.config.doAddParallelOverscanRamp = False 

1281 self.config.doAddSerialOverscanRamp = False 

1282 self.config.doAddCrosstalk = False 

1283 self.config.doAddBias = False 

1284 self.config.doAdd2DBias = False 

1285 self.config.doAddDark = False 

1286 self.config.doApplyGain = False 

1287 self.config.doAddFlat = False 

1288 self.config.doAddClockInjectedOffset = False 

1289 

1290 self.config.biasLevel = 0.0 

1291 # Assume combined calibrations are made with 16 inputs. 

1292 self.config.readNoise *= 0.25 

1293 

1294 self.config.doRoundAdu = False 

1295 

1296 

1297class ReferenceMockLSST(IsrMockLSST): 

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

1299 """ 

1300 def __init__(self, **kwargs): 

1301 # If we want the calibration in adu units, we need to apply 

1302 # the gain. Default is electron units, so do not apply the gain. 

1303 doApplyGain = kwargs.pop("adu", False) 

1304 

1305 super().__init__(**kwargs) 

1306 self.config.isTrimmed = True 

1307 self.config.doGenerateImage = True 

1308 

1309 self.config.calibMode = True 

1310 

1311 self.config.doAddSky = False 

1312 self.config.doAddSource = False 

1313 

1314 self.config.doAddFringe = False 

1315 

1316 self.config.doAddParallelOverscanRamp = False 

1317 self.config.doAddSerialOverscanRamp = False 

1318 self.config.doAddCrosstalk = False 

1319 self.config.doAddBias = False 

1320 self.config.doAdd2DBias = False 

1321 self.config.doAddDark = False 

1322 self.config.doApplyGain = doApplyGain 

1323 self.config.doAddFlat = False 

1324 self.config.doAddClockInjectedOffset = False 

1325 

1326 # Reference calibrations are not integerized. 

1327 self.config.doRoundAdu = False 

1328 

1329 

1330# Classes to generate calibration products mocks. 

1331class DarkMockLSST(ReferenceMockLSST): 

1332 """Simulated reference dark calibration. 

1333 """ 

1334 def __init__(self, **kwargs): 

1335 super().__init__(**kwargs) 

1336 self.config.doAddDark = True 

1337 self.config.darkTime = 1.0 

1338 

1339 

1340class BiasMockLSST(ReferenceMockLSST): 

1341 """Simulated combined bias calibration. 

1342 """ 

1343 def __init__(self, **kwargs): 

1344 super().__init__(**kwargs) 

1345 # This is a "2D bias residual" frame which has only 

1346 # the 2D bias in it. 

1347 self.config.doAdd2DBias = True 

1348 

1349 

1350class FlatMockLSST(ReferenceMockLSST): 

1351 """Simulated reference flat calibration. 

1352 """ 

1353 def __init__(self, **kwargs): 

1354 super().__init__(**kwargs) 

1355 self.config.doAddFlat = True 

1356 

1357 

1358class FringeMockLSST(ReferenceMockLSST): 

1359 """Simulated reference fringe calibration. 

1360 """ 

1361 def __init__(self, **kwargs): 

1362 super().__init__(**kwargs) 

1363 self.config.doAddFringe = True 

1364 

1365 

1366class BfKernelMockLSST(IsrMockLSST): 

1367 """Simulated brighter-fatter kernel. 

1368 """ 

1369 def __init__(self, **kwargs): 

1370 super().__init__(**kwargs) 

1371 self.config.doGenerateImage = False 

1372 self.config.doGenerateData = True 

1373 

1374 self.config.doBrighterFatter = True 

1375 self.config.doDefects = False 

1376 self.config.doCrosstalkCoeffs = False 

1377 self.config.doTransmissionCurve = False 

1378 self.config.doLinearizer = False 

1379 

1380 

1381class DeferredChargeMockLSST(IsrMockLSST): 

1382 """Simulated deferred charge calibration. 

1383 """ 

1384 def __init__(self, **kwargs): 

1385 super().__init__(**kwargs) 

1386 self.config.doGenerateImage = False 

1387 self.config.doGenerateData = True 

1388 self.config.doDeferredCharge = True 

1389 self.config.doDefects = False 

1390 self.config.doCrosstalkCoeffs = False 

1391 self.config.doTransmissionCurve = False 

1392 

1393 

1394class DefectMockLSST(IsrMockLSST): 

1395 """Simulated defect list. 

1396 """ 

1397 def __init__(self, **kwargs): 

1398 super().__init__(**kwargs) 

1399 self.config.doGenerateImage = False 

1400 self.config.doGenerateData = True 

1401 

1402 self.config.doBrighterFatter = False 

1403 self.config.doDefects = True 

1404 self.config.doCrosstalkCoeffs = False 

1405 self.config.doTransmissionCurve = False 

1406 self.config.doLinearizer = False 

1407 

1408 

1409class CrosstalkCoeffMockLSST(IsrMockLSST): 

1410 """Simulated crosstalk coefficient matrix. 

1411 """ 

1412 def __init__(self, **kwargs): 

1413 super().__init__(**kwargs) 

1414 self.config.doGenerateImage = False 

1415 self.config.doGenerateData = True 

1416 

1417 self.config.doBrighterFatter = False 

1418 self.config.doDefects = False 

1419 self.config.doCrosstalkCoeffs = True 

1420 self.config.doTransmissionCurve = False 

1421 self.config.doLinearizer = False 

1422 

1423 

1424class LinearizerMockLSST(IsrMockLSST): 

1425 """Simulated linearizer. 

1426 """ 

1427 def __init__(self, **kwargs): 

1428 super().__init__(**kwargs) 

1429 self.config.doGenerateImage = False 

1430 self.config.doGenerateData = True 

1431 

1432 self.config.doBrighterFatter = False 

1433 self.config.doDefects = False 

1434 self.config.doCrosstalkCoeffs = False 

1435 self.config.doTransmissionCurve = False 

1436 self.config.doLinearizer = True 

1437 

1438 

1439class TransmissionMockLSST(IsrMockLSST): 

1440 """Simulated transmission curve. 

1441 """ 

1442 def __init__(self, **kwargs): 

1443 super().__init__(**kwargs) 

1444 self.config.doGenerateImage = False 

1445 self.config.doGenerateData = True 

1446 

1447 self.config.doBrighterFatter = False 

1448 self.config.doDefects = False 

1449 self.config.doCrosstalkCoeffs = False 

1450 self.config.doTransmissionCurve = True 

1451 self.config.doLinearizer = False