emccd-detect 2.2.5__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- emccd_detect/__init__.py +2 -0
- emccd_detect/cosmics.py +124 -0
- emccd_detect/emccd_detect.py +693 -0
- emccd_detect/rand_em_gain.py +187 -0
- emccd_detect/util/__init__.py +1 -0
- emccd_detect/util/metadata.yaml +60 -0
- emccd_detect/util/read_metadata.py +90 -0
- emccd_detect/util/read_metadata_wrapper.py +157 -0
- emccd_detect-2.2.5.dist-info/METADATA +69 -0
- emccd_detect-2.2.5.dist-info/RECORD +12 -0
- emccd_detect-2.2.5.dist-info/WHEEL +5 -0
- emccd_detect-2.2.5.dist-info/top_level.txt +1 -0
@@ -0,0 +1,693 @@
|
|
1
|
+
# -*- coding: utf-8 -*-
|
2
|
+
"""Simulation for EMCCD detector."""
|
3
|
+
|
4
|
+
import os
|
5
|
+
import warnings
|
6
|
+
from pathlib import Path
|
7
|
+
|
8
|
+
import numpy as np
|
9
|
+
|
10
|
+
from emccd_detect.cosmics import cosmic_hits, sat_tails
|
11
|
+
from emccd_detect.rand_em_gain import rand_em_gain
|
12
|
+
from emccd_detect.util.read_metadata_wrapper import MetadataWrapper
|
13
|
+
try:
|
14
|
+
from arcticpy import add_cti, CCD, ROE, Trap, TrapInstantCapture
|
15
|
+
except:
|
16
|
+
pass
|
17
|
+
|
18
|
+
|
19
|
+
|
20
|
+
class EMCCDDetectException(Exception):
|
21
|
+
"""Exception class for emccd_detect module."""
|
22
|
+
|
23
|
+
|
24
|
+
class EMCCDDetectBase:
|
25
|
+
"""Base class for EMCCD detector.
|
26
|
+
|
27
|
+
Parameters
|
28
|
+
----------
|
29
|
+
em_gain : float
|
30
|
+
Electron multiplying gain (e-/photoelectron).
|
31
|
+
full_well_image : float
|
32
|
+
Image area full well capacity (e-).
|
33
|
+
full_well_serial : float
|
34
|
+
Serial (gain) register full well capacity (e-).
|
35
|
+
dark_current: float
|
36
|
+
Dark current rate (e-/pix/s).
|
37
|
+
cic : float
|
38
|
+
Clock induced charge (e-/pix/frame).
|
39
|
+
read_noise : float
|
40
|
+
Read noise (e-/pix/frame).
|
41
|
+
bias : float
|
42
|
+
Bias offset (e-).
|
43
|
+
qe : float
|
44
|
+
Quantum efficiency.
|
45
|
+
cr_rate : float
|
46
|
+
Cosmic ray rate (hits/cm^2/s).
|
47
|
+
pixel_pitch : float
|
48
|
+
Distance between pixel centers (m).
|
49
|
+
eperdn : float
|
50
|
+
Electrons per dn.
|
51
|
+
nbits : int
|
52
|
+
Number of bits used by the ADC readout. Must be between 1 and 64,
|
53
|
+
inclusive.
|
54
|
+
numel_gain_register : int
|
55
|
+
Number of gain register elements. For eventually modeling partial CIC.
|
56
|
+
|
57
|
+
"""
|
58
|
+
def __init__(
|
59
|
+
self,
|
60
|
+
em_gain,
|
61
|
+
full_well_image,
|
62
|
+
full_well_serial,
|
63
|
+
dark_current,
|
64
|
+
cic,
|
65
|
+
read_noise,
|
66
|
+
bias,
|
67
|
+
qe,
|
68
|
+
cr_rate,
|
69
|
+
pixel_pitch,
|
70
|
+
eperdn,
|
71
|
+
nbits,
|
72
|
+
numel_gain_register
|
73
|
+
):
|
74
|
+
# Input checks
|
75
|
+
if not isinstance(nbits, (int, np.integer)):
|
76
|
+
raise EMCCDDetectException('nbits must be an integer')
|
77
|
+
if nbits < 1 or nbits > 64:
|
78
|
+
raise EMCCDDetectException('nbits must be between 1 and 64, '
|
79
|
+
'inclusive')
|
80
|
+
|
81
|
+
self.em_gain = em_gain
|
82
|
+
self.full_well_image = full_well_image
|
83
|
+
self.full_well_serial = full_well_serial
|
84
|
+
self.dark_current = dark_current
|
85
|
+
self.cic = cic
|
86
|
+
self.read_noise = read_noise
|
87
|
+
self.bias = bias
|
88
|
+
self.qe = qe
|
89
|
+
self.cr_rate = cr_rate
|
90
|
+
self.pixel_pitch = pixel_pitch
|
91
|
+
self.eperdn = eperdn
|
92
|
+
self.nbits = nbits
|
93
|
+
self.numel_gain_register = numel_gain_register
|
94
|
+
|
95
|
+
# Placeholders for trap parameters
|
96
|
+
self.ccd = None
|
97
|
+
self.roe = None
|
98
|
+
self.traps = None
|
99
|
+
self.express = None
|
100
|
+
self.offset = None
|
101
|
+
self.window_range = None
|
102
|
+
|
103
|
+
# Placeholders for derived values
|
104
|
+
self.mean_expected_rate = None
|
105
|
+
|
106
|
+
@property
|
107
|
+
def eperdn(self):
|
108
|
+
return self._eperdn
|
109
|
+
|
110
|
+
@eperdn.setter
|
111
|
+
def eperdn(self, eperdn):
|
112
|
+
try:
|
113
|
+
eperdn = float(eperdn)
|
114
|
+
except Exception:
|
115
|
+
raise EMCCDDetectException('eperdn value must be a float')
|
116
|
+
|
117
|
+
if eperdn <= 0:
|
118
|
+
raise EMCCDDetectException('eperdn values must be positve.')
|
119
|
+
else:
|
120
|
+
self._eperdn = eperdn
|
121
|
+
|
122
|
+
try:
|
123
|
+
def update_cti(
|
124
|
+
self,
|
125
|
+
ccd=None,
|
126
|
+
roe=None,
|
127
|
+
traps=None,
|
128
|
+
express=1,
|
129
|
+
offset=0,
|
130
|
+
window_range=None
|
131
|
+
):
|
132
|
+
# Update parameters
|
133
|
+
self.ccd = ccd
|
134
|
+
self.roe = roe
|
135
|
+
self.traps = traps
|
136
|
+
|
137
|
+
self.express = express
|
138
|
+
self.offset = offset
|
139
|
+
self.window_range = window_range
|
140
|
+
|
141
|
+
# Instantiate defaults for any class instances not provided
|
142
|
+
if self.ccd is None:
|
143
|
+
self.ccd = CCD()
|
144
|
+
if roe is None:
|
145
|
+
self.roe = ROE()
|
146
|
+
if traps is None:
|
147
|
+
#self.traps = [Trap()]
|
148
|
+
self.traps = [TrapInstantCapture()]
|
149
|
+
|
150
|
+
def unset_cti(self):
|
151
|
+
# Remove CTI simulation
|
152
|
+
self.ccd = None
|
153
|
+
self.roe = None
|
154
|
+
self.traps = None
|
155
|
+
except:
|
156
|
+
pass
|
157
|
+
|
158
|
+
def sim_sub_frame(self, fluxmap, frametime):
|
159
|
+
"""Simulate a partial detector frame.
|
160
|
+
|
161
|
+
This runs the same algorithm as sim_full_frame, but only on the given
|
162
|
+
fluxmap without surrounding it with prescan/overscan. The input fluxmap
|
163
|
+
array may be arbitrary in shape and an image array of the same shape
|
164
|
+
will be returned.
|
165
|
+
|
166
|
+
Parameters
|
167
|
+
----------
|
168
|
+
fluxmap : array_like
|
169
|
+
Input fluxmap of arbitrary shape (phot/pix/s).
|
170
|
+
frametime : float
|
171
|
+
Frame exposure time (s).
|
172
|
+
|
173
|
+
Returns
|
174
|
+
-------
|
175
|
+
output_counts : array_like
|
176
|
+
Detector output counts, same shape as input fluxmap (dn).
|
177
|
+
|
178
|
+
Notes
|
179
|
+
-----
|
180
|
+
This method is just as accurate and will return the same results as if
|
181
|
+
the user ran sim_full_frame and then subsectioned the input fluxmap,
|
182
|
+
with the exception of cosmic tails.
|
183
|
+
|
184
|
+
It is slightly less accurate when cosmics are used, since the tail
|
185
|
+
wrapping will be too strong. In a full frame the cosmic tails wrap into
|
186
|
+
the next row in the prescan and trail off significantly before getting
|
187
|
+
back to the image area, but here there is no prescan so the tails will
|
188
|
+
be immediately wrapped back into the image.
|
189
|
+
|
190
|
+
"""
|
191
|
+
# Simulate the integration process
|
192
|
+
exposed_pix_m = np.ones_like(fluxmap).astype(bool) # No unexposed pixels
|
193
|
+
actualized_e = self.integrate(fluxmap.copy(), frametime, exposed_pix_m)
|
194
|
+
|
195
|
+
# Simulate parallel clocking
|
196
|
+
parallel_counts = self.clock_parallel(actualized_e)
|
197
|
+
|
198
|
+
# Simulate serial clocking (output will be flattened to 1d)
|
199
|
+
empty_element_m = np.zeros_like(parallel_counts).astype(bool) # No empty elements
|
200
|
+
gain_counts = self.clock_serial(parallel_counts, empty_element_m)
|
201
|
+
|
202
|
+
# Simulate amplifier and adc redout
|
203
|
+
output_dn = self.readout(gain_counts)
|
204
|
+
|
205
|
+
# Reshape from 1d to 2d
|
206
|
+
return output_dn.reshape(actualized_e.shape)
|
207
|
+
|
208
|
+
def integrate(self, fluxmap_full, frametime, exposed_pix_m):
|
209
|
+
# Add cosmic ray effects
|
210
|
+
# XXX Maybe change this to units of flux later
|
211
|
+
cosm_actualized_e = cosmic_hits(np.zeros_like(fluxmap_full),
|
212
|
+
self.cr_rate, frametime,
|
213
|
+
self.pixel_pitch, self.full_well_image)
|
214
|
+
|
215
|
+
# Mask flux out of unexposed (covered) pixels
|
216
|
+
fluxmap_full[~exposed_pix_m] = 0
|
217
|
+
cosm_actualized_e[~exposed_pix_m] = 0
|
218
|
+
|
219
|
+
# Simulate imaging area pixel effects over time
|
220
|
+
actualized_e = self._imaging_area_elements(fluxmap_full, frametime,
|
221
|
+
cosm_actualized_e)
|
222
|
+
|
223
|
+
return actualized_e
|
224
|
+
|
225
|
+
def clock_parallel(self, actualized_e):
|
226
|
+
# Only add CTI if update_cti has been called
|
227
|
+
if self.ccd is not None and self.roe is not None and self.traps is not None:
|
228
|
+
parallel_counts = add_cti(
|
229
|
+
actualized_e.copy(),
|
230
|
+
parallel_roe=self.roe,
|
231
|
+
parallel_ccd=self.ccd,
|
232
|
+
parallel_traps=self.traps,
|
233
|
+
parallel_express=self.express,
|
234
|
+
parallel_offset=self.offset,
|
235
|
+
parallel_window_range=self.window_range
|
236
|
+
)
|
237
|
+
else:
|
238
|
+
parallel_counts = actualized_e
|
239
|
+
|
240
|
+
return parallel_counts
|
241
|
+
|
242
|
+
def clock_serial(self, actualized_e_full, empty_element_m):
|
243
|
+
# Actualize cic electrons in prescan and overscan pixels
|
244
|
+
# XXX Another place where we are fudging a little
|
245
|
+
actualized_e_full[empty_element_m] = np.random.poisson(actualized_e_full[empty_element_m]
|
246
|
+
+ self.cic)
|
247
|
+
# XXX Call arcticpy here
|
248
|
+
# Flatten row by row
|
249
|
+
actualized_e_full_flat = actualized_e_full.ravel()
|
250
|
+
|
251
|
+
# Clock electrons through serial register elements
|
252
|
+
serial_counts = self._serial_register_elements(actualized_e_full_flat)
|
253
|
+
|
254
|
+
# Clock electrons through gain register elements
|
255
|
+
gain_counts = self._gain_register_elements(serial_counts)
|
256
|
+
|
257
|
+
return gain_counts
|
258
|
+
|
259
|
+
def readout(self, gain_counts):
|
260
|
+
# Pass electrons through amplifier
|
261
|
+
amp_ev = self._amp(gain_counts)
|
262
|
+
|
263
|
+
# Pass amp electron volt counts through analog to digital converter
|
264
|
+
output_dn = self._adc(amp_ev)
|
265
|
+
|
266
|
+
return output_dn
|
267
|
+
|
268
|
+
def _imaging_area_elements(self, fluxmap_full, frametime, cosm_actualized_e):
|
269
|
+
"""Simulate imaging area pixel behavior for a given fluxmap and
|
270
|
+
frametime.
|
271
|
+
|
272
|
+
Note that the imaging area is defined as the active pixels which are
|
273
|
+
exposed to light plus the surrounding dark reference and transition
|
274
|
+
areas, which are covered and recieve no light. These pixels are
|
275
|
+
indentical to the active area, so while they recieve none of the
|
276
|
+
fluxmap they still have the same noise profile.
|
277
|
+
|
278
|
+
Parameters
|
279
|
+
----------
|
280
|
+
fluxmap_full : array_like
|
281
|
+
Incident photon rate fluxmap (phot/pix/s).
|
282
|
+
frametime : float
|
283
|
+
Frame exposure time (s).
|
284
|
+
cosm_actualized_e : array_like
|
285
|
+
Electrons actualized from cosmic rays, same size as fluxmap_full (-e).
|
286
|
+
|
287
|
+
Returns
|
288
|
+
-------
|
289
|
+
actualized_e : array_like
|
290
|
+
Map of actualized electrons (e-).
|
291
|
+
|
292
|
+
"""
|
293
|
+
# Calculate mean photo-electrons after integrating over frametime
|
294
|
+
mean_phe_map = fluxmap_full * frametime * self.qe
|
295
|
+
|
296
|
+
# Calculate mean expected rate after integrating over frametime
|
297
|
+
mean_dark = self.dark_current * frametime
|
298
|
+
mean_noise = mean_dark + self.cic
|
299
|
+
|
300
|
+
# Set mean expected rate (commonly referred to as lambda)
|
301
|
+
self.mean_expected_rate = mean_phe_map + mean_noise
|
302
|
+
|
303
|
+
# Actualize electrons at the pixels
|
304
|
+
actualized_e = np.random.poisson(self.mean_expected_rate).astype(float)
|
305
|
+
|
306
|
+
# Add cosmic ray effects
|
307
|
+
# XXX Maybe change this to units of flux later
|
308
|
+
actualized_e += cosm_actualized_e
|
309
|
+
|
310
|
+
# Cap at pixel full well capacity
|
311
|
+
actualized_e[actualized_e > self.full_well_image] = self.full_well_image
|
312
|
+
|
313
|
+
return actualized_e
|
314
|
+
|
315
|
+
def _serial_register_elements(self, actualized_e_full_flat):
|
316
|
+
"""Simulate serial register element behavior.
|
317
|
+
|
318
|
+
Parameters
|
319
|
+
----------
|
320
|
+
actualized_e_full_flat : array_like
|
321
|
+
Electrons actualized before clocking through the serial register.
|
322
|
+
|
323
|
+
Returns
|
324
|
+
-------
|
325
|
+
serial_counts : array_like
|
326
|
+
Electrons counts after passing through serial register elements.
|
327
|
+
|
328
|
+
"""
|
329
|
+
serial_counts = actualized_e_full_flat
|
330
|
+
return serial_counts
|
331
|
+
|
332
|
+
def _gain_register_elements(self, serial_counts):
|
333
|
+
"""Simulate gain register element behavior.
|
334
|
+
|
335
|
+
Parameters
|
336
|
+
----------
|
337
|
+
serial_counts : array_like
|
338
|
+
Electrons counts after passing through serial register elements.
|
339
|
+
|
340
|
+
Returns
|
341
|
+
-------
|
342
|
+
gain_counts : array_like
|
343
|
+
Electron counts after passing through gain register elements.
|
344
|
+
|
345
|
+
"""
|
346
|
+
# Apply EM gain
|
347
|
+
gain_counts = np.zeros_like(serial_counts)
|
348
|
+
|
349
|
+
gain_counts = rand_em_gain(
|
350
|
+
n_in_array=serial_counts,
|
351
|
+
em_gain=self.em_gain)
|
352
|
+
|
353
|
+
# Simulate saturation tails
|
354
|
+
if self.cr_rate != 0:
|
355
|
+
gain_counts = sat_tails(gain_counts, self.full_well_serial)
|
356
|
+
|
357
|
+
# Cap at full well capacity of gain register
|
358
|
+
gain_counts[gain_counts > self.full_well_serial] = self.full_well_serial
|
359
|
+
|
360
|
+
return gain_counts
|
361
|
+
|
362
|
+
def _amp(self, serial_counts):
|
363
|
+
"""Simulate amp behavior.
|
364
|
+
|
365
|
+
Parameters
|
366
|
+
----------
|
367
|
+
serial_counts : array_like
|
368
|
+
Electron counts from the serial register.
|
369
|
+
|
370
|
+
Returns
|
371
|
+
-------
|
372
|
+
amp_ev : array_like
|
373
|
+
Output from amp (eV).
|
374
|
+
|
375
|
+
Notes
|
376
|
+
-----
|
377
|
+
Read noise is the amplifier read noise and not the effective read noise
|
378
|
+
after the application of EM gain.
|
379
|
+
|
380
|
+
"""
|
381
|
+
# Create read noise distribution in units of electrons
|
382
|
+
read_noise_e = self.read_noise * np.random.normal(size=serial_counts.shape)
|
383
|
+
|
384
|
+
# Apply read noise and bias to counts to get output electron volts
|
385
|
+
amp_ev = serial_counts + read_noise_e + self.bias
|
386
|
+
|
387
|
+
return amp_ev
|
388
|
+
|
389
|
+
def _adc(self, amp_ev):
|
390
|
+
"""Simulate analog to digital converter behavior.
|
391
|
+
|
392
|
+
Parameters
|
393
|
+
----------
|
394
|
+
amp_ev : array_like
|
395
|
+
Electron volt counts from amp (eV).
|
396
|
+
|
397
|
+
Returns
|
398
|
+
-------
|
399
|
+
output_dn : array_like
|
400
|
+
Analog to digital converter output (dn).
|
401
|
+
|
402
|
+
"""
|
403
|
+
# Convert from electron volts to dn
|
404
|
+
dn_min = 0
|
405
|
+
dn_max = 2**self.nbits - 1
|
406
|
+
output_dn = np.clip(amp_ev / self.eperdn, dn_min, dn_max).astype(np.uint64)
|
407
|
+
|
408
|
+
return output_dn
|
409
|
+
|
410
|
+
|
411
|
+
class EMCCDDetect(EMCCDDetectBase):
|
412
|
+
"""Create an EMCCD-detected image for a given fluxmap.
|
413
|
+
|
414
|
+
This class gives a method for simulating full frames (sim_full_frame) and
|
415
|
+
also for adding simulated noise only to the input fluxmap (sim_sub_frame).
|
416
|
+
|
417
|
+
Parameters
|
418
|
+
----------
|
419
|
+
em_gain : float
|
420
|
+
Electron multiplying gain (e-/photoelectron). Defaults to 5000.
|
421
|
+
full_well_image : float
|
422
|
+
Image area full well capacity (e-). Defaults to 78000.
|
423
|
+
full_well_serial : float
|
424
|
+
Serial (gain) register full well capacity (e-). Defaults to None.
|
425
|
+
dark_current: float
|
426
|
+
Dark current rate (e-/pix/s). Defaults to 0.00031.
|
427
|
+
cic : float
|
428
|
+
Clock induced charge (e-/pix/frame). Defaults to 0.016.
|
429
|
+
read_noise : float
|
430
|
+
Read noise (e-/pix/frame). Defaults to 110.
|
431
|
+
bias : float
|
432
|
+
Bias offset (e-). Defaults to 1500.
|
433
|
+
qe : float
|
434
|
+
Quantum efficiency. Defaults to 0.9.
|
435
|
+
cr_rate : float
|
436
|
+
Cosmic ray rate (hits/cm^2/s). Defaults to 0.
|
437
|
+
pixel_pitch : float
|
438
|
+
Distance between pixel centers (m). Defaults to 13e-6.
|
439
|
+
eperdn : float
|
440
|
+
Electrons per dn. Defaults to None.
|
441
|
+
nbits : int
|
442
|
+
Number of bits used by the ADC readout. Must be between 1 and 64,
|
443
|
+
inclusive. Defaults to 14.
|
444
|
+
numel_gain_register : int
|
445
|
+
Number of gain register elements. For eventually modeling partial CIC.
|
446
|
+
Defaults to 604.
|
447
|
+
meta_path : str
|
448
|
+
Full path of metadata.yaml.
|
449
|
+
|
450
|
+
"""
|
451
|
+
def __init__(
|
452
|
+
self,
|
453
|
+
em_gain=1.,
|
454
|
+
full_well_image=78000.,
|
455
|
+
full_well_serial=None,
|
456
|
+
dark_current=0.00031,
|
457
|
+
cic=0.016,
|
458
|
+
read_noise=110.,
|
459
|
+
bias=1500.,
|
460
|
+
qe=0.9,
|
461
|
+
cr_rate=0.,
|
462
|
+
pixel_pitch=13e-6,
|
463
|
+
eperdn=None,
|
464
|
+
nbits=14,
|
465
|
+
numel_gain_register=604,
|
466
|
+
meta_path=None
|
467
|
+
):
|
468
|
+
# If no metadata file path specified, default to metadata.yaml in util
|
469
|
+
if meta_path is None:
|
470
|
+
here = os.path.abspath(os.path.dirname(__file__))
|
471
|
+
meta_path = Path(here, 'util', 'metadata.yaml')
|
472
|
+
|
473
|
+
# Before inheriting base class, get metadata
|
474
|
+
self.meta_path = meta_path
|
475
|
+
self.meta = MetadataWrapper(self.meta_path)
|
476
|
+
|
477
|
+
# Set defaults from metadata
|
478
|
+
if full_well_serial is None:
|
479
|
+
full_well_serial = self.meta.fwc
|
480
|
+
if eperdn is None:
|
481
|
+
eperdn = self.meta.eperdn
|
482
|
+
|
483
|
+
super().__init__(
|
484
|
+
em_gain=em_gain,
|
485
|
+
full_well_image=full_well_image,
|
486
|
+
full_well_serial=full_well_serial,
|
487
|
+
dark_current=dark_current,
|
488
|
+
cic=cic,
|
489
|
+
read_noise=read_noise,
|
490
|
+
bias=bias,
|
491
|
+
qe=qe,
|
492
|
+
cr_rate=cr_rate,
|
493
|
+
pixel_pitch=pixel_pitch,
|
494
|
+
eperdn=eperdn,
|
495
|
+
nbits=nbits,
|
496
|
+
numel_gain_register=numel_gain_register
|
497
|
+
)
|
498
|
+
|
499
|
+
def sim_full_frame(self, fluxmap, frametime):
|
500
|
+
"""Simulate a full detector frame.
|
501
|
+
|
502
|
+
Note that the fluxmap provided must be the same size as the exposed
|
503
|
+
detector pixels (specified in self.meta.geom['image']). A full frame
|
504
|
+
including prescan and overscan regions will be made around the fluxmap.
|
505
|
+
|
506
|
+
Parameters
|
507
|
+
----------
|
508
|
+
fluxmap : array_like
|
509
|
+
Input fluxmap, same shape as self.meta.geom['image'] (phot/pix/s).
|
510
|
+
frametime : float
|
511
|
+
Frame exposure time (s).
|
512
|
+
|
513
|
+
Returns
|
514
|
+
-------
|
515
|
+
output_counts : array_like
|
516
|
+
Detector output counts, including prescan/overscan (dn).
|
517
|
+
|
518
|
+
"""
|
519
|
+
# Initialize the imaging area pixels
|
520
|
+
imaging_area_zeros = self.meta.imaging_area_zeros.copy()
|
521
|
+
# Embed the fluxmap within the imaging area. Create a mask for
|
522
|
+
# referencing the input fluxmap subsection later
|
523
|
+
fluxmap_full = self.meta.embed_im(imaging_area_zeros, 'image',
|
524
|
+
fluxmap.copy())
|
525
|
+
exposed_pix_m = self.meta.imaging_slice(self.meta.mask('image'))
|
526
|
+
# Simulate the integration process
|
527
|
+
actualized_e = self.integrate(fluxmap_full, frametime, exposed_pix_m)
|
528
|
+
|
529
|
+
# Simulate parallel clocking
|
530
|
+
parallel_counts = self.clock_parallel(actualized_e)
|
531
|
+
|
532
|
+
# Initialize the serial register elements.
|
533
|
+
full_frame_zeros = self.meta.full_frame_zeros.copy()
|
534
|
+
# Embed the imaging area within the full frame. Create a mask for
|
535
|
+
# referencing the prescan and overscan subsections later
|
536
|
+
parallel_counts_full = self.meta.imaging_embed(full_frame_zeros, parallel_counts)
|
537
|
+
empty_element_m = (self.meta.mask('prescan')
|
538
|
+
+ self.meta.mask('parallel_overscan')
|
539
|
+
+ self.meta.mask('serial_overscan'))
|
540
|
+
# Simulate serial clocking
|
541
|
+
gain_counts = self.clock_serial(parallel_counts_full, empty_element_m)
|
542
|
+
|
543
|
+
# Simulate amplifier and adc redout
|
544
|
+
output_dn = self.readout(gain_counts)
|
545
|
+
|
546
|
+
# Reshape from 1d to 2d
|
547
|
+
return output_dn.reshape(parallel_counts_full.shape)
|
548
|
+
|
549
|
+
def slice_fluxmap(self, full_frame):
|
550
|
+
"""Return only the fluxmap portion of a full frame.
|
551
|
+
|
552
|
+
Parameters
|
553
|
+
----------
|
554
|
+
full_frame : array_like
|
555
|
+
Simulated full frame.
|
556
|
+
|
557
|
+
Returns
|
558
|
+
-------
|
559
|
+
array_like
|
560
|
+
Fluxmap area of full frame.
|
561
|
+
|
562
|
+
"""
|
563
|
+
return self.meta.slice_section(full_frame, 'image')
|
564
|
+
|
565
|
+
def slice_prescan(self, full_frame):
|
566
|
+
"""Return only the prescan portion of a full frame.
|
567
|
+
|
568
|
+
Parameters
|
569
|
+
----------
|
570
|
+
full_frame : array_like
|
571
|
+
Simulated full frame.
|
572
|
+
|
573
|
+
Returns
|
574
|
+
-------
|
575
|
+
array_like
|
576
|
+
Prescan area of a full frame.
|
577
|
+
|
578
|
+
"""
|
579
|
+
return self.meta.slice_section(full_frame, 'prescan')
|
580
|
+
|
581
|
+
def get_e_frame(self, frame_dn):
|
582
|
+
"""Take a raw frame output from EMCCDDetect and convert to a gain
|
583
|
+
divided, bias subtracted frame in units of electrons.
|
584
|
+
|
585
|
+
This will give the pre-readout image, i.e. the image in units of e- on
|
586
|
+
the imaging plane.
|
587
|
+
|
588
|
+
Parameters
|
589
|
+
----------
|
590
|
+
frame_dn : array_like
|
591
|
+
Raw output frame from EMCCDDetect, units of dn.
|
592
|
+
|
593
|
+
Returns
|
594
|
+
-------
|
595
|
+
array_like
|
596
|
+
Bias subtracted, gain divided frame in units of e-.
|
597
|
+
|
598
|
+
"""
|
599
|
+
return (frame_dn * self.eperdn - self.bias) / self.em_gain
|
600
|
+
|
601
|
+
|
602
|
+
def emccd_detect(
|
603
|
+
fluxmap,
|
604
|
+
frametime,
|
605
|
+
em_gain,
|
606
|
+
full_well_image=50000.,
|
607
|
+
full_well_serial=90000.,
|
608
|
+
dark_current=0.0028,
|
609
|
+
cic=0.01,
|
610
|
+
read_noise=100,
|
611
|
+
bias=0.,
|
612
|
+
qe=0.9,
|
613
|
+
cr_rate=0.,
|
614
|
+
pixel_pitch=13e-6,
|
615
|
+
shot_noise_on=None
|
616
|
+
):
|
617
|
+
"""Create an EMCCD-detected image for a given fluxmap.
|
618
|
+
|
619
|
+
This is a convenience function which wraps the base class implementation
|
620
|
+
of the EMCCD simulator. It maintains the API of emccd_detect version 1.0.1.
|
621
|
+
Note that output is in units of electrons, not dn.
|
622
|
+
|
623
|
+
Parameters
|
624
|
+
----------
|
625
|
+
fluxmap : array_like, float
|
626
|
+
Input fluxmap (photons/pix/s).
|
627
|
+
frametime : float
|
628
|
+
Frame time (s).
|
629
|
+
em_gain : float
|
630
|
+
Electron multiplying gain (e-/photoelectron).
|
631
|
+
full_well_image : float
|
632
|
+
Image area full well capacity (e-). Defaults to 50000.
|
633
|
+
full_well_serial : float
|
634
|
+
Serial (gain) register full well capacity (e-). Defaults to 90000.
|
635
|
+
dark_current: float
|
636
|
+
Dark current rate (e-/pix/s). Defaults to 0.0028.
|
637
|
+
cic : float
|
638
|
+
Clock induced charge (e-/pix/frame). Defaults to 0.01.
|
639
|
+
read_noise : float
|
640
|
+
Read noise (e-/pix/frame). Defaults to 100.
|
641
|
+
bias : float
|
642
|
+
Bias offset (e-). Defaults to 0.
|
643
|
+
qe : float
|
644
|
+
Quantum efficiency. Defaults to 0.9.
|
645
|
+
cr_rate : float
|
646
|
+
Cosmic ray rate (hits/cm^2/s). Defaults to 0.
|
647
|
+
pixel_pitch : float
|
648
|
+
Distance between pixel centers (m). Defaults to 13e-6.
|
649
|
+
shot_noise_on : bool, optional
|
650
|
+
Apply shot noise. Defaults to None. [No longer supported as of v2.1.0.
|
651
|
+
Input will have no effect.
|
652
|
+
|
653
|
+
Returns
|
654
|
+
-------
|
655
|
+
serial_frame : array_like, float
|
656
|
+
Detector output (e-).
|
657
|
+
|
658
|
+
Notes
|
659
|
+
-----
|
660
|
+
The value for eperdn (electrons per dn) is hardcoded to 1. This is for
|
661
|
+
legacy purposes, as the version 1.0.1 implementation output electrons
|
662
|
+
instead of dn.
|
663
|
+
|
664
|
+
The legacy version also has no gain register CIC, so
|
665
|
+
numel_gain_register is irrelevant.
|
666
|
+
|
667
|
+
The legacy version also had no ADC (it just output floats), so the number
|
668
|
+
of bits is set as high as possible (64) and the output is converted to
|
669
|
+
floats. This will still be different from the legacy version as there will
|
670
|
+
no longer be negative numbers.
|
671
|
+
|
672
|
+
"""
|
673
|
+
if shot_noise_on is not None:
|
674
|
+
warnings.warn('Shot noise parameter no longer supported. Input has no '
|
675
|
+
'effect')
|
676
|
+
|
677
|
+
emccd = EMCCDDetectBase(
|
678
|
+
em_gain=em_gain,
|
679
|
+
full_well_image=full_well_image,
|
680
|
+
full_well_serial=full_well_serial,
|
681
|
+
dark_current=dark_current,
|
682
|
+
cic=cic,
|
683
|
+
read_noise=read_noise,
|
684
|
+
bias=bias,
|
685
|
+
qe=qe,
|
686
|
+
cr_rate=cr_rate,
|
687
|
+
pixel_pitch=pixel_pitch,
|
688
|
+
eperdn=1.,
|
689
|
+
nbits=64,
|
690
|
+
numel_gain_register=604
|
691
|
+
)
|
692
|
+
|
693
|
+
return emccd.sim_sub_frame(fluxmap, frametime).astype(float)
|