LSST Applications g00d0e8bbd7+edbf708997,g03191d30f7+6b31559d11,g118115db7c+ac820e85d2,g199a45376c+5137f08352,g1fd858c14a+90100aa1a7,g262e1987ae+64df5f6984,g29ae962dfc+1eb4aece83,g2cef7863aa+73c82f25e4,g3541666cd7+1e37cdad5c,g35bb328faa+edbf708997,g3fd5ace14f+fb4e2866cc,g47891489e3+19fcc35de2,g53246c7159+edbf708997,g5b326b94bb+d622351b67,g64539dfbff+dfe1dff262,g67b6fd64d1+19fcc35de2,g74acd417e5+cfdc02aca8,g786e29fd12+af89c03590,g7aefaa3e3d+dc1a598170,g87389fa792+a4172ec7da,g88cb488625+60ba2c3075,g89139ef638+19fcc35de2,g8d4809ba88+dfe1dff262,g8d7436a09f+db94b797be,g8ea07a8fe4+79658f16ab,g90f42f885a+6577634e1f,g9722cb1a7f+d8f85438e7,g98df359435+7fdd888faa,ga2180abaac+edbf708997,ga9e74d7ce9+128cc68277,gbf99507273+edbf708997,gca7fc764a6+19fcc35de2,gd7ef33dd92+19fcc35de2,gdab6d2f7ff+cfdc02aca8,gdbb4c4dda9+dfe1dff262,ge410e46f29+19fcc35de2,ge41e95a9f2+dfe1dff262,geaed405ab2+062dfc8cdc,w.2025.46
LSST Data Management Base Package
Loading...
Searching...
No Matches
isrMockLSST.py
Go to the documentation of this file.
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
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
201 raise NotImplementedError("Low signal non-linearity is not implemented.")
202
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
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
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.
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:
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).
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:
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.
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.
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.
741 parallelOverscanData,
742 0.0,
743 self.config.readNoise,
744 rng=rngOverscan,
745 )
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 = [
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
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
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.
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
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
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
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
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
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.
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
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
1351 """Simulated reference flat calibration.
1352 """
1353 def __init__(self, **kwargs):
1354 super().__init__(**kwargs)
1355 self.config.doAddFlat = True
1356
1357
1359 """Simulated reference fringe calibration.
1360 """
1361 def __init__(self, **kwargs):
1362 super().__init__(**kwargs)
1363 self.config.doAddFringe = True
1364
1365
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
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
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
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
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
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
Class for storing ordered metadata with comments.
An integer coordinate rectangle.
Definition Box.h:55
localCoordToExpCoord(self, ampData, x, y)
Definition isrMock.py:739
amplifierAddNoise(self, ampData, mean, sigma, rng=None)
Definition isrMock.py:770
amplifierAddSource(self, ampData, scale, x0, y0)
Definition isrMock.py:814
amplifierMultiplyFlat(self, amp, ampData, fracDrop, u0=100.0, v0=100.0)
Definition isrMock.py:868
amplifierAddYGradient(self, ampData, start, end)
Definition isrMock.py:795
getCamera(self, isForAssembly=False)
Definition isrMock.py:558
getExposure(self, isTrimmed=None)
Definition isrMock.py:585
amplifierAddFringe(self, amp, ampData, scale, x0=100, y0=0)
Definition isrMock.py:836
addBiasLevel(self, ampData, biasLevel)
amplifierAddDeferredCharge(self, exposure, amp)
makeDefectList(self, isTrimmed=True)
amplifierAddNonlinearity(self, ampData, centers, values, offset)
amplifierAddBrighterFatter(self, ampImageData, rng, bfStrength, nRecalc)
amplifierAddXGradient(self, ampData, start, end)
amplifierMultiplyFlat(self, amp, ampData, fracDrop, u0=100.0, v0=100.0)
Interpolate::Style stringToInterpStyle(std::string const &style)
Conversion function to switch a string to an Interpolate::Style.
std::shared_ptr< Interpolate > makeInterpolate(std::vector< double > const &x, std::vector< double > const &y, Interpolate::Style const style=Interpolate::AKIMA_SPLINE)
A factory function to make Interpolate objects.