LSST Applications g04e9c324dd+8c5ae1fdc5,g134cb467dc+b203dec576,g18429d2f64+358861cd2c,g199a45376c+0ba108daf9,g1fd858c14a+dd066899e3,g262e1987ae+ebfced1d55,g29ae962dfc+72fd90588e,g2cef7863aa+aef1011c0b,g35bb328faa+8c5ae1fdc5,g3fd5ace14f+b668f15bc5,g4595892280+3897dae354,g47891489e3+abcf9c3559,g4d44eb3520+fb4ddce128,g53246c7159+8c5ae1fdc5,g67b6fd64d1+abcf9c3559,g67fd3c3899+1f72b5a9f7,g74acd417e5+cb6b47f07b,g786e29fd12+668abc6043,g87389fa792+8856018cbb,g89139ef638+abcf9c3559,g8d7436a09f+bcf525d20c,g8ea07a8fe4+9f5ccc88ac,g90f42f885a+6054cc57f1,g97be763408+06f794da49,g9dd6db0277+1f72b5a9f7,ga681d05dcb+7e36ad54cd,gabf8522325+735880ea63,gac2eed3f23+abcf9c3559,gb89ab40317+abcf9c3559,gbf99507273+8c5ae1fdc5,gd8ff7fe66e+1f72b5a9f7,gdab6d2f7ff+cb6b47f07b,gdc713202bf+1f72b5a9f7,gdfd2d52018+8225f2b331,ge365c994fd+375fc21c71,ge410e46f29+abcf9c3559,geaed405ab2+562b3308c0,gf9a733ac38+8c5ae1fdc5,w.2025.35
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 = {'driftScale': {'C:0,0': 0.000127,
355 'C:0,1': 0.000137,
356 'C:0,2': 0.000138,
357 'C:0,3': 0.000147,
358 'C:1,0': 0.000147,
359 'C:1,1': 0.000122,
360 'C:1,2': 0.000123,
361 'C:1,3': 0.000116},
362 'decayTime': {'C:0,0': 2.30,
363 'C:0,1': 2.21,
364 'C:0,2': 2.28,
365 'C:0,3': 2.34,
366 'C:1,0': 2.30,
367 'C:1,1': 2.40,
368 'C:1,2': 2.51,
369 'C:1,3': 2.21},
370 'globalCti': {'C:0,0': 5.25e-07,
371 'C:0,1': 5.38e-07,
372 'C:0,2': 5.80e-07,
373 'C:0,3': 5.91e-07,
374 'C:1,0': 6.24e-07,
375 'C:1,1': 5.72e-07,
376 'C:1,2': 5.60e-07,
377 'C:1,3': 4.40e-07},
378 'signals': {'C:0,0': np.linspace(1.0e2, 1.0e5, 10),
379 'C:0,1': np.linspace(1.0e2, 1.0e5, 10),
380 'C:0,2': np.linspace(1.0e2, 1.0e5, 10),
381 'C:0,3': np.linspace(1.0e2, 1.0e5, 10),
382 'C:1,0': np.linspace(1.0e2, 1.0e5, 10),
383 'C:1,1': np.linspace(1.0e2, 1.0e5, 10),
384 'C:1,2': np.linspace(1.0e2, 1.0e5, 10),
385 'C:1,3': np.linspace(1.0e2, 1.0e5, 10)},
386 'serialEper': {'C:0,0': np.full(10, 5.25e-07),
387 'C:0,1': np.full(10, 5.38e-07),
388 'C:0,2': np.full(10, 5.80e-07),
389 'C:0,3': np.full(10, 5.91e-07),
390 'C:1,0': np.full(10, 6.24e-07),
391 'C:1,1': np.full(10, 5.72e-07),
392 'C:1,2': np.full(10, 5.60e-07),
393 'C:1,3': np.full(10, 4.40e-07)},
394 'parallelEper': {'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 'serialCtiTurnoff': {'C:0,0': 1.0e5,
403 'C:0,1': 1.0e5,
404 'C:0,2': 1.0e5,
405 'C:0,3': 1.0e5,
406 'C:1,0': 1.0e5,
407 'C:1,1': 1.0e5,
408 'C:1,2': 1.0e5,
409 'C:1,3': 1.0e5},
410 'parallelCtiTurnoff': {'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 'serialCtiTurnoffSamplingErr': {'C:0,0': 1.0e3,
419 'C:0,1': 1.0e3,
420 'C:0,2': 1.0e3,
421 'C:0,3': 1.0e3,
422 'C:1,0': 1.0e3,
423 'C:1,1': 1.0e3,
424 'C:1,2': 1.0e3,
425 'C:1,3': 1.0e3},
426 'parallelCtiTurnoffSamplingErr': {'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 'serialTraps': {'C:0,0': {'size': 20000.0,
435 'emissionTime': 0.4,
436 'pixel': 1,
437 'trap_type': 'spline',
438 'coeffs': self.splineTrapCoeffs},
439 'C:0,1': {'size': 20000.0,
440 'emissionTime': 0.4,
441 'pixel': 1,
442 'trap_type': 'spline',
443 'coeffs': self.splineTrapCoeffs},
444 'C:0,2': {'size': 20000.0,
445 'emissionTime': 0.4,
446 'pixel': 1,
447 'trap_type': 'spline',
448 'coeffs': self.splineTrapCoeffs},
449 'C:0,3': {'size': 20000.0,
450 'emissionTime': 0.4,
451 'pixel': 1,
452 'trap_type': 'spline',
453 'coeffs': self.splineTrapCoeffs},
454 'C:1,0': {'size': 20000.0,
455 'emissionTime': 0.4,
456 'pixel': 1,
457 'trap_type': 'spline',
458 'coeffs': self.splineTrapCoeffs},
459 'C:1,1': {'size': 20000.0,
460 'emissionTime': 0.4,
461 'pixel': 1,
462 'trap_type': 'spline',
463 'coeffs': self.splineTrapCoeffs},
464 'C:1,2': {'size': 20000.0,
465 'emissionTime': 0.4,
466 'pixel': 1,
467 'trap_type': 'spline',
468 'coeffs': self.splineTrapCoeffs},
469 'C:1,3': {'size': 20000.0,
470 'emissionTime': 0.4,
471 'pixel': 1,
472 'trap_type': 'spline',
473 'coeffs': self.splineTrapCoeffs}}}
474
476
477 # Cross-talk coeffs are defined in the parent class.
478
479 self.makeSubtask("assembleCcd")
480
481 def run(self):
482 """Generate a mock ISR product following LSSTCam ISR, and return it.
483
484 Returns
485 -------
486 image : `lsst.afw.image.Exposure`
487 Simulated ISR image with signals added.
488 dataProduct :
489 Simulated ISR data products.
490 None :
491 Returned if no valid configuration was found.
492
493 Raises
494 ------
495 RuntimeError
496 Raised if both doGenerateImage and doGenerateData are specified.
497 """
498 if self.config.doGenerateImage and self.config.doGenerateData:
499 raise RuntimeError("Only one of doGenerateImage and doGenerateData may be specified.")
500 elif self.config.doGenerateImage:
501 return self.makeImage()
502 elif self.config.doGenerateData:
503 return self.makeData()
504 else:
505 return None
506
507 def makeImage(self):
508 """Generate a simulated ISR LSST image.
509
510 Returns
511 -------
512 exposure : `lsst.afw.image.Exposure` or `dict`
513 Simulated ISR image data.
514
515 Notes
516 -----
517 This method constructs a "raw" data image.
518 """
519 exposure = self.getExposure()
520
521 # Set up random number generators for consistency of components,
522 # no matter the group that are configured.
523 rngSky = np.random.RandomState(seed=self.config.rngSeed + 1)
524 rngDark = np.random.RandomState(seed=self.config.rngSeed + 2)
525 rng2DBias = np.random.RandomState(seed=self.config.rngSeed + 3)
526 rngOverscan = np.random.RandomState(seed=self.config.rngSeed + 4)
527 rngReadNoise = np.random.RandomState(seed=self.config.rngSeed + 5)
528 rngBrighterFatter = galsim.BaseDeviate(self.config.rngSeed + 6)
529
530 # Create the linearizer if we will need it.
531 if self.config.doAddHighSignalNonlinearity:
532 linearizer = LinearizerMockLSST().run()
533
534 # We introduce effects as they happen from a source to the signal,
535 # so the effects go from electron to adu.
536 # The ISR steps will then correct these effects in the reverse order.
537 for idx, amp in enumerate(exposure.getDetector()):
538 # Get image bbox and data
539 bbox = None
540 if self.config.isTrimmed:
541 bbox = amp.getBBox()
542 bboxFull = bbox
543 else:
544 bbox = amp.getRawDataBBox()
545 bboxFull = amp.getRawBBox()
546
547 # This is the image data (excluding pre/overscans).
548 ampImageData = exposure.image[bbox]
549 # This is the full data (including pre/overscans if untrimmed).
550 ampFullData = exposure.image[bboxFull]
551
552 # Astrophysical signals are all in electron (e-).
553 # These are only applied to the imaging portion of the
554 # amplifier (ampImageData)
555
556 if self.config.doAddSky:
557 # The sky effects are in electron.
559 ampImageData,
560 self.config.skyLevel,
561 np.sqrt(self.config.skyLevel),
562 rng=rngSky,
563 )
564
565 if self.config.doAddSource:
566 for sourceAmp, sourceFlux, sourceX, sourceY in zip(self.config.sourceAmp,
567 self.config.sourceFlux,
568 self.config.sourceX,
569 self.config.sourceY):
570 if idx == sourceAmp:
571 # The source flux is in electron.
572 self.amplifierAddSource(ampImageData, sourceFlux, sourceX, sourceY)
573
574 if self.config.doAddFringe:
575 # Fringes are added in electron.
576 self.amplifierAddFringe(amp,
577 ampImageData,
578 np.array(self.config.fringeScale),
579 x0=np.array(self.config.fringeX0),
580 y0=np.array(self.config.fringeY0))
581
582 if self.config.doAddFlat:
583 if self.config.calibMode:
584 # In case we are making a combined flat,
585 # add a non-zero signal so the mock flat can be multiplied
586 self.amplifierAddNoise(ampImageData, 1.0, 0.0)
587 # Multiply each amplifier by a Gaussian centered on u0 and v0
588 u0 = exposure.getDetector().getBBox().getDimensions().getX()/2.
589 v0 = exposure.getDetector().getBBox().getDimensions().getY()/2.
590 self.amplifierMultiplyFlat(amp, ampImageData, self.config.flatDrop, u0=u0, v0=v0)
591
592 # On-chip electronic effects.
593
594 # 1. Add bright defect(s).
595 if self.config.doAddBrightDefects:
596 defectList = self.makeDefectList(isTrimmed=self.config.isTrimmed)
597
598 for defect in defectList:
599 exposure.image[defect.getBBox()] = self.config.brightDefectLevel
600
601 for idx, amp in enumerate(exposure.getDetector()):
602 # Get image bbox and data
603 bbox = None
604 if self.config.isTrimmed:
605 bbox = amp.getBBox()
606 bboxFull = bbox
607 else:
608 bbox = amp.getRawDataBBox()
609 bboxFull = amp.getRawBBox()
610
611 # This is the image data (excluding pre/overscans).
612 ampImageData = exposure.image[bbox]
613 # This is the full data (including pre/overscans if untrimmed).
614 ampFullData = exposure.image[bboxFull]
615
616 # 2. Add dark current (electron) to imaging portion of the amp.
617 if self.config.doAddDark or self.config.doAddDarkNoiseOnly:
618 if self.config.doAddDarkNoiseOnly:
619 darkLevel = 0.0
620 else:
621 darkLevel = self.config.darkRate * self.config.darkTime
622 if self.config.calibMode:
623 darkNoise = 0.0
624 else:
625 darkNoise = np.sqrt(self.config.darkRate * self.config.darkTime)
626
627 self.amplifierAddNoise(ampImageData, darkLevel, darkNoise, rng=rngDark)
628
629 # 3. Add BF effect (electron) to imaging portion of the amp.
630 if self.config.doAddBrighterFatter is True:
632 ampImageData,
633 rngBrighterFatter,
634 self.config.bfStrength,
635 self.config.nRecalc,
636 )
637
638 # 4. Add serial CTI (electron) to amplifier (imaging + overscan).
639 if self.config.doAddDeferredCharge:
640 # Get the free charge area for the amplifier.
641 self.amplifierAddDeferredCharge(exposure, amp)
642
643 # 5. Add 2D bias residual (electron) to imaging portion of the amp.
644 if self.config.doAdd2DBias:
645 # For now we use an unstructured noise field to add some
646 # consistent 2D bias residual that can be subtracted. In
647 # the future this can be made into a warm corner (for example).
649 ampImageData,
650 0.0,
651 self.config.noise2DBias,
652 rng=rng2DBias,
653 )
654
655 # 6. Add clock-injected offset (electron) to amplifer
656 # (imaging + overscan).
657 # This is just an offset that will be crosstalked and modified by
658 # the gain, and does not have a noise associated with it.
659 if self.config.doAddClockInjectedOffset:
661 ampFullData,
662 self.config.clockInjectedOffsetLevel,
663 0.0,
664 )
665
666 # 7./8. Add serial and parallel overscan slopes (electron)
667 # (imaging + overscan)
668 if (self.config.doAddParallelOverscanRamp or self.config.doAddSerialOverscanRamp) and \
669 not self.config.isTrimmed:
670
671 if self.config.doAddParallelOverscanRamp:
672 # Apply gradient along the X axis.
673 self.amplifierAddXGradient(ampFullData, -1.0 * self.config.overscanScale,
674 1.0 * self.config.overscanScale)
675
676 if self.config.doAddSerialOverscanRamp:
677 # Apply the gradient along the Y axis.
678 self.amplifierAddYGradient(ampFullData, -1.0 * self.config.overscanScale,
679 1.0 * self.config.overscanScale)
680
681 # 9. Add non-linearity (electron) to amplifier
682 # (imaging + overscan).
683 if self.config.doAddHighSignalNonlinearity:
684 # The linearizer coefficients come from makeLinearizer().
685 if linearizer.linearityType[amp.getName()] != "Spline":
686 raise RuntimeError("IsrMockLSST only supports spline non-linearity.")
687
688 coeffs = linearizer.linearityCoeffs[amp.getName()]
689 centers, values = np.split(coeffs, 2)
690
691 # This is an application of high signal non-linearity, so we
692 # set the lower values to 0.0 (this cut is arbitrary).
693 values[centers < self.config.highSignalNonlinearityThreshold] = 0.0
694
695 # The linearizer is units of adu, so convert to electron
696 values *= self.config.gainDict[amp.getName()]
697
698 # Note that the linearity spline is in "overscan subtracted"
699 # units so needs to be applied without the clock-injected
700 # offset.
702 ampFullData,
703 centers,
704 values,
705 self.config.clockInjectedOffsetLevel if self.config.doAddClockInjectedOffset else 0.0,
706 )
707
708 # 10. Add read noise (electron) to the amplifier
709 # (imaging + overscan).
710 # Unsure if this should be before or after crosstalk.
711 # Probably some of both; hopefully doesn't matter.
712 if not self.config.calibMode:
713 # Add read noise to the imaging region.
715 ampImageData,
716 0.0,
717 self.config.readNoise,
718 rng=rngReadNoise,
719 )
720
721 # If not trimmed, add to the overscan regions.
722 if not self.config.isTrimmed:
723 parallelOverscanBBox = amp.getRawParallelOverscanBBox()
724 parallelOverscanData = exposure.image[parallelOverscanBBox]
725
726 serialOverscanBBox = self.getFullSerialOverscanBBox(amp)
727 serialOverscanData = exposure.image[serialOverscanBBox]
728
729 # Add read noise of mean 0
730 # to the parallel and serial overscan regions.
732 parallelOverscanData,
733 0.0,
734 self.config.readNoise,
735 rng=rngOverscan,
736 )
738 serialOverscanData,
739 0.0,
740 self.config.readNoise,
741 rng=rngOverscan,
742 )
743
744 # 7b. Add bad column to the parallel overscan region.
745 if self.config.doAddBadParallelOverscanColumn and not self.config.isTrimmed:
746 # We want to place this right above the defect, to simulate
747 # bleeding into the parallel overscan region.
748 amp = exposure.getDetector()[2]
749 parBBox = amp.getRawParallelOverscanBBox()
750 bboxBad = geom.Box2I(
751 corner=geom.Point2I(50, parBBox.getMinY()),
752 dimensions=geom.Extent2I(1, parBBox.getHeight()),
753 )
754 exposure[bboxBad].image.array[:, :] += self.config.badParallelOverscanColumnLevel
755
756 if self.config.doAddBadParallelOverscanColumnNeighbors:
757 for neighbor in [49, 51]:
758 bboxBad = geom.Box2I(
759 corner=geom.Point2I(neighbor, parBBox.getMinY()),
760 dimensions=geom.Extent2I(1, parBBox.getHeight()),
761 )
762 exposure[bboxBad].image.array[:, :] += self.config.badParallelOverscanColumnNeighborsLevel
763
764 # 11. Add crosstalk (electron) to all the amplifiers
765 # (imaging + overscan).
766 if self.config.doAddCrosstalk:
767 ctCalib = CrosstalkCalib()
768 # We use the regular subtractCrosstalk code but with a negative
769 # sign on the crosstalk coefficients so it adds instead of
770 # subtracts. We only apply the signal plane (ignoreVariance,
771 # subtrahendMasking) with a very large pixel to mask to ensure
772 # no crosstalk mask bits are set.
773 ctCalib.subtractCrosstalk(
774 exposure,
775 crosstalkCoeffs=-1*self.crosstalkCoeffs,
776 doSubtrahendMasking=True,
777 minPixelToMask=np.inf,
778 ignoreVariance=True,
779 fullAmplifier=True,
780 )
781
782 for amp in exposure.getDetector():
783 # Get image bbox and data (again).
784 bbox = None
785 if self.config.isTrimmed:
786 bbox = amp.getBBox()
787 bboxFull = bbox
788 else:
789 bbox = amp.getRawDataBBox()
790 bboxFull = amp.getRawBBox()
791
792 # This is the image data (excluding pre/overscans).
793 ampImageData = exposure.image[bbox]
794 # This is the full data (including pre/overscans if untrimmed).
795 ampFullData = exposure.image[bboxFull]
796
797 # 12. Gain un-normalize (from electron to floating point adu)
798 if self.config.doApplyGain:
799 gain = self.config.gainDict.get(amp.getName(), self.config.gain)
800 self.applyGain(ampFullData, gain)
801
802 # 13. Add overall bias level (adu) to the amplifier
803 # (imaging + overscan)
804 if self.config.doAddBias:
805 self.addBiasLevel(ampFullData, self.config.biasLevel)
806
807 # 14. Round/Truncate to integers (adu)
808 if self.config.doRoundAdu:
809 self.roundADU(ampFullData)
810
811 # Add units metadata to calibrations.
812 if self.config.calibMode:
813 if self.config.doApplyGain:
814 exposure.metadata["LSST ISR UNITS"] = "adu"
815 else:
816 exposure.metadata["LSST ISR UNITS"] = "electron"
817
818 # Add a variance plane appropriate for a calibration frame.
819 # We take the absolute value for biases which have no signal.
820 exposure.variance.array[:, :] = np.abs(np.median(exposure.image.array)/10.)
821
822 exposure.metadata["BSSVBS"] = 50.0
823 exposure.metadata["HVBIAS"] = "ON"
824
825 if self.config.doGenerateAmpDict:
826 expDict = dict()
827 for amp in exposure.getDetector():
828 expDict[amp.getName()] = exposure
829 return expDict
830 else:
831 return exposure
832
833 def addBiasLevel(self, ampData, biasLevel):
834 """Add bias level to an amplifier's image data.
835
836 Parameters
837 ----------
838 ampData : `lsst.afw.image.ImageF`
839 Amplifier image to operate on.
840 biasLevel : `float`
841 Bias level to be added to the image.
842 """
843 ampArr = ampData.array
844 ampArr[:] = ampArr[:] + biasLevel
845
846 def makeDefectList(self, isTrimmed=True):
847 """Generate a simple defect list.
848
849 Parameters
850 ----------
851 isTrimmed : `bool`, optional
852 Return defects in trimmed coordinates?
853
854 Returns
855 -------
856 defectList : `lsst.meas.algorithms.Defects`
857 Simulated defect list
858 """
859 defectBoxesUntrimmed = [
861 geom.Point2I(50, 118),
862 geom.Extent2I(1, 51),
863 ),
864 ]
865
866 if not isTrimmed:
867 return Defects(defectBoxesUntrimmed)
868
869 # If trimmed, we need to convert.
870 tempExp = self.getExposure(isTrimmed=False)
871 tempExp.image.array[:, :] = 0.0
872 for bbox in defectBoxesUntrimmed:
873 tempExp.image[bbox] = 1.0
874
875 assembledExp = self.assembleCcd.assembleCcd(tempExp)
876
877 # Use thresholding code to find defect footprints/boxes.
878 threshold = afwDetection.createThreshold(1.0, "value", polarity=True)
879 footprintSet = afwDetection.FootprintSet(assembledExp.image, threshold)
880
881 return Defects.fromFootprintList(footprintSet.getFootprints())
882
883 def makeBfKernel(self):
884 """Generate a simple simulated brighter-fatter kernel.
885 Returns
886 -------
887 kernel : `lsst.ip.isr.BrighterFatterKernel`
888 Simulated brighter-fatter kernel.
889 """
890 bfkArray = super().makeBfKernel()
891 bfKernelObject = BrighterFatterKernel()
892 bfKernelObject.level = 'AMP'
893 bfKernelObject.gain = self.config.gainDict
894
895 for amp in self.getExposure().getDetector():
896 # Kernel must be in (y,x) orientation
897 bfKernelObject.ampKernels[amp.getName()] = bfkArray.T
898 bfKernelObject.valid[amp.getName()] = True
899
900 return bfKernelObject
901
903 """Generate a CTI calibration.
904
905 Returns
906 -------
907 cti : `lsst.ip.isr.deferredCharge.DeferredChargeCalib`
908 Simulated deferred charge calibration.
909 """
910
911 metadataDict = {'metadata': PropertyList()}
912 metadataDict['metadata'].add(name="OBSTYPE", value="CTI")
913 metadataDict['metadata'].add(name="CALIBCLS",
914 value="lsst.ip.isr.deferredCharge.DeferredChargeCalib")
915 self.ctiCalibDict = {**metadataDict, **self.ctiCalibDict}
916 deferredChargeCalib = DeferredChargeCalib()
917 self.cti = deferredChargeCalib.fromDict(self.ctiCalibDict)
918
919 return self.cti
920
921 def amplifierAddBrighterFatter(self, ampImageData, rng, bfStrength, nRecalc):
922 """Add brighter fatter effect and/or diffusion to the image.
923 Parameters
924 ----------
925 ampImageData : `lsst.afw.image.ImageF`
926 Amplifier image to operate on.
927 rng : `galsim.BaseDeviate`
928 Random number generator.
929 bfStrength : `float`
930 Scaling parameter of the brighter fatter effect (nominally = 1)
931 nRecalc: 'int'
932 The number of electrons to accumulate before recalculating the
933 distortion of the pixel shapes.
934 """
935
936 incidentImage = galsim.Image(ampImageData.array, scale=1)
937 measuredImage = galsim.ImageF(
938 ampImageData.array.shape[1],
939 ampImageData.array.shape[0],
940 scale=1,
941 )
942 photons = galsim.PhotonArray.makeFromImage(incidentImage)
943
944 sensorModel = galsim.SiliconSensor(
945 strength=bfStrength,
946 rng=rng,
947 diffusion_factor=0.0,
948 nrecalc=nRecalc,
949 )
950
951 totalFluxAdded = sensorModel.accumulate(photons, measuredImage)
952 ampImageData.array = measuredImage.array
953
954 return totalFluxAdded
955
956 def amplifierAddDeferredCharge(self, exposure, amp):
957 """Add serial CTI to the amplifier data.
958
959 Parameters
960 ----------
961 exposure : `lsst.afw.image.ExposureF`
962 The exposure object containing the amplifier
963 to apply deferred charge to.
964 amp : `lsst.afw.image.Amplifier`
965 The amplifier object (contains geometry info).
966 """
967 # Get the amplifier's geometry parameters.
968 # When adding deferred charge, we have already assured that
969 # isTrimmed is False. Therefore we want to make sure that we
970 # get the RawDataBBox.
971 readoutCorner = amp.getReadoutCorner()
972 prescanWidth = amp.getRawHorizontalPrescanBBox().getWidth()
973 serialOverscanWidth = amp.getRawHorizontalOverscanBBox().getWidth()
974 parallelOverscanWidth = amp.getRawVerticalOverscanBBox().getHeight()
975 bboxFreeCharge = amp.getRawDataBBox()
976 bboxFreeCharge = bboxFreeCharge.expandedTo(amp.getRawHorizontalOverscanBBox())
977 bboxFreeCharge = bboxFreeCharge.expandedTo(amp.getRawHorizontalPrescanBBox())
978 bboxFreeCharge = bboxFreeCharge.expandedTo(amp.getRawVerticalOverscanBBox())
979
980 ampFreeChargeData = exposure.image[bboxFreeCharge]
981 ampImageData = exposure.image[amp.getRawDataBBox()]
982
983 # Get the deferred charge parameters for this amplifier.
984 cti = self.deferredChargeCalib.globalCti[amp.getName()]
985 traps = self.deferredChargeCalib.serialTraps[amp.getName()]
986 driftScale = self.deferredChargeCalib.driftScale[amp.getName()]
987 decayTime = self.deferredChargeCalib.decayTime[amp.getName()]
988
989 # Create a fake amplifier object that contains some deferred charge
990 # paramters.
991 floatingOutputAmplifier = FloatingOutputAmplifier(
992 gain=1.0, # Everything is already in electrons.
993 scale=driftScale,
994 decay_time=decayTime,
995 noise=0.0,
996 offset=0.0,
997 )
998
999 def flipImage(arr, readoutCorner):
1000 # Flip an array so that the readout corner is in
1001 # the lower left.
1002 if readoutCorner == ReadoutCorner.LR:
1003 return np.fliplr(arr)
1004 elif readoutCorner == ReadoutCorner.UR:
1005 return np.fliplr(np.flipud(arr))
1006 elif readoutCorner == ReadoutCorner.UL:
1007 return np.flipud(arr)
1008 else:
1009 pass
1010
1011 return arr
1012
1013 # The algorithm expects that the readout corner is in
1014 # the lower left corner. Flip it to be so:
1015 imageData = ampImageData.array
1016 imageData = flipImage(imageData, readoutCorner)
1017
1018 # Simulate the amplifier.
1019 ampSim = SegmentSimulator(
1020 imarr=imageData,
1021 prescan_width=prescanWidth,
1022 output_amplifier=floatingOutputAmplifier,
1023 cti=cti,
1024 traps=traps,
1025 )
1026
1027 # Simulate deferred charge!
1028 # Note that the readout() method uses the image region data and the
1029 # overscan dimensions provided as input parameters. It then creates
1030 # overscan nd adds it to the image data to create the raw image.
1031 result = ampSim.readout(
1032 serial_overscan_width=serialOverscanWidth,
1033 parallel_overscan_width=parallelOverscanWidth,
1034 )
1035
1036 # Flip the image back to the original orientation.
1037 result = flipImage(result, readoutCorner)
1038
1039 # Set the image with the deferred charge added.
1040 ampFreeChargeData.array[:, :] = result
1041
1043 # docstring inherited.
1044
1045 # The linearizer has units of adu.
1046 nNodes = 10
1047 # Set this to just above the mock saturation (adu)
1048 maxADU = 101_000
1049 nonLinSplineNodes = np.linspace(0, maxADU, nNodes)
1050 # These values come from cp_pipe/tests/test_linearity.py and
1051 # are based on a test fit to LSSTCam data, run 7193D, detector 22,
1052 # amp C00.
1053 nonLinSplineValues = np.array(
1054 [0.0, -8.87, 1.46, 1.69, -6.92, -68.23, -78.01, -11.56, 80.26, 185.01]
1055 )
1056
1057 if self.config.doAddHighSignalNonlinearity and not self.config.doAddLowSignalNonlinearity:
1058 nonLinSplineValues[nonLinSplineNodes < self.config.highSignalNonlinearityThreshold] = 0.0
1059 elif self.config.doAddLowSignalNonlinearity:
1060 raise NotImplementedError("Low signal non-linearity is not implemented.")
1061
1062 exp = self.getExposure()
1063 detector = exp.getDetector()
1064
1065 linearizer = Linearizer(detector=detector)
1066 linearizer.updateMetadataFromExposures([exp])
1067
1068 # We need to set override by hand because we are constructing a
1069 # linearizer manually and not from a serialized object.
1070 linearizer.override = True
1071 linearizer.hasLinearity = True
1072 linearizer.validate()
1073 linearizer.updateMetadata(camera=self.getCamera(), detector=detector, filterName='NONE')
1074 linearizer.updateMetadata(setDate=True, setCalibId=True)
1075
1076 for amp in detector:
1077 ampName = amp.getName()
1078 linearizer.linearityType[ampName] = "Spline"
1079 linearizer.linearityCoeffs[ampName] = np.concatenate([nonLinSplineNodes, nonLinSplineValues])
1080 # We need to specify the raw bbox here.
1081 linearizer.linearityBBox[ampName] = amp.getRawBBox()
1082
1083 return linearizer
1084
1085 def amplifierAddNonlinearity(self, ampData, centers, values, offset):
1086 """Add non-linearity to amplifier data.
1087
1088 Parameters
1089 ----------
1090 ampData : `lsst.afw.image.ImageF`
1091 Amplifier image to operate on.
1092 centers : `np.ndarray`
1093 Spline nodes.
1094 values : `np.ndarray`
1095 Spline values.
1096 offset : `float`
1097 Offset zero-point between linearizer (internal vs external).
1098 """
1099 # I'm not sure what to do about negative values...
1100
1101 # Note that we are using the afw AKIMA_SPLINE to offset the
1102 # data but using the equivalent but faster scipy Akima1DInterpolator to
1103 # correct the data.
1105 centers,
1106 values,
1107 afwMath.stringToInterpStyle("AKIMA_SPLINE"),
1108 )
1109
1110 delta = np.asarray(spl.interpolate(ampData.array.ravel() - offset))
1111
1112 ampData.array[:, :] += delta.reshape(ampData.array.shape)
1113
1114 def amplifierMultiplyFlat(self, amp, ampData, fracDrop, u0=100.0, v0=100.0):
1115 """Multiply an amplifier's image data by a flat-like pattern.
1116
1117 Parameters
1118 ----------
1119 amp : `lsst.afw.ampInfo.AmpInfoRecord`
1120 Amplifier to operate on. Needed for amp<->exp coordinate
1121 transforms.
1122 ampData : `lsst.afw.image.ImageF`
1123 Amplifier image to operate on.
1124 fracDrop : `float`
1125 Fractional drop from center to edge of detector along x-axis.
1126 u0 : `float`
1127 Peak location in detector coordinates.
1128 v0 : `float`
1129 Peak location in detector coordinates.
1130 """
1131 if fracDrop >= 1.0:
1132 raise RuntimeError("Flat fractional drop cannot be greater than 1.0")
1133
1134 sigma = u0 / np.sqrt(2.0 * fracDrop)
1135
1136 for x in range(0, ampData.getDimensions().getX()):
1137 for y in range(0, ampData.getDimensions().getY()):
1138 (u, v) = self.localCoordToExpCoord(amp, x, y)
1139 f = np.exp(-0.5 * ((u - u0)**2 + (v - v0)**2) / sigma**2)
1140 ampData.array[y][x] = (ampData.array[y][x] * f)
1141
1142 def applyGain(self, ampData, gain):
1143 """Apply gain to the amplifier's data.
1144 This method divides the data by the gain
1145 because the mocks need to convert the data in electron to adu,
1146 so it does the inverse operation to applyGains in isrFunctions.
1147
1148 Parameters
1149 ----------
1150 ampData : `lsst.afw.image.ImageF`
1151 Amplifier image to operate on.
1152 gain : `float`
1153 Gain value in electron/adu.
1154 """
1155 ampArr = ampData.array
1156 ampArr[:] = ampArr[:] / gain
1157
1158 def roundADU(self, ampData):
1159 """Round adu to nearest integer.
1160
1161 Parameters
1162 ----------
1163 ampData : `lsst.afw.image.ImageF`
1164 Amplifier image to operate on.
1165 """
1166 ampArr = ampData.array
1167 ampArr[:] = np.around(ampArr)
1168
1169 def amplifierAddXGradient(self, ampData, start, end):
1170 """Add a x-axis linear gradient to an amplifier's image data.
1171
1172 This method operates in the amplifier coordinate frame.
1173
1174 Parameters
1175 ----------
1176 ampData : `lsst.afw.image.ImageF`
1177 Amplifier image to operate on.
1178 start : `float`
1179 Start value of the gradient (at x=0).
1180 end : `float`
1181 End value of the gradient (at x=xmax).
1182 """
1183 nPixX = ampData.getDimensions().getX()
1184 ampArr = ampData.array
1185 ampArr[:] = ampArr[:] + (np.interp(range(nPixX), (0, nPixX - 1), (start, end)).reshape(1, nPixX)
1186 + np.zeros(ampData.getDimensions()).transpose())
1187
1189 """Get the full serial overscan bounding box from an amplifier.
1190
1191 This includes the serial/parallel overscan region.
1192
1193 Parameters
1194 ----------
1195 amp : `lsst.afw.ampInfo.AmpInfoRecord`
1196 Amplifier to operate on.
1197
1198 Returns
1199 -------
1200 bbox : `lsst.geom.Box2I`
1201 """
1202 # This only works for untrimmed data.
1203 bbox = amp.getRawDataBBox()
1204
1205 parallelOverscanBBox = amp.getRawParallelOverscanBBox()
1206 grownImageBBox = bbox.expandedTo(parallelOverscanBBox)
1207
1208 serialOverscanBBox = amp.getRawSerialOverscanBBox()
1209 # Extend the serial overscan bbox to include corners
1210 serialOverscanBBox = geom.Box2I(
1211 geom.Point2I(serialOverscanBBox.getMinX(),
1212 grownImageBBox.getMinY()),
1213 geom.Extent2I(serialOverscanBBox.getWidth(),
1214 grownImageBBox.getHeight()))
1215
1216 return serialOverscanBBox
1217
1218
1220 """Generate a raw exposure suitable for ISR.
1221 """
1222 def __init__(self, **kwargs):
1223 super().__init__(**kwargs)
1224 self.config.isTrimmed = False
1225 self.config.doGenerateImage = True
1226 self.config.doGenerateAmpDict = False
1227
1228 # Add astro effects
1229 self.config.doAddSky = True
1230 self.config.doAddSource = True
1231
1232 # Add optical effects
1233 self.config.doAddFringe = True
1234
1235 # Add instrument effects
1236 self.config.doAddParallelOverscanRamp = True
1237 self.config.doAddSerialOverscanRamp = True
1238 self.config.doAddCrosstalk = True
1239 self.config.doAddBias = True
1240 self.config.doAddDark = True
1241
1242 self.config.doAddFlat = True
1243
1244
1246 """Generate a trimmed raw exposure.
1247 """
1248 def __init__(self, **kwargs):
1249 super().__init__(**kwargs)
1250 self.config.isTrimmed = True
1251 self.config.doAddParallelOverscanRamp = False
1252 self.config.doAddSerialOverscanRamp = False
1253
1254
1256 """Generate a trimmed raw exposure.
1257
1258 This represents a "truth" image that can be compared to a
1259 post-ISR cleaned image.
1260 """
1261 def __init__(self, **kwargs):
1262 super().__init__(**kwargs)
1263 self.config.isTrimmed = True
1264 self.config.doGenerateImage = True
1265
1266 self.config.doAddSky = True
1267 self.config.doAddSource = True
1268
1269 self.config.doAddFringe = True
1270
1271 self.config.doAddParallelOverscanRamp = False
1272 self.config.doAddSerialOverscanRamp = False
1273 self.config.doAddCrosstalk = False
1274 self.config.doAddBias = False
1275 self.config.doAdd2DBias = False
1276 self.config.doAddDark = False
1277 self.config.doApplyGain = False
1278 self.config.doAddFlat = False
1279 self.config.doAddClockInjectedOffset = False
1280
1281 self.config.biasLevel = 0.0
1282 # Assume combined calibrations are made with 16 inputs.
1283 self.config.readNoise *= 0.25
1284
1285 self.config.doRoundAdu = False
1286
1287
1289 """Parent class for those that make reference calibrations.
1290 """
1291 def __init__(self, **kwargs):
1292 # If we want the calibration in adu units, we need to apply
1293 # the gain. Default is electron units, so do not apply the gain.
1294 doApplyGain = kwargs.pop("adu", False)
1295
1296 super().__init__(**kwargs)
1297 self.config.isTrimmed = True
1298 self.config.doGenerateImage = True
1299
1300 self.config.calibMode = True
1301
1302 self.config.doAddSky = False
1303 self.config.doAddSource = False
1304
1305 self.config.doAddFringe = False
1306
1307 self.config.doAddParallelOverscanRamp = False
1308 self.config.doAddSerialOverscanRamp = False
1309 self.config.doAddCrosstalk = False
1310 self.config.doAddBias = False
1311 self.config.doAdd2DBias = False
1312 self.config.doAddDark = False
1313 self.config.doApplyGain = doApplyGain
1314 self.config.doAddFlat = False
1315 self.config.doAddClockInjectedOffset = False
1316
1317 # Reference calibrations are not integerized.
1318 self.config.doRoundAdu = False
1319
1320
1321# Classes to generate calibration products mocks.
1323 """Simulated reference dark calibration.
1324 """
1325 def __init__(self, **kwargs):
1326 super().__init__(**kwargs)
1327 self.config.doAddDark = True
1328 self.config.darkTime = 1.0
1329
1330
1332 """Simulated combined bias calibration.
1333 """
1334 def __init__(self, **kwargs):
1335 super().__init__(**kwargs)
1336 # This is a "2D bias residual" frame which has only
1337 # the 2D bias in it.
1338 self.config.doAdd2DBias = True
1339
1340
1342 """Simulated reference flat calibration.
1343 """
1344 def __init__(self, **kwargs):
1345 super().__init__(**kwargs)
1346 self.config.doAddFlat = True
1347
1348
1350 """Simulated reference fringe calibration.
1351 """
1352 def __init__(self, **kwargs):
1353 super().__init__(**kwargs)
1354 self.config.doAddFringe = True
1355
1356
1358 """Simulated brighter-fatter kernel.
1359 """
1360 def __init__(self, **kwargs):
1361 super().__init__(**kwargs)
1362 self.config.doGenerateImage = False
1363 self.config.doGenerateData = True
1364
1365 self.config.doBrighterFatter = True
1366 self.config.doDefects = False
1367 self.config.doCrosstalkCoeffs = False
1368 self.config.doTransmissionCurve = False
1369 self.config.doLinearizer = False
1370
1371
1373 """Simulated deferred charge calibration.
1374 """
1375 def __init__(self, **kwargs):
1376 super().__init__(**kwargs)
1377 self.config.doGenerateImage = False
1378 self.config.doGenerateData = True
1379 self.config.doDeferredCharge = True
1380 self.config.doDefects = False
1381 self.config.doCrosstalkCoeffs = False
1382 self.config.doTransmissionCurve = False
1383
1384
1386 """Simulated defect list.
1387 """
1388 def __init__(self, **kwargs):
1389 super().__init__(**kwargs)
1390 self.config.doGenerateImage = False
1391 self.config.doGenerateData = True
1392
1393 self.config.doBrighterFatter = False
1394 self.config.doDefects = True
1395 self.config.doCrosstalkCoeffs = False
1396 self.config.doTransmissionCurve = False
1397 self.config.doLinearizer = False
1398
1399
1401 """Simulated crosstalk coefficient matrix.
1402 """
1403 def __init__(self, **kwargs):
1404 super().__init__(**kwargs)
1405 self.config.doGenerateImage = False
1406 self.config.doGenerateData = True
1407
1408 self.config.doBrighterFatter = False
1409 self.config.doDefects = False
1410 self.config.doCrosstalkCoeffs = True
1411 self.config.doTransmissionCurve = False
1412 self.config.doLinearizer = False
1413
1414
1416 """Simulated linearizer.
1417 """
1418 def __init__(self, **kwargs):
1419 super().__init__(**kwargs)
1420 self.config.doGenerateImage = False
1421 self.config.doGenerateData = True
1422
1423 self.config.doBrighterFatter = False
1424 self.config.doDefects = False
1425 self.config.doCrosstalkCoeffs = False
1426 self.config.doTransmissionCurve = False
1427 self.config.doLinearizer = True
1428
1429
1431 """Simulated transmission curve.
1432 """
1433 def __init__(self, **kwargs):
1434 super().__init__(**kwargs)
1435 self.config.doGenerateImage = False
1436 self.config.doGenerateData = True
1437
1438 self.config.doBrighterFatter = False
1439 self.config.doDefects = False
1440 self.config.doCrosstalkCoeffs = False
1441 self.config.doTransmissionCurve = True
1442 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.