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
« 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/>.
22__all__ = ["IsrMockLSSTConfig", "IsrMockLSST", "RawMockLSST",
23 "CalibratedRawMockLSST", "ReferenceMockLSST",
24 "BiasMockLSST", "DarkMockLSST", "FlatMockLSST", "FringeMockLSST",
25 "BfKernelMockLSST", "DefectMockLSST", "CrosstalkCoeffMockLSST",
26 "TransmissionMockLSST"]
28import numpy as np
29import galsim
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)
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 )
197 def validate(self):
198 super().validate()
200 if self.doAddLowSignalNonlinearity:
201 raise NotImplementedError("Low signal non-linearity is not implemented.")
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.")
207 def setDefaults(self):
208 super().setDefaults()
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
220class IsrMockLSST(IsrMock):
221 """Class to generate consistent mock images for ISR testing.
222 """
223 ConfigClass = IsrMockLSSTConfig
224 _DefaultName = "isrMockLSST"
226 def __init__(self, **kwargs):
227 super().__init__(**kwargs)
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
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])
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}}}
483 self.deferredChargeCalib = self.makeDeferredChargeCalib()
485 # Cross-talk coeffs are defined in the parent class.
487 self.makeSubtask("assembleCcd")
489 def run(self):
490 """Generate a mock ISR product following LSSTCam ISR, and return it.
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.
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
515 def makeImage(self):
516 """Generate a simulated ISR LSST image.
518 Returns
519 -------
520 exposure : `lsst.afw.image.Exposure` or `dict`
521 Simulated ISR image data.
523 Notes
524 -----
525 This method constructs a "raw" data image.
526 """
527 exposure = self.getExposure()
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)
538 # Create the linearizer if we will need it.
539 if self.config.doAddHighSignalNonlinearity:
540 linearizer = LinearizerMockLSST().run()
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()
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]
560 # Astrophysical signals are all in electron (e-).
561 # These are only applied to the imaging portion of the
562 # amplifier (ampImageData)
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 )
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)
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))
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)
600 # On-chip electronic effects.
602 # 1. Add bright defect(s).
603 if self.config.doAddBrightDefects:
604 defectList = self.makeDefectList(isTrimmed=self.config.isTrimmed)
606 for defect in defectList:
607 exposure.image[defect.getBBox()] = self.config.brightDefectLevel
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()
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]
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)
635 self.amplifierAddNoise(ampImageData, darkLevel, darkNoise, rng=rngDark)
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 )
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)
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 )
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 )
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:
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)
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)
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.")
696 coeffs = linearizer.linearityCoeffs[amp.getName()]
697 centers, values = np.split(coeffs, 2)
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
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()]
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 )
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 )
730 # If not trimmed, add to the overscan regions.
731 if not self.config.isTrimmed:
732 parallelOverscanBBox = amp.getRawParallelOverscanBBox()
733 parallelOverscanData = exposure.image[parallelOverscanBBox]
735 serialOverscanBBox = self.getFullSerialOverscanBBox(amp)
736 serialOverscanData = exposure.image[serialOverscanBBox]
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 )
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
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
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 )
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()
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]
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)
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)
816 # 14. Round/Truncate to integers (adu)
817 if self.config.doRoundAdu:
818 self.roundADU(ampFullData)
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"
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.)
831 exposure.metadata["BSSVBS"] = 50.0
832 exposure.metadata["HVBIAS"] = "ON"
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
842 def addBiasLevel(self, ampData, biasLevel):
843 """Add bias level to an amplifier's image data.
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
855 def makeDefectList(self, isTrimmed=True):
856 """Generate a simple defect list.
858 Parameters
859 ----------
860 isTrimmed : `bool`, optional
861 Return defects in trimmed coordinates?
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 ]
875 if not isTrimmed:
876 return Defects(defectBoxesUntrimmed)
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
884 assembledExp = self.assembleCcd.assembleCcd(tempExp)
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)
890 return Defects.fromFootprintList(footprintSet.getFootprints())
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
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
909 return bfKernelObject
911 def makeDeferredChargeCalib(self):
912 """Generate a CTI calibration.
914 Returns
915 -------
916 cti : `lsst.ip.isr.deferredCharge.DeferredChargeCalib`
917 Simulated deferred charge calibration.
918 """
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)
928 return self.cti
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 """
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)
953 sensorModel = galsim.SiliconSensor(
954 strength=bfStrength,
955 rng=rng,
956 diffusion_factor=0.0,
957 nrecalc=nRecalc,
958 )
960 totalFluxAdded = sensorModel.accumulate(photons, measuredImage)
961 ampImageData.array = measuredImage.array
963 return totalFluxAdded
965 def amplifierAddDeferredCharge(self, exposure, amp):
966 """Add serial CTI to the amplifier data.
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())
989 ampFreeChargeData = exposure.image[bboxFreeCharge]
990 ampImageData = exposure.image[amp.getRawDataBBox()]
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()]
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 )
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
1020 return arr
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)
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 )
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 )
1045 # Flip the image back to the original orientation.
1046 result = flipImage(result, readoutCorner)
1048 # Set the image with the deferred charge added.
1049 ampFreeChargeData.array[:, :] = result
1051 def makeLinearizer(self):
1052 # docstring inherited.
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 )
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.")
1071 exp = self.getExposure()
1072 detector = exp.getDetector()
1074 linearizer = Linearizer(detector=detector)
1075 linearizer.updateMetadataFromExposures([exp])
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)
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()
1092 return linearizer
1094 def amplifierAddNonlinearity(self, ampData, centers, values, offset):
1095 """Add non-linearity to amplifier data.
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...
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 )
1119 delta = np.asarray(spl.interpolate(ampData.array.ravel() - offset))
1121 ampData.array[:, :] += delta.reshape(ampData.array.shape)
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.
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")
1143 sigma = u0 / np.sqrt(2.0 * fracDrop)
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)
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.
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
1167 def roundADU(self, ampData):
1168 """Round adu to nearest integer.
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)
1178 def amplifierAddXGradient(self, ampData, start, end):
1179 """Add a x-axis linear gradient to an amplifier's image data.
1181 This method operates in the amplifier coordinate frame.
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())
1197 def getFullSerialOverscanBBox(self, amp):
1198 """Get the full serial overscan bounding box from an amplifier.
1200 This includes the serial/parallel overscan region.
1202 Parameters
1203 ----------
1204 amp : `lsst.afw.ampInfo.AmpInfoRecord`
1205 Amplifier to operate on.
1207 Returns
1208 -------
1209 bbox : `lsst.geom.Box2I`
1210 """
1211 # This only works for untrimmed data.
1212 bbox = amp.getRawDataBBox()
1214 parallelOverscanBBox = amp.getRawParallelOverscanBBox()
1215 grownImageBBox = bbox.expandedTo(parallelOverscanBBox)
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()))
1225 return serialOverscanBBox
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
1237 # Add astro effects
1238 self.config.doAddSky = True
1239 self.config.doAddSource = True
1241 # Add optical effects
1242 self.config.doAddFringe = True
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
1251 self.config.doAddFlat = True
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
1264class CalibratedRawMockLSST(RawMockLSST):
1265 """Generate a trimmed raw exposure.
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
1275 self.config.doAddSky = True
1276 self.config.doAddSource = True
1278 self.config.doAddFringe = True
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
1290 self.config.biasLevel = 0.0
1291 # Assume combined calibrations are made with 16 inputs.
1292 self.config.readNoise *= 0.25
1294 self.config.doRoundAdu = False
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)
1305 super().__init__(**kwargs)
1306 self.config.isTrimmed = True
1307 self.config.doGenerateImage = True
1309 self.config.calibMode = True
1311 self.config.doAddSky = False
1312 self.config.doAddSource = False
1314 self.config.doAddFringe = False
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
1326 # Reference calibrations are not integerized.
1327 self.config.doRoundAdu = False
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
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
1350class FlatMockLSST(ReferenceMockLSST):
1351 """Simulated reference flat calibration.
1352 """
1353 def __init__(self, **kwargs):
1354 super().__init__(**kwargs)
1355 self.config.doAddFlat = True
1358class FringeMockLSST(ReferenceMockLSST):
1359 """Simulated reference fringe calibration.
1360 """
1361 def __init__(self, **kwargs):
1362 super().__init__(**kwargs)
1363 self.config.doAddFringe = True
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
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
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
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
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
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
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
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
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
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
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