getframes 2.1.1__tar.gz → 2.3.0__tar.gz

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.
Files changed (119) hide show
  1. {getframes-2.1.1 → getframes-2.3.0}/.gitignore +3 -1
  2. {getframes-2.1.1 → getframes-2.3.0}/CHANGELOG.md +249 -1
  3. getframes-2.3.0/PKG-INFO +216 -0
  4. getframes-2.3.0/README.md +170 -0
  5. getframes-2.3.0/benchmarks/bench_detector_workspace.py +122 -0
  6. {getframes-2.1.1 → getframes-2.3.0}/benchmarks/bench_devices.py +10 -4
  7. getframes-2.3.0/benchmarks/bench_fixed_map_dtype.py +140 -0
  8. getframes-2.3.0/benchmarks/detector-workspace-results.json +41 -0
  9. {getframes-2.1.1 → getframes-2.3.0}/benchmarks/device-results.json +55 -55
  10. {getframes-2.1.1 → getframes-2.3.0}/benchmarks/device-results.md +7 -7
  11. getframes-2.3.0/benchmarks/fixed-map-dtype-results.json +85 -0
  12. getframes-2.3.0/examples/15_detector_characterization.py +259 -0
  13. getframes-2.3.0/examples/16_detector_showcase.py +514 -0
  14. {getframes-2.1.1 → getframes-2.3.0}/examples/README.md +10 -0
  15. getframes-2.3.0/examples/detector_showcase.webp +0 -0
  16. {getframes-2.1.1 → getframes-2.3.0}/pyproject.toml +3 -2
  17. {getframes-2.1.1 → getframes-2.3.0}/src/getframes/__about__.py +1 -1
  18. {getframes-2.1.1 → getframes-2.3.0}/src/getframes/__init__.py +16 -0
  19. getframes-2.3.0/src/getframes/analysis/__init__.py +50 -0
  20. getframes-2.3.0/src/getframes/analysis/characterize.py +678 -0
  21. getframes-2.3.0/src/getframes/analysis/nondestructive.py +384 -0
  22. getframes-2.3.0/src/getframes/camera.py +1521 -0
  23. {getframes-2.1.1 → getframes-2.3.0}/src/getframes/cli.py +6 -1
  24. {getframes-2.1.1 → getframes-2.3.0}/src/getframes/config.py +332 -10
  25. {getframes-2.1.1 → getframes-2.3.0}/src/getframes/frame.py +4 -2
  26. {getframes-2.1.1 → getframes-2.3.0}/src/getframes/noise.py +681 -72
  27. {getframes-2.1.1 → getframes-2.3.0}/src/getframes/presets/data/andor_cb1_0_5mp.toml +15 -0
  28. getframes-2.3.0/src/getframes/presets/data/andor_marana_4_2b_11.toml +51 -0
  29. {getframes-2.1.1 → getframes-2.3.0}/src/getframes/presets/data/andor_ocam2k.toml +3 -0
  30. getframes-2.3.0/src/getframes/presets/data/first_light_imaging_cred_one.toml +139 -0
  31. {getframes-2.1.1 → getframes-2.3.0}/src/getframes/presets/data/generic_scmos.toml +3 -1
  32. {getframes-2.1.1 → getframes-2.3.0}/src/getframes/presets/data/hamamatsu_orca_fusion.toml +3 -1
  33. {getframes-2.1.1 → getframes-2.3.0}/src/getframes/presets/data/hamamatsu_orca_quest_2.toml +14 -0
  34. getframes-2.3.0/src/getframes/presets/data/leonardo_saphira.toml +36 -0
  35. {getframes-2.1.1 → getframes-2.3.0}/src/getframes/presets/data/photometrics_prime_95b.toml +17 -8
  36. {getframes-2.1.1 → getframes-2.3.0}/src/getframes/presets/data/princeton_instruments_kuro_1200b.toml +17 -8
  37. {getframes-2.1.1 → getframes-2.3.0}/src/getframes/presets/data/tucsen_aries_6504_pro.toml +14 -1
  38. {getframes-2.1.1 → getframes-2.3.0}/src/getframes/scene/optics.py +3 -2
  39. {getframes-2.1.1 → getframes-2.3.0}/src/getframes/scene/photometry.py +43 -5
  40. {getframes-2.1.1 → getframes-2.3.0}/src/getframes/scene/psf.py +47 -34
  41. {getframes-2.1.1 → getframes-2.3.0}/src/getframes/scene/thermal.py +2 -5
  42. {getframes-2.1.1 → getframes-2.3.0}/src/getframes/spectral.py +10 -1
  43. getframes-2.3.0/tests/test_camera.py +606 -0
  44. getframes-2.3.0/tests/test_characterize.py +389 -0
  45. {getframes-2.1.1 → getframes-2.3.0}/tests/test_config.py +76 -0
  46. getframes-2.3.0/tests/test_conformance.py +83 -0
  47. {getframes-2.1.1 → getframes-2.3.0}/tests/test_detector.py +116 -0
  48. {getframes-2.1.1 → getframes-2.3.0}/tests/test_gain.py +64 -0
  49. {getframes-2.1.1 → getframes-2.3.0}/tests/test_gpu.py +55 -0
  50. {getframes-2.1.1 → getframes-2.3.0}/tests/test_noise.py +23 -1
  51. getframes-2.3.0/tests/test_nondestructive_analysis.py +73 -0
  52. {getframes-2.1.1 → getframes-2.3.0}/tests/test_presets.py +34 -1
  53. {getframes-2.1.1 → getframes-2.3.0}/tests/test_radiometry.py +32 -0
  54. getframes-2.3.0/tests/test_realism.py +380 -0
  55. {getframes-2.1.1 → getframes-2.3.0}/tests/test_scale.py +31 -0
  56. {getframes-2.1.1 → getframes-2.3.0}/tests/test_scene.py +23 -0
  57. {getframes-2.1.1 → getframes-2.3.0}/tests/test_signal.py +79 -0
  58. {getframes-2.1.1 → getframes-2.3.0}/tests/test_spectral.py +13 -2
  59. {getframes-2.1.1 → getframes-2.3.0}/tests/test_validation.py +58 -0
  60. getframes-2.3.0/tests/test_workspace.py +188 -0
  61. getframes-2.1.1/PKG-INFO +0 -271
  62. getframes-2.1.1/README.md +0 -225
  63. getframes-2.1.1/src/getframes/analysis/__init__.py +0 -19
  64. getframes-2.1.1/src/getframes/camera.py +0 -805
  65. getframes-2.1.1/src/getframes/presets/data/andor_marana_4_2b_11.toml +0 -38
  66. getframes-2.1.1/src/getframes/presets/data/leonardo_saphira.toml +0 -32
  67. getframes-2.1.1/tests/test_camera.py +0 -103
  68. getframes-2.1.1/tests/test_realism.py +0 -105
  69. {getframes-2.1.1 → getframes-2.3.0}/LICENSE +0 -0
  70. {getframes-2.1.1 → getframes-2.3.0}/benchmarks/__init__.py +0 -0
  71. {getframes-2.1.1 → getframes-2.3.0}/benchmarks/render_device_table.py +0 -0
  72. {getframes-2.1.1 → getframes-2.3.0}/benchmarks/run.py +0 -0
  73. {getframes-2.1.1 → getframes-2.3.0}/examples/01_basic_dark_frame.py +0 -0
  74. {getframes-2.1.1 → getframes-2.3.0}/examples/02_custom_camera.py +0 -0
  75. {getframes-2.1.1 → getframes-2.3.0}/examples/03_master_dark.py +0 -0
  76. {getframes-2.1.1 → getframes-2.3.0}/examples/04_browse_presets.py +0 -0
  77. {getframes-2.1.1 → getframes-2.3.0}/examples/05_visualise.py +0 -0
  78. {getframes-2.1.1 → getframes-2.3.0}/examples/06_photon_transfer_curve.py +0 -0
  79. {getframes-2.1.1 → getframes-2.3.0}/examples/07_star_field_exposure.py +0 -0
  80. {getframes-2.1.1 → getframes-2.3.0}/examples/08_ao_limiting_magnitude.py +0 -0
  81. {getframes-2.1.1 → getframes-2.3.0}/examples/09_transit_photometry.py +0 -0
  82. {getframes-2.1.1 → getframes-2.3.0}/examples/10_detector_realism.py +0 -0
  83. {getframes-2.1.1 → getframes-2.3.0}/examples/11_radiometry_and_ir.py +0 -0
  84. {getframes-2.1.1 → getframes-2.3.0}/examples/12_ml_dataset.py +0 -0
  85. {getframes-2.1.1 → getframes-2.3.0}/examples/13_crowded_field.py +0 -0
  86. {getframes-2.1.1 → getframes-2.3.0}/examples/14_keck_lgs_ttf_trade_study.ipynb +0 -0
  87. {getframes-2.1.1 → getframes-2.3.0}/examples/_common.py +0 -0
  88. {getframes-2.1.1 → getframes-2.3.0}/src/getframes/analysis/apertures.py +0 -0
  89. {getframes-2.1.1 → getframes-2.3.0}/src/getframes/analysis/ptc.py +0 -0
  90. {getframes-2.1.1 → getframes-2.3.0}/src/getframes/backend.py +0 -0
  91. {getframes-2.1.1 → getframes-2.3.0}/src/getframes/calibrate.py +0 -0
  92. {getframes-2.1.1 → getframes-2.3.0}/src/getframes/dataset.py +0 -0
  93. {getframes-2.1.1 → getframes-2.3.0}/src/getframes/observation.py +0 -0
  94. {getframes-2.1.1 → getframes-2.3.0}/src/getframes/presets/__init__.py +0 -0
  95. {getframes-2.1.1 → getframes-2.3.0}/src/getframes/presets/data/__init__.py +0 -0
  96. {getframes-2.1.1 → getframes-2.3.0}/src/getframes/presets/data/andor_ikon_m934.toml +0 -0
  97. {getframes-2.1.1 → getframes-2.3.0}/src/getframes/presets/data/andor_ixon_ultra_888.toml +0 -0
  98. {getframes-2.1.1 → getframes-2.3.0}/src/getframes/presets/data/generic_ccd.toml +0 -0
  99. {getframes-2.1.1 → getframes-2.3.0}/src/getframes/presets/data/generic_cmos.toml +0 -0
  100. {getframes-2.1.1 → getframes-2.3.0}/src/getframes/presets/data/generic_eapd.toml +0 -0
  101. {getframes-2.1.1 → getframes-2.3.0}/src/getframes/presets/data/generic_emccd.toml +0 -0
  102. {getframes-2.1.1 → getframes-2.3.0}/src/getframes/presets/data/nuvu_hnu_128_omega.toml +0 -0
  103. {getframes-2.1.1 → getframes-2.3.0}/src/getframes/presets/data/nuvu_hnu_240.toml +0 -0
  104. {getframes-2.1.1 → getframes-2.3.0}/src/getframes/presets/data/qhy530_pro_ii.toml +0 -0
  105. {getframes-2.1.1 → getframes-2.3.0}/src/getframes/presets/data/scimeasure_little_joe_ccd39.toml +0 -0
  106. {getframes-2.1.1 → getframes-2.3.0}/src/getframes/presets/data/zwo_asi2600mm.toml +0 -0
  107. {getframes-2.1.1 → getframes-2.3.0}/src/getframes/py.typed +0 -0
  108. {getframes-2.1.1 → getframes-2.3.0}/src/getframes/scene/__init__.py +0 -0
  109. {getframes-2.1.1 → getframes-2.3.0}/src/getframes/scene/scene.py +0 -0
  110. {getframes-2.1.1 → getframes-2.3.0}/src/getframes/scene/sources.py +0 -0
  111. {getframes-2.1.1 → getframes-2.3.0}/src/getframes/scene/wcs.py +0 -0
  112. {getframes-2.1.1 → getframes-2.3.0}/tests/test_analysis.py +0 -0
  113. {getframes-2.1.1 → getframes-2.3.0}/tests/test_benchmarks.py +0 -0
  114. {getframes-2.1.1 → getframes-2.3.0}/tests/test_calibrate.py +0 -0
  115. {getframes-2.1.1 → getframes-2.3.0}/tests/test_cli.py +0 -0
  116. {getframes-2.1.1 → getframes-2.3.0}/tests/test_dataset.py +0 -0
  117. {getframes-2.1.1 → getframes-2.3.0}/tests/test_frame.py +0 -0
  118. {getframes-2.1.1 → getframes-2.3.0}/tests/test_observation.py +0 -0
  119. {getframes-2.1.1 → getframes-2.3.0}/tests/test_scene_enrich.py +0 -0
