getframes 2.5.0__tar.gz → 2.6.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.
- {getframes-2.5.0 → getframes-2.6.0}/CHANGELOG.md +62 -0
- {getframes-2.5.0 → getframes-2.6.0}/PKG-INFO +5 -1
- {getframes-2.5.0 → getframes-2.6.0}/README.md +4 -0
- getframes-2.6.0/benchmarks/bench_small_frames.py +192 -0
- getframes-2.6.0/benchmarks/small-frame-results.json +2118 -0
- getframes-2.6.0/benchmarks/small-frame-results.md +85 -0
- {getframes-2.5.0 → getframes-2.6.0}/src/getframes/__about__.py +1 -1
- {getframes-2.5.0 → getframes-2.6.0}/src/getframes/__init__.py +2 -1
- getframes-2.6.0/src/getframes/calibrate.py +615 -0
- {getframes-2.5.0 → getframes-2.6.0}/src/getframes/camera.py +83 -0
- {getframes-2.5.0 → getframes-2.6.0}/src/getframes/noise.py +52 -1
- {getframes-2.5.0 → getframes-2.6.0}/tests/test_gpu.py +39 -0
- getframes-2.6.0/tests/test_wfs_calibration.py +354 -0
- getframes-2.5.0/src/getframes/calibrate.py +0 -182
- {getframes-2.5.0 → getframes-2.6.0}/.gitignore +0 -0
- {getframes-2.5.0 → getframes-2.6.0}/LICENSE +0 -0
- {getframes-2.5.0 → getframes-2.6.0}/benchmarks/__init__.py +0 -0
- {getframes-2.5.0 → getframes-2.6.0}/benchmarks/bench_detector_workspace.py +0 -0
- {getframes-2.5.0 → getframes-2.6.0}/benchmarks/bench_devices.py +0 -0
- {getframes-2.5.0 → getframes-2.6.0}/benchmarks/bench_fixed_map_dtype.py +0 -0
- {getframes-2.5.0 → getframes-2.6.0}/benchmarks/detector-workspace-results.json +0 -0
- {getframes-2.5.0 → getframes-2.6.0}/benchmarks/device-results-neoverse-n1-rtx4060.json +0 -0
- {getframes-2.5.0 → getframes-2.6.0}/benchmarks/device-results-neoverse-n1-rtxa400.json +0 -0
- {getframes-2.5.0 → getframes-2.6.0}/benchmarks/device-results-neoverse-n1.md +0 -0
- {getframes-2.5.0 → getframes-2.6.0}/benchmarks/device-results.json +0 -0
- {getframes-2.5.0 → getframes-2.6.0}/benchmarks/device-results.md +0 -0
- {getframes-2.5.0 → getframes-2.6.0}/benchmarks/fixed-map-dtype-results.json +0 -0
- {getframes-2.5.0 → getframes-2.6.0}/benchmarks/render_device_table.py +0 -0
- {getframes-2.5.0 → getframes-2.6.0}/benchmarks/run.py +0 -0
- {getframes-2.5.0 → getframes-2.6.0}/examples/01_basic_dark_frame.py +0 -0
- {getframes-2.5.0 → getframes-2.6.0}/examples/02_custom_camera.py +0 -0
- {getframes-2.5.0 → getframes-2.6.0}/examples/03_master_dark.py +0 -0
- {getframes-2.5.0 → getframes-2.6.0}/examples/04_browse_presets.py +0 -0
- {getframes-2.5.0 → getframes-2.6.0}/examples/05_visualise.py +0 -0
- {getframes-2.5.0 → getframes-2.6.0}/examples/06_photon_transfer_curve.py +0 -0
- {getframes-2.5.0 → getframes-2.6.0}/examples/07_star_field_exposure.py +0 -0
- {getframes-2.5.0 → getframes-2.6.0}/examples/08_ao_limiting_magnitude.py +0 -0
- {getframes-2.5.0 → getframes-2.6.0}/examples/09_transit_photometry.py +0 -0
- {getframes-2.5.0 → getframes-2.6.0}/examples/10_detector_realism.py +0 -0
- {getframes-2.5.0 → getframes-2.6.0}/examples/11_radiometry_and_ir.py +0 -0
- {getframes-2.5.0 → getframes-2.6.0}/examples/12_ml_dataset.py +0 -0
- {getframes-2.5.0 → getframes-2.6.0}/examples/13_crowded_field.py +0 -0
- {getframes-2.5.0 → getframes-2.6.0}/examples/14_keck_lgs_ttf_trade_study.ipynb +0 -0
- {getframes-2.5.0 → getframes-2.6.0}/examples/15_detector_characterization.py +0 -0
- {getframes-2.5.0 → getframes-2.6.0}/examples/16_detector_showcase.py +0 -0
- {getframes-2.5.0 → getframes-2.6.0}/examples/README.md +0 -0
- {getframes-2.5.0 → getframes-2.6.0}/examples/_common.py +0 -0
- {getframes-2.5.0 → getframes-2.6.0}/examples/detector_showcase.webp +0 -0
- {getframes-2.5.0 → getframes-2.6.0}/pyproject.toml +0 -0
- {getframes-2.5.0 → getframes-2.6.0}/src/getframes/_cuda.py +0 -0
- {getframes-2.5.0 → getframes-2.6.0}/src/getframes/analysis/__init__.py +0 -0
- {getframes-2.5.0 → getframes-2.6.0}/src/getframes/analysis/apertures.py +0 -0
- {getframes-2.5.0 → getframes-2.6.0}/src/getframes/analysis/characterize.py +0 -0
- {getframes-2.5.0 → getframes-2.6.0}/src/getframes/analysis/nondestructive.py +0 -0
- {getframes-2.5.0 → getframes-2.6.0}/src/getframes/analysis/ptc.py +0 -0
- {getframes-2.5.0 → getframes-2.6.0}/src/getframes/backend.py +0 -0
- {getframes-2.5.0 → getframes-2.6.0}/src/getframes/cli.py +0 -0
- {getframes-2.5.0 → getframes-2.6.0}/src/getframes/config.py +0 -0
- {getframes-2.5.0 → getframes-2.6.0}/src/getframes/dataset.py +0 -0
- {getframes-2.5.0 → getframes-2.6.0}/src/getframes/frame.py +0 -0
- {getframes-2.5.0 → getframes-2.6.0}/src/getframes/observation.py +0 -0
- {getframes-2.5.0 → getframes-2.6.0}/src/getframes/presets/__init__.py +0 -0
- {getframes-2.5.0 → getframes-2.6.0}/src/getframes/presets/data/__init__.py +0 -0
- {getframes-2.5.0 → getframes-2.6.0}/src/getframes/presets/data/andor_cb1_0_5mp.toml +0 -0
- {getframes-2.5.0 → getframes-2.6.0}/src/getframes/presets/data/andor_ikon_m934.toml +0 -0
- {getframes-2.5.0 → getframes-2.6.0}/src/getframes/presets/data/andor_ixon_ultra_888.toml +0 -0
- {getframes-2.5.0 → getframes-2.6.0}/src/getframes/presets/data/andor_marana_4_2b_11.toml +0 -0
- {getframes-2.5.0 → getframes-2.6.0}/src/getframes/presets/data/andor_ocam2k.toml +0 -0
- {getframes-2.5.0 → getframes-2.6.0}/src/getframes/presets/data/first_light_imaging_cred_one.toml +0 -0
- {getframes-2.5.0 → getframes-2.6.0}/src/getframes/presets/data/generic_ccd.toml +0 -0
- {getframes-2.5.0 → getframes-2.6.0}/src/getframes/presets/data/generic_cmos.toml +0 -0
- {getframes-2.5.0 → getframes-2.6.0}/src/getframes/presets/data/generic_eapd.toml +0 -0
- {getframes-2.5.0 → getframes-2.6.0}/src/getframes/presets/data/generic_emccd.toml +0 -0
- {getframes-2.5.0 → getframes-2.6.0}/src/getframes/presets/data/generic_scmos.toml +0 -0
- {getframes-2.5.0 → getframes-2.6.0}/src/getframes/presets/data/hamamatsu_orca_fusion.toml +0 -0
- {getframes-2.5.0 → getframes-2.6.0}/src/getframes/presets/data/hamamatsu_orca_quest_2.toml +0 -0
- {getframes-2.5.0 → getframes-2.6.0}/src/getframes/presets/data/leonardo_saphira.toml +0 -0
- {getframes-2.5.0 → getframes-2.6.0}/src/getframes/presets/data/nuvu_hnu_128_omega.toml +0 -0
- {getframes-2.5.0 → getframes-2.6.0}/src/getframes/presets/data/nuvu_hnu_240.toml +0 -0
- {getframes-2.5.0 → getframes-2.6.0}/src/getframes/presets/data/photometrics_prime_95b.toml +0 -0
- {getframes-2.5.0 → getframes-2.6.0}/src/getframes/presets/data/princeton_instruments_kuro_1200b.toml +0 -0
- {getframes-2.5.0 → getframes-2.6.0}/src/getframes/presets/data/qhy530_pro_ii.toml +0 -0
- {getframes-2.5.0 → getframes-2.6.0}/src/getframes/presets/data/scimeasure_little_joe_ccd39.toml +0 -0
- {getframes-2.5.0 → getframes-2.6.0}/src/getframes/presets/data/tucsen_aries_6504_pro.toml +0 -0
- {getframes-2.5.0 → getframes-2.6.0}/src/getframes/presets/data/zwo_asi2600mm.toml +0 -0
- {getframes-2.5.0 → getframes-2.6.0}/src/getframes/py.typed +0 -0
- {getframes-2.5.0 → getframes-2.6.0}/src/getframes/scene/__init__.py +0 -0
- {getframes-2.5.0 → getframes-2.6.0}/src/getframes/scene/optics.py +0 -0
- {getframes-2.5.0 → getframes-2.6.0}/src/getframes/scene/photometry.py +0 -0
- {getframes-2.5.0 → getframes-2.6.0}/src/getframes/scene/psf.py +0 -0
- {getframes-2.5.0 → getframes-2.6.0}/src/getframes/scene/scene.py +0 -0
- {getframes-2.5.0 → getframes-2.6.0}/src/getframes/scene/sources.py +0 -0
- {getframes-2.5.0 → getframes-2.6.0}/src/getframes/scene/thermal.py +0 -0
- {getframes-2.5.0 → getframes-2.6.0}/src/getframes/scene/wcs.py +0 -0
- {getframes-2.5.0 → getframes-2.6.0}/src/getframes/spectral.py +0 -0
- {getframes-2.5.0 → getframes-2.6.0}/tests/test_analysis.py +0 -0
- {getframes-2.5.0 → getframes-2.6.0}/tests/test_backend.py +0 -0
- {getframes-2.5.0 → getframes-2.6.0}/tests/test_benchmarks.py +0 -0
- {getframes-2.5.0 → getframes-2.6.0}/tests/test_calibrate.py +0 -0
- {getframes-2.5.0 → getframes-2.6.0}/tests/test_camera.py +0 -0
- {getframes-2.5.0 → getframes-2.6.0}/tests/test_characterize.py +0 -0
- {getframes-2.5.0 → getframes-2.6.0}/tests/test_cli.py +0 -0
- {getframes-2.5.0 → getframes-2.6.0}/tests/test_config.py +0 -0
- {getframes-2.5.0 → getframes-2.6.0}/tests/test_conformance.py +0 -0
- {getframes-2.5.0 → getframes-2.6.0}/tests/test_dataset.py +0 -0
- {getframes-2.5.0 → getframes-2.6.0}/tests/test_detector.py +0 -0
- {getframes-2.5.0 → getframes-2.6.0}/tests/test_frame.py +0 -0
- {getframes-2.5.0 → getframes-2.6.0}/tests/test_gain.py +0 -0
- {getframes-2.5.0 → getframes-2.6.0}/tests/test_noise.py +0 -0
- {getframes-2.5.0 → getframes-2.6.0}/tests/test_nondestructive_analysis.py +0 -0
- {getframes-2.5.0 → getframes-2.6.0}/tests/test_observation.py +0 -0
- {getframes-2.5.0 → getframes-2.6.0}/tests/test_presets.py +0 -0
- {getframes-2.5.0 → getframes-2.6.0}/tests/test_radiometry.py +0 -0
- {getframes-2.5.0 → getframes-2.6.0}/tests/test_realism.py +0 -0
- {getframes-2.5.0 → getframes-2.6.0}/tests/test_scale.py +0 -0
- {getframes-2.5.0 → getframes-2.6.0}/tests/test_scene.py +0 -0
- {getframes-2.5.0 → getframes-2.6.0}/tests/test_scene_enrich.py +0 -0
- {getframes-2.5.0 → getframes-2.6.0}/tests/test_signal.py +0 -0
- {getframes-2.5.0 → getframes-2.6.0}/tests/test_spectral.py +0 -0
- {getframes-2.5.0 → getframes-2.6.0}/tests/test_validation.py +0 -0
- {getframes-2.5.0 → getframes-2.6.0}/tests/test_workspace.py +0 -0
|
@@ -6,6 +6,68 @@ to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
|
6
6
|
|
|
7
7
|
## [Unreleased]
|
|
8
8
|
|
|
9
|
+
## [2.6.0] - 2026-10-10
|
|
10
|
+
|
|
11
|
+
### Added
|
|
12
|
+
|
|
13
|
+
- **`Calibration`: ADU to input-referred electrons and incident photons**
|
|
14
|
+
(`gf.Calibration`, `Camera.calibration`). `Calibration.from_camera(cam,
|
|
15
|
+
exposure, n_darks=64, seed=...)` averages exposure-matched dark frames into a
|
|
16
|
+
mean master dark on the camera's device and precision; `to_electrons` applies
|
|
17
|
+
`L^-1[(ADU - dark) * gain_e_per_adu / em_gain] / flat`, inverting the
|
|
18
|
+
configured nonlinearity (closed form or Newton) by default; `to_photons`
|
|
19
|
+
divides by the scalar QE `expose` applies, a `quantum_efficiency`, the
|
|
20
|
+
`qe_curve` at `wavelength_nm`, or its effective value over a `bandpass`;
|
|
21
|
+
`saturated` flags pixels whose charge reached the input well, the output
|
|
22
|
+
register or the ADC ceiling. It works on NumPy and CuPy frames, takes an
|
|
23
|
+
optional unit-mean flat, and handles binned frames. Tests check that the mean
|
|
24
|
+
calibrated flux matches the incident photons to 0.5% for EMCCD, qCMOS, sCMOS,
|
|
25
|
+
eAPD, CCD and CMOS presets (issue #10).
|
|
26
|
+
- **`Camera.effective_read_noise_e(binning=..., binning_mode=...)`**: the
|
|
27
|
+
per-frame read noise referred to the input (electrons collected in the
|
|
28
|
+
pixel): output read noise (the quadrature mean of a per-pixel map), kTC/reset
|
|
29
|
+
noise and avalanche-scaled noise, divided by the EM/avalanche gain. The
|
|
30
|
+
docstring and the new guide give the excess-noise-factor SNR.
|
|
31
|
+
- **Guide: [A WFS camera in a closed loop](docs/guides/wfs-closed-loop.md)**:
|
|
32
|
+
exposure vs. frame period, ADU calibration and its accuracy, input-referred
|
|
33
|
+
noise and the excess noise factor, recommended EMCCD/qCMOS/sCMOS/eAPD/CCD
|
|
34
|
+
presets, and keeping small frames cheap.
|
|
35
|
+
- `benchmarks/bench_small_frames.py`: seeded 256x256 WFS frames for one preset
|
|
36
|
+
per detector class, with before/after comparison and a SHA-256 digest of
|
|
37
|
+
seeded output for bit-identity checks; results in
|
|
38
|
+
`benchmarks/small-frame-results.md`.
|
|
39
|
+
|
|
40
|
+
### Performance
|
|
41
|
+
|
|
42
|
+
- **Photon-starved EMCCD and eAPD frames are faster on the CPU, with
|
|
43
|
+
bit-identical seeded output.** NumPy returns `Gamma(shape=0)` as exactly zero
|
|
44
|
+
without consuming a draw, but still visits every such pixel; the gain stage
|
|
45
|
+
now samples only the pixels that hold charge when they are a minority (a
|
|
46
|
+
wavefront sensor's dark background), and skips the `* 1` shape scaling at
|
|
47
|
+
`F = sqrt(2)`. Same draws, same order, same generator state afterwards;
|
|
48
|
+
seeded frames of every preset, in both precisions, are bit-identical to 2.5.0.
|
|
49
|
+
On one Neoverse-N1 core (256x256 Shack-Hartmann spots, 1 ms, in-process A/B):
|
|
50
|
+
the Gamma draw 1.43 → 0.78 ms; whole frames `andor_ocam2k` 6.57 → 5.95 ms
|
|
51
|
+
(1.10x), `leonardo_saphira` 4.79 → 4.29 ms (1.12x), `first_light_imaging_cred_one`
|
|
52
|
+
7.16 → 6.63 ms (1.08x); presets without a gain stage are unchanged. The CPU
|
|
53
|
+
chain is otherwise at its sampler floor (about 85% of a frame is NumPy's
|
|
54
|
+
Poisson, Gamma and Gaussian draws). An exact vectorised replica of NumPy's
|
|
55
|
+
small-rate Poisson for the CIC draw was tried and measured no faster, so it
|
|
56
|
+
was not kept. See `benchmarks/small-frame-results.md`.
|
|
57
|
+
|
|
58
|
+
### Documented
|
|
59
|
+
|
|
60
|
+
- The 1--2% calibrated-flux deficit seen in the dithered Shack-Hartmann
|
|
61
|
+
prototype is not a deterministic offset of the signal chain: averaged over
|
|
62
|
+
300 frames the prototype recovers 1.002 (OCAM2K), 0.999 (ORCA-Quest 2) and
|
|
63
|
+
1.001 (generic sCMOS) of the incident flux, while a single 288x288 frame
|
|
64
|
+
scatters by 1.2%, 1.0% and 3.3%. The guide lists the deterministic offsets a
|
|
65
|
+
pipeline can introduce (median master dark, scalar vs. spectral QE,
|
|
66
|
+
nonlinearity, saturation, unflattened fixed patterns) and their remedies.
|
|
67
|
+
- The Nüvü HNü 240 / 128 presets cap the *amplified* charge at their 500 / 800
|
|
68
|
+
e- `full_well_e` (no `output_full_well_e`), so at EM gain 1000 a pixel holding
|
|
69
|
+
one electron saturates; the guide warns about it (#11).
|
|
70
|
+
|
|
9
71
|
## [2.5.0] - 2026-10-07
|
|
10
72
|
|
|
11
73
|
### Added
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: getframes
|
|
3
|
-
Version: 2.
|
|
3
|
+
Version: 2.6.0
|
|
4
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
5
|
Project-URL: Homepage, https://github.com/jacotay7/getframes
|
|
6
6
|
Project-URL: Documentation, https://jacotay7.github.io/getframes/
|
|
@@ -174,6 +174,10 @@ for the methodology.
|
|
|
174
174
|
- **Calibration & ground truth** — master bias/dark/flat builders and a
|
|
175
175
|
`calibrate` reduction that closes the
|
|
176
176
|
**[raw → reduced → truth loop](https://jacotay7.github.io/getframes/guides/calibration/)**.
|
|
177
|
+
- **Wavefront-sensor cameras** — `Calibration` turns ADU back into
|
|
178
|
+
input-referred electrons and photons, `Camera.effective_read_noise_e` gives
|
|
179
|
+
the input-referred noise, and a guide covers
|
|
180
|
+
**[a WFS camera in a closed loop](https://jacotay7.github.io/getframes/guides/wfs-closed-loop/)**.
|
|
177
181
|
- **Observations** — `Observation` drives time series with jitter, drift, dither
|
|
178
182
|
and persistence, carrying per-frame
|
|
179
183
|
**[truth](https://jacotay7.github.io/getframes/guides/time-series/)**.
|
|
@@ -127,6 +127,10 @@ for the methodology.
|
|
|
127
127
|
- **Calibration & ground truth** — master bias/dark/flat builders and a
|
|
128
128
|
`calibrate` reduction that closes the
|
|
129
129
|
**[raw → reduced → truth loop](https://jacotay7.github.io/getframes/guides/calibration/)**.
|
|
130
|
+
- **Wavefront-sensor cameras** — `Calibration` turns ADU back into
|
|
131
|
+
input-referred electrons and photons, `Camera.effective_read_noise_e` gives
|
|
132
|
+
the input-referred noise, and a guide covers
|
|
133
|
+
**[a WFS camera in a closed loop](https://jacotay7.github.io/getframes/guides/wfs-closed-loop/)**.
|
|
130
134
|
- **Observations** — `Observation` drives time series with jitter, drift, dither
|
|
131
135
|
and persistence, carrying per-frame
|
|
132
136
|
**[truth](https://jacotay7.github.io/getframes/guides/time-series/)**.
|
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
# SPDX-License-Identifier: MIT
|
|
2
|
+
"""Benchmark small, frequent wavefront-sensor frames on the CPU.
|
|
3
|
+
|
|
4
|
+
A closed-loop wavefront-sensor study calls :meth:`getframes.Camera.expose` once
|
|
5
|
+
per loop step on a small frame, so per-frame latency, not throughput on large
|
|
6
|
+
detectors, is what limits it. This script times seeded ``expose`` calls on a
|
|
7
|
+
256x256 Shack-Hartmann-like spot pattern (16x16 subapertures of 16x16 pixels,
|
|
8
|
+
one Gaussian spot each) for one preset of each WFS detector class, at a
|
|
9
|
+
photon-starved and a bright flux, in two call styles:
|
|
10
|
+
|
|
11
|
+
* ``default``: ``expose(rate, t, seed=s, include_truth=False)``;
|
|
12
|
+
* ``workspace``: the same with a reused :class:`getframes.DetectorWorkspace` and
|
|
13
|
+
a caller-owned ``out`` frame.
|
|
14
|
+
|
|
15
|
+
It also records a SHA-256 digest of a few seeded frames per configuration, so two
|
|
16
|
+
revisions can be compared for bit-identical output (``--compare``). Timings are
|
|
17
|
+
process CPU time per frame over interleaved, warmed blocks (the chain is
|
|
18
|
+
single-threaded); they are machine dependent and are not part of the test gate.
|
|
19
|
+
Each case reports the median and the best (minimum) block; on a shared host,
|
|
20
|
+
compare the best blocks, which other load can only inflate.
|
|
21
|
+
|
|
22
|
+
Run from the repository root::
|
|
23
|
+
|
|
24
|
+
python benchmarks/bench_small_frames.py --output small-frame-results.json
|
|
25
|
+
python benchmarks/bench_small_frames.py --compare before.json after.json
|
|
26
|
+
"""
|
|
27
|
+
|
|
28
|
+
from __future__ import annotations
|
|
29
|
+
|
|
30
|
+
import argparse
|
|
31
|
+
import hashlib
|
|
32
|
+
import json
|
|
33
|
+
import platform
|
|
34
|
+
import statistics
|
|
35
|
+
import time
|
|
36
|
+
from pathlib import Path
|
|
37
|
+
from typing import Any
|
|
38
|
+
|
|
39
|
+
import numpy as np
|
|
40
|
+
|
|
41
|
+
import getframes as gf
|
|
42
|
+
|
|
43
|
+
SHAPE = (256, 256)
|
|
44
|
+
SUBAPERTURE_PX = 16
|
|
45
|
+
EXPOSURE_S = 1e-3
|
|
46
|
+
#: One preset per detector class a WFS is built from.
|
|
47
|
+
PRESETS = (
|
|
48
|
+
("EMCCD", "andor_ocam2k"),
|
|
49
|
+
("EMCCD", "andor_ixon_ultra_888"),
|
|
50
|
+
("qCMOS", "hamamatsu_orca_quest_2"),
|
|
51
|
+
("sCMOS", "generic_scmos"),
|
|
52
|
+
("eAPD", "first_light_imaging_cred_one"),
|
|
53
|
+
)
|
|
54
|
+
#: Photons per subaperture per frame: photon-starved and bright.
|
|
55
|
+
FLUXES = (100.0, 2000.0)
|
|
56
|
+
DIGEST_SEEDS = (0, 1, 2)
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def spot_pattern(photons_per_subaperture: float) -> np.ndarray[Any, np.dtype[np.float64]]:
|
|
60
|
+
"""A 256x256 photon map of one Gaussian spot (sigma 1.5 px) per subaperture."""
|
|
61
|
+
coordinates = np.arange(SUBAPERTURE_PX) - (SUBAPERTURE_PX - 1) / 2
|
|
62
|
+
spot = np.exp(-(coordinates[:, None] ** 2 + coordinates[None, :] ** 2) / (2 * 1.5**2))
|
|
63
|
+
spot *= photons_per_subaperture / spot.sum()
|
|
64
|
+
reps = (SHAPE[0] // SUBAPERTURE_PX, SHAPE[1] // SUBAPERTURE_PX)
|
|
65
|
+
return np.tile(spot, reps)
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def make_camera(preset: str) -> gf.Camera:
|
|
69
|
+
return gf.Camera.from_preset(preset).with_config(resolution=SHAPE, roi=None)
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def _block_ms(function: Any, calls: int) -> float:
|
|
73
|
+
# Process CPU time, not wall time: the chain is single-threaded, and CPU time
|
|
74
|
+
# is far less sensitive to other load on a shared host.
|
|
75
|
+
start = time.process_time_ns()
|
|
76
|
+
for index in range(calls):
|
|
77
|
+
function(index)
|
|
78
|
+
return (time.process_time_ns() - start) / calls / 1e6
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def bench_case(preset: str, flux: float, calls: int, repetitions: int) -> dict[str, Any]:
|
|
82
|
+
camera = make_camera(preset)
|
|
83
|
+
rate = spot_pattern(flux) / EXPOSURE_S
|
|
84
|
+
workspace = gf.DetectorWorkspace()
|
|
85
|
+
out = np.empty(SHAPE, dtype=np.uint32)
|
|
86
|
+
|
|
87
|
+
def default(seed: int) -> None:
|
|
88
|
+
camera.expose(rate, EXPOSURE_S, seed=seed, include_truth=False)
|
|
89
|
+
|
|
90
|
+
def reused(seed: int) -> None:
|
|
91
|
+
camera.expose(
|
|
92
|
+
rate, EXPOSURE_S, seed=seed, include_truth=False, workspace=workspace, out=out
|
|
93
|
+
)
|
|
94
|
+
|
|
95
|
+
for seed in range(10):
|
|
96
|
+
default(seed)
|
|
97
|
+
reused(seed)
|
|
98
|
+
samples: dict[str, list[float]] = {"default": [], "workspace": []}
|
|
99
|
+
variants = [("default", default), ("workspace", reused)]
|
|
100
|
+
for repetition in range(repetitions):
|
|
101
|
+
for name, function in variants if repetition % 2 == 0 else variants[::-1]:
|
|
102
|
+
samples[name].append(_block_ms(function, calls))
|
|
103
|
+
|
|
104
|
+
digest = hashlib.sha256()
|
|
105
|
+
for seed in DIGEST_SEEDS:
|
|
106
|
+
frame = camera.expose(rate, EXPOSURE_S, seed=seed)
|
|
107
|
+
digest.update(np.ascontiguousarray(frame.data).tobytes())
|
|
108
|
+
assert frame.truth is not None
|
|
109
|
+
digest.update(np.ascontiguousarray(frame.truth.mean_electrons).tobytes())
|
|
110
|
+
return {
|
|
111
|
+
"preset": preset,
|
|
112
|
+
"photons_per_subaperture": flux,
|
|
113
|
+
"default_ms": statistics.median(samples["default"]),
|
|
114
|
+
"workspace_ms": statistics.median(samples["workspace"]),
|
|
115
|
+
"default_best_ms": min(samples["default"]),
|
|
116
|
+
"workspace_best_ms": min(samples["workspace"]),
|
|
117
|
+
"default_samples_ms": samples["default"],
|
|
118
|
+
"workspace_samples_ms": samples["workspace"],
|
|
119
|
+
"seeded_digest": digest.hexdigest(),
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
def run(calls: int, repetitions: int) -> dict[str, Any]:
|
|
124
|
+
results = []
|
|
125
|
+
for sensor, preset in PRESETS:
|
|
126
|
+
for flux in FLUXES:
|
|
127
|
+
case = bench_case(preset, flux, calls, repetitions)
|
|
128
|
+
case["class"] = sensor
|
|
129
|
+
results.append(case)
|
|
130
|
+
print(
|
|
131
|
+
f"{sensor:6s} {preset:30s} {flux:7.0f} ph/subap "
|
|
132
|
+
f"default {case['default_ms']:6.2f} ms workspace {case['workspace_ms']:6.2f} ms",
|
|
133
|
+
flush=True,
|
|
134
|
+
)
|
|
135
|
+
return {
|
|
136
|
+
"schema_version": 1,
|
|
137
|
+
"getframes": gf.__version__,
|
|
138
|
+
"numpy": np.__version__,
|
|
139
|
+
"platform": platform.platform(),
|
|
140
|
+
"processor": platform.processor() or platform.machine(),
|
|
141
|
+
"shape": list(SHAPE),
|
|
142
|
+
"exposure_s": EXPOSURE_S,
|
|
143
|
+
"calls_per_block": calls,
|
|
144
|
+
"repetitions": repetitions,
|
|
145
|
+
"results": results,
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
def compare(before_path: Path, after_path: Path) -> str:
|
|
150
|
+
"""A Markdown table of before/after best blocks and whether seeded output matches."""
|
|
151
|
+
before = json.loads(before_path.read_text(encoding="utf-8"))
|
|
152
|
+
after = json.loads(after_path.read_text(encoding="utf-8"))
|
|
153
|
+
lines = [
|
|
154
|
+
f"Before: getframes {before['getframes']}; after: getframes {after['getframes']} "
|
|
155
|
+
f"({after['processor']}, NumPy {after['numpy']}, {after['shape'][0]}x{after['shape'][1]}).",
|
|
156
|
+
"",
|
|
157
|
+
"| Class | Preset | Photons/subap | Before (ms, best) | After (ms, best) | Speed-up | "
|
|
158
|
+
"After, workspace+out (ms, best) | Seeded output |",
|
|
159
|
+
"| --- | --- | ---: | ---: | ---: | ---: | ---: | --- |",
|
|
160
|
+
]
|
|
161
|
+
old = {(r["preset"], r["photons_per_subaperture"]): r for r in before["results"]}
|
|
162
|
+
for row in after["results"]:
|
|
163
|
+
ref = old.get((row["preset"], row["photons_per_subaperture"]))
|
|
164
|
+
if ref is None:
|
|
165
|
+
continue
|
|
166
|
+
same = "identical" if ref["seeded_digest"] == row["seeded_digest"] else "CHANGED"
|
|
167
|
+
lines.append(
|
|
168
|
+
f"| {row['class']} | {row['preset']} | {row['photons_per_subaperture']:.0f} | "
|
|
169
|
+
f"{ref['default_best_ms']:.2f} | {row['default_best_ms']:.2f} | "
|
|
170
|
+
f"{ref['default_best_ms'] / row['default_best_ms']:.2f}x | "
|
|
171
|
+
f"{row['workspace_best_ms']:.2f} | {same} |"
|
|
172
|
+
)
|
|
173
|
+
return "\n".join(lines) + "\n"
|
|
174
|
+
|
|
175
|
+
|
|
176
|
+
def main() -> None:
|
|
177
|
+
parser = argparse.ArgumentParser(description=__doc__)
|
|
178
|
+
parser.add_argument("--calls", type=int, default=40)
|
|
179
|
+
parser.add_argument("--repetitions", type=int, default=7)
|
|
180
|
+
parser.add_argument("--output", type=Path)
|
|
181
|
+
parser.add_argument("--compare", nargs=2, type=Path, metavar=("BEFORE", "AFTER"))
|
|
182
|
+
arguments = parser.parse_args()
|
|
183
|
+
if arguments.compare:
|
|
184
|
+
print(compare(*arguments.compare), end="")
|
|
185
|
+
return
|
|
186
|
+
payload = run(arguments.calls, arguments.repetitions)
|
|
187
|
+
if arguments.output is not None:
|
|
188
|
+
arguments.output.write_text(json.dumps(payload, indent=2) + "\n", encoding="utf-8")
|
|
189
|
+
|
|
190
|
+
|
|
191
|
+
if __name__ == "__main__":
|
|
192
|
+
main()
|