obdeect 0.6.0__tar.gz → 0.7.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.
- {obdeect-0.6.0 → obdeect-0.7.0}/.github/workflows/ci.yml +1 -3
- {obdeect-0.6.0 → obdeect-0.7.0}/.github/workflows/pypi.yml +1 -1
- {obdeect-0.6.0 → obdeect-0.7.0}/.github/workflows/tests.yml +1 -1
- {obdeect-0.6.0 → obdeect-0.7.0}/AGENTS.md +39 -8
- {obdeect-0.6.0 → obdeect-0.7.0}/PKG-INFO +6 -4
- {obdeect-0.6.0 → obdeect-0.7.0}/cpp/bindings/core.cpp +17 -9
- {obdeect-0.6.0 → obdeect-0.7.0}/cpp/include/obdeect/bunches.hpp +87 -25
- {obdeect-0.6.0 → obdeect-0.7.0}/cpp/include/obdeect/model.hpp +3 -0
- {obdeect-0.6.0 → obdeect-0.7.0}/cpp/include/obdeect/model_json.hpp +8 -1
- {obdeect-0.6.0 → obdeect-0.7.0}/cpp/tests/test_model_json.cpp +83 -0
- {obdeect-0.6.0 → obdeect-0.7.0}/cpp/tests/test_trace.cpp +64 -1
- {obdeect-0.6.0 → obdeect-0.7.0}/docs/VALIDATION.md +41 -0
- obdeect-0.7.0/docs/air-shower-photons.md +103 -0
- {obdeect-0.6.0 → obdeect-0.7.0}/docs/batch-tracing.md +48 -6
- obdeect-0.7.0/docs/changes/27.feature.md +1 -0
- obdeect-0.7.0/docs/changes/28.maintenance.md +1 -0
- {obdeect-0.6.0 → obdeect-0.7.0}/docs/installation.md +5 -0
- {obdeect-0.6.0 → obdeect-0.7.0}/docs/optical-models.md +19 -1
- {obdeect-0.6.0 → obdeect-0.7.0}/docs/usage.md +2 -1
- {obdeect-0.6.0 → obdeect-0.7.0}/docs/user-guide.md +1 -0
- {obdeect-0.6.0 → obdeect-0.7.0}/pyproject.toml +4 -3
- {obdeect-0.6.0 → obdeect-0.7.0}/python/obdeect/cli.py +6 -1
- {obdeect-0.6.0 → obdeect-0.7.0}/python/obdeect/compiler.py +38 -0
- {obdeect-0.6.0 → obdeect-0.7.0}/python/obdeect/model.py +150 -10
- obdeect-0.7.0/python/tests/test_axes_offset.py +199 -0
- {obdeect-0.6.0 → obdeect-0.7.0}/python/tests/test_bunches.py +322 -4
- {obdeect-0.6.0 → obdeect-0.7.0}/python/tests/test_docs_versions.py +15 -0
- {obdeect-0.6.0 → obdeect-0.7.0}/python/tests/test_model.py +16 -2
- {obdeect-0.6.0 → obdeect-0.7.0}/.clang-format +0 -0
- {obdeect-0.6.0 → obdeect-0.7.0}/.clang-tidy +0 -0
- {obdeect-0.6.0 → obdeect-0.7.0}/.editorconfig +0 -0
- {obdeect-0.6.0 → obdeect-0.7.0}/.gitattributes +0 -0
- {obdeect-0.6.0 → obdeect-0.7.0}/.github/workflows/docs.yml +0 -0
- {obdeect-0.6.0 → obdeect-0.7.0}/.gitignore +0 -0
- {obdeect-0.6.0 → obdeect-0.7.0}/.pre-commit-config.yaml +0 -0
- {obdeect-0.6.0 → obdeect-0.7.0}/CHANGELOG.md +0 -0
- {obdeect-0.6.0 → obdeect-0.7.0}/CITATION.cff +0 -0
- {obdeect-0.6.0 → obdeect-0.7.0}/CMakeLists.txt +0 -0
- {obdeect-0.6.0 → obdeect-0.7.0}/CMakePresets.json +0 -0
- {obdeect-0.6.0 → obdeect-0.7.0}/LICENSE +0 -0
- {obdeect-0.6.0 → obdeect-0.7.0}/README.md +0 -0
- {obdeect-0.6.0 → obdeect-0.7.0}/cpp/bench/bench_trace.cpp +0 -0
- {obdeect-0.6.0 → obdeect-0.7.0}/cpp/bench/replay.cpp +0 -0
- {obdeect-0.6.0 → obdeect-0.7.0}/cpp/bench/replay_compare.hpp +0 -0
- {obdeect-0.6.0 → obdeect-0.7.0}/cpp/bindings/README.md +0 -0
- {obdeect-0.6.0 → obdeect-0.7.0}/cpp/include/obdeect/bounds.hpp +0 -0
- {obdeect-0.6.0 → obdeect-0.7.0}/cpp/include/obdeect/json.hpp +0 -0
- {obdeect-0.6.0 → obdeect-0.7.0}/cpp/include/obdeect/obscurers.hpp +0 -0
- {obdeect-0.6.0 → obdeect-0.7.0}/cpp/include/obdeect/random.hpp +0 -0
- {obdeect-0.6.0 → obdeect-0.7.0}/cpp/include/obdeect/source.hpp +0 -0
- {obdeect-0.6.0 → obdeect-0.7.0}/cpp/include/obdeect/surfaces.hpp +0 -0
- {obdeect-0.6.0 → obdeect-0.7.0}/cpp/include/obdeect/table.hpp +0 -0
- {obdeect-0.6.0 → obdeect-0.7.0}/cpp/include/obdeect/trace.hpp +0 -0
- {obdeect-0.6.0 → obdeect-0.7.0}/cpp/include/obdeect/vec.hpp +0 -0
- {obdeect-0.6.0 → obdeect-0.7.0}/cpp/tests/check.hpp +0 -0
- {obdeect-0.6.0 → obdeect-0.7.0}/cpp/tests/data/make_polynomial_reference.py +0 -0
- {obdeect-0.6.0 → obdeect-0.7.0}/cpp/tests/legacy_asphere.hpp +0 -0
- {obdeect-0.6.0 → obdeect-0.7.0}/cpp/tests/test_asphere_steps.cpp +0 -0
- {obdeect-0.6.0 → obdeect-0.7.0}/cpp/tests/test_bounds.cpp +0 -0
- {obdeect-0.6.0 → obdeect-0.7.0}/cpp/tests/test_kernels.cpp +0 -0
- {obdeect-0.6.0 → obdeect-0.7.0}/cpp/tests/test_replay_compare.cpp +0 -0
- {obdeect-0.6.0 → obdeect-0.7.0}/docs/_static/figures/baffle_area.png +0 -0
- {obdeect-0.6.0 → obdeect-0.7.0}/docs/_static/figures/mst_shadowing.png +0 -0
- {obdeect-0.6.0 → obdeect-0.7.0}/docs/_static/figures/telescope_lst.png +0 -0
- {obdeect-0.6.0 → obdeect-0.7.0}/docs/_static/figures/telescope_mst_flashcam.png +0 -0
- {obdeect-0.6.0 → obdeect-0.7.0}/docs/_static/figures/telescope_mst_nectarcam.png +0 -0
- {obdeect-0.6.0 → obdeect-0.7.0}/docs/_static/figures/telescope_sct.png +0 -0
- {obdeect-0.6.0 → obdeect-0.7.0}/docs/_static/figures/telescope_sst.png +0 -0
- {obdeect-0.6.0 → obdeect-0.7.0}/docs/_static/figures/telescopes-provenance.json +0 -0
- {obdeect-0.6.0 → obdeect-0.7.0}/docs/_static/figures/validation_psf_area.png +0 -0
- {obdeect-0.6.0 → obdeect-0.7.0}/docs/_static/images/obdeect_21ratio_large.jpg +0 -0
- {obdeect-0.6.0 → obdeect-0.7.0}/docs/_static/images/obdeect_21ratio_small.jpg +0 -0
- {obdeect-0.6.0 → obdeect-0.7.0}/docs/_static/images/obdeect_large.jpg +0 -0
- {obdeect-0.6.0 → obdeect-0.7.0}/docs/_static/images/obdeect_logo.jpg +0 -0
- {obdeect-0.6.0 → obdeect-0.7.0}/docs/_static/images/obdeect_small.jpg +0 -0
- {obdeect-0.6.0 → obdeect-0.7.0}/docs/_static/obdeect.css +0 -0
- {obdeect-0.6.0 → obdeect-0.7.0}/docs/api.md +0 -0
- {obdeect-0.6.0 → obdeect-0.7.0}/docs/build_versions.py +0 -0
- {obdeect-0.6.0 → obdeect-0.7.0}/docs/changelog.md +0 -0
- {obdeect-0.6.0 → obdeect-0.7.0}/docs/changes/.gitkeep +0 -0
- {obdeect-0.6.0 → obdeect-0.7.0}/docs/conf.py +0 -0
- {obdeect-0.6.0 → obdeect-0.7.0}/docs/figures/make_figures.py +0 -0
- {obdeect-0.6.0 → obdeect-0.7.0}/docs/figures/telescopes.yml +0 -0
- {obdeect-0.6.0 → obdeect-0.7.0}/docs/index.md +0 -0
- {obdeect-0.6.0 → obdeect-0.7.0}/docs/light-sources.md +0 -0
- {obdeect-0.6.0 → obdeect-0.7.0}/docs/limitations.md +0 -0
- {obdeect-0.6.0 → obdeect-0.7.0}/docs/optical-elements.md +0 -0
- {obdeect-0.6.0 → obdeect-0.7.0}/docs/optics.md +0 -0
- {obdeect-0.6.0 → obdeect-0.7.0}/docs/python-guide.md +0 -0
- {obdeect-0.6.0 → obdeect-0.7.0}/docs/shadowing.md +0 -0
- {obdeect-0.6.0 → obdeect-0.7.0}/docs/telescopes.md +0 -0
- {obdeect-0.6.0 → obdeect-0.7.0}/python/obdeect/__init__.py +0 -0
- {obdeect-0.6.0 → obdeect-0.7.0}/python/obdeect/formats.py +0 -0
- {obdeect-0.6.0 → obdeect-0.7.0}/python/obdeect/plot.py +0 -0
- {obdeect-0.6.0 → obdeect-0.7.0}/python/obdeect/propagation.py +0 -0
- {obdeect-0.6.0 → obdeect-0.7.0}/python/obdeect/psf.py +0 -0
- {obdeect-0.6.0 → obdeect-0.7.0}/python/obdeect/repository.py +0 -0
- {obdeect-0.6.0 → obdeect-0.7.0}/python/obdeect/simtel_tables.py +0 -0
- {obdeect-0.6.0 → obdeect-0.7.0}/python/tests/telescopes.py +0 -0
- {obdeect-0.6.0 → obdeect-0.7.0}/python/tests/test_compiler.py +0 -0
- {obdeect-0.6.0 → obdeect-0.7.0}/python/tests/test_formats.py +0 -0
- {obdeect-0.6.0 → obdeect-0.7.0}/python/tests/test_propagation.py +0 -0
|
@@ -22,11 +22,9 @@ jobs:
|
|
|
22
22
|
with:
|
|
23
23
|
python-version: '3.14'
|
|
24
24
|
- name: Install linters
|
|
25
|
-
run: python -m pip install --upgrade pip pre-commit
|
|
25
|
+
run: python -m pip install --upgrade pip pre-commit clang-format
|
|
26
26
|
- name: Run repository hooks
|
|
27
27
|
run: pre-commit run --all-files
|
|
28
|
-
- name: Check Python formatting and lint
|
|
29
|
-
run: ruff format --check python && ruff check python
|
|
30
28
|
- name: Check C++ formatting
|
|
31
29
|
run: >-
|
|
32
30
|
find cpp -type f \( -name '*.cpp' -o -name '*.hpp' \) -print0
|
|
@@ -36,7 +36,7 @@ jobs:
|
|
|
36
36
|
- name: Build and smoke-test wheels
|
|
37
37
|
env:
|
|
38
38
|
CIBW_BUILD: "cp314-*"
|
|
39
|
-
CIBW_TEST_REQUIRES: "pytest
|
|
39
|
+
CIBW_TEST_REQUIRES: "pytest matplotlib"
|
|
40
40
|
CIBW_TEST_COMMAND: python -m pytest {project}/python/tests -q
|
|
41
41
|
run: python -m cibuildwheel --output-dir wheelhouse
|
|
42
42
|
- uses: actions/upload-artifact@v7
|
|
@@ -27,7 +27,7 @@ jobs:
|
|
|
27
27
|
with:
|
|
28
28
|
python-version: '3.14'
|
|
29
29
|
- name: Install Python package and test dependencies
|
|
30
|
-
run: python -m pip install --upgrade pip && python -m pip install . pytest
|
|
30
|
+
run: python -m pip install --upgrade pip && python -m pip install ".[plot]" pytest
|
|
31
31
|
- name: Configure and build
|
|
32
32
|
run: cmake -S . -B build -DCMAKE_BUILD_TYPE=Release && cmake --build build --parallel
|
|
33
33
|
- name: Run native and Python tests
|
|
@@ -5,8 +5,20 @@
|
|
|
5
5
|
`obdeect` is a portable C++20 optical ray tracer with a thin Python interface.
|
|
6
6
|
It traces photons from a defined entrance surface to a focal/sensor surface.
|
|
7
7
|
Air-shower generation, electronics, triggers, and event writing are out of
|
|
8
|
-
scope. Read the relevant code and tests first; use
|
|
9
|
-
|
|
8
|
+
scope. Read the relevant code and tests first; use `docs/VALIDATION.md` and
|
|
9
|
+
`docs/optical-models.md` for architectural or scientific changes.
|
|
10
|
+
|
|
11
|
+
## Layout
|
|
12
|
+
|
|
13
|
+
- `cpp/include/obdeect/`: C++ core headers (kernels, optical models, public API).
|
|
14
|
+
- `cpp/bindings/core.cpp`: nanobind extension; the only C++ file that may
|
|
15
|
+
depend on Python.
|
|
16
|
+
- `cpp/tests/`: native tests (`test_*.cpp`), shared helpers (`check.hpp`),
|
|
17
|
+
and fixtures in `cpp/tests/data/`.
|
|
18
|
+
- `cpp/bench/`: benchmarks and the replay comparison tool.
|
|
19
|
+
- `python/obdeect/`: Python boundary (compile, trace, formats, plotting, CLI).
|
|
20
|
+
- `python/tests/`: pytest suite; `telescopes.py` holds shared test helpers.
|
|
21
|
+
- `docs/`: Sphinx sources. `docs/changes/` is towncrier input (see below).
|
|
10
22
|
|
|
11
23
|
## Design rules
|
|
12
24
|
|
|
@@ -34,7 +46,7 @@ validation matrix for architectural or scientific changes.
|
|
|
34
46
|
- CTAO support is an explicit importer/adapter or versioned fixture only. It
|
|
35
47
|
must not be required to build, test, or run the generic engine, and it must
|
|
36
48
|
never be an implicit fallback.
|
|
37
|
-
- Use
|
|
49
|
+
- Use Python >=3.14 (as in `pyproject.toml`). Do not fall back to earlier versions.
|
|
38
50
|
|
|
39
51
|
## Changes and validation
|
|
40
52
|
|
|
@@ -55,9 +67,11 @@ validation matrix for architectural or scientific changes.
|
|
|
55
67
|
## Build, test, and lint
|
|
56
68
|
|
|
57
69
|
Run from this directory. Configure before static analysis so the compilation
|
|
58
|
-
database is current.
|
|
70
|
+
database is current. The `debug` preset enables `OBDEECT_BUILD_TESTING` and
|
|
71
|
+
writes to `build/debug/`; wheels build with `OBDEECT_BUILD_TESTING=OFF`.
|
|
59
72
|
|
|
60
73
|
```bash
|
|
74
|
+
python -m pip install -e '.[dev]' # editable install; rebuilds the extension on C++ changes
|
|
61
75
|
cmake --preset debug
|
|
62
76
|
cmake --build --preset debug
|
|
63
77
|
ctest --test-dir build/debug --output-on-failure
|
|
@@ -77,7 +91,24 @@ the focused tests before the full suite. A clean handoff requires both
|
|
|
77
91
|
`ruff format --check python` and `ruff check python`; do not rely on a passing
|
|
78
92
|
test run alone because stale test references can fail collection or linting.
|
|
79
93
|
|
|
80
|
-
Run `clang-tidy -p build/debug
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
94
|
+
Run `clang-tidy -p build/debug <changed-file>` for changed C++ translation units
|
|
95
|
+
(`cpp/tests/`, `cpp/bench/`, `cpp/bindings/`, or `cpp/include/obdeect/` headers)
|
|
96
|
+
when available, and `pre-commit run --all-files` before handoff. Pre-commit runs
|
|
97
|
+
ruff, codespell, markdownlint, yamllint, actionlint, pyproject-fmt, shellcheck,
|
|
98
|
+
and the towncrier check. Start with the smallest relevant test; run the full
|
|
99
|
+
checks for shared kernels, interfaces, configuration, or tooling changes.
|
|
100
|
+
|
|
101
|
+
CI also builds and tests a `-DOBDEECT_USE_FMA=ON` Release configuration and an
|
|
102
|
+
AddressSanitizer/UBSan Debug configuration. Run those locally for changes to
|
|
103
|
+
floating-point kernels or memory-handling code:
|
|
104
|
+
|
|
105
|
+
```bash
|
|
106
|
+
cmake -S . -B build-sanitize -DCMAKE_BUILD_TYPE=Debug \
|
|
107
|
+
-DCMAKE_CXX_FLAGS="-fsanitize=address,undefined -fno-omit-frame-pointer" \
|
|
108
|
+
-DCMAKE_EXE_LINKER_FLAGS="-fsanitize=address,undefined"
|
|
109
|
+
cmake --build build-sanitize --parallel
|
|
110
|
+
ctest --test-dir build-sanitize --output-on-failure
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
Docs build with warnings as errors: `sphinx-build -W --keep-going -b html docs docs/_build/html`.
|
|
114
|
+
Regenerate figures only from new validation results, via `docs/figures/make_figures.py`.
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.2
|
|
2
2
|
Name: obdeect
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.7.0
|
|
4
4
|
Summary: Ray tracing for imaging atmospheric Cherenkov telescopes
|
|
5
5
|
Author: obdeect contributors
|
|
6
6
|
License: BSD 3-Clause License
|
|
@@ -39,21 +39,23 @@ Classifier: Programming Language :: C++
|
|
|
39
39
|
Classifier: Programming Language :: Python :: 3 :: Only
|
|
40
40
|
Classifier: Programming Language :: Python :: 3.14
|
|
41
41
|
Requires-Python: >=3.14
|
|
42
|
-
Requires-Dist: matplotlib>=3.8
|
|
43
42
|
Requires-Dist: numpy>=2.3
|
|
44
43
|
Provides-Extra: dev
|
|
45
44
|
Requires-Dist: nanobind>=3.1; extra == "dev"
|
|
46
45
|
Requires-Dist: pre-commit; extra == "dev"
|
|
47
46
|
Requires-Dist: pytest; extra == "dev"
|
|
48
47
|
Requires-Dist: ruff; extra == "dev"
|
|
49
|
-
Requires-Dist: scipy; extra == "dev"
|
|
50
48
|
Provides-Extra: docs
|
|
51
|
-
Requires-Dist: astropy; extra == "docs"
|
|
52
49
|
Requires-Dist: myst-parser; extra == "docs"
|
|
53
50
|
Requires-Dist: pydata-sphinx-theme; extra == "docs"
|
|
54
51
|
Requires-Dist: pyyaml; extra == "docs"
|
|
55
52
|
Requires-Dist: sphinx; extra == "docs"
|
|
56
53
|
Requires-Dist: sphinx-design; extra == "docs"
|
|
54
|
+
Provides-Extra: figures
|
|
55
|
+
Requires-Dist: astropy; extra == "figures"
|
|
56
|
+
Requires-Dist: matplotlib>=3.8; extra == "figures"
|
|
57
|
+
Provides-Extra: plot
|
|
58
|
+
Requires-Dist: matplotlib>=3.8; extra == "plot"
|
|
57
59
|
Description-Content-Type: text/markdown
|
|
58
60
|
|
|
59
61
|
# obdeect
|
|
@@ -189,14 +189,14 @@ public:
|
|
|
189
189
|
|
|
190
190
|
// Photon bunches (see obdeect::trace_bunches). Acceptance and density are
|
|
191
191
|
// given as arrays; the output arrays are views of the rows written.
|
|
192
|
-
nb::dict
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
192
|
+
nb::dict trace_bunches(
|
|
193
|
+
In1<float> x, In1<float> y, In1<float> cx, In1<float> cy, In1<float> time_ns, In1<float> zem,
|
|
194
|
+
In1<float> size, In1<float> wavelength_nm, In1<std::uint64_t> bunch_id, In1<double> rotation,
|
|
195
|
+
In1<double> offset_m, double length_unit_m, double plane_z, std::uint64_t seed,
|
|
196
|
+
const std::optional<std::tuple<In1<double>, In1<double>, In1<double>>> &acceptance,
|
|
197
|
+
const std::optional<std::tuple<In1<double>, In1<double>>> &density,
|
|
198
|
+
const std::optional<std::tuple<double, double>> &wavelength_law, bool roulette,
|
|
199
|
+
bool classify_losses, bool normalise_transmission, obdeect::ThreadPool &pool) const {
|
|
200
200
|
const std::size_t n = x.shape(0);
|
|
201
201
|
for (const std::size_t length :
|
|
202
202
|
{y.shape(0), cx.shape(0), cy.shape(0), time_ns.shape(0), zem.shape(0), size.shape(0),
|
|
@@ -213,9 +213,11 @@ public:
|
|
|
213
213
|
|
|
214
214
|
obdeect::Map2D acceptance_map;
|
|
215
215
|
obdeect::LinearDensity wavelength_density;
|
|
216
|
+
obdeect::InverseSquareLaw law;
|
|
216
217
|
obdeect::BunchOptions options;
|
|
217
218
|
options.roulette = roulette;
|
|
218
219
|
options.classify_losses = classify_losses;
|
|
220
|
+
options.normalise_transmission = normalise_transmission;
|
|
219
221
|
try {
|
|
220
222
|
if (acceptance) {
|
|
221
223
|
const auto &[wavelengths, heights, values] = *acceptance;
|
|
@@ -234,6 +236,11 @@ public:
|
|
|
234
236
|
{weights.data(), weights.data() + weights.shape(0)});
|
|
235
237
|
options.wavelength_density = &wavelength_density;
|
|
236
238
|
}
|
|
239
|
+
if (wavelength_law) {
|
|
240
|
+
const auto &[lo_nm, hi_nm] = *wavelength_law;
|
|
241
|
+
law = {lo_nm, hi_nm};
|
|
242
|
+
options.wavelength_law = &law;
|
|
243
|
+
}
|
|
237
244
|
} catch (const std::invalid_argument &error) {
|
|
238
245
|
throw nb::value_error(error.what());
|
|
239
246
|
}
|
|
@@ -501,7 +508,8 @@ NB_MODULE(_core, module) {
|
|
|
501
508
|
.def("trace_bunches", &Model::trace_bunches, "x"_a, "y"_a, "cx"_a, "cy"_a, "time_ns"_a,
|
|
502
509
|
"zem"_a, "size"_a, "wavelength_nm"_a, "bunch_id"_a, "rotation"_a, "offset_m"_a,
|
|
503
510
|
"length_unit_m"_a, "plane_z"_a, nb::kw_only(), "seed"_a, "acceptance"_a = nb::none(),
|
|
504
|
-
"wavelength_density"_a = nb::none(), "
|
|
511
|
+
"wavelength_density"_a = nb::none(), "wavelength_law"_a = nb::none(),
|
|
512
|
+
"roulette"_a = true, "classify_losses"_a = true, "normalise_transmission"_a = false,
|
|
505
513
|
"pool"_a)
|
|
506
514
|
.def("obscurer_crossings", &Model::obscurer_crossings, "start"_a, "end"_a, "leg"_a);
|
|
507
515
|
module.def("trace_many", &trace_many, "items"_a, nb::kw_only(), "seed"_a, "pool"_a);
|
|
@@ -33,6 +33,13 @@ struct BunchFrame {
|
|
|
33
33
|
double plane_z{}; // Input units.
|
|
34
34
|
};
|
|
35
35
|
|
|
36
|
+
// Wavelength of bunches with wavelength 0: density proportional to 1/lambda^2
|
|
37
|
+
// on [lo_nm, hi_nm], truncated to integer nm.
|
|
38
|
+
struct InverseSquareLaw {
|
|
39
|
+
double lo_nm{};
|
|
40
|
+
double hi_nm{};
|
|
41
|
+
};
|
|
42
|
+
|
|
36
43
|
struct BunchOptions {
|
|
37
44
|
// Acceptance outside the telescope as a function of wavelength [nm] (x) and
|
|
38
45
|
// emission height [m] (y), values in [0, 1]; null means 1.
|
|
@@ -40,6 +47,10 @@ struct BunchOptions {
|
|
|
40
47
|
const LinearDensity *wavelength_density{}; // Over nm, for bunches of wavelength 0.
|
|
41
48
|
bool roulette{true}; // Survivors get weight 1 (else the weight ratio).
|
|
42
49
|
bool classify_losses{true};
|
|
50
|
+
// Roulette weights and bound relative to the telescope transmission's on-axis value.
|
|
51
|
+
bool normalise_transmission{false};
|
|
52
|
+
// Alternative to wavelength_density for bunches of wavelength 0 (not both).
|
|
53
|
+
const InverseSquareLaw *wavelength_law{};
|
|
43
54
|
};
|
|
44
55
|
|
|
45
56
|
// Output columns, with room for the rows the allocator was asked for. Vectors
|
|
@@ -102,8 +113,9 @@ public:
|
|
|
102
113
|
const double size = in_.size[i];
|
|
103
114
|
const double lambda = in_.wavelength_nm[i];
|
|
104
115
|
if (!(size > 0.0 && size <= kMaxBunchSize) || !std::isfinite(lambda) || lambda < 0.0 ||
|
|
105
|
-
(lambda == 0.0 && !options_.wavelength_density
|
|
106
|
-
!std::isfinite(in_.
|
|
116
|
+
(lambda == 0.0 && !options_.wavelength_density && !options_.wavelength_law) ||
|
|
117
|
+
!std::isfinite(in_.x[i]) || !std::isfinite(in_.y[i]) || !std::isfinite(in_.cx[i]) ||
|
|
118
|
+
!std::isfinite(in_.cy[i]) ||
|
|
107
119
|
!(static_cast<double>(in_.cx[i]) * in_.cx[i] +
|
|
108
120
|
static_cast<double>(in_.cy[i]) * in_.cy[i] <=
|
|
109
121
|
1.0 + 1e-6) ||
|
|
@@ -117,30 +129,40 @@ public:
|
|
|
117
129
|
|
|
118
130
|
// Trace the bunches [begin, end), writing rows from `row` on; returns the
|
|
119
131
|
// number of rows written.
|
|
120
|
-
std::size_t run(std::size_t begin, std::size_t end, const BunchResult &out,
|
|
121
|
-
std::
|
|
132
|
+
std::size_t run(std::size_t begin, std::size_t end, const BunchResult &out, std::size_t row,
|
|
133
|
+
std::uint8_t *keep_flags, std::vector<double> &roulette_draws,
|
|
134
|
+
bool thinning) const {
|
|
122
135
|
const std::size_t first = row;
|
|
123
136
|
const double u = frame_.length_unit_m;
|
|
124
137
|
const double *r = frame_.rotation;
|
|
125
138
|
// Position and direction in the telescope frame, computed for the first
|
|
126
139
|
// photon of a bunch that survives the thinning.
|
|
140
|
+
// The model's axes offset O displaces the optical origin: P is traced at R P - O.
|
|
141
|
+
const auto &axes = model_.axes_offset_m;
|
|
127
142
|
const auto transform = [&](std::size_t i) {
|
|
128
143
|
const double x = u * in_.x[i], y = u * in_.y[i], z = u * frame_.plane_z;
|
|
129
144
|
const double cx = in_.cx[i], cy = in_.cy[i];
|
|
130
145
|
const double cz = -std::sqrt(std::max(0.0, 1.0 - cx * cx - cy * cy));
|
|
131
|
-
return Ray{{r[0] * x + r[1] * y + r[2] * z + frame_.offset_m[0],
|
|
132
|
-
r[3] * x + r[4] * y + r[5] * z + frame_.offset_m[1],
|
|
133
|
-
r[6] * x + r[7] * y + r[8] * z + frame_.offset_m[2]},
|
|
146
|
+
return Ray{{r[0] * x + r[1] * y + r[2] * z + frame_.offset_m[0] - axes[0],
|
|
147
|
+
r[3] * x + r[4] * y + r[5] * z + frame_.offset_m[1] - axes[1],
|
|
148
|
+
r[6] * x + r[7] * y + r[8] * z + frame_.offset_m[2] - axes[2]},
|
|
134
149
|
{r[0] * cx + r[1] * cy + r[2] * cz, r[3] * cx + r[4] * cy + r[5] * cz,
|
|
135
150
|
r[6] * cx + r[7] * cy + r[8] * cz}};
|
|
136
151
|
};
|
|
137
|
-
//
|
|
138
|
-
//
|
|
152
|
+
// Divisor of the bound and of the roulette weight: the on-axis transmission,
|
|
153
|
+
// if normalised. The telescope transmission enters the photon weights as
|
|
154
|
+
// t(theta) / t0 because sim_telarray already applies t0 in its bunch loop.
|
|
155
|
+
const double norm = options_.normalise_transmission && model_.telescope_transmission &&
|
|
156
|
+
model_.telescope_transmission->on_axis > 0.0
|
|
157
|
+
? model_.telescope_transmission->on_axis
|
|
158
|
+
: 1.0;
|
|
159
|
+
// Bound (relative to norm) and acceptance of the last wavelength (and
|
|
160
|
+
// height): showers have many bunches of one wavelength.
|
|
139
161
|
double cached_lambda = -1.0, cached_zem = 0.0, cached_bound = 0.0, cached_acceptance = 1.0;
|
|
140
162
|
const auto response = [&](double lambda, double zem_m) {
|
|
141
163
|
if (lambda != cached_lambda) {
|
|
142
164
|
cached_lambda = lambda;
|
|
143
|
-
cached_bound = response_bound(model_, lambda);
|
|
165
|
+
cached_bound = response_bound(model_, lambda) / norm;
|
|
144
166
|
cached_zem = std::numeric_limits<double>::quiet_NaN();
|
|
145
167
|
}
|
|
146
168
|
if (zem_m != cached_zem) {
|
|
@@ -148,6 +170,7 @@ public:
|
|
|
148
170
|
cached_acceptance = options_.acceptance ? options_.acceptance->at(lambda, zem_m) : 1.0;
|
|
149
171
|
}
|
|
150
172
|
};
|
|
173
|
+
std::size_t candidate = 0, survivor = 0;
|
|
151
174
|
for (std::size_t i = begin; i < end; ++i) {
|
|
152
175
|
const double zem_m = u * in_.zem[i];
|
|
153
176
|
const double size = in_.size[i], time = in_.time_ns[i];
|
|
@@ -161,17 +184,36 @@ public:
|
|
|
161
184
|
Ray ray;
|
|
162
185
|
for (std::size_t k = 0; k < n_sub; ++k) {
|
|
163
186
|
const auto sub = static_cast<std::uint32_t>(k);
|
|
187
|
+
const std::size_t flag = candidate++;
|
|
188
|
+
if (!thinning && !keep_flags[flag])
|
|
189
|
+
continue;
|
|
164
190
|
const double frac = static_cast<double>(k + 1) <= size ? 1.0 : size - std::floor(size);
|
|
165
191
|
if (!fixed) {
|
|
166
|
-
|
|
167
|
-
|
|
192
|
+
if (options_.wavelength_law) {
|
|
193
|
+
// Inverse transform of 1/lambda^2; u in (0, 1), so the value lies in (lo, hi).
|
|
194
|
+
const auto &law = *options_.wavelength_law;
|
|
195
|
+
const double u = uniform4(seed_, id, Stream::wavelength, sub)[0];
|
|
196
|
+
const double exact = 1.0 / (1.0 / law.lo_nm - u * (1.0 / law.lo_nm - 1.0 / law.hi_nm));
|
|
197
|
+
// Truncation to integer nm as sim_telarray does: lambda in [floor(lo), hi).
|
|
198
|
+
lambda = std::floor(exact);
|
|
199
|
+
} else {
|
|
200
|
+
lambda = options_.wavelength_density->sample(
|
|
201
|
+
uniform4(seed_, id, Stream::wavelength, sub)[0]);
|
|
202
|
+
}
|
|
168
203
|
response(lambda, zem_m);
|
|
169
204
|
}
|
|
170
205
|
const double bound = cached_bound;
|
|
171
206
|
const double keep = frac * cached_acceptance * bound;
|
|
172
|
-
|
|
173
|
-
|
|
207
|
+
if (thinning) {
|
|
208
|
+
const auto draw = uniform4(seed_, id, Stream::thinning, sub);
|
|
209
|
+
keep_flags[flag] = draw[0] < keep;
|
|
210
|
+
if (keep_flags[flag]) {
|
|
211
|
+
roulette_draws.push_back(draw[1]);
|
|
212
|
+
++row;
|
|
213
|
+
}
|
|
174
214
|
continue;
|
|
215
|
+
}
|
|
216
|
+
const double roulette_draw = roulette_draws[survivor++];
|
|
175
217
|
if (!placed) {
|
|
176
218
|
ray = transform(i);
|
|
177
219
|
placed = true;
|
|
@@ -180,8 +222,8 @@ public:
|
|
|
180
222
|
options_.classify_losses, sub);
|
|
181
223
|
if (t.status != Status::detected)
|
|
182
224
|
continue;
|
|
183
|
-
const double ratio = std::min(1.0, t.weight /
|
|
184
|
-
if (options_.roulette && !(
|
|
225
|
+
const double ratio = std::min(1.0, (t.weight / norm) / cached_bound);
|
|
226
|
+
if (options_.roulette && !(roulette_draw < ratio))
|
|
185
227
|
continue;
|
|
186
228
|
out.position_m[3 * row] = t.position.x;
|
|
187
229
|
out.position_m[3 * row + 1] = t.position.y;
|
|
@@ -219,12 +261,12 @@ private:
|
|
|
219
261
|
// unique per seed.
|
|
220
262
|
//
|
|
221
263
|
// Invalid input (a non-orthonormal frame, acceptance outside [0, 1], a bunch of
|
|
222
|
-
// wavelength 0 without a density, bad sizes) throws std::invalid_argument
|
|
264
|
+
// wavelength 0 without a density or law, bad sizes) throws std::invalid_argument
|
|
223
265
|
// before any tracing; so does a transmission without a known maximum
|
|
224
266
|
// (ModelError).
|
|
225
267
|
//
|
|
226
|
-
// `allocate(rows)` is called once, after validation, with the
|
|
227
|
-
// of
|
|
268
|
+
// `allocate(rows)` is called once, after validation, with the number
|
|
269
|
+
// of thinning survivors, and returns the output columns for that many
|
|
228
270
|
// rows. Returns the rows written; they come first, in input order, bunch by
|
|
229
271
|
// bunch and sub-photon by sub-photon.
|
|
230
272
|
template <class Allocate>
|
|
@@ -236,6 +278,15 @@ std::size_t trace_bunches(const OpticalModel &model, const BunchBlock &in, const
|
|
|
236
278
|
detail::validate_acceptance(*options.acceptance);
|
|
237
279
|
if (options.wavelength_density && !(options.wavelength_density->lower() > 0.0))
|
|
238
280
|
throw std::invalid_argument("bunch wavelength density: nodes must start above 0 nm");
|
|
281
|
+
if (options.wavelength_law) {
|
|
282
|
+
const auto &law = *options.wavelength_law;
|
|
283
|
+
// lo_nm >= 1 keeps floor(lambda) >= 1 for every sample, so no photon is at wavelength 0.
|
|
284
|
+
if (!std::isfinite(law.lo_nm) || !std::isfinite(law.hi_nm) || !(law.lo_nm >= 1.0) ||
|
|
285
|
+
!(law.hi_nm > law.lo_nm))
|
|
286
|
+
throw std::invalid_argument("bunch wavelength law: need finite 1 <= lo_nm < hi_nm");
|
|
287
|
+
if (options.wavelength_density)
|
|
288
|
+
throw std::invalid_argument("bunch wavelength: set one wavelength law");
|
|
289
|
+
}
|
|
239
290
|
validate_bound_support(model); // Throws ModelError for an unsupported transmission.
|
|
240
291
|
const detail::BunchTask task(model, in, frame, options, seed);
|
|
241
292
|
|
|
@@ -250,15 +301,26 @@ std::size_t trace_bunches(const OpticalModel &model, const BunchBlock &in, const
|
|
|
250
301
|
});
|
|
251
302
|
if (std::find(bad.begin(), bad.end(), char{1}) != bad.end())
|
|
252
303
|
throw std::invalid_argument("bunches: invalid size, position, direction, time or wavelength "
|
|
253
|
-
"(wavelength 0 needs a wavelength density)");
|
|
304
|
+
"(wavelength 0 needs a wavelength density or law)");
|
|
254
305
|
for (std::size_t c = 0; c < chunks; ++c)
|
|
255
306
|
start[c + 1] += start[c];
|
|
256
307
|
|
|
257
|
-
|
|
308
|
+
// Record thinning once, including the roulette draw needed after tracing.
|
|
309
|
+
std::vector<std::uint8_t> keep_flags(start.back());
|
|
310
|
+
std::vector<std::vector<double>> roulette_draws(chunks);
|
|
311
|
+
pool.run(chunks, [&](std::size_t c) {
|
|
312
|
+
found[c] = task.run(c * kChunkSize, std::min(in.n, (c + 1) * kChunkSize), BunchResult{}, 0,
|
|
313
|
+
keep_flags.data() + start[c], roulette_draws[c], true);
|
|
314
|
+
});
|
|
315
|
+
std::vector<std::size_t> output_start(chunks + 1, 0);
|
|
316
|
+
for (std::size_t c = 0; c < chunks; ++c)
|
|
317
|
+
output_start[c + 1] = output_start[c] + found[c];
|
|
318
|
+
const BunchResult out = allocate(output_start.back());
|
|
258
319
|
|
|
259
|
-
//
|
|
320
|
+
// Trace survivors into their chunk's allocated range.
|
|
260
321
|
pool.run(chunks, [&](std::size_t c) {
|
|
261
|
-
found[c] = task.run(c * kChunkSize, std::min(in.n, (c + 1) * kChunkSize), out,
|
|
322
|
+
found[c] = task.run(c * kChunkSize, std::min(in.n, (c + 1) * kChunkSize), out, output_start[c],
|
|
323
|
+
keep_flags.data() + start[c], roulette_draws[c], false);
|
|
262
324
|
});
|
|
263
325
|
|
|
264
326
|
std::vector<detail::Column> columns;
|
|
@@ -269,8 +331,8 @@ std::size_t trace_bunches(const OpticalModel &model, const BunchBlock &in, const
|
|
|
269
331
|
detail::add_column(columns, out.wavelength_nm, 1);
|
|
270
332
|
detail::add_column(columns, out.bunch_index, 1);
|
|
271
333
|
detail::add_column(columns, out.sub_photon, 1);
|
|
272
|
-
|
|
273
|
-
return detail::compact_chunks(columns,
|
|
334
|
+
output_start.pop_back();
|
|
335
|
+
return detail::compact_chunks(columns, output_start, found);
|
|
274
336
|
}
|
|
275
337
|
|
|
276
338
|
} // namespace obdeect
|
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
#include "obdeect/surfaces.hpp"
|
|
5
5
|
#include "obdeect/table.hpp"
|
|
6
6
|
|
|
7
|
+
#include <array>
|
|
7
8
|
#include <cstdint>
|
|
8
9
|
#include <optional>
|
|
9
10
|
#include <stdexcept>
|
|
@@ -72,6 +73,8 @@ struct OpticalModel {
|
|
|
72
73
|
Obscurers obscurers;
|
|
73
74
|
std::optional<TelescopeTransmission> telescope_transmission;
|
|
74
75
|
double propagation_group_index{1.0};
|
|
76
|
+
// Optical origin offset O [m, telescope frame]: a ground point P is traced at R P - O.
|
|
77
|
+
std::array<double, 3> axes_offset_m{};
|
|
75
78
|
};
|
|
76
79
|
|
|
77
80
|
} // namespace obdeect
|
|
@@ -439,8 +439,15 @@ inline Obscurer obscurer(const Node &node, std::vector<Plane> &planes) {
|
|
|
439
439
|
root["format"].fail("expected '" + std::string(kModelFormat) + "'");
|
|
440
440
|
const Node optics = root["optics"];
|
|
441
441
|
(void)optics.object({"propagation_group_index", "telescope_transmission", "primary", "secondary",
|
|
442
|
-
"secondary_blocks_incoming", "focal_surface", "obscurers"});
|
|
442
|
+
"secondary_blocks_incoming", "focal_surface", "obscurers", "axes_offset_m"});
|
|
443
443
|
OpticalModel model;
|
|
444
|
+
if (optics.has("axes_offset_m")) {
|
|
445
|
+
const auto offset = optics["axes_offset_m"].array();
|
|
446
|
+
if (offset.size() != 3)
|
|
447
|
+
optics["axes_offset_m"].fail("expected three components");
|
|
448
|
+
for (std::size_t i = 0; i < 3; ++i)
|
|
449
|
+
model.axes_offset_m[i] = offset[i].number();
|
|
450
|
+
}
|
|
444
451
|
std::vector<std::uint32_t> ids;
|
|
445
452
|
model.propagation_group_index = optics["propagation_group_index"].number();
|
|
446
453
|
if (!(model.propagation_group_index >= 1.0))
|
|
@@ -1,9 +1,16 @@
|
|
|
1
1
|
// Optical-model loading: valid documents load; every error names its path.
|
|
2
2
|
#include "check.hpp"
|
|
3
3
|
|
|
4
|
+
#include "obdeect/bunches.hpp"
|
|
4
5
|
#include "obdeect/model_json.hpp"
|
|
5
6
|
|
|
7
|
+
#include <algorithm>
|
|
8
|
+
#include <array>
|
|
9
|
+
#include <cmath>
|
|
10
|
+
#include <cstddef>
|
|
11
|
+
#include <cstdint>
|
|
6
12
|
#include <string>
|
|
13
|
+
#include <vector>
|
|
7
14
|
|
|
8
15
|
using namespace obdeect;
|
|
9
16
|
|
|
@@ -63,6 +70,79 @@ bool contains(const std::string &text, const std::string &part) {
|
|
|
63
70
|
return text.find(part) != std::string::npos;
|
|
64
71
|
}
|
|
65
72
|
|
|
73
|
+
// Columns of one trace_bunches call without roulette: positions and their row count.
|
|
74
|
+
struct Traced {
|
|
75
|
+
std::vector<double> position;
|
|
76
|
+
std::size_t rows{};
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
Traced trace_bunches_of(const OpticalModel &model, const BunchBlock &in, const BunchFrame &frame) {
|
|
80
|
+
Traced out;
|
|
81
|
+
ThreadPool pool(2);
|
|
82
|
+
std::vector<double> direction, time, weight, wavelength;
|
|
83
|
+
std::vector<std::uint64_t> bunch;
|
|
84
|
+
std::vector<std::uint32_t> sub;
|
|
85
|
+
const auto allocate = [&](std::size_t rows) {
|
|
86
|
+
out.position.resize(3 * rows);
|
|
87
|
+
direction.resize(3 * rows);
|
|
88
|
+
time.resize(rows);
|
|
89
|
+
weight.resize(rows);
|
|
90
|
+
wavelength.resize(rows);
|
|
91
|
+
bunch.resize(rows);
|
|
92
|
+
sub.resize(rows);
|
|
93
|
+
return BunchResult{out.position.data(), direction.data(), time.data(), weight.data(),
|
|
94
|
+
wavelength.data(), bunch.data(), sub.data()};
|
|
95
|
+
};
|
|
96
|
+
out.rows = trace_bunches(model, in, frame, BunchOptions{}, 7, pool, allocate);
|
|
97
|
+
return out;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// The loaded axes offset is applied by the tracer: a ground point P is traced at R P - O, the
|
|
101
|
+
// same as the model without offset and frame offset -O.
|
|
102
|
+
void axes_offset_is_applied_by_the_tracer() {
|
|
103
|
+
const std::string key = "\"optics\": {";
|
|
104
|
+
std::string text = kValid;
|
|
105
|
+
text.insert(text.find(key) + key.size(), "\n \"axes_offset_m\": [0.5, -0.2, 0.1],");
|
|
106
|
+
auto shifted = load_model(text);
|
|
107
|
+
auto plain = load_model(kValid);
|
|
108
|
+
CHECK((shifted.axes_offset_m == std::array<double, 3>{0.5, -0.2, 0.1}));
|
|
109
|
+
CHECK((plain.axes_offset_m == std::array<double, 3>{}));
|
|
110
|
+
// Without the obscurers, which would shadow the photons, the photons reach the primary.
|
|
111
|
+
shifted.obscurers = Obscurers(std::vector<Obscurer>{}, std::vector<Plane>{});
|
|
112
|
+
plain.obscurers = Obscurers(std::vector<Obscurer>{}, std::vector<Plane>{});
|
|
113
|
+
|
|
114
|
+
constexpr std::size_t n = 200;
|
|
115
|
+
std::vector<float> x(n), y(n), zero(n, 0.0F), size(n, 1.0F), wavelength(n, 400.0F), time(n, 0.0F);
|
|
116
|
+
std::vector<std::uint64_t> ids(n);
|
|
117
|
+
for (std::size_t i = 0; i < n; ++i) {
|
|
118
|
+
x[i] = static_cast<float>(0.2 + 0.6 * static_cast<double>(i) / n);
|
|
119
|
+
y[i] = static_cast<float>(-0.4 + 0.8 * static_cast<double>((37 * i) % n) / n);
|
|
120
|
+
ids[i] = i;
|
|
121
|
+
}
|
|
122
|
+
const BunchBlock in{x.data(), y.data(), zero.data(), zero.data(), time.data(),
|
|
123
|
+
zero.data(), size.data(), wavelength.data(), ids.data(), n};
|
|
124
|
+
BunchFrame origin;
|
|
125
|
+
origin.plane_z = 30.0;
|
|
126
|
+
BunchFrame explicit_offset = origin;
|
|
127
|
+
explicit_offset.offset_m[0] = -0.5;
|
|
128
|
+
explicit_offset.offset_m[1] = 0.2;
|
|
129
|
+
explicit_offset.offset_m[2] = -0.1;
|
|
130
|
+
|
|
131
|
+
const Traced offset_model = trace_bunches_of(shifted, in, origin);
|
|
132
|
+
const Traced reference = trace_bunches_of(plain, in, explicit_offset);
|
|
133
|
+
CHECK(offset_model.rows > 0);
|
|
134
|
+
CHECK(offset_model.rows == reference.rows);
|
|
135
|
+
CHECK(offset_model.position.size() == reference.position.size());
|
|
136
|
+
double largest = 0.0;
|
|
137
|
+
for (std::size_t i = 0; i < std::min(offset_model.position.size(), reference.position.size());
|
|
138
|
+
++i)
|
|
139
|
+
largest = std::max(largest, std::abs(offset_model.position[i] - reference.position[i]));
|
|
140
|
+
CHECK(largest <= 1e-12);
|
|
141
|
+
// The offset is not a no-op: the same model without it traces the photons elsewhere.
|
|
142
|
+
const Traced unshifted = trace_bunches_of(plain, in, origin);
|
|
143
|
+
CHECK(unshifted.position != offset_model.position);
|
|
144
|
+
}
|
|
145
|
+
|
|
66
146
|
} // namespace
|
|
67
147
|
|
|
68
148
|
void all_tests() {
|
|
@@ -92,6 +172,9 @@ void all_tests() {
|
|
|
92
172
|
CHECK(contains(error_after(R"("plane_z_m": 16.0)", R"("asphere_x": 1)"),
|
|
93
173
|
"focal_surface: unknown key"));
|
|
94
174
|
CHECK(contains(error_after("\"optics\": {", "\"optics\": {,"), "JSON syntax error"));
|
|
175
|
+
CHECK(contains(error_after("\"optics\": {", "\"optics\": {\"axes_offset_m\": [1, 2],"),
|
|
176
|
+
"expected three components"));
|
|
177
|
+
axes_offset_is_applied_by_the_tracer();
|
|
95
178
|
}
|
|
96
179
|
|
|
97
180
|
int main() { return run_checks(all_tests); }
|
|
@@ -591,7 +591,53 @@ template <class F> bool throws_invalid_argument(F &&f) {
|
|
|
591
591
|
return false;
|
|
592
592
|
}
|
|
593
593
|
|
|
594
|
-
//
|
|
594
|
+
// Output allocation equals the number of thinning survivors, including fractional bunches.
|
|
595
|
+
void bunch_survivor_allocation() {
|
|
596
|
+
constexpr std::size_t n = 2050;
|
|
597
|
+
const auto model = single_facet(10.0, 0.5);
|
|
598
|
+
std::vector<float> zero(n, 0.0F), size(n, 2.5F), wavelength(n, 400.0F);
|
|
599
|
+
std::vector<std::uint64_t> ids(n);
|
|
600
|
+
for (std::size_t i = 0; i < n; ++i)
|
|
601
|
+
ids[i] = i;
|
|
602
|
+
BunchBlock in{zero.data(), zero.data(), zero.data(), zero.data(), zero.data(),
|
|
603
|
+
zero.data(), size.data(), wavelength.data(), ids.data(), n};
|
|
604
|
+
const Map2D acceptance{{100.0, 1000.0}, {0.0, 1.0}, std::vector<double>(4, 0.2), false};
|
|
605
|
+
BunchOptions options;
|
|
606
|
+
options.acceptance = &acceptance;
|
|
607
|
+
BunchFrame frame;
|
|
608
|
+
frame.plane_z = 20.0;
|
|
609
|
+
constexpr std::uint64_t seed = 42;
|
|
610
|
+
std::size_t expected = 0;
|
|
611
|
+
const double bound = response_bound(model, 400.0);
|
|
612
|
+
for (std::size_t i = 0; i < n; ++i)
|
|
613
|
+
for (std::uint32_t k = 0; k < 3; ++k)
|
|
614
|
+
expected +=
|
|
615
|
+
uniform4(seed, ids[i], Stream::thinning, k)[0] < (k == 2 ? 0.5 : 1.0) * 0.2 * bound;
|
|
616
|
+
for (const std::size_t threads : {1, 6}) {
|
|
617
|
+
ThreadPool pool(threads);
|
|
618
|
+
std::vector<double> position, direction, time, weight, lambda;
|
|
619
|
+
std::vector<std::uint64_t> bunch;
|
|
620
|
+
std::vector<std::uint32_t> sub;
|
|
621
|
+
std::size_t allocated = 0;
|
|
622
|
+
const auto allocate = [&](std::size_t rows) {
|
|
623
|
+
allocated = rows;
|
|
624
|
+
position.resize(3 * rows);
|
|
625
|
+
direction.resize(3 * rows);
|
|
626
|
+
time.resize(rows);
|
|
627
|
+
weight.resize(rows);
|
|
628
|
+
lambda.resize(rows);
|
|
629
|
+
bunch.resize(rows);
|
|
630
|
+
sub.resize(rows);
|
|
631
|
+
return BunchResult{position.data(), direction.data(), time.data(), weight.data(),
|
|
632
|
+
lambda.data(), bunch.data(), sub.data()};
|
|
633
|
+
};
|
|
634
|
+
const auto written = trace_bunches(model, in, frame, options, seed, pool, allocate);
|
|
635
|
+
CHECK(allocated == expected);
|
|
636
|
+
CHECK(written <= allocated);
|
|
637
|
+
}
|
|
638
|
+
}
|
|
639
|
+
|
|
640
|
+
// trace_bunches rejects a wavelength density or law reaching 0 nm and bad acceptance axes.
|
|
595
641
|
void bunch_option_validation() {
|
|
596
642
|
const auto model = single_facet(10.0, 0.5);
|
|
597
643
|
const float zero = 0.0F, one = 1.0F;
|
|
@@ -617,6 +663,11 @@ void bunch_option_validation() {
|
|
|
617
663
|
options.wavelength_density = &from_below;
|
|
618
664
|
CHECK(throws_invalid_argument([&] { run(options); }));
|
|
619
665
|
|
|
666
|
+
const InverseSquareLaw from_sub_nm{0.5, 0.75};
|
|
667
|
+
options.wavelength_density = nullptr;
|
|
668
|
+
options.wavelength_law = &from_sub_nm;
|
|
669
|
+
CHECK(throws_invalid_argument([&] { run(options); }));
|
|
670
|
+
|
|
620
671
|
const double nan = std::numeric_limits<double>::quiet_NaN();
|
|
621
672
|
const std::vector<double> ok{100.0, 1000.0};
|
|
622
673
|
for (const auto &[x, y] : {std::pair{std::vector<double>{100.0, 100.0}, ok},
|
|
@@ -628,6 +679,17 @@ void bunch_option_validation() {
|
|
|
628
679
|
bad.acceptance = &acceptance;
|
|
629
680
|
CHECK(throws_invalid_argument([&] { run(bad); }));
|
|
630
681
|
}
|
|
682
|
+
|
|
683
|
+
// An unsupported telescope transmission is rejected with ModelError before any bunch is traced.
|
|
684
|
+
auto unsupported = model;
|
|
685
|
+
unsupported.telescope_transmission = TelescopeTransmission{1.0, -1.0, 0.1, 2.0, 1.0};
|
|
686
|
+
bool model_error = false;
|
|
687
|
+
try {
|
|
688
|
+
trace_bunches(unsupported, in, frame, BunchOptions{}, 1, pool, allocate);
|
|
689
|
+
} catch (const ModelError &) {
|
|
690
|
+
model_error = true;
|
|
691
|
+
}
|
|
692
|
+
CHECK(model_error);
|
|
631
693
|
}
|
|
632
694
|
|
|
633
695
|
} // namespace
|
|
@@ -645,6 +707,7 @@ void all_tests() {
|
|
|
645
707
|
thread_pool();
|
|
646
708
|
thread_pool_exceptions();
|
|
647
709
|
bunch_option_validation();
|
|
710
|
+
bunch_survivor_allocation();
|
|
648
711
|
}
|
|
649
712
|
|
|
650
713
|
int main() { return run_checks(all_tests); }
|