phepex 0.2.0__tar.gz → 0.2.2__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 (67) hide show
  1. {phepex-0.2.0 → phepex-0.2.2}/.gitignore +2 -0
  2. {phepex-0.2.0 → phepex-0.2.2}/CHANGELOG.md +28 -1
  3. {phepex-0.2.0 → phepex-0.2.2}/CMakeLists.txt +1 -0
  4. {phepex-0.2.0 → phepex-0.2.2}/PKG-INFO +10 -8
  5. {phepex-0.2.0 → phepex-0.2.2}/README.md +26 -21
  6. {phepex-0.2.0 → phepex-0.2.2}/README.pypi.md +9 -7
  7. {phepex-0.2.0 → phepex-0.2.2}/benchmarks/README.md +22 -3
  8. phepex-0.2.2/docs/_static/slides-viewer.css +113 -0
  9. phepex-0.2.2/docs/_static/slides-viewer.js +103 -0
  10. phepex-0.2.2/docs/conf.py +265 -0
  11. {phepex-0.2.0 → phepex-0.2.2}/docs/cpp/index.md +5 -9
  12. {phepex-0.2.0 → phepex-0.2.2}/docs/getting-started.md +0 -2
  13. {phepex-0.2.0 → phepex-0.2.2}/docs/index.md +5 -7
  14. {phepex-0.2.0 → phepex-0.2.2}/docs/python/index.md +2 -9
  15. {phepex-0.2.0 → phepex-0.2.2}/docs/requirements.txt +3 -0
  16. phepex-0.2.2/docs/slides/README.md +56 -0
  17. phepex-0.2.2/docs/slides/phepex-intro.typ +430 -0
  18. phepex-0.2.2/docs/slides/phepex-logo-light.svg +30 -0
  19. phepex-0.2.2/docs/slides.md +37 -0
  20. {phepex-0.2.0 → phepex-0.2.2}/include/phepex/clip.hpp +7 -3
  21. {phepex-0.2.0 → phepex-0.2.2}/include/phepex/extract.hpp +13 -6
  22. {phepex-0.2.0 → phepex-0.2.2}/include/phepex/generate.hpp +23 -17
  23. {phepex-0.2.0 → phepex-0.2.2}/include/phepex/neighbor.hpp +13 -5
  24. {phepex-0.2.0 → phepex-0.2.2}/include/phepex/phepex.hpp +5 -5
  25. {phepex-0.2.0 → phepex-0.2.2}/include/phepex/preprocess.hpp +38 -44
  26. {phepex-0.2.0 → phepex-0.2.2}/python/phepex/__init__.py +4 -3
  27. {phepex-0.2.0 → phepex-0.2.2}/python/phepex/extractor.py +17 -7
  28. {phepex-0.2.0 → phepex-0.2.2}/python/phepex/kernels.py +38 -34
  29. {phepex-0.2.0 → phepex-0.2.2}/src/bindings.cpp +27 -21
  30. {phepex-0.2.0 → phepex-0.2.2}/src/preprocess.cpp +25 -15
  31. {phepex-0.2.0 → phepex-0.2.2}/tests/cpp/test_phepex.cpp +37 -23
  32. phepex-0.2.2/tests/cpp/test_smoothing_precision.cpp +535 -0
  33. {phepex-0.2.0 → phepex-0.2.2}/tests/test_extractor_equivalence.py +8 -4
  34. {phepex-0.2.0 → phepex-0.2.2}/tests/test_kernel_guards.py +2 -3
  35. {phepex-0.2.0 → phepex-0.2.2}/tests/test_preprocess.py +2 -2
  36. phepex-0.2.0/docs/conf.py +0 -149
  37. {phepex-0.2.0 → phepex-0.2.2}/LICENSE +0 -0
  38. {phepex-0.2.0 → phepex-0.2.2}/benchmarks/benchmark-fast-extractor.py +0 -0
  39. {phepex-0.2.0 → phepex-0.2.2}/benchmarks/cpp/microbench.cpp +0 -0
  40. {phepex-0.2.0 → phepex-0.2.2}/benchmarks/flashcam-config.txt +0 -0
  41. {phepex-0.2.0 → phepex-0.2.2}/benchmarks/generate_events.py +0 -0
  42. {phepex-0.2.0 → phepex-0.2.2}/cmake/GitVersion.cmake +0 -0
  43. {phepex-0.2.0 → phepex-0.2.2}/cmake/phepexConfig.cmake.in +0 -0
  44. {phepex-0.2.0 → phepex-0.2.2}/docs/.nojekyll +0 -0
  45. {phepex-0.2.0 → phepex-0.2.2}/docs/Doxyfile.in +0 -0
  46. {phepex-0.2.0 → phepex-0.2.2}/docs/_static/phepex-icon-dark.svg +0 -0
  47. {phepex-0.2.0 → phepex-0.2.2}/docs/_static/phepex-icon-light.svg +0 -0
  48. {phepex-0.2.0 → phepex-0.2.2}/docs/_static/version-switcher.css +0 -0
  49. {phepex-0.2.0 → phepex-0.2.2}/docs/_static/version-switcher.js +0 -0
  50. {phepex-0.2.0 → phepex-0.2.2}/docs/_templates/sidebar/version-switcher.html +0 -0
  51. {phepex-0.2.0 → phepex-0.2.2}/docs/changelog.md +0 -0
  52. {phepex-0.2.0 → phepex-0.2.2}/docs/gen_switcher.py +0 -0
  53. {phepex-0.2.0 → phepex-0.2.2}/examples/example.cpp +0 -0
  54. {phepex-0.2.0 → phepex-0.2.2}/include/phepex/version.hpp.in +0 -0
  55. {phepex-0.2.0 → phepex-0.2.2}/phepex.pc.in +0 -0
  56. {phepex-0.2.0 → phepex-0.2.2}/pyproject.toml +0 -0
  57. {phepex-0.2.0 → phepex-0.2.2}/scripts/export-camera-config.py +0 -0
  58. {phepex-0.2.0 → phepex-0.2.2}/src/clip.cpp +0 -0
  59. {phepex-0.2.0 → phepex-0.2.2}/src/extract.cpp +0 -0
  60. {phepex-0.2.0 → phepex-0.2.2}/src/generate.cpp +0 -0
  61. {phepex-0.2.0 → phepex-0.2.2}/src/internal.hpp +0 -0
  62. {phepex-0.2.0 → phepex-0.2.2}/src/neighbor.cpp +0 -0
  63. {phepex-0.2.0 → phepex-0.2.2}/tests/test_extract_centroid.py +0 -0
  64. {phepex-0.2.0 → phepex-0.2.2}/tests/test_generator.py +0 -0
  65. {phepex-0.2.0 → phepex-0.2.2}/tests/test_neighbor.py +0 -0
  66. {phepex-0.2.0 → phepex-0.2.2}/third_party/catch2/catch_amalgamated.cpp +0 -0
  67. {phepex-0.2.0 → phepex-0.2.2}/third_party/catch2/catch_amalgamated.hpp +0 -0
