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