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.
@@ -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)