@@ -8,3 +8,5 @@ docs/_doxygen/
8
8
  docs/Doxyfile
9
9
  include/phepex/version.hpp
10
10
  wheelhouse/
11
+ docs/slides/*.pdf
12
+ docs/_static/slides/
@@ -9,6 +9,32 @@ numbers are derived from git tags (`vX.Y.Z`) by setuptools-scm (Python) and GitV
9
9
 
10
10
  ## [Unreleased]
11
11
 
12
+ ### Changed
13
+ - The Deriche smoothing pass (`smooth_waveform` and its batched counterpart
14
+ `smooth_batched`) now evaluates its recursion entirely in float32. The coefficients are
15
+ narrowed to float once on entry, so the products in the forward and backward loops no
16
+ longer promote to double, and the backward pass carries float state. Measured with the
17
+ per-kernel micro-benchmark on Apple M1 Pro (1764-pixel FlashCam, 22 samples, upsampling
18
+ 4): smoothing alone 163 -> 96 us/event (-41%), upsampling + smoothing 1148 -> 837
19
+ us/event (-27%), the tiled full sweep `preprocess up/pz/smoothing batch` 213 -> 165
20
+ us/event (-23%). The numerical deviation was quantified against a long double evaluation
21
+ of the same recursion in `tests/cpp/test_smoothing_precision.cpp`: ~10^-7 relative on
22
+ the samples, < 10^-3 p.e. on the extracted charge and < 10^-4 upsampled samples on the
23
+ centroid. The error does not accumulate with trace length; it grows with FWHM as
24
+ 1/(1-r)^2, reaching ~10^-6 relative at FWHM 20. Both the impulse-response width and the
25
+ unity DC gain are preserved (< 10^-4 and ~10^-6 deviation respectively).
26
+
27
+ ## [0.2.1] - 2026-07-30
28
+
29
+ ### Changed
30
+ - Documentation and docstrings revised for accuracy and consistency across the README, the
31
+ Sphinx pages, the C++ header comments and the Python/binding docstrings.
32
+
33
+ ### Added
34
+ - Introductory slide deck (`docs/slides/phepex-intro.typ`, [Typst](https://typst.app)): eight
35
+ slides on where phepex sits relative to libdvr and ctapipe, the kernels it exposes, and the
36
+ 0.2.0 benchmark figures.
37
+
12
38
  ## [0.2.0] - 2026-07-28
13
39
 
14
40
  ### Changed
@@ -206,6 +232,7 @@ numbers are derived from git tags (`vX.Y.Z`) by setuptools-scm (Python) and GitV
206
232
  - Unified Sphinx documentation covering the C++ (Doxygen + Breathe) and Python (autodoc)
207
233
  API surfaces, published to GitHub Pages.
208
234
 
209
- [Unreleased]: https://github.com/phepex/phepex/compare/v0.2.0...HEAD
235
+ [Unreleased]: https://github.com/phepex/phepex/compare/v0.2.1...HEAD
236
+ [0.2.1]: https://github.com/phepex/phepex/releases/tag/v0.2.1
210
237
  [0.2.0]: https://github.com/phepex/phepex/releases/tag/v0.2.0
211
238
  [0.1.0]: https://github.com/phepex/phepex/releases/tag/v0.1.0
@@ -114,6 +114,7 @@ if(PHEPEX_BUILD_TESTS)
114
114
  enable_testing()
115
115
  add_executable(phepex_tests
116
116
  tests/cpp/test_phepex.cpp
117
+ tests/cpp/test_smoothing_precision.cpp
117
118
  third_party/catch2/catch_amalgamated.cpp)
118
119
  target_include_directories(phepex_tests PRIVATE third_party/catch2)
119
120
  target_link_libraries(phepex_tests PRIVATE phepex::phepex)
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: phepex
3
- Version: 0.2.0
3
+ Version: 0.2.2
4
4
  Summary: Photo-electron pulse extraction: fast PMT/SiPM waveform charge & timing extraction for Python.
5
5
  Keywords: waveform,charge-extraction,PMT,SiPM,Cherenkov,signal-processing,DSP
6
6
  Author: Max-Planck-Institut für Kernphysik
@@ -26,10 +26,11 @@ Description-Content-Type: text/markdown
26
26
 
27
27
  # phepex — photo-electron pulse extraction
28
28
 
29
- Fast numeric kernels for extracting charge and timing from digitised PMT/SiPM
30
- waveforms (e.g. Cherenkov telescopes or Water Cherenkov Detectors): pole-zero
29
+ Compiled numeric kernels for extracting charge and timing from digitised PMT/SiPM
30
+ waveforms (e.g. from Cherenkov telescopes or Water Cherenkov Detectors): pole-zero
31
31
  deconvolution + upsampling, neighbour-sum peak finding, soft clipping, window
32
- integration and leading-edge timing — plus a fast waveform generator for testing.
32
+ integration and leading-edge timing — plus a waveform generator for tests and
33
+ benchmarks.
33
34
 
34
35
  The heavy lifting runs in a compiled extension; `import phepex` needs only numpy.
35
36
 
@@ -43,14 +44,15 @@ pip install phepex
43
44
 
44
45
  ```python
45
46
  import phepex
46
- # phepex.deconvolve, phepex.pos_soft_clip, phepex.neighbor_peak_indices,
47
+ # phepex.preprocess, phepex.deconvolve, phepex.preprocess_valid_range,
48
+ # phepex.deconvolve_valid_range, phepex.pos_soft_clip, phepex.neighbor_peak_indices,
47
49
  # phepex.extract_around_peak, phepex.adaptive_centroid, phepex.generate_waveforms
48
50
  ```
49
51
 
50
52
  The ctapipe-integrated `phepex.extractor.FastFlashCamExtractor` — a drop-in
51
- accelerating ctapipe's `FlashCamExtractor` (typically ~5× faster end-to-end, with
52
- bit-exact / ~1e-7 agreement on signal pixels) — lives in a separate submodule
53
- imported on demand. It needs the `bench` extra:
53
+ accelerating ctapipe's `FlashCamExtractor` (typically ~5× faster end-to-end, agreeing
54
+ on signal-pixel charge to a median relative deviation of ~1e-7) — lives in a separate
55
+ submodule imported on demand. It needs the `bench` extra:
54
56
 
55
57
  ```bash
56
58
  pip install phepex[bench] # pulls in ctapipe
@@ -1,16 +1,16 @@
1
1
  # phepex — photo-electron pulse extraction
2
2
 
3
3
  <!-- intro:start -->
4
- A small, dependency-free **C++17 library** of the numeric kernels used to extract charge and timing from digitised PMT/SiPM (e.g. Cherenkov telescopes or Water Cherenkov Detectors) waveforms — pole-zero deconvolution + upsampling, neighbour-sum peak finding, soft clipping, window integration and leading-edge timing — plus a fast waveform generator for testing.
4
+ A dependency-free **C++17 library** of the numeric kernels used to extract charge and timing from digitised PMT/SiPM waveforms (e.g. from Cherenkov telescopes or Water Cherenkov Detectors) — pole-zero deconvolution + upsampling, neighbour-sum peak finding, soft clipping, window integration and leading-edge timing — plus a waveform generator for tests and benchmarks.
5
5
 
6
- Ships **`libphepex.so`** and **`libphepex.a`**, public headers under `phepex/`, and optional **Python bindings** (`import phepex`).
6
+ Ships `libphepex.so` and `libphepex.a`, public headers under `phepex/`, and optional **Python bindings** (`import phepex`).
7
7
 
8
- The C++ library depends only on the C++ standard library, so it drops into any C++ project. `ctapipe` is used only by the Python benchmark/extractor layer, never by the library itself.
8
+ The C++ library depends only on the C++ standard library, so it drops into any C++ project. `ctapipe` is used only by the Python benchmark/extractor layer, not by the library itself.
9
9
  <!-- intro:end -->
10
10
 
11
11
  ## Documentation
12
12
 
13
- Full API documentation covering **both** the C++ and Python surfaces is published to GitHub Pages: **https://phepex.github.io/phepex/** (built from source by the `docs` workflow on every push to `main`). Build it locally with:
13
+ API documentation for both the C++ and Python surfaces is published to **https://phepex.github.io/phepex/**, rebuilt from source by the `docs` workflow on every push to `main`. Build it locally with:
14
14
 
15
15
  ```bash
16
16
  pip install .[bench] && pip install -r docs/requirements.txt
@@ -24,8 +24,10 @@ All kernels are free functions in namespace `phepex`, operating on caller-owned
24
24
 
25
25
  ```cpp
26
26
  #include <phepex/phepex.hpp>
27
- // phepex::preprocess_waveform(wf, n_samples, upsampling, pole_zero, smoothing, offset, scale, out);
28
- // phepex::pos_soft_clip / neighbor_peak_indices / extract_around_peak / adaptive_centroid / generate_waveforms
27
+ // preprocess_waveform / preprocess_waveforms (batched) / preprocess_valid_range
28
+ // / calculate_smoothing_coefficients / preprocess_waveforms_scratch_size
29
+ // pos_soft_clip / neighbor_peak_indices / extract_around_peak / adaptive_centroid
30
+ // generate_waveforms / generate_shower_image
29
31
  ```
30
32
 
31
33
  Build & install:
@@ -64,7 +66,7 @@ bindings automatically.
64
66
  | `PHEPEX_BUILD_EXAMPLES` | `OFF` | Build the C++ example (`examples/example.cpp`). |
65
67
  | `PHEPEX_BUILD_TESTS` | `OFF` | Build the standalone C++ unit tests (vendored Catch2). |
66
68
  | `PHEPEX_BUILD_BENCHMARKS` | `OFF` | Build the C++ per-kernel micro-benchmark. |
67
- | `PHEPEX_NEIGHBOR_PAIRWISE_SUM` | `ON` | Accumulate the neighbour sum in `neighbor_peak_indices` pairwise (~18-22% faster on the benchmarked aarch64 core; see the CHANGELOG for the measurement). `OFF` selects the sequential sum, which is what ctapipe computes; pairwise grouping alters the float32 summation order, so the peak index can differ in near-ties within ~1 ULP. See the note in `include/phepex/neighbor.hpp`. |
69
+ | `PHEPEX_NEIGHBOR_PAIRWISE_SUM` | `ON` | Accumulate the neighbour sum in `neighbor_peak_indices` pairwise (~18–22% faster on the benchmarked aarch64 core; see the CHANGELOG for the measurement). `OFF` selects the sequential sum, bit-identical to a left-to-right accumulation in CSR order; pairwise grouping alters the float32 summation order, so the peak index can differ in near-ties within ~1 ULP. See the note in `include/phepex/neighbor.hpp`. |
68
70
  | `PHEPEX_PREPROCESS_TILE_WIDTH` | `24` | Number of waveforms mapped onto SIMD lanes per tile in the batched `preprocess_waveforms` path. Bit-identical for any value `>= 1`; performance only. See the tuning notes below. |
69
71
 
70
72
  ### Tuning `PHEPEX_PREPROCESS_TILE_WIDTH`
@@ -83,9 +85,9 @@ Recommendations:
83
85
  that inside L1; above it the tiles spill and the gain reverses.
84
86
  - Build the micro-benchmark (`-DPHEPEX_BUILD_BENCHMARKS=ON
85
87
  -DCMAKE_CXX_FLAGS="-march=native" -DPHEPEX_PREPROCESS_TILE_WIDTH=K`) at a few widths and
86
- compare the `preprocess up/pz(/smoothing) batch` rows
87
- - `24` (the default) works best for Apple Silicon (NEON) and Zen 4 architectures, even though
88
- the latter supports AVX-512 (float SIMD width of 16).
88
+ compare the `preprocess up/pz(/smoothing) batch` rows.
89
+ - `24` (the default) measured best on both Apple Silicon (NEON) and Zen 4, even though the
90
+ latter supports AVX-512 (float SIMD width of 16).
89
91
 
90
92
  To obtain an optimised Python wheel, pass it through scikit-build-core, e.g.
91
93
  `CMAKE_CXX_FLAGS="-march=native" pip install . -C cmake.define.PHEPEX_PREPROCESS_TILE_WIDTH=K`.
@@ -99,7 +101,8 @@ pip install . # builds the phepex wheel (scikit-build-core + nanobind
99
101
 
100
102
  ```python
101
103
  import phepex
102
- # phepex.deconvolve, phepex.pos_soft_clip, phepex.neighbor_peak_indices,
104
+ # phepex.preprocess, phepex.deconvolve, phepex.preprocess_valid_range,
105
+ # phepex.deconvolve_valid_range, phepex.pos_soft_clip, phepex.neighbor_peak_indices,
103
106
  # phepex.extract_around_peak, phepex.adaptive_centroid, phepex.generate_waveforms
104
107
  ```
105
108
 
@@ -136,15 +139,17 @@ ctest --test-dir build --output-on-failure # or: ./build/phepex_tests
136
139
 
137
140
  ## C++ micro-benchmark (no Python/ctapipe)
138
141
 
139
- `benchmarks/cpp/microbench.cpp` times each `phepex::` kernel in isolation on a realistic
140
- camera configuration, so a regression can be attributed to a specific kernel. It links only
141
- `libphepex` and reads the configuration at run time from a text file, so it needs neither
142
- Python nor ctapipe. One "op" is one full-camera sweep (the per-event cost). The input is one
143
- artificial event — a skewed-Gaussian shower image over ~200 pixels at ~1–100 p.e. with a
144
- ~1 ns/pixel time gradient, plus 200 MHz NSB on a 200 LSB pedestal, digitised to 12-bit ADC
145
- counts — because the peak-search and centroid kernels are data-dependent. The image is sized
146
- in pixel pitches, so a config for another camera gives a comparable event without code
147
- changes.
142
+ `benchmarks/cpp/microbench.cpp` times each `phepex::` kernel in isolation, so a regression
143
+ can be attributed to a specific kernel. It links only `libphepex` and reads the camera
144
+ configuration at run time from a text file, so it needs neither Python nor ctapipe. One "op"
145
+ is one full-camera sweep (the per-event cost); the reported statistic is the minimum over
146
+ all reps.
147
+
148
+ Because the peak-search and centroid kernels are data-dependent, the input is one realistic
149
+ artificial event: a skewed-Gaussian shower image over ~200 pixels at ~1–100 p.e. with a
150
+ 1 ns/pixel-pitch time gradient, plus 200 MHz NSB on a 200 LSB pedestal, digitised to 12-bit
151
+ ADC counts. Image axes and gradient are expressed in pixel pitches, so a config for another
152
+ camera gives a comparable event without code changes.
148
153
 
149
154
  ```bash
150
155
  cmake -S . -B build -DPHEPEX_BUILD_BENCHMARKS=ON -DCMAKE_CXX_FLAGS="-march=native"
@@ -172,7 +177,7 @@ python3 scripts/export-camera-config.py --camera FlashCam \
172
177
  python3 benchmarks/benchmark-fast-extractor.py --events 5000
173
178
  ```
174
179
 
175
- Typical result: **~5× faster** end-to-end (leading-edge timing on), with bit-exact / ~1e-7 agreement on signal pixels. Tests: `python3 -m pytest tests/ -q`.
180
+ Typical result: **~5× faster** end-to-end (leading-edge timing on). Extracted charge agrees with stock ctapipe to a median relative deviation of ~1e-7 on signal pixels (99th percentile ~2e-5); a small tail differs materially where the neighbour peak search lands on a different local maximum. See `benchmarks/README.md` for the per-step breakdown and the cause of that tail. Tests: `python3 -m pytest tests/ -q`.
176
181
  <!-- benchmark:end -->
177
182
 
178
183
  ## Notes
@@ -1,9 +1,10 @@
1
1
  # phepex — photo-electron pulse extraction
2
2
 
3
- Fast numeric kernels for extracting charge and timing from digitised PMT/SiPM
4
- waveforms (e.g. Cherenkov telescopes or Water Cherenkov Detectors): pole-zero
3
+ Compiled numeric kernels for extracting charge and timing from digitised PMT/SiPM
4
+ waveforms (e.g. from Cherenkov telescopes or Water Cherenkov Detectors): pole-zero
5
5
  deconvolution + upsampling, neighbour-sum peak finding, soft clipping, window
6
- integration and leading-edge timing — plus a fast waveform generator for testing.
6
+ integration and leading-edge timing — plus a waveform generator for tests and
7
+ benchmarks.
7
8
 
8
9
  The heavy lifting runs in a compiled extension; `import phepex` needs only numpy.
9
10
 
@@ -17,14 +18,15 @@ pip install phepex
17
18
 
18
19
  ```python
19
20
  import phepex
20
- # phepex.deconvolve, phepex.pos_soft_clip, phepex.neighbor_peak_indices,
21
+ # phepex.preprocess, phepex.deconvolve, phepex.preprocess_valid_range,
22
+ # phepex.deconvolve_valid_range, phepex.pos_soft_clip, phepex.neighbor_peak_indices,
21
23
  # phepex.extract_around_peak, phepex.adaptive_centroid, phepex.generate_waveforms
22
24
  ```
23
25
 
24
26
  The ctapipe-integrated `phepex.extractor.FastFlashCamExtractor` — a drop-in
25
- accelerating ctapipe's `FlashCamExtractor` (typically ~5× faster end-to-end, with
26
- bit-exact / ~1e-7 agreement on signal pixels) — lives in a separate submodule
27
- imported on demand. It needs the `bench` extra:
27
+ accelerating ctapipe's `FlashCamExtractor` (typically ~5× faster end-to-end, agreeing
28
+ on signal-pixel charge to a median relative deviation of ~1e-7) — lives in a separate
29
+ submodule imported on demand. It needs the `bench` extra:
28
30
 
29
31
  ```bash
30
32
  pip install phepex[bench] # pulls in ctapipe
@@ -1,7 +1,8 @@
1
1
  # Comparative benchmark: stock ctapipe vs phepex
2
2
 
3
3
  Compares ctapipe's `FlashCamExtractor` against `phepex.extractor.FastFlashCamExtractor`
4
- (same algorithm, numeric kernels in C++) on a realistic synthetic workload:
4
+ (same algorithm bar one documented divergence, numeric kernels in C++) on a synthetic
5
+ workload:
5
6
 
6
7
  - unique gamma-like events for a **1764-pixel FlashCam-MST** camera
7
8
  - **200 MHz night-sky background** (temporally-distributed Poisson p.e.)
@@ -26,10 +27,28 @@ python3 benchmarks/benchmark-fast-extractor.py --events 5000
26
27
  ```
27
28
 
28
29
  Typical result: **~5× faster** end-to-end with leading-edge timing on (~2700 →
29
- ~600 µs/event), **~4.5×** off. The extracted charge/peak_time match stock ctapipe
30
- bit-exactly with no NSB and to ~1e-7 on signal pixels (validated in `tests/`).
30
+ ~600 µs/event), **~4.5×** off.
31
+
32
+ Charge agreement with stock ctapipe, measured over 60 events (1764 pixels, upsampling 4,
33
+ 22 samples); relative deviations are over signal pixels (true charge > 1 p.e.):
34
+
35
+ | | no NSB | 200 MHz NSB |
36
+ | --- | --- | --- |
37
+ | signal pixels bit-exact | 40% | 8% |
38
+ | relative deviation, median | 7.4e-8 | 1.0e-7 |
39
+ | relative deviation, 99th percentile | 2.7e-7 | 2.4e-5 |
40
+ | relative deviation, max | 3.2e-7 | O(1) |
41
+
42
+ Without NSB the deviation is pure float32 rounding, bounded at ~3e-7. With NSB, a small
43
+ tail of pixels differs significantly: the neighbour-sum peak search resolves a near-tie
44
+ between two local maxima differently, or the true peak falls in an edge margin this
45
+ extractor excludes (see the notes below), and the integration window then lands elsewhere.
46
+ `tests/test_extractor_equivalence.py` asserts bounds that admit this tail.
31
47
 
32
48
  ## Notes
33
49
 
34
50
  - The deconvolution (two pole-zero passes/event) dominates the extractor runtime (~50%);
35
51
  the phepex kernel is ~7–9× faster than ctapipe's scipy `deconvolve`/`repeat`/`filtfilt`.
52
+ - `FastFlashCamExtractor` restricts the clip and neighbour peak search to the non-edge
53
+ deconvolution samples, so a peak inside an edge margin is found by stock ctapipe but not
54
+ here. This is the one intentional algorithmic difference.
@@ -0,0 +1,113 @@
1
+ /* In-page viewer for the Typst slide deck (see docs/slides.md, slides-viewer.js).
2
+ *
3
+ * The slides carry their own white background and accent colours, so they are framed rather
4
+ * than recoloured: inverting them in furo's dark theme would misrender the logo and the
5
+ * accent fills. The frame and controls follow the theme; the slide itself does not. */
6
+
7
+ .phepex-deck {
8
+ margin: 1.5rem 0;
9
+ }
10
+
11
+ /* The `display` rules below are author-level and would otherwise beat the user agent's
12
+ * `[hidden] { display: none }`, leaving the controls visible without JavaScript and a broken
13
+ * image when no SVGs were exported. Class+attribute outranks the bare class, so this needs
14
+ * no !important. */
15
+ .phepex-deck [hidden] {
16
+ display: none;
17
+ }
18
+
19
+ .phepex-deck-stage {
20
+ display: block;
21
+ width: 100%;
22
+ /* Reserve the box before the first SVG loads, so advancing never shifts the page. */
23
+ aspect-ratio: 16 / 9;
24
+ background: #fff;
25
+ border: 1px solid var(--color-background-border);
26
+ border-radius: 6px;
27
+ /* The deck is keyboard-operable, so it must show where focus is. */
28
+ outline-offset: 2px;
29
+ }
30
+
31
+ .phepex-deck-stage:focus-visible {
32
+ outline: 2px solid var(--color-brand-primary);
33
+ }
34
+
35
+ /* Click-to-advance only makes sense once the script is driving the deck. */
36
+ .phepex-deck[data-ready="1"] .phepex-deck-stage {
37
+ cursor: pointer;
38
+ }
39
+
40
+ .phepex-deck-stage:fullscreen {
41
+ aspect-ratio: auto;
42
+ height: 100%;
43
+ border: 0;
44
+ border-radius: 0;
45
+ object-fit: contain;
46
+ }
47
+
48
+ .phepex-deck-bar {
49
+ display: flex;
50
+ gap: 0.5rem;
51
+ align-items: center;
52
+ justify-content: center;
53
+ margin-top: 0.5rem;
54
+ font-size: var(--font-size--small);
55
+ }
56
+
57
+ .phepex-deck-bar button {
58
+ min-width: 2.2rem;
59
+ padding: 0.15rem 0.5rem;
60
+ color: var(--color-foreground-primary);
61
+ background: var(--color-background-secondary);
62
+ border: 1px solid var(--color-background-border);
63
+ border-radius: 4px;
64
+ cursor: pointer;
65
+ font: inherit;
66
+ line-height: 1.6;
67
+ }
68
+
69
+ .phepex-deck-bar button:hover {
70
+ border-color: var(--color-brand-primary);
71
+ }
72
+
73
+ .phepex-deck-position {
74
+ min-width: 4.5rem;
75
+ text-align: center;
76
+ color: var(--color-foreground-secondary);
77
+ font-variant-numeric: tabular-nums;
78
+ }
79
+
80
+ /* Shown only when no SVGs were exported; the script reveals it in that case. */
81
+ .phepex-deck-unavailable {
82
+ color: var(--color-foreground-secondary);
83
+ font-size: var(--font-size--small);
84
+ }
85
+
86
+ /* The slide outline: the deck's text alternative, collapsed by default so it does not
87
+ * restate the slides to a reader who can see them. A plain <details> rather than a
88
+ * `:class: dropdown` admonition, which furo renders as an ordinary open block, and rather
89
+ * than a sphinx-design directive, which would add a dependency for one disclosure. Closing
90
+ * it hides nothing from Sphinx search or from slides-viewer.js: both read the text from the
91
+ * document, not from what is painted. */
92
+ .phepex-deck-outline-details {
93
+ margin: 1rem 0;
94
+ padding: 0.4rem 0.75rem;
95
+ background: var(--color-background-secondary);
96
+ border: 1px solid var(--color-background-border);
97
+ border-radius: 6px;
98
+ font-size: var(--font-size--small);
99
+ }
100
+
101
+ .phepex-deck-outline-details > summary {
102
+ cursor: pointer;
103
+ font-weight: 600;
104
+ }
105
+
106
+ .phepex-deck-outline-details[open] > summary {
107
+ margin-bottom: 0.5rem;
108
+ }
109
+
110
+ .phepex-deck-outline-details > summary:focus-visible {
111
+ outline: 2px solid var(--color-brand-primary);
112
+ outline-offset: 2px;
113
+ }
@@ -0,0 +1,103 @@
1
+ // In-page viewer for the Typst slide deck rendered on docs/slides.md.
2
+ //
3
+ // Each slide is one SVG file swapped into a single <img>, rather than eight SVGs inlined in
4
+ // the page: Typst names every glyph <symbol id="..."> and those ids recur across slides, so
5
+ // inlining would put duplicate ids in one document (see docs/conf.py). The trade-off is that
6
+ // SVG text is glyph outlines, hence the authored outline list the page carries as the
7
+ // accessible and searchable text alternative; alt text is taken from it here.
8
+ //
9
+ // Loaded on every page, so it must do nothing when there is no deck.
10
+ function phepexInitDeck() {
11
+ "use strict";
12
+
13
+ const deck = document.querySelector(".phepex-deck");
14
+ if (!deck) return;
15
+
16
+ const stage = deck.querySelector(".phepex-deck-stage");
17
+ const bar = deck.querySelector(".phepex-deck-bar");
18
+ const position = deck.querySelector(".phepex-deck-position");
19
+ const note = deck.querySelector(".phepex-deck-unavailable");
20
+ const base = deck.dataset.base || "";
21
+
22
+ // One entry per slide, so the outline that supplies the alt text also fixes the slide
23
+ // count -- the two cannot drift apart here. docs/conf.py checks the list against the
24
+ // exported deck at build time.
25
+ const titles = Array.from(document.querySelectorAll(".phepex-deck-outline li"), (li) =>
26
+ (li.textContent || "").trim().replace(/\s+/g, " "),
27
+ );
28
+ const count = titles.length;
29
+
30
+ function unavailable() {
31
+ if (stage) stage.hidden = true;
32
+ if (bar) bar.hidden = true;
33
+ if (note) note.hidden = false;
34
+ }
35
+
36
+ // A docs build without Typst exports no SVGs, and nothing in the page says so: the markup
37
+ // is static and MyST does not substitute into it. Take the first image failing to load as
38
+ // the signal, which also covers a partial export.
39
+ if (count < 1 || !stage) return unavailable();
40
+ stage.addEventListener("error", unavailable);
41
+
42
+ const src = (i) => `${base}${i + 1}.svg`;
43
+ let current = -1;
44
+
45
+ function show(index, focus) {
46
+ const i = ((index % count) + count) % count;
47
+ if (i === current) return;
48
+ current = i;
49
+ stage.src = src(i);
50
+ const title = titles[i];
51
+ stage.alt = title
52
+ ? `Slide ${i + 1} of ${count}: ${title}`
53
+ : `Slide ${i + 1} of ${count}`;
54
+ if (position) position.textContent = `${i + 1} / ${count}`;
55
+ // replaceState, not pushState: paging through the deck must not fill the back button.
56
+ history.replaceState(null, "", `#slide-${i + 1}`);
57
+ if (focus) stage.focus();
58
+ // Warm the next slide so a click or arrow key paints without a fetch.
59
+ if (count > 1) new Image().src = src((i + 1) % count);
60
+ }
61
+
62
+ stage.addEventListener("click", () => show(current + 1, false));
63
+
64
+ stage.addEventListener("keydown", (event) => {
65
+ if (event.altKey || event.ctrlKey || event.metaKey) return;
66
+ const step = { ArrowRight: 1, ArrowLeft: -1, PageDown: 1, PageUp: -1 }[event.key];
67
+ if (step !== undefined) show(current + step, false);
68
+ else if (event.key === "Home") show(0, false);
69
+ else if (event.key === "End") show(count - 1, false);
70
+ else return; // leave every other key to the page (furo binds "/" to search)
71
+ event.preventDefault();
72
+ });
73
+
74
+ deck.querySelector(".phepex-deck-prev")?.addEventListener("click", () => {
75
+ show(current - 1, true);
76
+ });
77
+ deck.querySelector(".phepex-deck-next")?.addEventListener("click", () => {
78
+ show(current + 1, true);
79
+ });
80
+ deck.querySelector(".phepex-deck-fullscreen")?.addEventListener("click", () => {
81
+ if (document.fullscreenElement) document.exitFullscreen?.();
82
+ else stage.requestFullscreen?.();
83
+ });
84
+
85
+ // Deep link: #slide-3 opens on that slide, and the hash stays shareable.
86
+ const fromHash = () => {
87
+ const match = /^#slide-(\d+)$/.exec(location.hash);
88
+ return match ? parseInt(match[1], 10) - 1 : 0;
89
+ };
90
+ window.addEventListener("hashchange", () => show(fromHash(), false));
91
+
92
+ if (bar) bar.hidden = false;
93
+ deck.dataset.ready = "1";
94
+ show(fromHash(), false);
95
+ }
96
+
97
+ // furo emits html_js_files at the end of <body>, so the deck is normally parsed by now; the
98
+ // guard matches version-switcher.js and keeps this correct if that ever changes.
99
+ if (document.readyState === "loading") {
100
+ document.addEventListener("DOMContentLoaded", phepexInitDeck);
101
+ } else {
102
+ phepexInitDeck();
103
+ }