@@ -223,4 +223,6 @@ __marimo__/
223
223
  # Streamlit
224
224
  .streamlit/secrets.toml
225
225
  keck_ttf
226
- *.png
226
+ *.png
227
+ .vscode
228
+ *.npz
@@ -6,6 +6,252 @@ to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
6
6
 
7
7
  ## [Unreleased]
8
8
 
9
+ ## [2.3.0] - 2026-10-07
10
+
11
+ ### Fixed
12
+
13
+ - **`MoffatPSF`, `EllipticalGaussianPSF` and `AiryPSF` no longer put light that
14
+ falls off the frame back onto it.** They normalised their stamp *after*
15
+ clipping it to the frame, so a source on the edge column deposited its full
16
+ flux instead of the part that lands on the detector (a Moffat star centred on
17
+ column 0 kept 100% of its light, where `GaussianPSF` correctly keeps about
18
+ half). The stamp is now normalised before clipping, which follows the AO stack
19
+ convention that light lost at a detector edge is lost, not renormalised
20
+ (aocore CONVENTIONS 3.3). Sources wholly inside the frame are unchanged.
21
+
22
+ ### Changed
23
+
24
+ - **Generic primitives now come from `aocore`.** getframes is part of an
25
+ adaptive-optics stack whose shared conventions and primitives live in
26
+ [`aocore`](https://github.com/jacotay7/aocore), now a core dependency
27
+ (`aocore>=0.1.2,<0.2`). `noise.block_sum` is `aocore.block_sum`, re-exported
28
+ so existing imports keep working; it accepts everything it did before and now
29
+ also takes a `(fy, fx)` factor and leading batch axes, and raises a clear
30
+ `ValueError` for a factor below 1 or a shape that does not divide (previously
31
+ a bare reshape error, or a `ZeroDivisionError` for a factor of 0).
32
+ `Frame.binned` sums through it. `AiryPSF` and `Thermal` take their radians per
33
+ arcsecond from `aocore.ARCSEC_TO_RAD`, and `Vignetting` its centred grid from
34
+ `aocore.coordinate_grid`. Every output is bit-for-bit unchanged.
35
+
36
+ ### Added
37
+
38
+ - **Conformance tests** (`tests/test_conformance.py`) run the
39
+ `aocore.conformance` checks that apply to a detector package: every PSF model
40
+ deposits unit flux, a source at `(n - 1) / 2` and the vignetting pattern are
41
+ centred on the optical axis for odd and even windows.
42
+
43
+ ## [2.2.0] - 2026-08-24
44
+
45
+ ### Added
46
+
47
+ - **`CameraConfig.read_noise_correlated_fraction`.** The part of read noise that
48
+ is common to every read of a nondestructive ramp, and so cancels when two
49
+ reads are differenced. Without it every read was an independent draw and
50
+ correlated double sampling could only ever cost a factor of `sqrt(2)` — but a
51
+ real CDS frame is often *quieter* than a single read, because reference-level
52
+ drift, bias settling, and 1/f noise persist between the two reads and
53
+ differencing removes them. Defaults to `0`, which is the previous behaviour.
54
+ Only a differenced measurement can constrain it; single-read data cannot.
55
+
56
+ - **Vega-system J, H, and Ks bands.** `Bandpass.johnson` previously stopped at
57
+ I, so any near-infrared work had to leave the Vega system for AB — a
58
+ different magnitude for the same star, by +0.89 (J), +1.37 (H), and +1.84
59
+ (Ks). The zero points are derived from the 2MASS absolute calibration (Cohen,
60
+ Wheaton & Megeath 2003) through the same integral the AB path uses, so the
61
+ two systems now differ only in the reference spectrum and not in the
62
+ convention. `SpectralBandpass.johnson` gained the matching tophats, which are
63
+ the same shapes the AB survey table already used: a filter's shape is a
64
+ property of the filter, not of the magnitude system.
65
+ - **Correlated double sampling as a first-class readout mode.**
66
+ `Camera.correlated_double_sample` resets, reads the pedestal, integrates, reads
67
+ again, and returns the signed `int32` ADU difference — the frame a camera in
68
+ CDS mode actually delivers. It shares one reset-correlated readout core with
69
+ `nondestructive_series`, so kTC noise and fixed bias structure cancel, read
70
+ noise grows by `sqrt(2)`, common mode is partly removed through its AR(1)
71
+ correlation, and reset settling leaves its pedestal-to-signal residual. The
72
+ interval-proportional bias rate survives differencing by construction and is
73
+ documented as a dark-subtractable pedestal. `pedestal_interval_s` models a
74
+ finite reset-to-read delay. `correlated_double_sample_spectral` is the
75
+ wavelength-resolved twin, standing to it as `expose_spectral` does to
76
+ `expose`; both spectral entry points now share one QE-folding step, so a cube
77
+ cannot have QE applied twice or applied differently per readout mode.
78
+ - **Global-reset nondestructive read series for eAPD arrays.**
79
+ `Camera.nondestructive_series` and `dark_nondestructive_series` accumulate
80
+ Poisson charge between resets, preserve a ramp's kTC realization, redraw read
81
+ noise on every sample, and expose ramp/read timing in frame metadata.
82
+ - **A measured C-RED One preset and hybrid-array readout structure.** The new
83
+ `first_light_imaging_cred_one` preset separates the complete camera from the
84
+ bare `leonardo_saphira` detector and models its 32 interleaved output channels,
85
+ edge-dependent pedestal/noise, stable per-channel offsets and noise scales, and
86
+ correlated frame-wide readout pedestal. Its spatial and temporal dark-frame
87
+ parameters are constrained by the July 2026 CRED1 dark stack.
88
+ - **Reset-aware stack characterization.** `nondestructive_stack_statistics`
89
+ measures strong-reset cadence, ramp slopes, common mode, CDS/pixel noise,
90
+ interleaved channels, and edge structure without treating correlated reads as
91
+ independent exposures. `ramp_photon_transfer` fits conversion gain and
92
+ split-ramp response nonuniformity from repeated NDR ramps.
93
+ - **`examples/16_detector_showcase.py`** renders an animated WebP that exposes one
94
+ incident photon field on a CCD, EMCCD, sCMOS, and eAPD side by side, each panel
95
+ overlaid with the frame rate that camera sustained on the running machine. The
96
+ clip is the README header image.
97
+
98
+ ### Changed
99
+
100
+ - **Refined the measured C-RED One response across gain and read rate.** Fixed
101
+ avalanche-gain nonuniformity, sublinear gain noise, NDR read-interval scaling,
102
+ observed 42/3/5-read electronic cadences, reset settling, and separate broad
103
+ side/narrow row-edge pedestal terms now reproduce the local operating-point
104
+ stacks. The C-RED One and SAPHIRA QE curves were corrected to the manufacturer
105
+ plot: 60% through 950 nm, linear to 80% at 1450 nm, then 80% to 2500 nm.
106
+
107
+ - **The README follows the documentation-first structure** used across the sibling
108
+ projects: docs link, showcase clip, install, quickstart, benchmarks, then the
109
+ feature list. The preset table and the dark-frame model walkthrough now live
110
+ only in the presets and noise-model guides, which they already duplicated.
111
+ - **Refreshed `benchmarks/device-results.{json,md}`** on the RTX 5090 / Ryzen 9
112
+ 9950X3D reference machine against getframes 2.1.1, NumPy 2.2.6, and CuPy 14.1.1.
113
+
114
+ ### Fixed
115
+
116
+ - **Benchmark provenance now records the CuPy version** when CuPy is installed
117
+ under a CUDA-specific wheel name (`cupy-cuda12x`/`cupy-cuda11x`). The metadata
118
+ block previously reported `cupy: null` on exactly the machines that had
119
+ produced the GPU column.
120
+
121
+ - **Persistent cameras now cache the dark-signal expectation.** Repeated exposures
122
+ at the same exposure time, temperature, precision, and detector configuration
123
+ reuse the full-detector DSNU/hot-pixel/glow map; changing either physical key
124
+ rebuilds it. Seeded stochastic frames and detector truth remain unchanged.
125
+
126
+ - **sCMOS per-pixel read noise is now a fixed property of the sensor.** The
127
+ per-pixel read-noise RMS map implied by `read_noise_nonuniformity` was drawn from
128
+ the *per-frame* generator, so it was re-randomised in every frame. Single-frame
129
+ spatial statistics were unaffected, but every pixel ended up with the same
130
+ expected noise *through time*, which is not how an sCMOS behaves: each pixel has
131
+ its own source-follower and column ADC. The map is now built once from
132
+ `fixed_pattern_seed` and cached in `FixedPatternMaps`, alongside PRNU and DSNU.
133
+ Verified against dark stacks from three real back-illuminated sCMOS cameras
134
+ (KURO 1200B, Prime 95B, Marana 4.2B-11): splitting a stack in half and
135
+ correlating the two per-pixel temporal-variance maps gives r = 0.89–0.94 on the
136
+ real detectors and r = 0.004 with the old model, now r ≈ 0.96.
137
+ **This changes generated pixel values** for any configuration with
138
+ `read_noise_nonuniformity > 0`; `read_noise_e` and the spatial statistics of a
139
+ single frame are unchanged.
140
+
141
+ ### Added
142
+
143
+ - `DetectorWorkspace` plus optional `workspace=` / caller-owned `out=` execution
144
+ on scalar and spectral camera exposures. Full-detector ROI inputs, private
145
+ photo/total scratch, and digitised destinations can now be reused without
146
+ allowing returned truth or default frames to alias mutable scratch. The
147
+ workspace is device/shape/precision-bound and rejects concurrent use.
148
+ - `CameraConfig.charge_diffusion_fwhm_px`, `charge_diffusion_kernel()`, and
149
+ `apply_charge_diffusion()` make lateral detector charge spreading available to
150
+ focal-plane simulators at their own oversampling. The OCAM2K preset declares
151
+ its measured 0.37-pixel FWHM; under-resolved kernels fail explicitly, while
152
+ native-resolution camera frames state in metadata that diffusion was not
153
+ applied rather than silently becoming a numerical no-op.
154
+ - Full-detector region-of-interest simulation through
155
+ `CameraConfig.roi=(left, top, width, height)`. Cameras accept and return
156
+ ROI-shaped arrays while evaluating detector physics and fixed patterns on the
157
+ native sensor before cropping. `Camera.sensor_resolution`,
158
+ `CameraConfig.output_resolution`, and active amplifier-boundary properties make
159
+ the full-versus-ROI geometry explicit. Exact full-detector split pixels remain
160
+ available when an ROI is active.
161
+ - **`getframes.analysis.characterize`: detector characterisation from frame
162
+ stacks.** Where `photon_transfer_curve` drives a *simulated* camera, this works
163
+ on stacks that already exist -- raw data off a real detector, or simulated
164
+ frames. `stack_statistics` reduces any iterable of frames (arrays, `Frame`s, a
165
+ `dark_series` generator, your own file reader) to per-pixel temporal mean and
166
+ variance in one streaming pass, so stacks larger than memory are fine.
167
+ `characterize_dark` then measures conversion gain, read noise (with its
168
+ per-pixel map, log-normal width and RTS tail), dark current, bias and DSNU from
169
+ darks alone -- no flat field needed, because dark charge is Poisson and so
170
+ serves as the PTC charge source. `characterize_flat` adds full well, PRNU and
171
+ linearity. `DarkCharacterization.to_config()` returns a `CameraConfig`, closing
172
+ the loop: measure a real camera, then simulate it. `StackStats.split=True`
173
+ additionally gives `temporal_repeatability`, the split-half test that separates
174
+ genuine per-pixel noise structure from chi-squared sampling scatter.
175
+ New guide (`docs/guides/characterization.md`) and example
176
+ (`examples/15_detector_characterization.py`).
177
+ - `read_noise_rts_fraction` / `read_noise_rts_factor`: an optional second,
178
+ noisier read-noise population modelling the random-telegraph-signal (RTS) pixels
179
+ of a real sCMOS array. Measured on three real sensors, ~0.5% of pixels sit above
180
+ 3x the median read noise where a single log-normal predicts ~0.01%; these are the
181
+ pixels that limit faint-source detection. Defaults to off.
182
+ - `detector_glow_edge_scale_px`: makes `detector_glow_e_per_s` edge-concentrated
183
+ with an exponential falloff, instead of uniform, modelling amplifier glow emitted
184
+ at the array periphery. Renormalised so the array mean is unchanged; still fixed
185
+ and exposure-scaling, so an exposure-matched master dark removes it. Defaults to
186
+ `0` (uniform, the previous behaviour).
187
+
188
+ ### Changed
189
+
190
+ - Float32 cameras now cache amplifier gain/offset and structured-bias maps in
191
+ float32 instead of retaining detector-sized float64 coefficients. The float64
192
+ reference path is unchanged. A matched 2048x2048 structured-digitization
193
+ benchmark improved by 1.328x on the local CPU and 1.382x on a Quadro P620 while
194
+ persistent coefficient storage fell from 96 MiB to 48 MiB.
195
+
196
+ - The `princeton_instruments_kuro_1200b`, `photometrics_prime_95b`, and
197
+ `andor_marana_4_2b_11` presets now carry **measured** conversion gain, read noise,
198
+ dark current, bias offset, and non-uniformity terms, fitted from a per-pixel dark
199
+ photon-transfer analysis of real frames rather than taken from datasheets. The
200
+ largest corrections: conversion gain (1.25-1.3 -> 0.77-0.87 e-/ADU, the low-signal
201
+ leg of these dual-gain modes) and `dark_current_nonuniformity` (0.03 -> 0.11-0.33,
202
+ which had been roughly an order of magnitude too low). Each preset documents the
203
+ operating mode and temperature the values apply to.
204
+ - `dark_current_nonuniformity` raised to `0.23` on the remaining sCMOS presets
205
+ (`generic_scmos`, `hamamatsu_orca_fusion`, `hamamatsu_orca_quest_2`,
206
+ `tucsen_aries_6504_pro`, `andor_cb1_0_5mp`), which previously carried 0.02-0.03 or
207
+ omitted the field entirely. `0.23` is the median of the three cameras measured
208
+ against real dark stacks (0.11, 0.23, 0.33); each preset documents that it is a
209
+ realistic default carried over from characterised hardware rather than a figure
210
+ from that camera's datasheet. The same four conventional sCMOS presets also gain
211
+ the measured RTS population (`read_noise_rts_fraction = 0.016`, factor 2.65), and
212
+ `andor_cb1_0_5mp` / `hamamatsu_orca_quest_2` gain a `read_noise_nonuniformity` of
213
+ 0.2 where they previously had none at all. `hamamatsu_orca_quest_2` deliberately
214
+ keeps no RTS population --- photon-number resolution depends on a tightly screened
215
+ read-noise distribution, and importing a tail measured on conventional 11 um sCMOS
216
+ would misrepresent it.
217
+ - `andor_marana_4_2b_11` gains its measured hot-pixel population
218
+ (`hot_pixel_fraction = 1e-4` above 10x the median dark rate).
219
+ - `docs/guides/validation.md` documents how to validate a preset against a real dark
220
+ stack: measuring conversion gain from darks alone (no flats needed), and the
221
+ split-half test for repeatable per-pixel read noise.
222
+ - **`examples/16_detector_showcase.py` now shows each detector in the regime it is
223
+ built for** rather than putting all four on one shared V-band field at a shared
224
+ 200 ms exposure. The old framing was a tidy controlled comparison but an unfair
225
+ showcase: once `leonardo_saphira` carried a realistic near-infrared
226
+ dark-plus-background ceiling, its panel was dark-noise dominated in a visible
227
+ 200 ms exposure (star/noise 0.93 against 7.4-22.6 for the other three), and no
228
+ exposure time fixes that, because amplified dark noise grows as `sqrt(t)` while
229
+ the star signal grows as `t`. The panels are now a 5 s deep-sky CCD field, a
230
+ 500 Hz EMCCD wavefront sensor, 100 ms wide-field sCMOS, and the C-RED One eAPD
231
+ doing H-band AO in correlated double sampling at its 1750 Hz maximum frame
232
+ rate — which the clip measures at ~1,789 frames/s on an RTX 5090. The clip is
233
+ the README header image and exercises this release's CDS path, C-RED One preset,
234
+ and Vega H band.
235
+
236
+ ### Fixed
237
+
238
+ - **The detector showcase no longer scales a panel to its own hot pixels.** The
239
+ clip took its per-panel display maximum at a hardcoded 99.95th percentile, which
240
+ is exactly the `hot_pixel_fraction` that `leonardo_saphira` declares, so the
241
+ display range was pinned to a defect population sitting near 20,000 ADU behind
242
+ the avalanche gain stage and every physical feature crushed to black. The
243
+ percentile is now taken from below the preset's declared defect fraction.
244
+ - **The detector showcase subtracts a measured dark pedestal rather than the bias
245
+ offset alone.** Bias is not the zero point once a detector carries real dark
246
+ current through a gain stage — 80 e-/s at 50x avalanche gain is ~400 ADU per
247
+ 200 ms frame — and a CDS frame sits on its own exposure-dependent bias-rate
248
+ pedestal rather than on the bias offset at all.
249
+ - Declared the license as a PEP 639 SPDX expression (`license = "MIT"` plus
250
+ `license-files`) instead of the deprecated `license = { text = "MIT" }` table,
251
+ and dropped the now-redundant `License ::` classifier. The built distribution
252
+ carries `License-Expression: MIT` and `License-File: LICENSE`. No change to the
253
+ license itself.
254
+
9
255
  ## [2.1.1] - 2026-07-26
10
256
 
11
257
  ### Added
@@ -346,7 +592,9 @@ together in 1.0.
346
592
  - Documentation, runnable examples, and CI (lint, type-check, test matrix, PyPI
347
593
  release via Trusted Publishing).
348
594
 
349
- [Unreleased]: https://github.com/jacotay7/getframes/compare/2.1.1...HEAD
595
+ [Unreleased]: https://github.com/jacotay7/getframes/compare/2.3.0...HEAD
596
+ [2.3.0]: https://github.com/jacotay7/getframes/compare/2.2.0...2.3.0
597
+ [2.2.0]: https://github.com/jacotay7/getframes/compare/2.1.1...2.2.0
350
598
  [2.1.1]: https://github.com/jacotay7/getframes/compare/2.1.0...2.1.1
351
599
  [2.1.0]: https://github.com/jacotay7/getframes/compare/2.0.0...2.1.0
352
600
  [2.0.0]: https://github.com/jacotay7/getframes/compare/1.0.0...2.0.0
@@ -0,0 +1,216 @@
1
+ Metadata-Version: 2.5
2
+ Name: getframes
3
+ Version: 2.3.0
4
+ Summary: Generate physically realistic synthetic camera frames (CCD/CMOS/EMCCD/eAPD/sCMOS) — dark, bias, flat, and rendered star fields — with auditable noise physics for scientific imaging pipelines.
5
+ Project-URL: Homepage, https://github.com/jacotay7/getframes
6
+ Project-URL: Documentation, https://jacotay7.github.io/getframes/
7
+ Project-URL: Repository, https://github.com/jacotay7/getframes
8
+ Project-URL: Issues, https://github.com/jacotay7/getframes/issues
9
+ Project-URL: Changelog, https://github.com/jacotay7/getframes/blob/main/CHANGELOG.md
10
+ Author-email: Jacob Taylor <jacobataylor7@gmail.com>
11
+ License-Expression: MIT
12
+ License-File: LICENSE
13
+ Keywords: astronomy,camera,ccd,cmos,detector,emccd,imaging,noise,simulation
14
+ Classifier: Development Status :: 5 - Production/Stable
15
+ Classifier: Intended Audience :: Science/Research
16
+ Classifier: Operating System :: OS Independent
17
+ Classifier: Programming Language :: Python :: 3
18
+ Classifier: Programming Language :: Python :: 3.10
19
+ Classifier: Programming Language :: Python :: 3.11
20
+ Classifier: Programming Language :: Python :: 3.12
21
+ Classifier: Programming Language :: Python :: 3.13
22
+ Classifier: Topic :: Scientific/Engineering :: Astronomy
23
+ Classifier: Topic :: Scientific/Engineering :: Image Processing
24
+ Classifier: Typing :: Typed
25
+ Requires-Python: >=3.10
26
+ Requires-Dist: aocore<0.2,>=0.1.2
27
+ Requires-Dist: astropy>=5.0
28
+ Requires-Dist: numpy>=1.23
29
+ Requires-Dist: scipy>=1.10
30
+ Requires-Dist: tomli>=2.0; python_version < '3.11'
31
+ Provides-Extra: dev
32
+ Requires-Dist: build>=1.0; extra == 'dev'
33
+ Requires-Dist: mypy>=1.8; extra == 'dev'
34
+ Requires-Dist: pytest-cov>=4.0; extra == 'dev'
35
+ Requires-Dist: pytest>=7.0; extra == 'dev'
36
+ Requires-Dist: ruff>=0.6; extra == 'dev'
37
+ Provides-Extra: docs
38
+ Requires-Dist: mkdocs-material>=9.5; extra == 'docs'
39
+ Requires-Dist: mkdocs>=1.5; extra == 'docs'
40
+ Requires-Dist: mkdocstrings[python]>=0.24; extra == 'docs'
41
+ Provides-Extra: examples
42
+ Requires-Dist: matplotlib>=3.7; extra == 'examples'
43
+ Provides-Extra: gpu
44
+ Requires-Dist: cupy-cuda12x>=13.0; extra == 'gpu'
45
+ Description-Content-Type: text/markdown
46
+
47
+ # getframes
48
+
49
+ [![CI](https://github.com/jacotay7/getframes/actions/workflows/ci.yml/badge.svg)](https://github.com/jacotay7/getframes/actions/workflows/ci.yml)
50
+ [![PyPI](https://img.shields.io/pypi/v/getframes.svg)](https://pypi.org/project/getframes/)
51
+ [![Python](https://img.shields.io/pypi/pyversions/getframes.svg)](https://pypi.org/project/getframes/)
52
+ [![Docs](https://img.shields.io/badge/docs-jacotay7.github.io%2Fgetframes-teal.svg)](https://jacotay7.github.io/getframes/)
53
+ [![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](LICENSE)
54
+
55
+ **Documentation: [jacotay7.github.io/getframes](https://jacotay7.github.io/getframes/)**
56
+
57
+ **Realistic synthetic camera frames for scientific imaging pipelines.**
58
+
59
+ <p align="center">
60
+ <img src="examples/detector_showcase.webp" width="503" alt="Animated detector showcase: a CCD, EMCCD, sCMOS and C-RED One eAPD each simulated in the regime it is built for — deep-sky, AO wavefront sensing, wide-field, and near-infrared CDS — with live throughput.">
61
+ </p>
62
+
63
+ `getframes` generates the frames a real detector would have produced: the full
64
+ **photon → electron → ADU** signal path for **CCD**, **CMOS**, **EMCCD**,
65
+ **eAPD** and **sCMOS** sensors, with auditable noise physics (read noise, dark
66
+ current, shot noise, fixed-pattern non-uniformity, a unified stochastic gain
67
+ stage, clock-induced charge, nonlinearity, cosmic rays). It produces **dark**,
68
+ **bias** and **flat** frames, and renders **star fields** through a PSF and
69
+ telescope into a realistic science frame — so you can build and validate
70
+ image-processing pipelines against ground truth. It runs on NumPy by default and
71
+ switches to CUDA (via CuPy) with a single argument.
72
+
73
+ ## Install
74
+
75
+ ```bash
76
+ pip install getframes # CPU (NumPy + SciPy + astropy + aocore)
77
+ pip install 'getframes[gpu]' # + CuPy for CUDA 12.x
78
+ pip install -e '.[dev]' # from a clone, for development
79
+ ```
80
+
81
+ ## Quickstart
82
+
83
+ ```python
84
+ import getframes as gf
85
+
86
+ cam = gf.Camera.from_preset("andor_ikon_m934") # 21 presets, or your own CameraConfig
87
+ frame = cam.dark_frame(exposure=60.0, temperature=-60.0, seed=0)
88
+
89
+ frame.data # (1024, 1024) array of ADU
90
+ frame.stats() # {'mean': ..., 'median': ..., 'std': ..., 'min': ..., 'max': ...}
91
+ frame.metadata # camera/exposure/temperature provenance
92
+
93
+ scene = gf.Scene( # render a sky, then expose it
94
+ shape=(256, 256),
95
+ optics=gf.Telescope(
96
+ aperture_diameter_m=2.5,
97
+ throughput=0.3,
98
+ plate_scale_arcsec_per_pixel=0.4,
99
+ band=gf.Bandpass.johnson("V"),
100
+ ),
101
+ psf=gf.MoffatPSF(fwhm_arcsec=1.1, beta=3.0),
102
+ sources=[gf.PointSource(x=128, y=128, magnitude=20.0)],
103
+ sky=gf.Sky(surface_brightness_mag_arcsec2=21.0),
104
+ )
105
+ frame = cam.with_config(resolution=(256, 256)).observe(scene, exposure=300.0, seed=0)
106
+
107
+ import cupy as cp # and the same path on a GPU
108
+
109
+ cam = gf.Camera.from_preset("andor_ocam2k", device="gpu", precision="float32")
110
+ rate = cp.full(cam.resolution, 2.0e6, dtype=cp.float32) # photons/s/pixel
111
+ frame = cam.expose(rate, exposure=1.0e-3, seed=0) # CuPy ADU, no host copy
112
+ ```
113
+
114
+ See **[Getting started](https://jacotay7.github.io/getframes/guides/getting-started/)**
115
+ for the full walkthrough,
116
+ **[Observing scenes](https://jacotay7.github.io/getframes/guides/scenes/)** for
117
+ sources, PSFs and telescopes,
118
+ **[Camera presets](https://jacotay7.github.io/getframes/guides/presets/)** for the
119
+ preset library, and
120
+ **[The noise model](https://jacotay7.github.io/getframes/guides/noise-model/)** for
121
+ the physics behind every stage.
122
+
123
+ ## Benchmarks
124
+
125
+ Warm bulk-frame throughput on an AMD Ryzen 9 9950X3D and an NVIDIA RTX 5090
126
+ (`float32`, truth enabled, persistent camera, device-resident input/output, no
127
+ host transfers). The raw artifact and its invocation are
128
+ [versioned with the benchmarks](benchmarks/device-results.json):
129
+
130
+ | Workflow | Native shape | CPU (frames/s) | GPU (frames/s) | Speedup |
131
+ | --- | ---: | ---: | ---: | ---: |
132
+ | Pyramid WFS CMOS | 80×80 | 5,240 | 11,514 | 2.20× |
133
+ | Shack-Hartmann WFS CMOS | 160×160 | 1,386 | 11,471 | 8.27× |
134
+ | OCAM2K EMCCD | 240×240 | 357 | 8,045 | 22.53× |
135
+ | SAPHIRA eAPD | 256×320 | 280 | 7,497 | 26.74× |
136
+ | Large science CMOS | 1024×1024 | 31 | 1,453 | 47.21× |
137
+
138
+ Higher is better; CUDA was synchronized around every timed region and
139
+ construction was excluded. Even the smallest case reaches about 2×, while larger
140
+ arrays and gain-stage detectors expose much more parallel work. Reproduce the
141
+ table with
142
+
143
+ ```bash
144
+ python benchmarks/bench_devices.py --seconds 2 --warmup 10 --device both
145
+ python benchmarks/run.py # the CPU hot-path sweep
146
+ ```
147
+
148
+ See the [full snapshot](benchmarks/device-results.md) and the
149
+ **[GPU guide](https://jacotay7.github.io/getframes/guides/gpu/#reference-throughput)**
150
+ for the methodology.
151
+
152
+ ## Features
153
+
154
+ - **Five detector families** — CCD, CMOS, EMCCD, eAPD and sCMOS, from a library
155
+ of sourced **[presets](https://jacotay7.github.io/getframes/guides/presets/)**
156
+ (`andor_ikon_m934`, `andor_ocam2k`, `leonardo_saphira`,
157
+ `first_light_imaging_cred_one`,
158
+ `andor_marana_4_2b_11`, `zwo_asi2600mm`, …) or any `CameraConfig` you define.
159
+ - **Auditable noise physics** — dark current vs. temperature, shot noise, a
160
+ unified stochastic gain stage (EM and avalanche) with realistic excess noise,
161
+ clock-induced charge, per-pixel sCMOS read noise, polynomial nonlinearity,
162
+ saturation and quantisation, each a small documented pure function in
163
+ **[the noise model](https://jacotay7.github.io/getframes/guides/noise-model/)**.
164
+ - **Detector realism** — CTI, blooming, IPC, kTC/reset noise, multi-amplifier
165
+ readout, cosmic-ray tracks, defect and structured-bias maps, vignetting and
166
+ radial distortion.
167
+ - **Fixed patterns that behave like silicon** — PRNU, DSNU, hot pixels, defects
168
+ and amplifier structure are keyed on `fixed_pattern_seed`, so they repeat in
169
+ every frame and are genuinely removable by a master frame.
170
+ - **Scenes** — point, extended and catalog sources, Gaussian/Moffat/Airy/array
171
+ PSFs, a `Telescope` with Vega (Johnson) and AB (ugriz, Gaia, 2MASS) bandpasses,
172
+ extinction, graybody thermal background, WCS pixel↔world, and light curves.
173
+ - **Calibration & ground truth** — master bias/dark/flat builders and a
174
+ `calibrate` reduction that closes the
175
+ **[raw → reduced → truth loop](https://jacotay7.github.io/getframes/guides/calibration/)**.
176
+ - **Observations** — `Observation` drives time series with jitter, drift, dither
177
+ and persistence, carrying per-frame
178
+ **[truth](https://jacotay7.github.io/getframes/guides/time-series/)**.
179
+ - **Spectral mode** (opt-in) — QE curves, relative or absolute SEDs, transmission
180
+ products and wavelength-resolved exposure; see
181
+ **[Spectral mode](https://jacotay7.github.io/getframes/guides/spectral/)** and
182
+ **[Radiometry & the infrared](https://jacotay7.github.io/getframes/guides/radiometry/)**.
183
+ - **Analysis on real data too** — aperture sums, centroids, photon-transfer
184
+ curves, independent-stack characterization, and reset-aware nondestructive-ramp
185
+ analysis run on measured detector frames as readily as on simulated ones.
186
+ - **Scale & datasets** — a float32 fast path, vectorised multi-source rendering,
187
+ a streaming raw+truth `dataset` generator and a `getframes` CLI; see
188
+ **[Scale & datasets](https://jacotay7.github.io/getframes/guides/datasets/)**.
189
+ - **GPU-optional** — every camera takes `device="gpu"` (CuPy) and keeps the
190
+ detector path and truth arrays device-resident. CPU and GPU have independent
191
+ RNG streams, so a `seed` repeats exactly on a fixed backend while parity across
192
+ backends means matching statistics, not identical pixels.
193
+ - **Reproducible and typed** — all randomness flows through a camera-owned seeded
194
+ generator, never global state; `mypy --strict` passes; every public name is
195
+ frozen under [SemVer](https://jacotay7.github.io/getframes/stability/) as of 2.0.
196
+ - **Validated** — noise models are checked against published forms in CI; see
197
+ **[Validation](https://jacotay7.github.io/getframes/guides/validation/)**.
198
+
199
+ See the **[API reference](https://jacotay7.github.io/getframes/reference/)** for
200
+ every public function and class, the
201
+ **[runnable examples](examples/)** for PTC, exposure planning, AO limiting
202
+ magnitude, transit photometry and detector realism, and the
203
+ **[roadmap](https://jacotay7.github.io/getframes/roadmap/)** for what is next.
204
+
205
+ ## Contributing
206
+
207
+ Contributions — especially new camera presets — are welcome. See
208
+ [CONTRIBUTING.md](CONTRIBUTING.md). Run the checks locally with:
209
+
210
+ ```bash
211
+ ruff check . && ruff format --check . && mypy && pytest
212
+ ```
213
+
214
+ ## License
215
+
216
+ MIT — see [LICENSE](LICENSE).
@@ -0,0 +1,170 @@
1
+ # getframes
2
+
3
+ [![CI](https://github.com/jacotay7/getframes/actions/workflows/ci.yml/badge.svg)](https://github.com/jacotay7/getframes/actions/workflows/ci.yml)
4
+ [![PyPI](https://img.shields.io/pypi/v/getframes.svg)](https://pypi.org/project/getframes/)
5
+ [![Python](https://img.shields.io/pypi/pyversions/getframes.svg)](https://pypi.org/project/getframes/)
6
+ [![Docs](https://img.shields.io/badge/docs-jacotay7.github.io%2Fgetframes-teal.svg)](https://jacotay7.github.io/getframes/)
7
+ [![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](LICENSE)
8
+
9
+ **Documentation: [jacotay7.github.io/getframes](https://jacotay7.github.io/getframes/)**
10
+
11
+ **Realistic synthetic camera frames for scientific imaging pipelines.**
12
+
13
+ <p align="center">
14
+ <img src="examples/detector_showcase.webp" width="503" alt="Animated detector showcase: a CCD, EMCCD, sCMOS and C-RED One eAPD each simulated in the regime it is built for — deep-sky, AO wavefront sensing, wide-field, and near-infrared CDS — with live throughput.">
15
+ </p>
16
+
17
+ `getframes` generates the frames a real detector would have produced: the full
18
+ **photon → electron → ADU** signal path for **CCD**, **CMOS**, **EMCCD**,
19
+ **eAPD** and **sCMOS** sensors, with auditable noise physics (read noise, dark
20
+ current, shot noise, fixed-pattern non-uniformity, a unified stochastic gain
21
+ stage, clock-induced charge, nonlinearity, cosmic rays). It produces **dark**,
22
+ **bias** and **flat** frames, and renders **star fields** through a PSF and
23
+ telescope into a realistic science frame — so you can build and validate
24
+ image-processing pipelines against ground truth. It runs on NumPy by default and
25
+ switches to CUDA (via CuPy) with a single argument.
26
+
27
+ ## Install
28
+
29
+ ```bash
30
+ pip install getframes # CPU (NumPy + SciPy + astropy + aocore)
31
+ pip install 'getframes[gpu]' # + CuPy for CUDA 12.x
32
+ pip install -e '.[dev]' # from a clone, for development
33
+ ```
34
+
35
+ ## Quickstart
36
+
37
+ ```python
38
+ import getframes as gf
39
+
40
+ cam = gf.Camera.from_preset("andor_ikon_m934") # 21 presets, or your own CameraConfig
41
+ frame = cam.dark_frame(exposure=60.0, temperature=-60.0, seed=0)
42
+
43
+ frame.data # (1024, 1024) array of ADU
44
+ frame.stats() # {'mean': ..., 'median': ..., 'std': ..., 'min': ..., 'max': ...}
45
+ frame.metadata # camera/exposure/temperature provenance
46
+
47
+ scene = gf.Scene( # render a sky, then expose it
48
+ shape=(256, 256),
49
+ optics=gf.Telescope(
50
+ aperture_diameter_m=2.5,
51
+ throughput=0.3,
52
+ plate_scale_arcsec_per_pixel=0.4,
53
+ band=gf.Bandpass.johnson("V"),
54
+ ),
55
+ psf=gf.MoffatPSF(fwhm_arcsec=1.1, beta=3.0),
56
+ sources=[gf.PointSource(x=128, y=128, magnitude=20.0)],
57
+ sky=gf.Sky(surface_brightness_mag_arcsec2=21.0),
58
+ )
59
+ frame = cam.with_config(resolution=(256, 256)).observe(scene, exposure=300.0, seed=0)
60
+
61
+ import cupy as cp # and the same path on a GPU
62
+
63
+ cam = gf.Camera.from_preset("andor_ocam2k", device="gpu", precision="float32")
64
+ rate = cp.full(cam.resolution, 2.0e6, dtype=cp.float32) # photons/s/pixel
65
+ frame = cam.expose(rate, exposure=1.0e-3, seed=0) # CuPy ADU, no host copy
66
+ ```
67
+
68
+ See **[Getting started](https://jacotay7.github.io/getframes/guides/getting-started/)**
69
+ for the full walkthrough,
70
+ **[Observing scenes](https://jacotay7.github.io/getframes/guides/scenes/)** for
71
+ sources, PSFs and telescopes,
72
+ **[Camera presets](https://jacotay7.github.io/getframes/guides/presets/)** for the
73
+ preset library, and
74
+ **[The noise model](https://jacotay7.github.io/getframes/guides/noise-model/)** for
75
+ the physics behind every stage.
76
+
77
+ ## Benchmarks
78
+
79
+ Warm bulk-frame throughput on an AMD Ryzen 9 9950X3D and an NVIDIA RTX 5090
80
+ (`float32`, truth enabled, persistent camera, device-resident input/output, no
81
+ host transfers). The raw artifact and its invocation are
82
+ [versioned with the benchmarks](benchmarks/device-results.json):
83
+
84
+ | Workflow | Native shape | CPU (frames/s) | GPU (frames/s) | Speedup |
85
+ | --- | ---: | ---: | ---: | ---: |
86
+ | Pyramid WFS CMOS | 80×80 | 5,240 | 11,514 | 2.20× |
87
+ | Shack-Hartmann WFS CMOS | 160×160 | 1,386 | 11,471 | 8.27× |
88
+ | OCAM2K EMCCD | 240×240 | 357 | 8,045 | 22.53× |
89
+ | SAPHIRA eAPD | 256×320 | 280 | 7,497 | 26.74× |
90
+ | Large science CMOS | 1024×1024 | 31 | 1,453 | 47.21× |
91
+
92
+ Higher is better; CUDA was synchronized around every timed region and
93
+ construction was excluded. Even the smallest case reaches about 2×, while larger
94
+ arrays and gain-stage detectors expose much more parallel work. Reproduce the
95
+ table with
96
+
97
+ ```bash
98
+ python benchmarks/bench_devices.py --seconds 2 --warmup 10 --device both
99
+ python benchmarks/run.py # the CPU hot-path sweep
100
+ ```
101
+
102
+ See the [full snapshot](benchmarks/device-results.md) and the
103
+ **[GPU guide](https://jacotay7.github.io/getframes/guides/gpu/#reference-throughput)**
104
+ for the methodology.
105
+
106
+ ## Features
107
+
108
+ - **Five detector families** — CCD, CMOS, EMCCD, eAPD and sCMOS, from a library
109
+ of sourced **[presets](https://jacotay7.github.io/getframes/guides/presets/)**
110
+ (`andor_ikon_m934`, `andor_ocam2k`, `leonardo_saphira`,
111
+ `first_light_imaging_cred_one`,
112
+ `andor_marana_4_2b_11`, `zwo_asi2600mm`, …) or any `CameraConfig` you define.
113
+ - **Auditable noise physics** — dark current vs. temperature, shot noise, a
114
+ unified stochastic gain stage (EM and avalanche) with realistic excess noise,
115
+ clock-induced charge, per-pixel sCMOS read noise, polynomial nonlinearity,
116
+ saturation and quantisation, each a small documented pure function in
117
+ **[the noise model](https://jacotay7.github.io/getframes/guides/noise-model/)**.
118
+ - **Detector realism** — CTI, blooming, IPC, kTC/reset noise, multi-amplifier
119
+ readout, cosmic-ray tracks, defect and structured-bias maps, vignetting and
120
+ radial distortion.
121
+ - **Fixed patterns that behave like silicon** — PRNU, DSNU, hot pixels, defects
122
+ and amplifier structure are keyed on `fixed_pattern_seed`, so they repeat in
123
+ every frame and are genuinely removable by a master frame.
124
+ - **Scenes** — point, extended and catalog sources, Gaussian/Moffat/Airy/array
125
+ PSFs, a `Telescope` with Vega (Johnson) and AB (ugriz, Gaia, 2MASS) bandpasses,
126
+ extinction, graybody thermal background, WCS pixel↔world, and light curves.
127
+ - **Calibration & ground truth** — master bias/dark/flat builders and a
128
+ `calibrate` reduction that closes the
129
+ **[raw → reduced → truth loop](https://jacotay7.github.io/getframes/guides/calibration/)**.
130
+ - **Observations** — `Observation` drives time series with jitter, drift, dither
131
+ and persistence, carrying per-frame
132
+ **[truth](https://jacotay7.github.io/getframes/guides/time-series/)**.
133
+ - **Spectral mode** (opt-in) — QE curves, relative or absolute SEDs, transmission
134
+ products and wavelength-resolved exposure; see
135
+ **[Spectral mode](https://jacotay7.github.io/getframes/guides/spectral/)** and
136
+ **[Radiometry & the infrared](https://jacotay7.github.io/getframes/guides/radiometry/)**.
137
+ - **Analysis on real data too** — aperture sums, centroids, photon-transfer
138
+ curves, independent-stack characterization, and reset-aware nondestructive-ramp
139
+ analysis run on measured detector frames as readily as on simulated ones.
140
+ - **Scale & datasets** — a float32 fast path, vectorised multi-source rendering,
141
+ a streaming raw+truth `dataset` generator and a `getframes` CLI; see
142
+ **[Scale & datasets](https://jacotay7.github.io/getframes/guides/datasets/)**.
143
+ - **GPU-optional** — every camera takes `device="gpu"` (CuPy) and keeps the
144
+ detector path and truth arrays device-resident. CPU and GPU have independent
145
+ RNG streams, so a `seed` repeats exactly on a fixed backend while parity across
146
+ backends means matching statistics, not identical pixels.
147
+ - **Reproducible and typed** — all randomness flows through a camera-owned seeded
148
+ generator, never global state; `mypy --strict` passes; every public name is
149
+ frozen under [SemVer](https://jacotay7.github.io/getframes/stability/) as of 2.0.
150
+ - **Validated** — noise models are checked against published forms in CI; see
151
+ **[Validation](https://jacotay7.github.io/getframes/guides/validation/)**.
152
+
153
+ See the **[API reference](https://jacotay7.github.io/getframes/reference/)** for
154
+ every public function and class, the
155
+ **[runnable examples](examples/)** for PTC, exposure planning, AO limiting
156
+ magnitude, transit photometry and detector realism, and the
157
+ **[roadmap](https://jacotay7.github.io/getframes/roadmap/)** for what is next.
158
+
159
+ ## Contributing
160
+
161
+ Contributions — especially new camera presets — are welcome. See
162
+ [CONTRIBUTING.md](CONTRIBUTING.md). Run the checks locally with:
163
+
164
+ ```bash
165
+ ruff check . && ruff format --check . && mypy && pytest
166
+ ```
167
+
168
+ ## License
169
+
170
+ MIT — see [LICENSE](LICENSE).