diff --git a/README.md b/README.md
index d768a24..ff56114 100644
--- a/README.md
+++ b/README.md
@@ -1,74 +1,144 @@
# open_rust_mc
[](https://github.com/sorcerer86pt/open_rust_mc/releases/latest)
+[](#tests)
+[](LICENSE)
+
+A pure-Rust continuous-energy Monte Carlo radiation-transport engine —
+neutron k-eigenvalue and fixed-source, photon transport, coupled
+neutron-photon γ-heating, burnup depletion, time-dependent point
+kinetics, and continuous-energy adjoint. It reads OpenMC HDF5 nuclear
+data directly (no C dependency), runs on CPU (rayon) **and** the GPU,
+and ships an interactive cross-section / 3-D scene viewer.
+
+
+
+
+
+
+ Scene geometry rendered straight from the engine's own recursive CSG
+ by the built-in cross-vendor GPU ray-caster — left: an LCT-008 fuel-rod
+ cluster (water rendered translucent so the pin forest reads in 3-D);
+ right: a depth-3 recursive 17×17 PWR assembly.
+
+
+## From a compression study to a full transport engine
+
+This project started as a **single research question**, written up in
+[`paper/main.tex`](paper/main.tex): *can a truncated SVD of the
+temperature-dependent cross-section grid replace the pointwise table a
+Monte Carlo code reads at every collision, and is it faster?* Answering
+that honestly meant building enough of a real MC engine around the SVD
+provider to measure it against a credible baseline — not a toy.
+
+That scaffolding kept growing, and it became the actual artifact. Today
+`open_rust_mc` is a genuine continuous-energy MC code:
-A pure-Rust continuous-energy Monte Carlo neutron **and photon**
-transport engine. Reads OpenMC HDF5 nuclear data directly (no C
-dependency), runs k-eigenvalue, fixed-source, time-dependent
-point-kinetics, and burnup simulations end-to-end on CPU (rayon) or
-CUDA GPU. Validated against OpenMC on Godiva, PWR pin cell,
-PWR-actinide depletion, and γ-heating; against ICSBEP handbook
-values on a 375-case scene corpus.
-
-Started as an SVD cross-section compression study (`paper/main.tex`);
-grew into a research vehicle for studying cross-section
-representation, recursive geometry on GPU, and CIELO-era nuclear-data
-evaluation effects across ENDF/B-VII.1 / VIII.0 / VIII.1.
-
-440 / 440 lib tests + 9 integration tests pass on every push
-(`cargo test`); 447 / 447 with `--features cuda`.
+- **Four interchangeable cross-section providers** behind one
+ `XsProvider` trait — pointwise `Table`, truncated `Svd`, and two
+ Windowed-Multipole hybrids — so SVD can be graded against the
+ industry-standard representations on the *same* geometry, in the same
+ run.
+- **Recursive universe/lattice geometry** (rectangular + hexagonal),
+ per-universe BVH, validated against OpenMC on Godiva, a PWR pin cell,
+ a 17×17 assembly, and a 375-case ICSBEP corpus.
+- **Coupled neutron–photon** transport with full condensed-history
+ electrons; **CRAM** depletion; **point kinetics**; **continuous-energy
+ adjoint** + random-ray FW-CADIS variance reduction.
+- A **GPU backend** that runs the same physics, and a **cross-vendor GPU
+ scene renderer** (the images above) written once and compiled to
+ CUDA, ROCm, Vulkan, Metal, and WebGPU.
+
+The headline SVD result is in the paper and reproduced
+[below](#the-svd-verdict-the-original-question) — and it's an honest one:
+SVD wins on small fast-spectrum problems and **loses** on realistic
+thermal pin cells. This engine is the measuring instrument that made
+that statement defensible.
+
+
+> **441 / 441** library tests + 9 integration tests pass on every push
+> (`cargo test`); **447 / 447** with `--features cuda`.
## Highlights
-- **Four interchangeable cross-section providers** behind one
- `XsProvider` trait — pointwise Table, SVD, Hybrid SVD+WMP, and
- Hybrid Table+WMP — selectable at runtime via `--mode`. A
- three-way honesty-test mode runs all providers back-to-back on
- the same geometry.
-- **Recursive universe geometry** (`CoordStack`-based) with
- rectangular + hexagonal lattices, per-cell `Mat3` rotations, and
- per-universe BVH acceleration. Full 17×17 PWR assembly + N-ring
- hex mini-cores ship as built-in benchmarks.
-- **CUDA backend** for transport, photon kernels, random-ray, SVD
- reconstruction, and constant-XS event-batched eigenvalue. Includes
- PHYSOR-2022-style refill-pool with auto-recommendation from device
- attributes.
-- **Coupled neutron-photon** pipeline. PWR γ-heating runs end-to-end
- in ~5 min with full electron condensed-history transport
- (Bethe-Bloch + Highland MS + Seltzer-Berger brems). Result agrees
- with OpenMC 0.15.3 fuel/gap/clad/water split within 1 pp.
-- **Depletion** via CRAM-16 / CRAM-48 with on-the-fly chain-XS
- spectrum collapse and fresh-corrector predictor. Xe equilibrium
- matches analytical to 1e-4 relative.
-- **Variance reduction**: forward weight windows + flux bootstrap +
- full multigroup random-ray (Tramm 2018, immortal-ray
- Tramm-Siegel 2021) feeding a measured FW-CADIS pipeline. 4.32×
- FOM gain on 200 cm water at 1 MeV.
-- **ENDF/B-VIII.1 default**, with VIII.0 and VII.1 supported as
- fallbacks. Engine handles the sibling-`thermal/` layout split
- transparently and ships an idempotent natural-element migration
- script for benchmark JSONs.
+- **Four cross-section providers, one trait.** Pointwise `Table`, rank-*k*
+ `Svd`, `HybridSvdWmp`, `HybridTableWmp` — selectable at runtime via
+ `--mode`. A three-way honesty test runs them back-to-back on identical
+ geometry.
+- **Recursive geometry.** `CoordStack`-based universes, rectangular +
+ hexagonal lattices, per-cell `Mat3` rotation, per-universe BVH. Full
+ 17×17 PWR assembly and N-ring hex mini-cores ship as benchmarks.
+- **Cross-vendor GPU, two ways.** A CUDA transport backend (event-batched,
+ recursive-CSG, sm_60+), **and** a new [CubeCL](https://github.com/tracel-ai/cubecl)
+ renderer that compiles one Rust kernel to CUDA / ROCm / Vulkan / Metal /
+ WebGPU — running the geometry walk in **f64** on any GPU, no vendor SDK.
+- **Coupled neutron-photon.** PWR γ-heating runs end-to-end with
+ Bethe-Bloch + Highland MS + Seltzer-Berger bremsstrahlung electron
+ transport; fuel/gap/clad/water split agrees with OpenMC 0.15.3 within
+ 1 pp.
+- **Depletion.** CRAM-16 / CRAM-48 with on-the-fly chain-XS spectrum
+ collapse and a fresh-corrector predictor; Xe equilibrium matches
+ analytic to 1e-4.
+- **Variance reduction.** Forward weight windows + multigroup random-ray
+ (Tramm 2018, immortal-ray Tramm-Siegel 2021) feeding a measured
+ FW-CADIS pipeline — 4.32× FOM on 200 cm of water.
+- **ENDF/B-VIII.1 by default**, with VIII.0 / VII.1 / JEFF-3.3 supported;
+ sibling-`thermal/` layout handled transparently; idempotent
+ natural-element migration for benchmark JSONs.
- **Python (PyO3) front-end** — `Scene`, `Material`, `Surface`,
- `run_eigenvalue`, `run_icsbep_case`, full XsMode / runner /
- depletion / refill-pool plumbing.
+ `run_eigenvalue`, `run_icsbep_case`, full provider / runner / depletion
+ plumbing.
+
+## See the geometry
+
+The `preview_scene` binary renders any scene JSON — 2-D cross-section or
+3-D — directly from the engine's recursive CSG. No meshing: every pixel
+is an exact ray-cast (or cell lookup) through the same geometry the
+transport kernels walk.
+
+
+
+
+
+
+
+ Left: Godiva bare-HEU sphere (3-D GPU ray-cast). Middle / right: 2-D
+ XY cross-sections of the PWR assembly and the LCT-008 cluster tank —
+ fuel pins, guide tubes, and moderator resolved per-pixel.
+
+
+```powershell
+# Cross-vendor GPU 3-D render (Vulkan / Metal / DX12 / CUDA / ROCm) — f64
+cargo run --release --bin preview_scene -- `
+ bench/icsbep/leu-comp-therm-008_case-1.json data\endfb-viii.1-hdf5\neutron `
+ --cubecl-out lct008.png --cam-azim 35 --cam-elev 30 --zoom 1.2
+
+# 2-D cross-section to PNG (no GPU needed)
+cargo run --release --bin preview_scene -- `
+ bench/icsbep/pwr_assembly_17x17.json data\endfb-viii.1-hdf5\neutron `
+ --png-out assembly_xy.png --resolution 1400
+```
## Quick start
### Prerequisites
- Rust stable (1.79+), `cargo`.
-- (Optional) CUDA toolkit 12.x + NVRTC for the GPU backend.
-- (Optional) Python 3.9+ + [`maturin`](https://www.maturin.rs/) for
- the Python wheel.
+- (Optional) CUDA toolkit 12.x + NVRTC for the CUDA transport backend.
+- (Optional) Python 3.9+ + [`maturin`](https://www.maturin.rs/) for the
+ Python wheel.
+
+The cross-vendor GPU renderer needs **no** vendor SDK — it runs on
+whatever Vulkan / Metal / DX12 adapter the OS provides.
### Build
```powershell
git clone https://github.com/sorcerer86pt/open_rust_mc
cd open_rust_mc/rust_prototype
-cargo build --release # CPU only
-cargo build --release --features cuda # + CUDA backend
-cargo test --lib # 440 / 440 default
+cargo build --release # CPU + cross-vendor GPU renderer
+cargo build --release --features cuda # + CUDA transport backend
+cargo test --lib # 441 / 441
cargo test --lib --features cuda # 447 / 447
```
@@ -80,217 +150,216 @@ cargo test --lib --features cuda # 447 / 447
.\scripts\setup_nuclear_data.ps1 -Vii1 # legacy VII.1 only
```
-The script auto-flattens the wrapper directory the VIII.1 tarball
-uses, and the verification block reports key-file presence per
-library.
-
### Run a benchmark
-Godiva HEU sphere, end-to-end, SVD provider, k=5:
+Godiva HEU sphere, end-to-end, SVD provider, rank-5:
```powershell
cargo run --release --bin godiva -- data\endfb-viii.1-hdf5\neutron `
--rank 5 --batches 80 --inactive 15 --particles 10000
```
-A single ICSBEP case via Python:
+A single ICSBEP case via Python; full corpus sweep:
```powershell
cd rust_prototype/bindings/python
maturin develop --release --features cuda
cd ../../..
python rust_prototype/bindings/python/examples/icsbep_run.py heu-met-fast-001_case-1 gpu
+.\rust_prototype\bindings\python\examples\run_benchmark.ps1 # full corpus, auto GPU/CPU
```
-Full corpus sweep (auto-detects GPU vs CPU):
-
-```powershell
-.\rust_prototype\bindings\python\examples\run_benchmark.ps1
-```
-
-A scoped sweep with explicit CLI overrides (explicit flag wins over
-the JSON's `recommended_settings`):
-
-```powershell
-python rust_prototype/bindings/python/examples/icsbep_sweep.py `
- --runner cpu --filter heu-comp-inter-003 `
- --particles 100000 --batches 150 --inactive 40 --seeds 5 `
- --csv outputs/heu_comp_inter_003_v81.csv `
- --stop-file outputs/STOP
-```
-
-## Headline benchmarks
-
-All k_eff numbers carry a scope tag — `[godiva]` means end-to-end
-3-nuclide Godiva sphere; `[pwr]` means 9-nuclide PWR pin cell;
-`[assembly]` is depth-3 recursive 17×17. See `STATUS.md` for the
-full table.
-
-Only numbers re-verified against `outputs/` and `results/` this
-audit are shown here. See [`STATUS.md`](STATUS.md) for the full
-audited table including older claims still awaiting re-measurement.
+## Benchmarks
+
+All k_eff numbers carry a **scope tag** — `[godiva]` = 3-nuclide HEU
+sphere, `[pwr]` = 9-nuclide PWR pin cell, `[assembly]` = depth-3 17×17,
+`[icsbep]` = handbook regression. A `[micro]` headline routinely shrinks
+or inverts under `[pwr]`/`[assembly]`; the tags keep that honest. See
+[`STATUS.md`](STATUS.md) for the fully audited table.
+
+### Validation against OpenMC and ICSBEP
+
+
+
+
+
+ PWR pin-cell four-way honesty test: all four providers land inside
+ OpenMC 0.15.3's band; SVD r=5 and Hybrid SVD+WMP sit within 1σ of the
+ ACE+WMP industry baseline. [pwr]
+
+
+
+
+
+
+ ICSBEP sweep: engine k_calc vs handbook k_ref across fast / epithermal
+ / thermal cases, with the ±150 pcm acceptance band. [icsbep]
+
+
+### The SVD verdict (the original question)
+
+
+
+
+
+ Why SVD is even plausible: the temperature-dependent σ(E,T) grid is
+ extremely low-rank — 43 of 47 non-redundant 235U reactions
+ are rank-1 to machine precision, and the singular values fall off a
+ cliff. (For these nuclides the reconstruction saturates by
+ rank 15: k_eff is bit-identical at rank 15 and rank 60.)
+
+
+
+
+
+
+
+ Throughput (ns/particle, lower is better). On a 3-nuclide fast
+ spectrum SVD edges out the table; on a 9-nuclide thermal pin cell with
+ S(α,β) it loses — the reconstruction cost is paid on every collision
+ and there are far more of them.
+
+
+
+
+
+
+ Working-set memory at a single temperature: here the
+ rank-5 SVD basis is ~5× the pointwise table. But this is SVD's
+ worst framing — see below.
+
+
+**Where SVD actually pays for itself: memory across temperatures, and
+GPU residency.** The bar chart above is single-temperature, which is
+exactly where a pointwise table is cheapest. The picture flips for the
+things SVD was built for:
+
+- **Off-table temperature reconstruction.** A pointwise representation
+ stores a *full energy grid per temperature* — and library sets run
+ {294, 600, 900, 1200, 2500} K, with Doppler feedback and depletion
+ wanting points *between* them. SVD stores **one** rank-*k* basis
+ `B = UₖΣₖ` plus a tiny `N_T×k` factor, and reconstructs *any*
+ temperature on the fly via the Ducru weight kernel (exact at the
+ library points). Its footprint is near-flat in the number of
+ temperatures while the table's grows linearly — and in the off-library
+ case the table must load **two** columns per nuclide and stochastically
+ interpolate per collision. That's the regime the paper identifies as
+ where SVD's single-basis reconstruction pays off
+ (`paper/sections/memory_vs_precision.tex`).
+- **GPU memory mapping & cache residency.** The factored form is small,
+ contiguous, and regular — it maps cleanly into GPU memory and stays
+ cache-resident, versus chasing large irregular pointwise tables with a
+ per-nuclide binary search per lookup. Cache-resident reconstruction
+ was this project's *original* thesis, and it's the regime where the
+ rank-*k* FMA sequence beats the table's memory-latency-bound lookup.
+
+So the throughput story is spectrum-dependent (win on fast, lose on
+thermal-with-S(α,β)), but the *memory* story favours SVD precisely when
+it matters: many temperatures, and GPU.
| Metric | Scope | Value | Source |
|---|---|---|---|
-| Lib tests (default) | — | **440 / 440 green** | `cargo test --lib` |
-| Lib tests (`--features cuda`) | — | **447 / 447 green** | `cargo test --lib --features cuda` |
-| ICSBEP family suite | `[icsbep]` | **6 / 6 PASS** (141 s, `max(150 pcm, 2σ)`) | `outputs/cuda_runs_after_rank_fix.txt` |
-| PWR γ-heating fuel/clad/water | `[photon]` | 84.12 % / 9.81 % / 5.72 % (gap 0 %) | `outputs/pwr_gamma_heating_benchmark.txt` |
-| Saturation knee (HMF-001, RTX 3080) | `[godiva]` | 500k–1M particles per batch | `outputs/saturation_*.csv` |
-| Peak throughput at saturation | `[godiva]` | ~1.2 M histories/sec at 1M particles | `outputs/saturation_1000000.csv` |
-| Refill 2× at mid-curve (HMF-008) | `[micro]` | 2.0× more collisions at same wall; σ 2.1× tighter; µs/coll −50 % | `outputs/hmf008_refill_*.csv` |
-| RR-CADIS shielding gain at 14 mfp (200 cm water) | `[shield]` | **1.18×** analog (RR-CADIS alone); **1.75×** with NEE | `outputs/method_comparison_2026-05-08.txt` |
-| SVD on Godiva vs Table | `[godiva]` | 1.22× faster; 5.14× memory | same |
-| SVD on PWR (9 nuc + S(α,β)) vs Table | `[pwr]` | **1.25× *slower***; 5.12× memory | same |
-| GPU SVD vs 20-core CPU SVD on Godiva | `[godiva]` | **0.77×** (1.3× *slower* — launch + memory penalty dominates) | same |
-
-The headline result of the SVD compression study (the engine's
-original purpose) is in `paper/main.pdf`: SVD beats the pointwise
-table on small fast-spectrum problems by ~22 %, loses by ~25 % on
-realistic thermal-spectrum PWR pin cells, and costs ~5× memory in
-both regimes. The honest reading is in the paper — this engine is
-also a *vehicle* for measuring SVD against realistic baselines,
-not a claim that SVD is universally faster.
+| Lib tests (default / `cuda`) | — | **441 / 441** · **447 / 447** | `cargo test --lib` |
+| ICSBEP family suite (CPU + CUDA) | `[icsbep]` | **6 / 6 PASS** under `max(150 pcm, 2σ)` | `outputs/cuda_runs_after_rank_fix.txt` |
+| SVD vs Table on Godiva | `[godiva]` | **1.22× faster**; 5.14× memory @ single-T | `paper/`, `outputs/pareto/` |
+| SVD vs Table on PWR pin cell | `[pwr]` | **1.25× *slower***; 5.12× memory @ single-T | same |
+| SVD memory vs N-temperature table | `[micro]` | near-flat in N_T vs linear — SVD wins for many T | `paper/sections/memory_vs_precision.tex` |
+| PWR γ-heating split vs OpenMC | `[photon]` | fuel 84.1 / clad 9.8 / water 5.7 % (gap 0) | `outputs/pwr_gamma_heating_benchmark.txt` |
+| RR-CADIS + NEE FOM at 14 mfp (200 cm water) | `[shield]` | **1.75× analog** | `outputs/method_comparison_2026-05-08.txt` |
+| Peak GPU throughput (Godiva, RTX 3080) | `[godiva]` | ~1.2 M histories/s at 1M particles | `outputs/saturation_1000000.csv` |
+
+**The honest reading** ([`paper/main.pdf`](paper/main.pdf)): on raw
+per-collision throughput, SVD beats the pointwise table by ~22 % on small
+fast-spectrum problems and loses by ~25 % on realistic thermal PWR pin
+cells. At a single temperature it costs ~5× memory — but it reconstructs
+*any* temperature from one basis (flat memory in N_T where a table is
+linear) and maps far better into GPU/cache memory than irregular
+pointwise tables. The engine exists to make all of that *measurable*, not
+to sell one answer.
## Architecture
-The engine is structured around one common particle-transport loop
-that runs against any geometry, any cross-section provider, and any
-backend:
+One particle-transport loop runs against any geometry, any cross-section
+provider, and either backend:
```
┌──────────────────────────────────────────────────────────────┐
-│ Scene JSON │
-│ └─> material_resolve ─> XsProvider │
-│ (natural-element ┌── Table (pointwise) │
-│ expansion, thermal ├── Svd (rank-k FMA) │
-│ binding, kernel ├── HybridTableWmp │
-│ dedup) └── HybridSvdWmp │
-│ │
-│ └─> Geometry ─> Surface / Cell / Region / Universe │
-│ BVH (per universe, ≥8 cells) │
-│ │
-│ └─> EigenvalueRunner ─> CpuRunner (rayon, history) │
-│ CudaRunner (sm_86+, event-batched)│
+│ Scene JSON │
+│ └─> material_resolve ─> XsProvider │
+│ (natural-element ├── Table (pointwise) │
+│ expansion, thermal ├── Svd (rank-k FMA) │
+│ binding, kernel ├── HybridTableWmp │
+│ dedup) └── HybridSvdWmp │
+│ │
+│ └─> Geometry ─> Surface / Cell / Region / Universe │
+│ recursive CoordStack + per-universe BVH │
+│ │ │
+│ ├─> EigenvalueRunner ─> CpuRunner (rayon, history) │
+│ │ CudaRunner (sm_60+, event) │
+│ │ │
+│ └─> geometry::flat ─> CubeCL renderer (CUDA/ROCm/ │
+│ (shared SoA) Vulkan/Metal/WebGPU, f64) │
└──────────────────────────────────────────────────────────────┘
```
Key modules under `rust_prototype/src/`:
-- `data_paths.rs` — layout-aware ENDF/B HDF5 path helpers
- (sibling-`thermal/` resolver, multi-library discovery).
-- `kernel.rs`, `decompose.rs`, `cp_decompose.rs` — SVD
- reconstruction + decomposition.
-- `hdf5_reader.rs`, `thermal.rs` — pure-Rust HDF5 + S(α,β).
+- `kernel.rs`, `decompose.rs`, `cp_decompose.rs` — SVD reconstruction +
+ decomposition.
- `table.rs`, `wmp.rs` — pointwise + Windowed Multipole providers.
-- `geometry/` — recursive universes, BVH, hex/rect lattices.
-- `physics/` — collision, scatter, kinematics.
-- `transport/` — `simulate`, `dispatch`, `material_resolve`,
- `xs_provider`, `hybrid_xs`, `urr_equivalence`, `weight_window`,
- `tally`, `statepoint`, `kinetics`, `adjoint_neutron`,
- `adjoint_photon`.
-- `photon/` — Compton, Rayleigh, photoelectric, pair, brems,
- electron, transport.
-- `random_ray/` — multigroup TRRM (forward + adjoint + immortal),
- CADIS, adjoint-SVD.
-- `depletion/` — CRAM, chain, predictor-corrector, mapping, flux.
-- `bin/` — 42 binaries; notable diagnostics include
- `u235_inelastic_audit` (per-MT σ SVD vs Table) and `inspect_mt91`
- (MT=91 law-61 / KalbachMann data inspector).
+- `hdf5_reader.rs`, `thermal.rs` — pure-Rust HDF5 + S(α,β).
+- `geometry/` — recursive universes, BVH, hex/rect lattices, and
+ `flat.rs` (backend-agnostic Geometry → SoA flattening, shared by both
+ GPU paths).
+- `physics/`, `transport/` — collision, scatter, kinematics, `simulate`,
+ `dispatch`, `xs_provider`, `hybrid_xs`, weight windows, kinetics,
+ adjoint.
+- `photon/` — Compton, Rayleigh, photoelectric, pair, brems, electron.
+- `random_ray/` — multigroup TRRM (forward + adjoint + immortal), CADIS.
+- `depletion/` — CRAM, chain, predictor-corrector.
+- `gpu_recursive.rs`, `gpu_transport.rs` — CUDA transport backend.
+- `gpu_render.rs` — **CubeCL cross-vendor geometry renderer**.
## Cross-section providers
All four implement `XsProvider`; selectable at runtime via `--mode`.
-| Mode | Provider | Implementation | What it does |
-|------|----------|---------------|--------------|
-| `table` | Pointwise table | `src/table.rs` | OpenMC-style binary search + log-log interpolation per reaction |
-| `svd` | Truncated SVD | `src/kernel.rs` | Rank-*k* reconstruction, one FMA sequence per lookup |
-| `hybrid_svd_wmp` | SVD + WMP | `src/transport/hybrid_xs.rs` | SVD everywhere, WMP override (pole/residue + Faddeeva) inside the RRR |
-| `hybrid_table_wmp` | Table + WMP | `src/transport/hybrid_xs.rs` | Pointwise everywhere, WMP in the RRR — industry low-memory baseline |
-
-Off-library temperatures use stochastic pseudo-interpolation
-(Table / Hybrid) or partition-of-unity 3-point Ducru reconstruction
-(SVD). Detailed numerical results live in `paper/main.tex`.
-
-## Photon physics
-
-Full four-channel transport on per-element OpenMC HDF5 photon
-libraries:
-
-- **Compton** — Klein-Nishina + `S(x,Z)/Z` bound-electron rejection
- + optional Hartree-Fock Doppler broadening.
-- **Photoelectric** — subshell absorption + EADL atomic-relaxation
- cascade (fluorescence + Auger).
-- **Pair production** — Bethe-Heitler nuclear + electron-field +
- in-flight positron annihilation.
-- **Rayleigh** — form-factor + Thomson rejection.
-
-Plus condensed-history electron transport (Bethe-Bloch dE/dx with
-per-element I, Highland multiple-scattering with per-cell X₀,
-Seltzer-Berger bremsstrahlung secondaries banked back into the
-photon loop). The He-gap deposition artefact present in the
-older Katz-Penfold CSDA path goes from 1.5 % to 0 %.
-
-Coupled neutron-photon: the neutron loop tallies a
-`PhotonSourceEvent { cell, pos, E_γ, MT }` at every capture
-(MT=102), fission (MT=18), (n,p) / (n,α) (MT=103 / 107,
-threshold-gated), and inelastic (MT=4 via discrete-level Q-value)
-collision. γ multiplicities and outgoing energies come from
-the HDF5 `reactions/reaction_{mt}/product_{N}` tree with
-`particle="photon"`.
-
-## Depletion (burnup)
-
-- **CRAM-16 / CRAM-48** matrix-exponential evaluator (IPF form,
- Pusa 2016 poles + residues from OpenMC's canonical source) with
- dense complex Gaussian elimination + partial pivoting.
-- `DepletionChain` — decay constants, branches, per-(parent, MT)
- one-group reaction XS with default ENDF yield inference for
- (n,γ) / (n,2n) / (n,3n) / (n,p) / (n,α).
-- `chain_io` JSON loader with three-way `yields` semantics:
- omitted → default ENDF, `{}` → pure removal, explicit map → use it.
-- **CE/LI predictor-corrector** with **fresh-corrector**: clones
- materials, runs a second eigenvalue at the predicted composition
- for the EOC flux, then CRAM with the averaged matrix.
-- **On-the-fly chain-XS spectrum collapse** — closes the
- 9× to 0.77× gap to OpenMC depletion.
-- Shipped chains: `chains/partial_xe.json` (4-nuclide Xe poisoning)
- and `chains/pwr_actinides.json` (17-nuclide actinide buildup +
- dominant FP poisons).
-
-## CUDA backend
-
-`--features cuda` enables an Ampere-class (sm_86+) backend that
-shares the same physics as CPU:
-
-- Recursive cell-find / trace-step / multi-step walk —
- bit-exact vs CPU (≤ 9.3e-11 max-rel-err), 3-24× speedups on
- RTX A1000.
-- Constant-XS transport with atomic-add fission banking (6.74×).
-- Multi-slot S(α,β) — concurrent TSLs on multiple nuclides in one
- run. Stochastic temperature interpolation between bracketing
- slots based on per-cell kT.
-- Per-nuclide kernel cache (`Arc::as_ptr`-keyed, LRU + bundle
- budget) eliminates redundant HtoD on multi-case sweeps.
-- Refill pool (PHYSOR 2022 Optimization F) — opt-in via
- `gpu_refill_pool_factor` or auto-recommended via device-attribute
- inspection. 2× histories at same wall time on mid-curve
- workloads.
-- HexLattice device functions ported; runtime parity test pending.
-
-GPU bench drivers: `gpu_bench`, `gpu_godiva_keff`,
-`gpu_const_xs_keff`, `gpu_assembly_keff`, `gpu_pwr_bench`,
-`gpu_hex_minicore`, `gpu_recursive_parity`, `gpu_photon_features`,
-`gpu_wmp_validate`.
+| Mode | Provider | What it does |
+|------|----------|--------------|
+| `table` | Pointwise | OpenMC-style binary search + log-log interpolation per reaction |
+| `svd` | Truncated SVD | Rank-*k* reconstruction, one FMA sequence per lookup |
+| `hybrid_svd_wmp` | SVD + WMP | SVD everywhere, WMP (pole/residue + Faddeeva) inside the RRR |
+| `hybrid_table_wmp` | Table + WMP | Pointwise everywhere, WMP in the RRR — industry low-memory baseline |
+
+Off-library temperatures use stochastic pseudo-interpolation (Table /
+Hybrid) or partition-of-unity 3-point Ducru reconstruction (SVD).
+
+## GPU: two backends
+
+**CUDA transport (`--features cuda`, sm_60+).** Recursive cell-find /
+trace-step bit-exact vs CPU (≤ 9.3e-11 rel-err), constant-XS event-batched
+eigenvalue, multi-slot S(α,β), per-nuclide kernel cache, PHYSOR-2022
+refill pool. Used by `CudaRunner` for production sweeps.
+
+**CubeCL geometry renderer (always on).** The `preview_scene --cubecl-out`
+path ray-casts the recursive CSG in a single CubeCL `#[cube]` kernel that
+compiles to **CUDA, HIP/ROCm, Vulkan, Metal, and WebGPU** — one Rust
+source, every GPU. It runs in **f64** (verified on the wgpu/Vulkan
+backend wherever the adapter exposes `SHADER_F64`), so the render matches
+the CPU/CUDA geometry walk exactly, with analytic surface normals and
+front-to-back alpha compositing for translucent moderator fluids. The SoA
+geometry layout (`geometry::flat`) is shared with the CUDA upload path so
+the device representation has one source of truth.
+
+> CubeCL is also the migration target for the transport kernels: porting
+> the hand-written `gpu/cuda/*.cu` to CubeCL would collapse the CUDA-only
+> backend into the same write-once-run-anywhere path (NVIDIA + AMD +
+> Vulkan), in f64. That work is in progress.
## Python bindings
```python
-from open_rust_mc import (
- Scene, Material, Sphere, Settings, Runner,
- run_eigenvalue, run_icsbep_case,
-)
+from open_rust_mc import Scene, Material, Sphere, Settings, run_eigenvalue
fuel = (Material("HEU", temperature=294.0)
.add_nuclide("U234.h5", atom_density=0.000483, awr=232.029, nubar=2.49)
@@ -307,45 +376,21 @@ result = run_eigenvalue(scene, Settings(batches=50, inactive=10, particles=5000)
print(result.k_eff, result.k_sigma)
```
-Full Scene builder + Material API documented in `PYTHON.md`. ICSBEP
-harness lives at `rust_prototype/bindings/python/examples/`.
+Full Scene builder + Material API in [`PYTHON.md`](PYTHON.md).
## Documentation
-- [`CLAUDE.md`](CLAUDE.md) — engineer-facing project memory: hard
- invariants, file layout, build commands, recent session changes.
-- [`STATUS.md`](STATUS.md) — current state: capabilities, headline
- numbers, open work, A/B against handbook references.
+- [`CLAUDE.md`](CLAUDE.md) — engineer-facing project memory: invariants,
+ layout, build commands.
+- [`STATUS.md`](STATUS.md) — current capabilities, audited headline
+ numbers, open work.
- [`PYTHON.md`](PYTHON.md) — Python API reference.
-- [`BENCHMARKS.md`](BENCHMARKS.md) — bench-suite layout + scene-JSON
- schema.
+- [`BENCHMARKS.md`](BENCHMARKS.md) — bench-suite layout + scene-JSON schema.
- [`ICSBEP.md`](ICSBEP.md) — phased ICSBEP rollout plan.
-- [`paper/main.tex`](paper/main.tex) — SVD cross-section
- compression paper (compiled to `paper/main.pdf`).
-
-## Recent (May 2026)
-
-- **Eigenvalue progress bar** (`indicatif`) — live per-batch k_eff +
- σ in the terminal; per-case and per-seed banners in sweep harness.
-- **GPU MT=91 Law 61 (KalbachMann) mu coupling** — inelastic
- outgoing-angle correlation ported to CUDA path, bit-tested vs CPU.
-- **GPU survival biasing** — kernel wired; Python
- `PySettings.survival_biasing` toggle exposed in sweep harness.
-- In-process benchmark pipeline rolled back; subprocess-per-case
- Python driver (`icsbep_sweep.py`) restored to isolate per-case
- GPU-state corruption.
-- ENDF/B-VIII.1 became the default library; sibling-`thermal/`
- layout handled transparently.
-- Natural-element migration on the ICSBEP corpus — 137 cases
- rewritten to per-isotope carbon. Engine-side fallback covers
- un-migrated future JSONs.
-- Sweep precedence inverted: explicit CLI flag > JSON
- `recommended_settings` > built-in default.
-- VII.1 → VIII.1 A/B on heu-comp-inter-003: VIII.1 shifts k upward
- by +820 pcm uniformly; localised driver is CIELO U-235
- (σ_f +5.3 %, α −10.2 % at 100 eV).
+- [`paper/main.tex`](paper/main.tex) — the SVD cross-section compression
+ paper (compiled to `paper/main.pdf`).
## License
-MIT. See `LICENSE` (every source file carries
+MIT. See [`LICENSE`](LICENSE) (every source file carries
`SPDX-License-Identifier: MIT`).
diff --git a/docs/cubecl_1336_comment.md b/docs/cubecl_1336_comment.md
new file mode 100644
index 0000000..2a6636b
--- /dev/null
+++ b/docs/cubecl_1336_comment.md
@@ -0,0 +1,62 @@
+I am hitting this on **NVIDIA / Vulkan (Windows 11)** too, with one difference worth noting: here it does not race silently like on AMD/RADV — it **hard-crashes at dispatch** with `STATUS_ACCESS_VIOLATION` (exit `0xC0000005`). I think it's the same cause (per-thread private `Array` going wrong under register pressure), but with a worse symptom.
+
+### Setup
+- cubecl `=0.10.0` from crates.io, `wgpu` runtime, Vulkan backend
+- NVIDIA GPU, Windows 11, f64 enabled (adapter has `SHADER_F64`)
+
+### What happens
+A kernel with a lot of private `Array` state compiles fine — `CUBECL_DEBUG_LOG` prints a full WGSL shader ending in `[END_KERNEL_COMPILATION]` — but the process dies at the first dispatch:
+
+```
+exit code: 0xC0000005 (STATUS_ACCESS_VIOLATION)
+```
+
+No nagging or validation error first. The crash is in driver code during dispatch part.
+
+### What I tested
+I split each piece into its own kernel on the same machine:
+
+| Kernel | Result |
+|---|---|
+| PCG RNG in `u64` + `Array>.fetch_add` | works |
+| one big geometry helper (`find_cell`, 9 `&mut Array` params, depth-4 loop) | works |
+| two levels of `#[cube]` helper calls | works |
+| a 27-parameter `#[cube]` helper, called once | works |
+| 8 × `Array::new(64)` (4 KB private), write then sum | works (no race, no crash) |
+| **full kernel: `find_cell` + `trace_step` + per-event loop, all those arrays live at once** | crashes at dispatch, even at 4 threads × 4 steps |
+
+So on my machine raw byte count alone does not trigger it (the 4 KB kernel is fine), and neither does nesting or param count on their own. It only fails when a lot of mixed-type private `Array` state is live across a long control-flow body — which fits the reported suspected cause (SPIR-V moving `Function`-storage variables out of real thread-private storage under pressure): the bad address then faults on NVIDIA where it just races on RADV.
+
+### Reproducer shape
+The smallest case that still crashes for me is a recursive-geometry Monte Carlo simulation step: one `#[cube(launch)]` entry calling two `#[cube]` helpers that share nine `Array::new(4)` stacks plus a 16-entry region stack, all f64/i32:
+
+```rust
+#[cube]
+fn cell_contains(/* … */) -> u32 {
+ let mut stack = Array::::new(16usize);
+ // postfix CSG stack machine
+}
+
+#[cube]
+fn find_cell(/* … */, st_cell: &mut Array, st_offx: &mut Array, /* 9 stacks */) -> u32 {
+ // depth-4 descent, calls cell_contains
+}
+
+#[cube]
+fn trace_step(/* … */, st_cell: &mut Array, /* same 9 stacks */, out: &mut Array) {
+ // nearest-surface scan, calls surf_dist/grid_dist
+}
+
+#[cube(launch)]
+fn transport(/* buffers */, atomics: &mut Array>) {
+ let mut st_cell = Array::::new(4usize);
+ let mut st_offx = Array::::new(4usize);
+ // 7 more length-4 stacks, a length-2 u64 RNG, a length-3 scratch
+ let depth = find_cell(/* … */, &mut st_cell, &mut st_offx, /* … */);
+ // per-step loop: trace_step + collision sampling + atomics.fetch_add(…)
+ // → crashes at dispatch; the WGSL it generates looks fine.
+}
+```
+
+Glad to attach a trimmed standalone code example if that helps.
+
diff --git a/docs/images/bench_icsbep_kcalc.png b/docs/images/bench_icsbep_kcalc.png
new file mode 100644
index 0000000..f5b7146
Binary files /dev/null and b/docs/images/bench_icsbep_kcalc.png differ
diff --git a/docs/images/bench_memory.png b/docs/images/bench_memory.png
new file mode 100644
index 0000000..51facd2
Binary files /dev/null and b/docs/images/bench_memory.png differ
diff --git a/docs/images/bench_pwr_four_way.png b/docs/images/bench_pwr_four_way.png
new file mode 100644
index 0000000..a6abb6a
Binary files /dev/null and b/docs/images/bench_pwr_four_way.png differ
diff --git a/docs/images/bench_svd_spectrum.png b/docs/images/bench_svd_spectrum.png
new file mode 100644
index 0000000..4ff12e5
Binary files /dev/null and b/docs/images/bench_svd_spectrum.png differ
diff --git a/docs/images/bench_throughput_godiva.png b/docs/images/bench_throughput_godiva.png
new file mode 100644
index 0000000..3a5bafb
Binary files /dev/null and b/docs/images/bench_throughput_godiva.png differ
diff --git a/docs/images/bench_throughput_pwr.png b/docs/images/bench_throughput_pwr.png
new file mode 100644
index 0000000..1cd7d38
Binary files /dev/null and b/docs/images/bench_throughput_pwr.png differ
diff --git a/docs/images/cubecl_godiva.png b/docs/images/cubecl_godiva.png
new file mode 100644
index 0000000..a526203
Binary files /dev/null and b/docs/images/cubecl_godiva.png differ
diff --git a/docs/images/cubecl_lct008.png b/docs/images/cubecl_lct008.png
new file mode 100644
index 0000000..bb03f07
Binary files /dev/null and b/docs/images/cubecl_lct008.png differ
diff --git a/docs/images/cubecl_pwr_assembly.png b/docs/images/cubecl_pwr_assembly.png
new file mode 100644
index 0000000..6695862
Binary files /dev/null and b/docs/images/cubecl_pwr_assembly.png differ
diff --git a/docs/images/lct008_2d.png b/docs/images/lct008_2d.png
new file mode 100644
index 0000000..e6750a6
Binary files /dev/null and b/docs/images/lct008_2d.png differ
diff --git a/docs/images/pwr_assembly_2d.png b/docs/images/pwr_assembly_2d.png
new file mode 100644
index 0000000..3ea36ab
Binary files /dev/null and b/docs/images/pwr_assembly_2d.png differ
diff --git a/rust_prototype/Cargo.lock b/rust_prototype/Cargo.lock
index e12494f..b3e1460 100644
--- a/rust_prototype/Cargo.lock
+++ b/rust_prototype/Cargo.lock
@@ -18,6 +18,15 @@ version = "0.1.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "366ffbaa4442f4684d91e2cd7c5ea7c4ed8add41959a31447066e279e432b618"
+[[package]]
+name = "addr2line"
+version = "0.25.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1b5d307320b3181d6d7954e663bd7c774a838b8220fe0593c86d9fb09f498b4b"
+dependencies = [
+ "gimli",
+]
+
[[package]]
name = "adler2"
version = "2.0.1"
@@ -202,6 +211,18 @@ dependencies = [
"libloading 0.8.9",
]
+[[package]]
+name = "async-channel"
+version = "2.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "924ed96dd52d1b75e9c1a3e6275715fd320f5f9439fb5a4a11fa51f4221158d2"
+dependencies = [
+ "concurrent-queue",
+ "event-listener-strategy",
+ "futures-core",
+ "pin-project-lite",
+]
+
[[package]]
name = "async-trait"
version = "0.1.89"
@@ -244,12 +265,47 @@ version = "1.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8"
+[[package]]
+name = "backtrace"
+version = "0.3.76"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bb531853791a215d7c62a30daf0dde835f381ab5de4589cfe7c649d2cbe92bd6"
+dependencies = [
+ "addr2line",
+ "cfg-if",
+ "libc",
+ "miniz_oxide",
+ "object",
+ "rustc-demangle",
+ "windows-link 0.2.1",
+]
+
[[package]]
name = "base64"
version = "0.22.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6"
+[[package]]
+name = "bindgen"
+version = "0.71.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5f58bf3d7db68cfbac37cfc485a8d711e87e064c3d0fe0435b92f7a407f9d6b3"
+dependencies = [
+ "bitflags 2.13.0",
+ "cexpr",
+ "clang-sys",
+ "itertools 0.13.0",
+ "log",
+ "prettyplease",
+ "proc-macro2",
+ "quote",
+ "regex",
+ "rustc-hash 2.1.2",
+ "shlex",
+ "syn 2.0.117",
+]
+
[[package]]
name = "bit-set"
version = "0.5.3"
@@ -291,6 +347,9 @@ name = "bitflags"
version = "2.13.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b4388bee8683e3d04af747c73422af53102d2bd24d9eadb6cbc100baef4b43f8"
+dependencies = [
+ "serde_core",
+]
[[package]]
name = "blake3"
@@ -409,6 +468,15 @@ dependencies = [
"wayland-client 0.31.14",
]
+[[package]]
+name = "caseless"
+version = "0.2.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8b6fd507454086c8edfd769ca6ada439193cdb209c7681712ef6275cccbfe5d8"
+dependencies = [
+ "unicode-normalization",
+]
+
[[package]]
name = "cast"
version = "0.3.0"
@@ -436,6 +504,15 @@ dependencies = [
"shlex",
]
+[[package]]
+name = "cexpr"
+version = "0.6.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6fac387a98bb7c37292057cffc56d62ecb629900026402633ae9160df93a8766"
+dependencies = [
+ "nom 7.1.3",
+]
+
[[package]]
name = "cfg-if"
version = "1.0.4"
@@ -448,6 +525,17 @@ version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724"
+[[package]]
+name = "chacha20"
+version = "0.10.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6f8d983286843e49675a4b7a2d174efe136dc93a18d69130dd18198a6c167601"
+dependencies = [
+ "cfg-if",
+ "cpufeatures 0.3.0",
+ "rand_core 0.10.1",
+]
+
[[package]]
name = "chrono"
version = "0.4.44"
@@ -487,6 +575,17 @@ dependencies = [
"half",
]
+[[package]]
+name = "clang-sys"
+version = "1.8.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0b023947811758c97c59bf9d1c188fd619ad4718dcaa767947df1cadb14f39f4"
+dependencies = [
+ "glob",
+ "libc",
+ "libloading 0.8.9",
+]
+
[[package]]
name = "clap"
version = "4.6.0"
@@ -568,6 +667,20 @@ dependencies = [
"static_assertions",
]
+[[package]]
+name = "comrak"
+version = "0.39.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2fefab951771fc3beeed0773ce66a4f7b706273fc6c4c95b08dd1615744abcf5"
+dependencies = [
+ "caseless",
+ "entities",
+ "memchr",
+ "slug",
+ "typed-arena",
+ "unicode_categories",
+]
+
[[package]]
name = "concurrent-queue"
version = "2.5.0"
@@ -596,6 +709,21 @@ version = "0.4.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3d52eff69cd5e647efe296129160853a42795992097e8af39800e1060caeea9b"
+[[package]]
+name = "constcat"
+version = "0.6.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "136d3e02915a2cea4d74caa8681e2d44b1c3254bdbf17d11d41d587ff858832c"
+
+[[package]]
+name = "convert_case"
+version = "0.8.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "baaaa0ecca5b51987b9423ccdc971514dd8b0bb7b4060b983d3664dad3f1f89f"
+dependencies = [
+ "unicode-segmentation",
+]
+
[[package]]
name = "convert_case"
version = "0.10.0"
@@ -865,11 +993,332 @@ dependencies = [
"phf",
]
+[[package]]
+name = "cubecl"
+version = "0.10.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "fd203fef6e359472e4cb8f6c0ef3eca062815a1edd560eb6d6c1d5c1397838d9"
+dependencies = [
+ "cubecl-core",
+ "cubecl-cpu",
+ "cubecl-cuda",
+ "cubecl-hip",
+ "cubecl-ir",
+ "cubecl-runtime",
+ "cubecl-std",
+ "cubecl-wgpu",
+ "half",
+]
+
+[[package]]
+name = "cubecl-common"
+version = "0.10.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cc956c2dcc993f16f748d03bfdbd900f75cab4d469f4e5d8924f8ee128e861ad"
+dependencies = [
+ "backtrace",
+ "bytemuck",
+ "cfg-if",
+ "cfg_aliases",
+ "ciborium",
+ "derive-new",
+ "derive_more",
+ "dirs",
+ "embassy-futures",
+ "embassy-time",
+ "float4",
+ "float8",
+ "futures-lite",
+ "half",
+ "hashbrown 0.16.1",
+ "log",
+ "num-traits",
+ "oneshot",
+ "parking_lot",
+ "portable-atomic",
+ "portable-atomic-util",
+ "rand 0.10.1",
+ "sanitize-filename",
+ "serde",
+ "serde_bytes",
+ "serde_json",
+ "spin",
+ "toml 1.1.2+spec-1.1.0",
+ "tynm",
+ "wasm-bindgen-futures",
+ "web-time",
+ "xxhash-rust",
+]
+
+[[package]]
+name = "cubecl-core"
+version = "0.10.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a7522e7acf25d7848032c7b5270780f9f2abe84e13c7ed6345aa27ba70c312ca"
+dependencies = [
+ "bitflags 2.13.0",
+ "bytemuck",
+ "cubecl-common",
+ "cubecl-ir",
+ "cubecl-macros",
+ "cubecl-runtime",
+ "cubecl-zspace",
+ "derive-new",
+ "derive_more",
+ "enumset",
+ "float-ord",
+ "half",
+ "hashbrown 0.16.1",
+ "log",
+ "num-traits",
+ "paste",
+ "serde",
+ "serde_json",
+ "variadics_please",
+]
+
+[[package]]
+name = "cubecl-cpp"
+version = "0.10.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "411af04828cbf4583ecd388579fb30cd7a644eec19a334bb8f0d9d08522e1458"
+dependencies = [
+ "bytemuck",
+ "cubecl-common",
+ "cubecl-core",
+ "cubecl-opt",
+ "cubecl-runtime",
+ "derive-new",
+ "half",
+ "itertools 0.14.0",
+ "log",
+]
+
+[[package]]
+name = "cubecl-cpu"
+version = "0.10.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f572143f54e42a49ee0635a4ec36005f639e62be029e4f49890c808985981b35"
+dependencies = [
+ "bytemuck",
+ "cubecl-common",
+ "cubecl-core",
+ "cubecl-opt",
+ "cubecl-runtime",
+ "cubecl-std",
+ "derive-new",
+ "half",
+ "log",
+ "paste",
+ "serde",
+ "sysinfo 0.38.4",
+ "tracel-llvm",
+ "tracel-llvm-bundler",
+]
+
+[[package]]
+name = "cubecl-cuda"
+version = "0.10.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b6b0a69ff45688d322ad8e92c8bf645167b9ca490fa8fa087fc6adac8c5e46be"
+dependencies = [
+ "bytemuck",
+ "cubecl-common",
+ "cubecl-core",
+ "cubecl-cpp",
+ "cubecl-runtime",
+ "cudarc",
+ "derive-new",
+ "half",
+ "log",
+ "serde",
+]
+
+[[package]]
+name = "cubecl-hip"
+version = "0.10.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3c6b510a9348f06ecd56b32cf10eb6241639e927a87f5c09ec61cc7a7610d5a3"
+dependencies = [
+ "bytemuck",
+ "cubecl-common",
+ "cubecl-core",
+ "cubecl-cpp",
+ "cubecl-hip-sys",
+ "cubecl-runtime",
+ "derive-new",
+ "half",
+ "log",
+ "paste",
+ "serde",
+]
+
+[[package]]
+name = "cubecl-hip-sys"
+version = "7.2.5321100"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "58887c6d217859c2261a868a3a6b66aa8f44ea2f4bfa51d0dbe4dcb0d1f5768d"
+dependencies = [
+ "libc",
+ "regex",
+]
+
+[[package]]
+name = "cubecl-ir"
+version = "0.10.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d28a897a40c8ee6c57a8b8d76d8823ba2e97f4c2b83db9338f17a0752132d486"
+dependencies = [
+ "cubecl-common",
+ "cubecl-macros-internal",
+ "derive-new",
+ "derive_more",
+ "enumset",
+ "float-ord",
+ "fnv",
+ "foldhash 0.2.0",
+ "half",
+ "hashbrown 0.16.1",
+ "num-traits",
+ "portable-atomic",
+ "serde",
+ "variadics_please",
+]
+
+[[package]]
+name = "cubecl-macros"
+version = "0.10.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "77aa6df563e8e6a0926d2e9eeacb968737940a0e1ae9be819f6a65a341006e12"
+dependencies = [
+ "cubecl-common",
+ "darling 0.23.0",
+ "derive-new",
+ "ident_case",
+ "inflections",
+ "prettyplease",
+ "proc-macro2",
+ "quote",
+ "syn 2.0.117",
+]
+
+[[package]]
+name = "cubecl-macros-internal"
+version = "0.10.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a515651a5a91e25d87f71f311f80a088e6cf815798b9922560a97636da6b8740"
+dependencies = [
+ "darling 0.23.0",
+ "proc-macro2",
+ "quote",
+ "syn 2.0.117",
+]
+
+[[package]]
+name = "cubecl-opt"
+version = "0.10.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0a599c6c3efefdcee8636994c90e58ef696c7efbed2d18b5b5ad8d5f29923abb"
+dependencies = [
+ "cubecl-common",
+ "cubecl-core",
+ "cubecl-ir",
+ "float-ord",
+ "log",
+ "num",
+ "petgraph",
+ "smallvec",
+ "stable-vec",
+ "type-map",
+]
+
+[[package]]
+name = "cubecl-runtime"
+version = "0.10.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b68491bf5b3e997ae36bdc4e63b4ccd6d2f0e86b3b596a5d7a48d2b9e92622a0"
+dependencies = [
+ "ahash",
+ "async-channel",
+ "bytemuck",
+ "cfg-if",
+ "cfg_aliases",
+ "cubecl-common",
+ "cubecl-ir",
+ "cubecl-zspace",
+ "derive-new",
+ "derive_more",
+ "dirs",
+ "enumset",
+ "hashbrown 0.16.1",
+ "log",
+ "md5",
+ "serde",
+ "serde_json",
+ "spin",
+ "thiserror 2.0.18",
+ "toml 1.1.2+spec-1.1.0",
+ "wasm-bindgen-futures",
+ "web-time",
+]
+
+[[package]]
+name = "cubecl-std"
+version = "0.10.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b391b584a4897683dd9fc0767f96337ac712b733126ce0f68a9ee8a2df073d2d"
+dependencies = [
+ "cubecl-common",
+ "cubecl-core",
+ "cubecl-runtime",
+ "half",
+ "num-traits",
+ "paste",
+ "serde",
+ "spin",
+ "variadics_please",
+]
+
+[[package]]
+name = "cubecl-wgpu"
+version = "0.10.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4d3663c6e1a86187172b2cee495b6b533e7d91a2cb37e26f421649e315fe4c3a"
+dependencies = [
+ "async-channel",
+ "bytemuck",
+ "cfg-if",
+ "cfg_aliases",
+ "cubecl-common",
+ "cubecl-core",
+ "cubecl-ir",
+ "cubecl-runtime",
+ "derive-new",
+ "derive_more",
+ "half",
+ "hashbrown 0.16.1",
+ "log",
+ "sanitize-filename",
+ "wasm-bindgen-futures",
+ "wgpu",
+]
+
+[[package]]
+name = "cubecl-zspace"
+version = "0.10.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "04054d95698afab785a4dda9f949e297831dd9e4cc6d036a93e6603f88e88b07"
+dependencies = [
+ "derive-new",
+ "serde",
+ "smallvec",
+]
+
[[package]]
name = "cudarc"
-version = "0.19.4"
+version = "0.19.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "f071cd6a7b5d51607df76aa2d426aaabc7a74bc6bdb885b8afa63a880572ad9b"
+checksum = "42310153e06cf4cd532901f7096beb27504d681736a29ee90728ae4e2d93b2a8"
dependencies = [
"libloading 0.9.0",
]
@@ -890,6 +1339,16 @@ dependencies = [
"darling_macro 0.20.11",
]
+[[package]]
+name = "darling"
+version = "0.21.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9cdf337090841a411e2a7f3deb9187445851f91b309c0c0a29e05f74a00a48c0"
+dependencies = [
+ "darling_core 0.21.3",
+ "darling_macro 0.21.3",
+]
+
[[package]]
name = "darling"
version = "0.23.0"
@@ -914,6 +1373,19 @@ dependencies = [
"syn 2.0.117",
]
+[[package]]
+name = "darling_core"
+version = "0.21.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1247195ecd7e3c85f83c8d2a366e4210d588e802133e1e355180a9870b517ea4"
+dependencies = [
+ "fnv",
+ "ident_case",
+ "proc-macro2",
+ "quote",
+ "syn 2.0.117",
+]
+
[[package]]
name = "darling_core"
version = "0.23.0"
@@ -938,6 +1410,17 @@ dependencies = [
"syn 2.0.117",
]
+[[package]]
+name = "darling_macro"
+version = "0.21.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d38308df82d1080de0afee5d069fa14b0326a88c14f15c5ccda35b4a6c414c81"
+dependencies = [
+ "darling_core 0.21.3",
+ "quote",
+ "syn 2.0.117",
+]
+
[[package]]
name = "darling_macro"
version = "0.23.0"
@@ -981,6 +1464,17 @@ version = "0.5.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7cd812cc2bc1d69d4764bd80df88b4317eaef9e773c75226407d9bc0876b211c"
+[[package]]
+name = "derive-new"
+version = "0.7.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2cdc8d50f426189eef89dac62fabfa0abb27d5cc008f25bf4156a0203325becc"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.117",
+]
+
[[package]]
name = "derive_arbitrary"
version = "1.4.2"
@@ -1007,13 +1501,20 @@ version = "2.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "799a97264921d8623a957f6c3b9011f3b5492f557bbb7a5a19b7fa6d06ba8dcb"
dependencies = [
- "convert_case",
+ "convert_case 0.10.0",
"proc-macro2",
"quote",
"rustc_version",
"syn 2.0.117",
+ "unicode-xid",
]
+[[package]]
+name = "deunicode"
+version = "1.6.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "abd57806937c9cc163efc8ea3910e00a62e2aeb0b8119f1793a978088f8f6b04"
+
[[package]]
name = "digest"
version = "0.10.7"
@@ -1024,6 +1525,27 @@ dependencies = [
"crypto-common",
]
+[[package]]
+name = "dirs"
+version = "6.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c3e8aa94d75141228480295a7d0e7feb620b1a5ad9f12bc40be62411e38cce4e"
+dependencies = [
+ "dirs-sys",
+]
+
+[[package]]
+name = "dirs-sys"
+version = "0.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e01a3366d27ee9890022452ee61b2b63a67e6f13f58900b651ff5665f0bb1fab"
+dependencies = [
+ "libc",
+ "option-ext",
+ "redox_users",
+ "windows-sys 0.61.2",
+]
+
[[package]]
name = "dispatch"
version = "0.2.0"
@@ -1109,12 +1631,74 @@ version = "1.15.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719"
+[[package]]
+name = "embassy-futures"
+version = "0.1.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "dc2d050bdc5c21e0862a89256ed8029ae6c290a93aecefc73084b3002cdebb01"
+
+[[package]]
+name = "embassy-time"
+version = "0.5.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "592b0c143ec626e821d4d90da51a2bd91d559d6c442b7c74a47d368c9e23d97a"
+dependencies = [
+ "cfg-if",
+ "critical-section",
+ "document-features",
+ "embassy-time-driver",
+ "embedded-hal 0.2.7",
+ "embedded-hal 1.0.0",
+ "embedded-hal-async",
+ "futures-core",
+]
+
+[[package]]
+name = "embassy-time-driver"
+version = "0.2.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6ee71af1b3a0deaa53eaf2d39252f83504c853646e472400b763060389b9fcc9"
+dependencies = [
+ "document-features",
+]
+
+[[package]]
+name = "embedded-hal"
+version = "0.2.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "35949884794ad573cf46071e41c9b60efb0cb311e3ca01f7af807af1debc66ff"
+dependencies = [
+ "nb 0.1.3",
+ "void",
+]
+
+[[package]]
+name = "embedded-hal"
+version = "1.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "361a90feb7004eca4019fb28352a9465666b24f840f5c3cddf0ff13920590b89"
+
+[[package]]
+name = "embedded-hal-async"
+version = "1.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0c4c685bbef7fe13c3c6dd4da26841ed3980ef33e841cddfa15ce8a8fb3f1884"
+dependencies = [
+ "embedded-hal 1.0.0",
+]
+
[[package]]
name = "encode_unicode"
version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "34aa73646ffb006b8f5147f3dc182bd4bcb190227ce861fc4a4844bf8e3cb2c0"
+[[package]]
+name = "entities"
+version = "1.0.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b5320ae4c3782150d900b79807611a59a99fc9a1d61d686faafc24b93fc8d7ca"
+
[[package]]
name = "enum-as-inner"
version = "0.6.1"
@@ -1127,6 +1711,28 @@ dependencies = [
"syn 2.0.117",
]
+[[package]]
+name = "enumset"
+version = "1.1.13"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "839c4174b41e75c8f7306110b2c51996a293b8d1d850edd529011841d9fede7d"
+dependencies = [
+ "enumset_derive",
+ "serde",
+]
+
+[[package]]
+name = "enumset_derive"
+version = "0.15.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4bd536557b58c682b217b8fb199afdff47cd3eff260623f19e77074eb073d63a"
+dependencies = [
+ "darling 0.21.3",
+ "proc-macro2",
+ "quote",
+ "syn 2.0.117",
+]
+
[[package]]
name = "equator"
version = "0.2.2"
@@ -1192,6 +1798,27 @@ dependencies = [
"num-traits",
]
+[[package]]
+name = "event-listener"
+version = "5.4.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e13b66accf52311f30a0db42147dadea9850cb48cd070028831ae5f5d4b856ab"
+dependencies = [
+ "concurrent-queue",
+ "parking",
+ "pin-project-lite",
+]
+
+[[package]]
+name = "event-listener-strategy"
+version = "0.5.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8be9f3dfaaffdae2972880079a491a1a8bb7cbed0b8dd7a347f668b4150a3b93"
+dependencies = [
+ "event-listener",
+ "pin-project-lite",
+]
+
[[package]]
name = "faer"
version = "0.22.6"
@@ -1212,7 +1839,7 @@ dependencies = [
"num-traits",
"private-gemm-x86",
"pulp",
- "rand",
+ "rand 0.8.5",
"rand_distr",
"rayon",
"reborrow",
@@ -1299,6 +1926,16 @@ dependencies = [
"winapi",
]
+[[package]]
+name = "filetime"
+version = "0.2.29"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5c287a33c7f0a620c38e641e7f60827713987b3c0f26e8ddc9462cc69cf75759"
+dependencies = [
+ "cfg-if",
+ "libc",
+]
+
[[package]]
name = "find-msvc-tools"
version = "0.1.9"
@@ -1315,7 +1952,13 @@ checksum = "9844ddc3a6e533d62bba727eb6c28b5d360921d5175e9ff0f1e621a5c590a4d5"
name = "fixedbitset"
version = "0.4.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "0ce7134b9999ecaf8bcd65542e436736ef32ddca1b3e06094cb6ec5755203b80"
+checksum = "0ce7134b9999ecaf8bcd65542e436736ef32ddca1b3e06094cb6ec5755203b80"
+
+[[package]]
+name = "fixedbitset"
+version = "0.5.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1d674e81391d1e1ab681a28d99df07927c6d4aa5b027d7da16ba32d1d21ecd99"
[[package]]
name = "flate2"
@@ -1327,6 +1970,27 @@ dependencies = [
"miniz_oxide",
]
+[[package]]
+name = "float-ord"
+version = "0.3.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8ce81f49ae8a0482e4c55ea62ebbd7e5a686af544c00b9d090bba3ff9be97b3d"
+
+[[package]]
+name = "float4"
+version = "0.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9a5404bf31d22893d61cf24d4dda149d8e6b2ff07601c3cb3be651031f61a4ed"
+
+[[package]]
+name = "float8"
+version = "0.7.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c2d1f04709a8ac06e8e8042875a3c466cc4832d3c1a18dbcb9dba3c6e83046bc"
+dependencies = [
+ "half",
+]
+
[[package]]
name = "fnv"
version = "1.0.7"
@@ -1378,6 +2042,15 @@ version = "0.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "aa9a19cbb55df58761df49b23516a86d432839add4af60fc256da840f66ed35b"
+[[package]]
+name = "form_urlencoded"
+version = "1.2.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cb4cb245038516f5f85277875cdaa4f7d2c9a0fa0468de06ed190163b1581fcf"
+dependencies = [
+ "percent-encoding",
+]
+
[[package]]
name = "fragile"
version = "2.1.0"
@@ -1435,6 +2108,19 @@ version = "0.3.32"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cecba35d7ad927e23624b22ad55235f2239cfa44fd10428eecbeba6d6a717718"
+[[package]]
+name = "futures-lite"
+version = "2.6.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f78e10609fe0e0b3f4157ffab1876319b5b0db102a2c60dc4626306dc46b44ad"
+dependencies = [
+ "fastrand",
+ "futures-core",
+ "futures-io",
+ "parking",
+ "pin-project-lite",
+]
+
[[package]]
name = "futures-macro"
version = "0.3.32"
@@ -1642,8 +2328,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0"
dependencies = [
"cfg-if",
+ "js-sys",
"libc",
"wasi",
+ "wasm-bindgen",
]
[[package]]
@@ -1653,9 +2341,11 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd"
dependencies = [
"cfg-if",
+ "js-sys",
"libc",
"r-efi 5.3.0",
"wasip2",
+ "wasm-bindgen",
]
[[package]]
@@ -1667,10 +2357,17 @@ dependencies = [
"cfg-if",
"libc",
"r-efi 6.0.0",
+ "rand_core 0.10.1",
"wasip2",
"wasip3",
]
+[[package]]
+name = "gimli"
+version = "0.32.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e629b9b98ef3dd8afe6ca2bd0f89306cec16d43d907889945bc5d6687f2f13c7"
+
[[package]]
name = "gl_generator"
version = "0.14.0"
@@ -1688,6 +2385,12 @@ version = "0.29.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8babf46d4c1c9d92deac9f7be466f76dfc4482b6452fc5024b5e8daf6ffeb3ee"
+[[package]]
+name = "glob"
+version = "0.3.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0cc23270f6e1808e30a928bdc84dea0b9b4136a8bc82338574f23baf47bbd280"
+
[[package]]
name = "glow"
version = "0.17.0"
@@ -1743,6 +2446,25 @@ dependencies = [
"bitflags 2.13.0",
]
+[[package]]
+name = "h2"
+version = "0.4.15"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6cb093c84e8bd9b188d4c4a8cb6579fc016968d14c99882163cd3ff402a4f155"
+dependencies = [
+ "atomic-waker",
+ "bytes",
+ "fnv",
+ "futures-core",
+ "futures-sink",
+ "http",
+ "indexmap",
+ "slab",
+ "tokio",
+ "tokio-util",
+ "tracing",
+]
+
[[package]]
name = "half"
version = "2.7.1"
@@ -1753,6 +2475,7 @@ dependencies = [
"cfg-if",
"crunchy",
"num-traits",
+ "serde",
"zerocopy",
]
@@ -1774,7 +2497,7 @@ dependencies = [
"regex",
"serde",
"serde_json",
- "sysinfo",
+ "sysinfo 0.36.1",
"system-configuration",
"tempfile",
"thiserror 2.0.18",
@@ -1808,6 +2531,8 @@ dependencies = [
"allocator-api2",
"equivalent",
"foldhash 0.2.0",
+ "serde",
+ "serde_core",
]
[[package]]
@@ -1855,6 +2580,105 @@ version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dfa686283ad6dd069f105e5ab091b04c62850d3e4cf5d67debad1933f55023df"
+[[package]]
+name = "http"
+version = "1.4.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6970f50e31d6fc17d3fa27329444bfa74e196cf62e95052a3f6fee181dba6425"
+dependencies = [
+ "bytes",
+ "itoa",
+]
+
+[[package]]
+name = "http-body"
+version = "1.0.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1efedce1fb8e6913f23e0c92de8e62cd5b772a67e7b3946df930a62566c93184"
+dependencies = [
+ "bytes",
+ "http",
+]
+
+[[package]]
+name = "http-body-util"
+version = "0.1.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b021d93e26becf5dc7e1b75b1bed1fd93124b374ceb73f43d4d4eafec896a64a"
+dependencies = [
+ "bytes",
+ "futures-core",
+ "http",
+ "http-body",
+ "pin-project-lite",
+]
+
+[[package]]
+name = "httparse"
+version = "1.10.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87"
+
+[[package]]
+name = "hyper"
+version = "1.10.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "55281c53a1894c864990125767da440a4e630446785086f52523b20033b74498"
+dependencies = [
+ "atomic-waker",
+ "bytes",
+ "futures-channel",
+ "futures-core",
+ "h2",
+ "http",
+ "http-body",
+ "httparse",
+ "itoa",
+ "pin-project-lite",
+ "smallvec",
+ "tokio",
+ "want",
+]
+
+[[package]]
+name = "hyper-rustls"
+version = "0.27.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "33ca68d021ef39cf6463ab54c1d0f5daf03377b70561305bb89a8f83aab66e0f"
+dependencies = [
+ "http",
+ "hyper",
+ "hyper-util",
+ "rustls",
+ "tokio",
+ "tokio-rustls",
+ "tower-service",
+ "webpki-roots",
+]
+
+[[package]]
+name = "hyper-util"
+version = "0.1.20"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "96547c2556ec9d12fb1578c4eaf448b04993e7fb79cbaad930a656880a6bdfa0"
+dependencies = [
+ "base64",
+ "bytes",
+ "futures-channel",
+ "futures-util",
+ "http",
+ "http-body",
+ "hyper",
+ "ipnet",
+ "libc",
+ "percent-encoding",
+ "pin-project-lite",
+ "socket2",
+ "tokio",
+ "tower-service",
+ "tracing",
+]
+
[[package]]
name = "iana-time-zone"
version = "0.1.65"
@@ -1879,6 +2703,88 @@ dependencies = [
"cc",
]
+[[package]]
+name = "icu_collections"
+version = "2.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2984d1cd16c883d7935b9e07e44071dca8d917fd52ecc02c04d5fa0b5a3f191c"
+dependencies = [
+ "displaydoc",
+ "potential_utf",
+ "utf8_iter",
+ "yoke",
+ "zerofrom",
+ "zerovec",
+]
+
+[[package]]
+name = "icu_locale_core"
+version = "2.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "92219b62b3e2b4d88ac5119f8904c10f8f61bf7e95b640d25ba3075e6cac2c29"
+dependencies = [
+ "displaydoc",
+ "litemap",
+ "tinystr",
+ "writeable",
+ "zerovec",
+]
+
+[[package]]
+name = "icu_normalizer"
+version = "2.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c56e5ee99d6e3d33bd91c5d85458b6005a22140021cc324cea84dd0e72cff3b4"
+dependencies = [
+ "icu_collections",
+ "icu_normalizer_data",
+ "icu_properties",
+ "icu_provider",
+ "smallvec",
+ "zerovec",
+]
+
+[[package]]
+name = "icu_normalizer_data"
+version = "2.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "da3be0ae77ea334f4da67c12f149704f19f81d1adf7c51cf482943e84a2bad38"
+
+[[package]]
+name = "icu_properties"
+version = "2.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bee3b67d0ea5c2cca5003417989af8996f8604e34fb9ddf96208a033901e70de"
+dependencies = [
+ "icu_collections",
+ "icu_locale_core",
+ "icu_properties_data",
+ "icu_provider",
+ "zerotrie",
+ "zerovec",
+]
+
+[[package]]
+name = "icu_properties_data"
+version = "2.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8e2bbb201e0c04f7b4b3e14382af113e17ba4f63e2c9d2ee626b720cbce54a14"
+
+[[package]]
+name = "icu_provider"
+version = "2.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "139c4cf31c8b5f33d7e199446eff9c1e02decfc2f0eec2c8d71f65befa45b421"
+dependencies = [
+ "displaydoc",
+ "icu_locale_core",
+ "writeable",
+ "yoke",
+ "zerofrom",
+ "zerotrie",
+ "zerovec",
+]
+
[[package]]
name = "id-arena"
version = "2.3.0"
@@ -1891,6 +2797,27 @@ version = "1.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39"
+[[package]]
+name = "idna"
+version = "1.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3b0875f23caa03898994f6ddc501886a45c7d3d62d04d2d90788d47be1b1e4de"
+dependencies = [
+ "idna_adapter",
+ "smallvec",
+ "utf8_iter",
+]
+
+[[package]]
+name = "idna_adapter"
+version = "1.2.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cb68373c0d6620ef8105e855e7745e18b0d00d3bdb07fb532e434244cdb9a714"
+dependencies = [
+ "icu_normalizer",
+ "icu_properties",
+]
+
[[package]]
name = "indexmap"
version = "2.14.0"
@@ -1925,6 +2852,12 @@ dependencies = [
"rustversion",
]
+[[package]]
+name = "inflections"
+version = "1.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a257582fdcde896fd96463bf2d40eefea0580021c0712a0e2b028b60b47a837a"
+
[[package]]
name = "instability"
version = "0.3.12"
@@ -1961,6 +2894,12 @@ dependencies = [
"syn 1.0.109",
]
+[[package]]
+name = "ipnet"
+version = "2.12.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d98f6fed1fde3f8c21bc40a1abb88dd75e67924f9cffc3ef95607bad8017f8e2"
+
[[package]]
name = "is_terminal_polyfill"
version = "1.70.2"
@@ -2128,9 +3067,9 @@ checksum = "09edd9e8b54e49e587e4f6295a7d29c3ea94d469cb40ab8ca70b288248a81db2"
[[package]]
name = "libc"
-version = "0.2.185"
+version = "0.2.186"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "52ff2c0fe9bc6cb6b14a0592c2ff4fa9ceb83eea9db979b0487cd054946a2b8f"
+checksum = "68ab91017fe16c622486840e4c83c9a37afeff978bd239b5293d61ece587de66"
[[package]]
name = "libloading"
@@ -2152,6 +3091,27 @@ dependencies = [
"windows-link 0.2.1",
]
+[[package]]
+name = "liblzma"
+version = "0.4.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b6033b77c21d1f56deeae8014eb9fbe7bdf1765185a6c508b5ca82eeaed7f899"
+dependencies = [
+ "liblzma-sys",
+ "num_cpus",
+]
+
+[[package]]
+name = "liblzma-sys"
+version = "0.4.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1a60851d15cd8c5346eca4ab8babff585be2ae4bc8097c067291d3ffe2add3b6"
+dependencies = [
+ "cc",
+ "libc",
+ "pkg-config",
+]
+
[[package]]
name = "libm"
version = "0.2.16"
@@ -2200,6 +3160,12 @@ version = "0.12.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "32a66949e030da00e8c7d4434b251670a91556f4144941d37452769c25d58a53"
+[[package]]
+name = "litemap"
+version = "0.8.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "92daf443525c4cce67b150400bc2316076100ce0b3686209eb8cf3c31612e6f0"
+
[[package]]
name = "litrs"
version = "1.0.0"
@@ -2243,6 +3209,12 @@ dependencies = [
"hashbrown 0.17.0",
]
+[[package]]
+name = "lru-slab"
+version = "0.1.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "112b39cec0b298b6c1999fee3e31427f74f676e4cb9879ed1a121b43661a4154"
+
[[package]]
name = "mac_address"
version = "1.1.8"
@@ -2272,6 +3244,12 @@ dependencies = [
"rawpointer",
]
+[[package]]
+name = "md5"
+version = "0.8.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ae960838283323069879657ca3de837e9f7bbb4c7bf6ea7f1b290d5e9476d2e0"
+
[[package]]
name = "memchr"
version = "2.8.0"
@@ -2420,7 +3398,7 @@ dependencies = [
"log",
"num-traits",
"once_cell",
- "rustc-hash",
+ "rustc-hash 1.1.0",
"spirv",
"thiserror 2.0.18",
"unicode-ident",
@@ -2496,6 +3474,21 @@ dependencies = [
"nano-gemm-core",
]
+[[package]]
+name = "nb"
+version = "0.1.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "801d31da0513b6ec5214e9bf433a77966320625a37860f910be265be6e18d06f"
+dependencies = [
+ "nb 1.1.0",
+]
+
+[[package]]
+name = "nb"
+version = "1.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8d5439c4ad607c3c23abf66de8c8bf57ba8adcd1f129e699851a6e43935d339d"
+
[[package]]
name = "ndarray"
version = "0.16.1"
@@ -2611,6 +3604,15 @@ dependencies = [
"minimal-lexical",
]
+[[package]]
+name = "nom"
+version = "8.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "df9761775871bdef83bee530e60050f7e54b1105350d6884eb0fb4f46c2f9405"
+dependencies = [
+ "memchr",
+]
+
[[package]]
name = "npyz"
version = "0.8.4"
@@ -2640,6 +3642,20 @@ dependencies = [
"windows-sys 0.61.2",
]
+[[package]]
+name = "num"
+version = "0.4.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "35bd024e8b2ff75562e5f34e7f4905839deb4b22955ef5e73d2fea1b9813cb23"
+dependencies = [
+ "num-bigint",
+ "num-complex",
+ "num-integer",
+ "num-iter",
+ "num-rational",
+ "num-traits",
+]
+
[[package]]
name = "num-bigint"
version = "0.4.6"
@@ -2658,7 +3674,7 @@ checksum = "73f88a1307638156682bada9d7604135552957b7818057dcef22705b4d509495"
dependencies = [
"bytemuck",
"num-traits",
- "rand",
+ "rand 0.8.5",
]
[[package]]
@@ -2687,6 +3703,28 @@ dependencies = [
"num-traits",
]
+[[package]]
+name = "num-iter"
+version = "0.1.45"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1429034a0490724d0075ebb2bc9e875d6503c3cf69e235a8941aa757d83ef5bf"
+dependencies = [
+ "autocfg",
+ "num-integer",
+ "num-traits",
+]
+
+[[package]]
+name = "num-rational"
+version = "0.4.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f83d14da390562dca69fc84082e73e548e1ad308d24accdedd2720017cb37824"
+dependencies = [
+ "num-bigint",
+ "num-integer",
+ "num-traits",
+]
+
[[package]]
name = "num-traits"
version = "0.2.19"
@@ -3036,6 +4074,15 @@ dependencies = [
"objc2-foundation 0.2.2",
]
+[[package]]
+name = "object"
+version = "0.37.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ff76201f031d8863c38aa7f905eca4f53abbfa15f609db4277d44cd8938f33fe"
+dependencies = [
+ "memchr",
+]
+
[[package]]
name = "once_cell"
version = "1.21.4"
@@ -3046,7 +4093,13 @@ checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50"
name = "once_cell_polyfill"
version = "1.70.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe"
+checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe"
+
+[[package]]
+name = "oneshot"
+version = "0.2.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cfe21416a02c693fb9f980befcb230ecc70b0b3d1cc4abf88b9675c4c1457f0c"
[[package]]
name = "oorandom"
@@ -3062,6 +4115,7 @@ dependencies = [
"bytemuck",
"clap",
"criterion 0.8.2",
+ "cubecl",
"cudarc",
"dashmap",
"faer",
@@ -3083,7 +4137,7 @@ dependencies = [
"serde_json",
"smallvec",
"thiserror 2.0.18",
- "toml",
+ "toml 0.8.23",
"wgpu",
"winit",
]
@@ -3099,6 +4153,12 @@ dependencies = [
"serde_json",
]
+[[package]]
+name = "option-ext"
+version = "0.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d"
+
[[package]]
name = "orbclient"
version = "0.3.54"
@@ -3162,6 +4222,12 @@ dependencies = [
"syn 2.0.117",
]
+[[package]]
+name = "parking"
+version = "2.2.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f38d5652c16fde515bb1ecef450ab0f6a219d619a7274976324d5e377f7dceba"
+
[[package]]
name = "parking_lot"
version = "0.12.5"
@@ -3240,6 +4306,18 @@ dependencies = [
"sha2",
]
+[[package]]
+name = "petgraph"
+version = "0.8.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8701b58ea97060d5e5b155d383a69952a60943f0e6dfe30b04c287beb0b27455"
+dependencies = [
+ "fixedbitset 0.5.7",
+ "hashbrown 0.15.5",
+ "indexmap",
+ "serde",
+]
+
[[package]]
name = "phf"
version = "0.11.3"
@@ -3267,7 +4345,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3c80231409c20246a13fddb31776fb942c38553c51e871f8cbd687a4cfb5843d"
dependencies = [
"phf_shared",
- "rand",
+ "rand 0.8.5",
]
[[package]]
@@ -3396,6 +4474,9 @@ name = "portable-atomic"
version = "1.13.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c33a9471896f1c69cecef8d20cbe2f7accd12527ce60845ff44c153bb2a21b49"
+dependencies = [
+ "serde",
+]
[[package]]
name = "portable-atomic-util"
@@ -3406,6 +4487,15 @@ dependencies = [
"portable-atomic",
]
+[[package]]
+name = "potential_utf"
+version = "0.1.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0103b1cef7ec0cf76490e969665504990193874ea05c85ff9bab8b911d0a0564"
+dependencies = [
+ "zerovec",
+]
+
[[package]]
name = "powerfmt"
version = "0.2.0"
@@ -3614,6 +4704,61 @@ dependencies = [
"memchr",
]
+[[package]]
+name = "quinn"
+version = "0.11.11"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0c1a41e437b6bbd489372cd4971de128e85c855f56c57f283d20ff016cf7c0a8"
+dependencies = [
+ "bytes",
+ "cfg_aliases",
+ "pin-project-lite",
+ "quinn-proto",
+ "quinn-udp",
+ "rustc-hash 2.1.2",
+ "rustls",
+ "socket2",
+ "thiserror 2.0.18",
+ "tokio",
+ "tracing",
+ "web-time",
+]
+
+[[package]]
+name = "quinn-proto"
+version = "0.11.15"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4fcb935c5bec503c2f0e306bdd3e58bb9029dcb14fa8d9ac76e3a5256ac0763e"
+dependencies = [
+ "bytes",
+ "getrandom 0.3.4",
+ "lru-slab",
+ "rand 0.9.4",
+ "ring",
+ "rustc-hash 2.1.2",
+ "rustls",
+ "rustls-pki-types",
+ "slab",
+ "thiserror 2.0.18",
+ "tinyvec",
+ "tracing",
+ "web-time",
+]
+
+[[package]]
+name = "quinn-udp"
+version = "0.5.14"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "addec6a0dcad8a8d96a771f815f0eaf55f9d1805756410b39f5fa81332574cbd"
+dependencies = [
+ "cfg_aliases",
+ "libc",
+ "once_cell",
+ "socket2",
+ "tracing",
+ "windows-sys 0.59.0",
+]
+
[[package]]
name = "quote"
version = "1.0.45"
@@ -3642,8 +4787,29 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404"
dependencies = [
"libc",
- "rand_chacha",
- "rand_core",
+ "rand_chacha 0.3.1",
+ "rand_core 0.6.4",
+]
+
+[[package]]
+name = "rand"
+version = "0.9.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "44c5af06bb1b7d3216d91932aed5265164bf384dc89cd6ba05cf59a35f5f76ea"
+dependencies = [
+ "rand_chacha 0.9.0",
+ "rand_core 0.9.5",
+]
+
+[[package]]
+name = "rand"
+version = "0.10.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d2e8e8bcc7961af1fdac401278c6a831614941f6164ee3bf4ce61b7edb162207"
+dependencies = [
+ "chacha20",
+ "getrandom 0.4.2",
+ "rand_core 0.10.1",
]
[[package]]
@@ -3653,7 +4819,17 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88"
dependencies = [
"ppv-lite86",
- "rand_core",
+ "rand_core 0.6.4",
+]
+
+[[package]]
+name = "rand_chacha"
+version = "0.9.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb"
+dependencies = [
+ "ppv-lite86",
+ "rand_core 0.9.5",
]
[[package]]
@@ -3665,6 +4841,21 @@ dependencies = [
"getrandom 0.2.17",
]
+[[package]]
+name = "rand_core"
+version = "0.9.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "76afc826de14238e6e8c374ddcc1fa19e374fd8dd986b0d2af0d02377261d83c"
+dependencies = [
+ "getrandom 0.3.4",
+]
+
+[[package]]
+name = "rand_core"
+version = "0.10.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "63b8176103e19a2643978565ca18b50549f6101881c443590420e4dc998a3c69"
+
[[package]]
name = "rand_distr"
version = "0.4.3"
@@ -3672,7 +4863,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "32cb0b9bc82b0a0876c2dd994a7e7a2683d3e7390ca40e6886785ef0c7e3ee31"
dependencies = [
"num-traits",
- "rand",
+ "rand 0.8.5",
]
[[package]]
@@ -3857,6 +5048,17 @@ dependencies = [
"bitflags 2.13.0",
]
+[[package]]
+name = "redox_users"
+version = "0.5.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a4e608c6638b9c18977b00b475ac1f28d14e84b27d8d42f70e0bf1e3dec127ac"
+dependencies = [
+ "getrandom 0.2.17",
+ "libredox",
+ "thiserror 2.0.18",
+]
+
[[package]]
name = "regex"
version = "1.12.3"
@@ -3892,6 +5094,61 @@ version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "19b30a45b0cd0bcca8037f3d0dc3421eaf95327a17cad11964fb8179b4fc4832"
+[[package]]
+name = "reqwest"
+version = "0.12.28"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "eddd3ca559203180a307f12d114c268abf583f59b03cb906fd0b3ff8646c1147"
+dependencies = [
+ "base64",
+ "bytes",
+ "futures-channel",
+ "futures-core",
+ "futures-util",
+ "h2",
+ "http",
+ "http-body",
+ "http-body-util",
+ "hyper",
+ "hyper-rustls",
+ "hyper-util",
+ "js-sys",
+ "log",
+ "percent-encoding",
+ "pin-project-lite",
+ "quinn",
+ "rustls",
+ "rustls-pki-types",
+ "serde",
+ "serde_json",
+ "serde_urlencoded",
+ "sync_wrapper",
+ "tokio",
+ "tokio-rustls",
+ "tower",
+ "tower-http",
+ "tower-service",
+ "url",
+ "wasm-bindgen",
+ "wasm-bindgen-futures",
+ "web-sys",
+ "webpki-roots",
+]
+
+[[package]]
+name = "ring"
+version = "0.17.14"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a4689e6c2294d81e88dc6261c768b63bc4fcdb852be6d1352498b114f61383b7"
+dependencies = [
+ "cc",
+ "cfg-if",
+ "getrandom 0.2.17",
+ "libc",
+ "untrusted",
+ "windows-sys 0.52.0",
+]
+
[[package]]
name = "rust-mc-sim"
version = "0.1.0"
@@ -3903,12 +5160,24 @@ dependencies = [
"rayon",
]
+[[package]]
+name = "rustc-demangle"
+version = "0.1.27"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b50b8869d9fc858ce7266cce0194bd74df58b9d0e3f6df3a9fc8eb470d95c09d"
+
[[package]]
name = "rustc-hash"
version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2"
+[[package]]
+name = "rustc-hash"
+version = "2.1.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "94300abf3f1ae2e2b8ffb7b58043de3d399c73fa6f4b73826402a5c457614dbe"
+
[[package]]
name = "rustc_version"
version = "0.4.1"
@@ -3944,6 +5213,41 @@ dependencies = [
"windows-sys 0.61.2",
]
+[[package]]
+name = "rustls"
+version = "0.23.40"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ef86cd5876211988985292b91c96a8f2d298df24e75989a43a3c73f2d4d8168b"
+dependencies = [
+ "once_cell",
+ "ring",
+ "rustls-pki-types",
+ "rustls-webpki",
+ "subtle",
+ "zeroize",
+]
+
+[[package]]
+name = "rustls-pki-types"
+version = "1.14.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "30a7197ae7eb376e574fe940d068c30fe0462554a3ddbe4eca7838e049c937a9"
+dependencies = [
+ "web-time",
+ "zeroize",
+]
+
+[[package]]
+name = "rustls-webpki"
+version = "0.103.13"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "61c429a8649f110dddef65e2a5ad240f747e85f7758a6bccc7e5777bd33f756e"
+dependencies = [
+ "ring",
+ "rustls-pki-types",
+ "untrusted",
+]
+
[[package]]
name = "rustversion"
version = "1.0.22"
@@ -3965,6 +5269,15 @@ dependencies = [
"winapi-util",
]
+[[package]]
+name = "sanitize-filename"
+version = "0.6.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bc984f4f9ceb736a7bb755c3e3bd17dc56370af2600c9780dcc48c66453da34d"
+dependencies = [
+ "regex",
+]
+
[[package]]
name = "scoped-tls"
version = "1.0.1"
@@ -4035,6 +5348,16 @@ dependencies = [
"serde_derive",
]
+[[package]]
+name = "serde_bytes"
+version = "0.11.19"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a5d440709e79d88e51ac01c4b72fc6cb7314017bb7da9eeff678aa94c10e3ea8"
+dependencies = [
+ "serde",
+ "serde_core",
+]
+
[[package]]
name = "serde_core"
version = "1.0.228"
@@ -4077,6 +5400,27 @@ dependencies = [
"serde",
]
+[[package]]
+name = "serde_spanned"
+version = "1.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6662b5879511e06e8999a8a235d848113e942c9124f211511b16466ee2995f26"
+dependencies = [
+ "serde_core",
+]
+
+[[package]]
+name = "serde_urlencoded"
+version = "0.7.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd"
+dependencies = [
+ "form_urlencoded",
+ "itoa",
+ "ryu",
+ "serde",
+]
+
[[package]]
name = "sha2"
version = "0.10.9"
@@ -4177,11 +5521,24 @@ dependencies = [
"version_check",
]
+[[package]]
+name = "slug"
+version = "0.1.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "882a80f72ee45de3cc9a5afeb2da0331d58df69e4e7d8eeb5d3c7784ae67e724"
+dependencies = [
+ "deunicode",
+ "wasm-bindgen",
+]
+
[[package]]
name = "smallvec"
version = "1.15.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03"
+dependencies = [
+ "serde",
+]
[[package]]
name = "smithay-client-toolkit"
@@ -4227,6 +5584,16 @@ dependencies = [
"windows-sys 0.61.2",
]
+[[package]]
+name = "spin"
+version = "0.10.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d5fe4ccb98d9c292d56fec89a5e07da7fc4cf0dc11e156b41793132775d3e591"
+dependencies = [
+ "lock_api",
+ "portable-atomic",
+]
+
[[package]]
name = "spindle"
version = "0.2.6"
@@ -4249,6 +5616,18 @@ dependencies = [
"bitflags 2.13.0",
]
+[[package]]
+name = "stable-vec"
+version = "0.4.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6dac7bc0f7d0d44329b200020effbc25a534d89fa142af95e3ddf76113412a5e"
+
+[[package]]
+name = "stable_deref_trait"
+version = "1.2.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596"
+
[[package]]
name = "static_assertions"
version = "1.1.0"
@@ -4288,6 +5667,12 @@ dependencies = [
"syn 2.0.117",
]
+[[package]]
+name = "subtle"
+version = "2.6.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292"
+
[[package]]
name = "syn"
version = "1.0.109"
@@ -4310,6 +5695,26 @@ dependencies = [
"unicode-ident",
]
+[[package]]
+name = "sync_wrapper"
+version = "1.0.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0bf256ce5efdfa370213c1dabab5935a12e49f2c58d15e9eac2870d3b4f27263"
+dependencies = [
+ "futures-core",
+]
+
+[[package]]
+name = "synstructure"
+version = "0.13.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.117",
+]
+
[[package]]
name = "sysctl"
version = "0.6.0"
@@ -4338,6 +5743,20 @@ dependencies = [
"windows 0.61.3",
]
+[[package]]
+name = "sysinfo"
+version = "0.38.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "92ab6a2f8bfe508deb3c6406578252e491d299cbbf3bc0529ecc3313aee4a52f"
+dependencies = [
+ "libc",
+ "memchr",
+ "ntapi",
+ "objc2-core-foundation",
+ "objc2-io-kit",
+ "windows 0.62.2",
+]
+
[[package]]
name = "system-configuration"
version = "0.6.1"
@@ -4355,8 +5774,19 @@ version = "0.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8e1d1b10ced5ca923a1fcb8d03e96b8d3268065d724548c0211415ff6ac6bac4"
dependencies = [
- "core-foundation-sys",
+ "core-foundation-sys",
+ "libc",
+]
+
+[[package]]
+name = "tar"
+version = "0.4.46"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3f6221d9a6003c78398e3b239969f352578258df48c8eb051caadae0015bc840"
+dependencies = [
+ "filetime",
"libc",
+ "xattr",
]
[[package]]
@@ -4394,7 +5824,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d4ea810f0692f9f51b382fff5893887bb4580f5fa246fde546e0b13e7fcee662"
dependencies = [
"fnv",
- "nom",
+ "nom 7.1.3",
"phf",
"phf_codegen",
]
@@ -4426,7 +5856,7 @@ dependencies = [
"fancy-regex",
"filedescriptor",
"finl_unicode",
- "fixedbitset",
+ "fixedbitset 0.4.2",
"hex",
"lazy_static",
"libc",
@@ -4551,6 +5981,16 @@ dependencies = [
"strict-num",
]
+[[package]]
+name = "tinystr"
+version = "0.8.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c8323304221c2a851516f22236c5722a72eaa19749016521d6dff0824447d96d"
+dependencies = [
+ "displaydoc",
+ "zerovec",
+]
+
[[package]]
name = "tinytemplate"
version = "1.2.1"
@@ -4561,6 +6001,21 @@ dependencies = [
"serde_json",
]
+[[package]]
+name = "tinyvec"
+version = "1.11.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3e61e67053d25a4e82c844e8424039d9745781b3fc4f32b8d55ed50f5f667ef3"
+dependencies = [
+ "tinyvec_macros",
+]
+
+[[package]]
+name = "tinyvec_macros"
+version = "0.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20"
+
[[package]]
name = "tokio"
version = "1.52.3"
@@ -4589,6 +6044,29 @@ dependencies = [
"syn 2.0.117",
]
+[[package]]
+name = "tokio-rustls"
+version = "0.26.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1729aa945f29d91ba541258c8df89027d5792d85a8841fb65e8bf0f4ede4ef61"
+dependencies = [
+ "rustls",
+ "tokio",
+]
+
+[[package]]
+name = "tokio-util"
+version = "0.7.18"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9ae9cec805b01e8fc3fd2fe289f89149a9b66dd16786abd8b19cfa7b48cb0098"
+dependencies = [
+ "bytes",
+ "futures-core",
+ "futures-sink",
+ "pin-project-lite",
+ "tokio",
+]
+
[[package]]
name = "toml"
version = "0.8.23"
@@ -4596,11 +6074,26 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dc1beb996b9d83529a9e75c17a1686767d148d70663143c7854d8b4a09ced362"
dependencies = [
"serde",
- "serde_spanned",
+ "serde_spanned 0.6.9",
"toml_datetime 0.6.11",
"toml_edit 0.22.27",
]
+[[package]]
+name = "toml"
+version = "1.1.2+spec-1.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "81f3d15e84cbcd896376e6730314d59fb5a87f31e4b038454184435cd57defee"
+dependencies = [
+ "indexmap",
+ "serde_core",
+ "serde_spanned 1.1.1",
+ "toml_datetime 1.1.1+spec-1.1.0",
+ "toml_parser",
+ "toml_writer",
+ "winnow 1.0.3",
+]
+
[[package]]
name = "toml_datetime"
version = "0.6.11"
@@ -4627,7 +6120,7 @@ checksum = "41fe8c660ae4257887cf66394862d21dbca4a6ddd26f04a3560410406a2f819a"
dependencies = [
"indexmap",
"serde",
- "serde_spanned",
+ "serde_spanned 0.6.9",
"toml_datetime 0.6.11",
"toml_write",
"winnow 0.7.15",
@@ -4660,6 +6153,136 @@ version = "0.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5d99f8c9a7727884afe522e9bd5edbfc91a3312b36a77b5fb8926e4c31a41801"
+[[package]]
+name = "toml_writer"
+version = "1.1.1+spec-1.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "756daf9b1013ebe47a8776667b466417e2d4c5679d441c26230efd9ef78692db"
+
+[[package]]
+name = "tower"
+version = "0.5.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ebe5ef63511595f1344e2d5cfa636d973292adc0eec1f0ad45fae9f0851ab1d4"
+dependencies = [
+ "futures-core",
+ "futures-util",
+ "pin-project-lite",
+ "sync_wrapper",
+ "tokio",
+ "tower-layer",
+ "tower-service",
+]
+
+[[package]]
+name = "tower-http"
+version = "0.6.11"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4cfcf7e2740e6fc6d4d688b4ef00650406bb94adf4731e43c096c3a19fe40840"
+dependencies = [
+ "bitflags 2.13.0",
+ "bytes",
+ "futures-util",
+ "http",
+ "http-body",
+ "pin-project-lite",
+ "tower",
+ "tower-layer",
+ "tower-service",
+ "url",
+]
+
+[[package]]
+name = "tower-layer"
+version = "0.3.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "121c2a6cda46980bb0fcd1647ffaf6cd3fc79a013de288782836f6df9c48780e"
+
+[[package]]
+name = "tower-service"
+version = "0.3.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8df9b6e13f2d32c91b9bd719c00d1958837bc7dec474d94952798cc8e69eeec3"
+
+[[package]]
+name = "tracel-llvm"
+version = "20.1.4-7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "982535db9eb1a30ac0f2c50239a0eec3e5cf50993a88e92b04747bd2f4d365b2"
+dependencies = [
+ "tracel-mlir-rs",
+ "tracel-mlir-sys",
+]
+
+[[package]]
+name = "tracel-llvm-bundler"
+version = "20.1.4-7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3c75b8e477cb8d49d907afab029ca74d48459f5b88c27bdb4c6cd6acb5e61977"
+dependencies = [
+ "anyhow",
+ "bytes",
+ "constcat",
+ "dirs",
+ "liblzma",
+ "regex",
+ "reqwest",
+ "serde",
+ "serde_json",
+ "sha2",
+ "tar",
+ "walkdir",
+]
+
+[[package]]
+name = "tracel-mlir-rs"
+version = "20.1.4-7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "77a478a35efd68d0ba73f747adfb7923b121c64e7f5be9cd8364ca1dcb772d5c"
+dependencies = [
+ "tracel-mlir-rs-macros",
+ "tracel-mlir-sys",
+]
+
+[[package]]
+name = "tracel-mlir-rs-macros"
+version = "20.1.4-7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7a94f36868c3b10b1825945223d99d106c73f4d249f063caa4651deeb9379344"
+dependencies = [
+ "comrak",
+ "convert_case 0.8.0",
+ "proc-macro2",
+ "quote",
+ "regex",
+ "syn 2.0.117",
+ "tracel-llvm-bundler",
+ "tracel-tblgen-rs",
+ "unindent",
+]
+
+[[package]]
+name = "tracel-mlir-sys"
+version = "20.1.4-7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "02f26d31af0c225a6d2e3d65d012fd6de848c9fc776897b152ee83b7d1bd15c4"
+dependencies = [
+ "tracel-llvm-bundler",
+]
+
+[[package]]
+name = "tracel-tblgen-rs"
+version = "20.1.4-7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "00d2581070380418ccc33b500f3739e4d4869421fdb477fcea51ff97c6253a52"
+dependencies = [
+ "bindgen",
+ "cc",
+ "paste",
+ "thiserror 2.0.18",
+ "tracel-llvm-bundler",
+]
+
[[package]]
name = "tracing"
version = "0.1.44"
@@ -4709,12 +6332,42 @@ dependencies = [
"tracing-log",
]
+[[package]]
+name = "try-lock"
+version = "0.2.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b"
+
[[package]]
name = "ttf-parser"
version = "0.25.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d2df906b07856748fa3f6e0ad0cbaa047052d4a7dd609e231c4f72cee8c36f31"
+[[package]]
+name = "tynm"
+version = "0.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a21cdb0fc8f85c98b1ec812bc4cd69faf6c0fa2fc17d44ea3c2cdd38dc08e999"
+dependencies = [
+ "nom 8.0.0",
+]
+
+[[package]]
+name = "type-map"
+version = "0.5.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cb30dbbd9036155e74adad6812e9898d03ec374946234fbcebd5dfc7b9187b90"
+dependencies = [
+ "rustc-hash 2.1.2",
+]
+
+[[package]]
+name = "typed-arena"
+version = "2.0.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6af6ae20167a9ece4bcb41af5b80f8a1f1df981f6391189ce00fd257af04126a"
+
[[package]]
name = "typenum"
version = "1.19.0"
@@ -4733,6 +6386,15 @@ version = "1.0.24"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75"
+[[package]]
+name = "unicode-normalization"
+version = "0.1.25"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5fd4f6878c9cb28d874b009da9e8d183b5abc80117c40bbd187a1fde336be6e8"
+dependencies = [
+ "tinyvec",
+]
+
[[package]]
name = "unicode-segmentation"
version = "1.13.3"
@@ -4762,12 +6424,42 @@ version = "0.2.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853"
+[[package]]
+name = "unicode_categories"
+version = "0.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "39ec24b3121d976906ece63c9daad25b85969647682eee313cb5779fdd69e14e"
+
[[package]]
name = "unindent"
version = "0.2.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7264e107f553ccae879d21fbea1d6724ac785e8c3bfc762137959b5802826ef3"
+[[package]]
+name = "untrusted"
+version = "0.9.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1"
+
+[[package]]
+name = "url"
+version = "2.5.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ff67a8a4397373c3ef660812acab3268222035010ab8680ec4215f38ba3d0eed"
+dependencies = [
+ "form_urlencoded",
+ "idna",
+ "percent-encoding",
+ "serde",
+]
+
+[[package]]
+name = "utf8_iter"
+version = "1.0.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be"
+
[[package]]
name = "utf8parse"
version = "0.2.2"
@@ -4792,6 +6484,17 @@ version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ba73ea9cf16a25df0c8caa16c51acb937d5712a8429db78a3ee29d5dcacd3a65"
+[[package]]
+name = "variadics_please"
+version = "1.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "41b6d82be61465f97d42bd1d15bf20f3b0a3a0905018f38f9d6f6962055b0b5c"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.117",
+]
+
[[package]]
name = "version-compare"
version = "0.1.1"
@@ -4804,6 +6507,12 @@ version = "0.9.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a"
+[[package]]
+name = "void"
+version = "1.0.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6a02e4885ed3bc0f2de90ea6dd45ebcbb66dacffe03547fadbb0eeae2770887d"
+
[[package]]
name = "vtparse"
version = "0.6.2"
@@ -4823,6 +6532,15 @@ dependencies = [
"winapi-util",
]
+[[package]]
+name = "want"
+version = "0.3.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bfa7760aed19e106de2c7c0b581b509f2f25d3dacaf737cb82ac61bc6d760b0e"
+dependencies = [
+ "try-lock",
+]
+
[[package]]
name = "wasi"
version = "0.11.1+wasi-snapshot-preview1"
@@ -5138,6 +6856,15 @@ dependencies = [
"wasm-bindgen",
]
+[[package]]
+name = "webpki-roots"
+version = "1.0.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bf85cb06032201fa7c6f829d7db5a7e5aa45bcc0655327713065f6f0576731bf"
+dependencies = [
+ "rustls-pki-types",
+]
+
[[package]]
name = "wezterm-bidi"
version = "0.2.3"
@@ -5262,7 +6989,7 @@ dependencies = [
"portable-atomic",
"profiling",
"raw-window-handle",
- "rustc-hash",
+ "rustc-hash 1.1.0",
"smallvec",
"thiserror 2.0.18",
"wgpu-core-deps-apple",
@@ -5947,6 +7674,12 @@ dependencies = [
"syn 2.0.117",
]
+[[package]]
+name = "writeable"
+version = "0.6.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1ffae5123b2d3fc086436f8834ae3ab053a283cfac8fe0a0b8eaae044768a4c4"
+
[[package]]
name = "x11-dl"
version = "2.21.0"
@@ -5979,6 +7712,16 @@ version = "0.13.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ea6fc2961e4ef194dcbfe56bb845534d0dc8098940c7e5c012a258bfec6701bd"
+[[package]]
+name = "xattr"
+version = "1.6.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "32e45ad4206f6d2479085147f02bc2ef834ac85886624a23575ae137c8aa8156"
+dependencies = [
+ "libc",
+ "rustix 1.1.4",
+]
+
[[package]]
name = "xcursor"
version = "0.3.10"
@@ -6010,6 +7753,35 @@ version = "0.8.28"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3ae8337f8a065cfc972643663ea4279e04e7256de865aa66fe25cec5fb912d3f"
+[[package]]
+name = "xxhash-rust"
+version = "0.8.15"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "fdd20c5420375476fbd4394763288da7eb0cc0b8c11deed431a91562af7335d3"
+
+[[package]]
+name = "yoke"
+version = "0.8.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "709fe23a0424b6a435d82152b1bd3fdfb0833487d5fa90d05d42762a9891fef5"
+dependencies = [
+ "stable_deref_trait",
+ "yoke-derive",
+ "zerofrom",
+]
+
+[[package]]
+name = "yoke-derive"
+version = "0.8.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "de844c262c8848816172cef550288e7dc6c7b7814b4ee56b3e1553f275f1858e"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.117",
+ "synstructure",
+]
+
[[package]]
name = "zerocopy"
version = "0.8.48"
@@ -6030,6 +7802,66 @@ dependencies = [
"syn 2.0.117",
]
+[[package]]
+name = "zerofrom"
+version = "0.1.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0ec05a11813ea801ff6d75110ad09cd0824ddba17dfe17128ea0d5f68e6c5272"
+dependencies = [
+ "zerofrom-derive",
+]
+
+[[package]]
+name = "zerofrom-derive"
+version = "0.1.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "11532158c46691caf0f2593ea8358fed6bbf68a0315e80aae9bd41fbade684a1"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.117",
+ "synstructure",
+]
+
+[[package]]
+name = "zeroize"
+version = "1.9.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e13c156562582aa81c60cb29407084cdb54c4164760106ab78e6c5b0858cf64e"
+
+[[package]]
+name = "zerotrie"
+version = "0.2.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0f9152d31db0792fa83f70fb2f83148effb5c1f5b8c7686c3459e361d9bc20bf"
+dependencies = [
+ "displaydoc",
+ "yoke",
+ "zerofrom",
+]
+
+[[package]]
+name = "zerovec"
+version = "0.11.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "90f911cbc359ab6af17377d242225f4d75119aec87ea711a880987b18cd7b239"
+dependencies = [
+ "yoke",
+ "zerofrom",
+ "zerovec-derive",
+]
+
+[[package]]
+name = "zerovec-derive"
+version = "0.11.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "625dc425cab0dca6dc3c3319506e6593dcb08a9f387ea3b284dbd52a92c40555"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.117",
+]
+
[[package]]
name = "zip"
version = "2.4.2"
diff --git a/rust_prototype/Cargo.toml b/rust_prototype/Cargo.toml
index 50e978a..3f8c974 100644
--- a/rust_prototype/Cargo.toml
+++ b/rust_prototype/Cargo.toml
@@ -16,6 +16,14 @@ faer = "0.22"
ndarray = "0.16"
serde = { version = "1", features = ["derive"] }
cudarc = { version = "0.19", features = ["cuda-version-from-build-system", "cublas", "nccl"] }
+# CubeCL — write GPU kernels once in Rust, compile to CUDA (NVIDIA),
+# HIP/ROCm (AMD), Vulkan/Metal/WebGPU (cross-vendor). The `wgpu`
+# runtime is always on so the geometry preview renders on any GPU
+# without a vendor SDK; the `cuda` runtime is additive for NVIDIA
+# fast paths. Alpha crate — pinned exact. This is the migration
+# target that replaces the hand-maintained gpu/cuda/*.cu kernels +
+# cudarc (see the transport-port + cudarc-removal tasks).
+cubecl = { version = "=0.10.0", features = ["wgpu"] }
rayon = "1"
clap = { version = "4", features = ["derive"] }
thiserror = "2"
@@ -63,6 +71,7 @@ thiserror = { workspace = true }
toml = { workspace = true }
dashmap = { workspace = true }
raw-cpuid = { workspace = true }
+cubecl = { workspace = true }
cudarc = { workspace = true, optional = true }
# Direct dep on minifb for the preview_scene binary's cursor-aware
# event loop. The upstream `rust_mc_sim::preview::show_window` zooms
@@ -132,7 +141,10 @@ indicatif = "0.17"
[features]
default = []
-cuda = ["cudarc"]
+# `cuda` enables both the legacy cudarc/.cu backend AND CubeCL's CUDA
+# runtime (so the same #[cube] kernels can compile to CUDA C, not just
+# SPIR-V — this dodges the Vulkan-only private-Array bug cubecl#1336).
+cuda = ["cudarc", "cubecl/cuda"]
preview = ["rust-mc-sim/preview", "dep:minifb"]
# In-terminal cross-section viewer for `preview_scene --tui`. Pulls in
# ratatui (+ its re-exported crossterm). Independent of `preview` — the
@@ -162,6 +174,10 @@ workspace = true
# --- Binaries ---
+[[bin]]
+name = "cubecl_vs_cu_keff"
+required-features = ["cuda"]
+
[[bin]]
name = "gpu_photon_features"
required-features = ["cuda"]
diff --git a/rust_prototype/src/bin/cubecl_vs_cu_keff.rs b/rust_prototype/src/bin/cubecl_vs_cu_keff.rs
new file mode 100644
index 0000000..76c1140
--- /dev/null
+++ b/rust_prototype/src/bin/cubecl_vs_cu_keff.rs
@@ -0,0 +1,163 @@
+// SPDX-License-Identifier: MIT
+//! A/B/C const-XS k comparison: CPU reference vs legacy CUDA `.cu`
+//! (`GpuRecursiveContext::const_xs_transport`) vs the new CubeCL kernel
+//! (`gpu_transport_cubecl::const_xs_transport` on the CUDA runtime), on
+//! the SAME 2×2 reflective lattice, same constant cross-sections, same
+//! per-particle RNG seeds.
+//!
+//! `k = fissions / source particle` (one-generation multiplication).
+//! The three legs are independent MC realizations (same seeds, but
+//! collision-vs-surface ties flip on float rounding across
+//! implementations), so they agree within MC noise, not bit-for-bit.
+//! This proves the CubeCL port reproduces the legacy `.cu` physics.
+//!
+//! cargo run --release --features cuda --bin cubecl_vs_cu_keff
+
+#[cfg(not(feature = "cuda"))]
+fn main() {
+ eprintln!("ERROR: requires the 'cuda' feature.");
+ eprintln!("cargo run --release --features cuda --bin cubecl_vs_cu_keff");
+ std::process::exit(1);
+}
+
+#[cfg(feature = "cuda")]
+fn main() {
+ cuda_main::run();
+}
+
+#[cfg(feature = "cuda")]
+mod cuda_main {
+ use std::time::Instant;
+
+ use open_rust_mc::geometry::cell::{self, Cell, CellFill, CellId, Region};
+ use open_rust_mc::geometry::lattice::RectLattice;
+ use open_rust_mc::geometry::surface::BoundaryCondition;
+ use open_rust_mc::geometry::universe::{Universe, UniverseId};
+ use open_rust_mc::geometry::{Aabb, Geometry, Vec3};
+ use open_rust_mc::geometry::flat::build_host_tables;
+ use open_rust_mc::gpu_recursive::{ConstXs as CuConstXs, GpuRecursiveContext};
+ use open_rust_mc::gpu_transport_cubecl::{
+ self as cc, ConstXs as CcConstXs,
+ };
+ use rust_mc_sim::Pcg64;
+
+ /// Same 2×2 reflective lattice as `gpu_const_xs_keff`: a fissile pin
+ /// (mat 0) in a water-like scatterer (mat 1), 2×2, reflective box.
+ fn build_geometry() -> Geometry {
+ let mut surfaces = open_rust_mc::geometry::shapes::pin_cylinders(0.5, 0.5, &[0.3]);
+ let outer_box = open_rust_mc::geometry::shapes::rect_box(
+ [1.0, 1.0, 10.0],
+ BoundaryCondition::Reflective,
+ surfaces.len(),
+ );
+ surfaces.extend(outer_box.surfaces);
+ let cells = vec![
+ Cell::new(CellId(0), cell::inside(0), CellFill::Material(0)),
+ Cell::new(CellId(1), cell::outside(0), CellFill::Material(1)),
+ Cell::new(CellId(2), outer_box.inside.clone(), CellFill::Lattice(0))
+ .with_aabb(Aabb::new(Vec3::new(-1.0, -1.0, -10.0), Vec3::new(1.0, 1.0, 10.0))),
+ Cell::new(
+ CellId(3),
+ Region::Complement(Box::new(outer_box.inside)),
+ CellFill::Void,
+ ),
+ ];
+ let universes = vec![
+ Universe::new(UniverseId(0), vec![2, 3]),
+ Universe::new(UniverseId(1), vec![0, 1]),
+ ];
+ let lattices = vec![RectLattice {
+ origin: Vec3::new(-1.0, -1.0, -10.0),
+ pitch: Vec3::new(1.0, 1.0, 20.0),
+ shape: [2, 2, 1],
+ universes: vec![UniverseId(1); 4],
+ material_overrides: None,
+ }];
+ Geometry::new(surfaces, cells, universes, lattices, UniverseId(0)).expect("geometry")
+ }
+
+ pub fn run() {
+ let geom = build_geometry();
+
+ // Synthetic constant XS — identical for both backends.
+ let cu_mats = vec![
+ CuConstXs { sigma_t: 1.0, sigma_a: 0.5, sigma_f: 0.4, nu_bar: 2.0 },
+ CuConstXs { sigma_t: 0.5, sigma_a: 0.0, sigma_f: 0.0, nu_bar: 0.0 },
+ ];
+ let cc_mats = vec![
+ CcConstXs { sigma_t: 1.0, sigma_a: 0.5, sigma_f: 0.4, nu_bar: 2.0 },
+ CcConstXs { sigma_t: 0.5, sigma_a: 0.0, sigma_f: 0.0, nu_bar: 0.0 },
+ ];
+
+ let limits = open_rust_mc::transport::sim_limits::SimLimits::default();
+ let n = 200_000usize;
+ let max_events = limits.max_events_per_history as i32;
+ let fis_capacity = limits.fis_capacity(n);
+
+ // Shared source + per-particle RNG seeds.
+ let mut rng = Pcg64::new(0xCAFEBEEF, 0);
+ let mut positions = Vec::with_capacity(n);
+ let mut directions = Vec::with_capacity(n);
+ let mut seeds = Vec::with_capacity(n);
+ for i in 0..n {
+ let x = -1.0 + 2.0 * rng.uniform();
+ let y = -1.0 + 2.0 * rng.uniform();
+ let z = -1.0 + 2.0 * rng.uniform();
+ let (dx, dy, dz) = rng.isotropic_direction();
+ positions.push((x, y, z));
+ directions.push((dx, dy, dz));
+ let p = Pcg64::for_particle(0, i as u64);
+ seeds.push((p.state(), p.stream()));
+ }
+
+ println!("=== const-XS k: legacy .cu vs CubeCL, 2×2 lattice ===");
+ println!(" particles = {n}, fissile k_inf ≈ 1.6\n");
+
+ // Leg A — legacy CUDA .cu.
+ let ctx = GpuRecursiveContext::build(&geom, n).expect("gpu ctx");
+ let t = Instant::now();
+ let cu = ctx
+ .const_xs_transport(&positions, &directions, &seeds, &cu_mats, max_events, fis_capacity)
+ .expect("legacy .cu transport");
+ let cu_ms = t.elapsed().as_secs_f64() * 1000.0;
+ let k_cu = cu.n_fissions as f64 / n as f64;
+
+ // Leg B — CubeCL on the CUDA runtime.
+ let tables = build_host_tables(&geom);
+ let packed = cc::pack_transport(&tables, &geom, &cc_mats).expect("cubecl pack_transport");
+ let device = cubecl::cuda::CudaDevice::default();
+ let t = Instant::now();
+ let ccb = cc::const_xs_transport::(
+ &device, &packed, &positions, &directions, &seeds, max_events as u32, fis_capacity,
+ )
+ .expect("cubecl transport");
+ let cc_ms = t.elapsed().as_secs_f64() * 1000.0;
+ let k_cc = ccb.n_fissions as f64 / n as f64;
+
+ let row = |label: &str, b: &str, coll: u64, abs: u64, fis: u64, leak: u64, k: f64, ms: f64| {
+ println!(
+ " {label:<12} [{b}] coll={coll:>9} abs={abs:>9} fis={fis:>9} leak={leak:>7} k={k:.5} {ms:.0} ms"
+ );
+ };
+ row("legacy .cu", "CUDA", cu.n_collisions, cu.n_absorptions, cu.n_fissions, cu.n_leakage, k_cu, cu_ms);
+ row("CubeCL", "CUDA", ccb.n_collisions, ccb.n_absorptions, ccb.n_fissions, ccb.n_leakage, k_cc, cc_ms);
+
+ let dk_pcm = (k_cu - k_cc).abs() * 1e5;
+ // 1σ on k ≈ sqrt(fissions)/n; a few-σ envelope is the gate.
+ let sigma_pcm = (cu.n_fissions as f64).sqrt() / n as f64 * 1e5;
+ println!(
+ "\n |Δk| = {dk_pcm:.0} pcm (1σ_MC ≈ {sigma_pcm:.0} pcm) {}",
+ if dk_pcm <= 5.0 * sigma_pcm.max(1.0) {
+ "PASS — within MC noise"
+ } else {
+ "FAIL — outside few-σ envelope"
+ }
+ );
+ // Bank size should also track.
+ println!(
+ " fission bank: legacy {} vs CubeCL {}",
+ cu.fission_sites.len(),
+ ccb.fission_sites.len()
+ );
+ }
+}
diff --git a/rust_prototype/src/bin/metal_stats_diag.rs b/rust_prototype/src/bin/metal_stats_diag.rs
index 2ade8ff..a46fb25 100644
--- a/rust_prototype/src/bin/metal_stats_diag.rs
+++ b/rust_prototype/src/bin/metal_stats_diag.rs
@@ -490,6 +490,11 @@ fn main() -> Result<(), Box> {
// low-absorption reflectors (Be), losing ~8% of thermal scatters
// and biasing k low. Raise via `maxev=N` to test/fix.
let mut arg_maxev: i32 = 10_000;
+ // Per-case OpenMC reference path (`openmc=`); consumed only in
+ // the cuda-gated comparison block below, so the binding is unused on
+ // a default build.
+ #[cfg_attr(not(feature = "cuda"), allow(unused_assignments, unused_variables))]
+ let mut arg_openmc: Option = None;
for a in std::env::args().skip(2) {
if let Some(v) = a.strip_prefix("b=") {
arg_b = v.parse().unwrap_or(arg_b);
@@ -511,6 +516,11 @@ fn main() -> Result<(), Box> {
arg_nxn_mode = 1; // legacy bank-as-fission
} else if let Some(v) = a.strip_prefix("maxev=") {
arg_maxev = v.parse().unwrap_or(arg_maxev);
+ } else if let Some(v) = a.strip_prefix("openmc=") {
+ #[cfg_attr(not(feature = "cuda"), allow(unused_assignments))]
+ {
+ arg_openmc = Some(v.to_string());
+ }
}
}
eprintln!(
diff --git a/rust_prototype/src/bin/preview_scene.rs b/rust_prototype/src/bin/preview_scene.rs
index 4ec86dd..cd77743 100644
--- a/rust_prototype/src/bin/preview_scene.rs
+++ b/rust_prototype/src/bin/preview_scene.rs
@@ -230,6 +230,16 @@ struct Args {
#[arg(long = "render3d-out")]
render3d_out: Option,
+ /// Headless 3D render via the **CubeCL** GPU ray-caster (no window).
+ /// Same analytic-CSG walk as `--render3d-out` but runs the kernel on
+ /// the GPU through CubeCL — cross-vendor (Vulkan / Metal / DX12 on
+ /// any NVIDIA/AMD/Intel card, plus native CUDA/ROCm), in f64. This
+ /// is the production 3D path; the CPU `--render3d-out` is the
+ /// reference fallback. Honours `--cam-azim` / `--cam-elev` / `--zoom`
+ /// / `--resolution`.
+ #[arg(long = "cubecl-out")]
+ cubecl_out: Option,
+
/// Camera azimuth (degrees around +z) for the 3D view's initial /
/// headless angle.
#[arg(long, default_value_t = 35.0)]
@@ -1418,9 +1428,107 @@ fn spawn_in_new_window() -> bool {
}
}
+/// Headless 3D render via the CubeCL cross-vendor GPU ray-caster.
+/// Mirrors `render3d::render_to_png`'s framing (orbit camera around the
+/// scene AABB, `--zoom` scales the distance) but runs the analytic-CSG
+/// walk on the GPU in f64. Writes a PNG.
+fn render_cubecl_to_png(args: &Args, out: &Path) {
+ use open_rust_mc::geometry::flat::build_host_tables;
+ use open_rust_mc::gpu_render::{CameraParams, pack_scene, render_rgb_wgpu};
+
+ let LoadedPreview {
+ geometry,
+ palette,
+ names,
+ ..
+ } = load_preview(args);
+ let opaque = render3d::opaque_mask(&names);
+ // Per-material haze (alpha-per-cm) for the transparent fluids:
+ // air / void / vacuum stay perfectly clear (0); water-like
+ // moderators / coolants / solutions get a faint absorption so the
+ // ray accumulates a tint through them and depth reads. Tuned so a
+ // typical pin pitch (~1-2 cm) stays nearly clear while a ~10 cm
+ // water span visibly dims — fuel structure shows through but the
+ // tank still reads as filled.
+ let absorb: Vec = names
+ .iter()
+ .map(|n| {
+ let n = n.to_lowercase();
+ if n.contains("air") || n.contains("void") || n.contains("vacuum") {
+ 0.0
+ } else if n.contains("water")
+ || n.contains("h2o")
+ || n.contains("d2o")
+ || n.contains("moderator")
+ || n.contains("coolant")
+ || n.contains("solution")
+ || n.contains("borated")
+ {
+ 0.045
+ } else {
+ 0.0
+ }
+ })
+ .collect();
+ let tables = build_host_tables(&geometry);
+ let packed = pack_scene(&tables, &geometry, &palette, &opaque, &absorb)
+ .expect("pack_scene (does this scene use an unsupported per-cell rotation?)");
+
+ let target = [
+ (packed.aabb_min[0] + packed.aabb_max[0]) * 0.5,
+ (packed.aabb_min[1] + packed.aabb_max[1]) * 0.5,
+ (packed.aabb_min[2] + packed.aabb_max[2]) * 0.5,
+ ];
+ let ex = packed.aabb_max[0] - packed.aabb_min[0];
+ let ey = packed.aabb_max[1] - packed.aabb_min[1];
+ let ez = packed.aabb_max[2] - packed.aabb_min[2];
+ let radius = (ex * ex + ey * ey + ez * ez).sqrt() * 0.9 * args.zoom;
+ let cam = CameraParams::orbit(
+ target,
+ args.cam_azim.to_radians(),
+ args.cam_elev.to_radians(),
+ radius,
+ 45.0,
+ 1.0,
+ );
+
+ let w = args.resolution;
+ let h = args.resolution;
+ let pixels = render_rgb_wgpu(&packed, &cam, w, h);
+
+ // u32 0xRRGGBB → RGB8 and write PNG.
+ let rgb: Vec<[u8; 3]> = pixels
+ .iter()
+ .map(|&c| [((c >> 16) & 0xff) as u8, ((c >> 8) & 0xff) as u8, (c & 0xff) as u8])
+ .collect();
+ let file = std::fs::File::create(out)
+ .unwrap_or_else(|e| panic!("create {}: {e}", out.display()));
+ let mut encoder = png::Encoder::new(std::io::BufWriter::new(file), w, h);
+ encoder.set_color(png::ColorType::Rgb);
+ encoder.set_depth(png::BitDepth::Eight);
+ let mut writer = encoder.write_header().expect("png header");
+ let flat: &[u8] = bytemuck::cast_slice(&rgb);
+ writer.write_image_data(flat).expect("png data");
+ eprintln!(
+ "wrote {} ({}×{}) [CubeCL/GPU] azim={:.1}° elev={:.1}° zoom={}",
+ out.display(),
+ w,
+ h,
+ args.cam_azim,
+ args.cam_elev,
+ args.zoom
+ );
+}
+
fn main() {
let args = Args::parse();
+ // Headless 3D ray-cast render to PNG via CubeCL (cross-vendor GPU).
+ if let Some(out) = args.cubecl_out.as_deref() {
+ render_cubecl_to_png(&args, out);
+ return;
+ }
+
// Headless 3D ray-cast render to PNG (works without `preview`).
if let Some(out) = args.render3d_out.as_deref() {
render3d::render_to_png(&args, out);
@@ -2217,15 +2325,30 @@ mod render3d {
}
}
- /// `true` for materials that should be drawn solid. Air / void /
- /// vacuum (and the no-material `Void` fill) are transparent so the
- /// camera sees the objects suspended inside them.
+ /// `true` for materials that should be drawn solid. Transparent
+ /// materials let the camera see the structure suspended inside them:
+ /// not just air / void / vacuum, but also the moderating / coolant
+ /// **fluids** that otherwise wrap every lattice scene in a solid
+ /// block (water, heavy water, the borated coolant, fissile
+ /// solutions). Without this a 17×17 assembly or an LCT rod cluster
+ /// renders as an opaque striped cube; with it the fuel-pin forest
+ /// reads in 3D — the standard reactor-geometry preview convention.
pub(crate) fn opaque_mask(names: &[String]) -> Vec {
names
.iter()
.map(|n| {
let n = n.to_lowercase();
- !(n.contains("air") || n.contains("void") || n.contains("vacuum"))
+ let transparent = n.contains("air")
+ || n.contains("void")
+ || n.contains("vacuum")
+ || n.contains("water")
+ || n.contains("h2o")
+ || n.contains("d2o")
+ || n.contains("moderator")
+ || n.contains("coolant")
+ || n.contains("solution")
+ || n.contains("borated");
+ !transparent
})
.collect()
}
@@ -2510,7 +2633,11 @@ mod render3d {
let opaque = opaque_mask(&names);
let aabb = scene_aabb(&geometry);
let target = (aabb.0 + aabb.1) * 0.5;
- let radius = (aabb.1 - aabb.0).length() * 0.9;
+ // `--zoom` scales the orbit radius (same role as the 2D path):
+ // factors < 1 push the camera in to frame a fixture suspended
+ // inside a large containment sphere / pool, where the raw AABB
+ // diagonal would otherwise shrink it to a speck.
+ let radius = (aabb.1 - aabb.0).length() * 0.9 * args.zoom;
let cam = Camera::orbit(
target,
args.cam_azim.to_radians(),
diff --git a/rust_prototype/src/geometry/flat.rs b/rust_prototype/src/geometry/flat.rs
new file mode 100644
index 0000000..221d85a
--- /dev/null
+++ b/rust_prototype/src/geometry/flat.rs
@@ -0,0 +1,424 @@
+// SPDX-License-Identifier: MIT
+//! Backend-agnostic flattening of a recursive [`Geometry`] into
+//! struct-of-arrays (SoA) tables.
+//!
+//! A `Geometry` is a tree of universes, cells, CSG region trees, and
+//! (rect / hex) lattices connected by indices. GPU kernels can't chase
+//! `Vec>` pointers, so this module linearises the whole thing
+//! into flat `Vec`s with explicit `off` / `len` index pairs — the
+//! layout every device backend consumes.
+//!
+//! This used to live inside the `cuda`-gated `gpu_recursive` module.
+//! It was lifted out (no behaviour change) so the CubeCL renderer and
+//! the legacy CUDA path share **one** definition of the on-device
+//! layout: the tag constants below are the single source of truth, and
+//! both `gpu/cuda/geom_recursive.cu` (`GR_*`) and the CubeCL kernel
+//! must agree with them.
+//!
+//! Pure: no GPU, no `unsafe`, no feature gate. `build_host_tables` is a
+//! deterministic function of the `Geometry` alone.
+
+use crate::geometry::cell::{CellFill, Region};
+use crate::geometry::surface::{BoundaryCondition, Surface};
+use crate::geometry::{Geometry, Vec3};
+
+// ── Tag constants — must match `gpu/cuda/geom_recursive.cu` (GR_*) and
+// the CubeCL kernel. Single source of truth. ──────────────────────
+
+pub const SURF_PLANE_X: i32 = 0;
+pub const SURF_PLANE_Y: i32 = 1;
+pub const SURF_PLANE_Z: i32 = 2;
+pub const SURF_SPHERE: i32 = 3;
+pub const SURF_CYL_Z: i32 = 4;
+pub const SURF_CYL_X: i32 = 5;
+pub const SURF_CYL_Y: i32 = 6;
+pub const SURF_PLANE_GENERAL: i32 = 7;
+
+pub const BC_TRANSMISSION: i32 = 0;
+pub const BC_VACUUM: i32 = 1;
+pub const BC_REFLECTIVE: i32 = 2;
+
+pub const REGION_HALFSPACE_POS: i32 = 0;
+pub const REGION_HALFSPACE_NEG: i32 = 1;
+pub const REGION_INTERSECTION: i32 = 2;
+pub const REGION_UNION: i32 = 3;
+pub const REGION_COMPLEMENT: i32 = 4;
+
+pub const FILL_MATERIAL: i32 = 0;
+pub const FILL_VOID: i32 = 1;
+pub const FILL_UNIVERSE: i32 = 2;
+pub const FILL_LATTICE: i32 = 3;
+pub const FILL_HEX_LATTICE: i32 = 4;
+
+// Hex orientation discriminants — match CUDA `GR_HEX_ORIENT_*`.
+pub const HEX_ORIENT_Y: i32 = 0;
+pub const HEX_ORIENT_X: i32 = 1;
+
+/// Doubles packed per surface in [`HostTables::surf_params`]. Fixed
+/// stride so a device kernel indexes `surf_params[s_idx * 8 + k]`.
+pub const SURF_PARAM_STRIDE: usize = 8;
+
+// ── Host-side SoA tables ────────────────────────────────────────────
+
+/// Flat SoA view of a `Geometry`, ready to upload to any GPU backend.
+///
+/// Index conventions (mirrored in every device walk):
+/// - `surf_params` holds [`SURF_PARAM_STRIDE`] f64 per surface.
+/// - `cell_aabb_min` / `cell_aabb_max` hold 3 f64 per cell.
+/// - per-cell region opcodes live at `region_op[cell_region_off[c] ..
+/// + cell_region_len[c]]` (postfix CSG stack machine).
+/// - per-universe cell / surface index lists use the matching
+/// `univ_*_off` / `univ_*_len` pairs into `univ_cell_indices` /
+/// `univ_surface_indices`.
+/// - lattice / hex universe arrays are flattened with `*_universes_off`.
+#[derive(Default)]
+pub struct HostTables {
+ pub surf_type: Vec,
+ pub surf_params: Vec, // SURF_PARAM_STRIDE doubles per surface
+ pub surf_bc: Vec,
+
+ pub cell_region_off: Vec,
+ pub cell_region_len: Vec,
+ pub cell_fill_type: Vec,
+ pub cell_fill_data: Vec,
+ pub cell_aabb_min: Vec, // 3 doubles per cell
+ pub cell_aabb_max: Vec,
+
+ pub region_op: Vec,
+ pub region_arg: Vec,
+
+ pub univ_cells_off: Vec,
+ pub univ_cells_len: Vec,
+ pub univ_surfaces_off: Vec,
+ pub univ_surfaces_len: Vec,
+ pub univ_cell_indices: Vec,
+ pub univ_surface_indices: Vec,
+
+ pub lat_origin: Vec,
+ pub lat_pitch: Vec,
+ pub lat_shape: Vec,
+ pub lat_universes_off: Vec,
+ pub lat_universes: Vec,
+ // Hex lattice SoA — parallel to the rect arrays. The `n_*`
+ // counters match the layout in `geom_recursive.cu::GrGeometry`.
+ pub hex_center: Vec,
+ pub hex_pitch_xy: Vec,
+ pub hex_pitch_z: Vec,
+ pub hex_n_rings: Vec,
+ pub hex_n_axial: Vec,
+ pub hex_orientation: Vec,
+ pub hex_universes_off: Vec,
+ pub hex_universes: Vec,
+}
+
+/// Pack one surface's parameters into `params_out` (stride-8) and its
+/// boundary condition into `bc_out`; return the surface type tag.
+///
+/// Cones are not yet supported on the recursive-GPU path (the ICSBEP
+/// scenes that exercise it use PlaneX/Y/Z + CylinderZ + Sphere); a cone
+/// packs as a sentinel `-1` type so the device walk yields a huge eval
+/// and the parity test flags it as a mismatch rather than silently
+/// rendering garbage.
+pub fn pack_surface(s: &Surface, params_out: &mut Vec, bc_out: &mut Vec) -> i32 {
+ fn push8(v: &mut Vec, slots: [f64; SURF_PARAM_STRIDE]) {
+ v.extend_from_slice(&slots);
+ }
+ let bc_int = |bc: BoundaryCondition| match bc {
+ BoundaryCondition::Transmission => BC_TRANSMISSION,
+ BoundaryCondition::Vacuum => BC_VACUUM,
+ BoundaryCondition::Reflective => BC_REFLECTIVE,
+ };
+ match *s {
+ Surface::PlaneX { x0, bc } => {
+ push8(params_out, [x0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0]);
+ bc_out.push(bc_int(bc));
+ SURF_PLANE_X
+ }
+ Surface::PlaneY { y0, bc } => {
+ push8(params_out, [y0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0]);
+ bc_out.push(bc_int(bc));
+ SURF_PLANE_Y
+ }
+ Surface::PlaneZ { z0, bc } => {
+ push8(params_out, [z0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0]);
+ bc_out.push(bc_int(bc));
+ SURF_PLANE_Z
+ }
+ Surface::Sphere { center, radius, bc } => {
+ push8(
+ params_out,
+ [center.x, center.y, center.z, radius, 0.0, 0.0, 0.0, 0.0],
+ );
+ bc_out.push(bc_int(bc));
+ SURF_SPHERE
+ }
+ Surface::CylinderZ {
+ center_x,
+ center_y,
+ radius,
+ bc,
+ } => {
+ push8(
+ params_out,
+ [center_x, center_y, radius, 0.0, 0.0, 0.0, 0.0, 0.0],
+ );
+ bc_out.push(bc_int(bc));
+ SURF_CYL_Z
+ }
+ Surface::CylinderX {
+ center_y,
+ center_z,
+ radius,
+ bc,
+ } => {
+ push8(
+ params_out,
+ [center_y, center_z, radius, 0.0, 0.0, 0.0, 0.0, 0.0],
+ );
+ bc_out.push(bc_int(bc));
+ SURF_CYL_X
+ }
+ Surface::CylinderY {
+ center_x,
+ center_z,
+ radius,
+ bc,
+ } => {
+ push8(
+ params_out,
+ [center_x, center_z, radius, 0.0, 0.0, 0.0, 0.0, 0.0],
+ );
+ bc_out.push(bc_int(bc));
+ SURF_CYL_Y
+ }
+ Surface::Plane { normal, offset, bc } => {
+ push8(
+ params_out,
+ [normal.x, normal.y, normal.z, offset, 0.0, 0.0, 0.0, 0.0],
+ );
+ bc_out.push(bc_int(bc));
+ SURF_PLANE_GENERAL
+ }
+ // Cones not yet supported on the recursive-GPU path — sentinel.
+ _ => {
+ push8(params_out, [0.0; SURF_PARAM_STRIDE]);
+ bc_out.push(BC_VACUUM);
+ -1
+ }
+ }
+}
+
+/// Walk a CSG region tree and emit postfix opcodes into `op` / `arg`.
+pub fn flatten_region(region: &Region, op: &mut Vec, arg: &mut Vec) {
+ match region {
+ Region::HalfSpace {
+ surface_idx,
+ positive,
+ } => {
+ op.push(if *positive {
+ REGION_HALFSPACE_POS
+ } else {
+ REGION_HALFSPACE_NEG
+ });
+ arg.push(*surface_idx as i32);
+ }
+ Region::Intersection(a, b) => {
+ flatten_region(a, op, arg);
+ flatten_region(b, op, arg);
+ op.push(REGION_INTERSECTION);
+ arg.push(0);
+ }
+ Region::Union(a, b) => {
+ flatten_region(a, op, arg);
+ flatten_region(b, op, arg);
+ op.push(REGION_UNION);
+ arg.push(0);
+ }
+ Region::Complement(a) => {
+ flatten_region(a, op, arg);
+ op.push(REGION_COMPLEMENT);
+ arg.push(0);
+ }
+ }
+}
+
+/// Clamp infinite AABB bounds to a large finite box so device slab
+/// tests stay numerically sane (an unbounded outer cell has ±∞ extent).
+pub fn finite_aabb(lo: Vec3, hi: Vec3) -> ([f64; 3], [f64; 3]) {
+ let clamp = |v: f64| {
+ if v.is_finite() {
+ v
+ } else if v > 0.0 {
+ 1e20
+ } else {
+ -1e20
+ }
+ };
+ (
+ [clamp(lo.x), clamp(lo.y), clamp(lo.z)],
+ [clamp(hi.x), clamp(hi.y), clamp(hi.z)],
+ )
+}
+
+/// Reject per-cell `Mat3` rotation, which the flattened SoA walk
+/// (CubeCL renderer / transport, and the legacy CUDA recursive
+/// descent) cannot represent, instead of silently mis-transporting or
+/// mis-rendering it.
+///
+/// CPU recursive descent applies rotations in `CoordStack::local_pos`
+/// / `local_dir` (`geometry/coord.rs`, `geometry/ray.rs`); every GPU
+/// walk descends axis-aligned only and never applies a rotation
+/// matrix during cell entry.
+///
+/// Call this before packing/uploading a scene to any CubeCL backend.
+/// [`check_gpu_supported`] also calls this and additionally rejects
+/// hex lattices, for backends (transport) where a hex cell resolving
+/// as a silent leak would corrupt physics results — the renderer
+/// instead documents hex as a contained, non-silent v1 limitation
+/// (renders as background), so it calls this rotation-only check
+/// directly rather than `check_gpu_supported`.
+pub fn check_gpu_rotation_supported(geom: &Geometry) -> Result<(), String> {
+ if let Some((cell_idx, _)) = geom
+ .cells
+ .iter()
+ .enumerate()
+ .find(|(_, c)| c.rotation.is_some())
+ {
+ return Err(format!(
+ "GPU geometry walk does not support per-cell Mat3 \
+ rotations yet (cell index {cell_idx} has `rotation = \
+ Some(...)`). CPU recursive descent applies rotations in \
+ `CoordStack::local_pos / local_dir`; the GPU descent path \
+ has no equivalent. Use Runner.Cpu for this scene or \
+ remove the cell rotation."
+ ));
+ }
+ Ok(())
+}
+
+/// Reject geometry features the flattened SoA transport walk (CubeCL
+/// `gpu_transport_cubecl` / `gpu_ce_cubecl`) cannot represent, instead
+/// of silently mis-transporting them.
+///
+/// - **Per-cell `Mat3` rotation** — see [`check_gpu_rotation_supported`].
+/// - **`CellFill::HexLattice`**: the CubeCL walk (`gpu_cubecl_geom`)
+/// has no hex descent (only the legacy CUDA `.cu` `gr_find_cell`
+/// does); a hex cell would resolve as an immediate leak instead of
+/// the fill it names, silently under-transporting the source.
+///
+/// Call this before packing/uploading a scene to `pack_transport` /
+/// `pack_ce_full`.
+pub fn check_gpu_supported(geom: &Geometry) -> Result<(), String> {
+ check_gpu_rotation_supported(geom)?;
+ if let Some((cell_idx, _)) = geom
+ .cells
+ .iter()
+ .enumerate()
+ .find(|(_, c)| matches!(c.fill, CellFill::HexLattice(_)))
+ {
+ return Err(format!(
+ "GPU geometry walk does not support CellFill::HexLattice \
+ yet (cell index {cell_idx}). Only the legacy CUDA \
+ recursive descent (`gr_find_cell` in geom_recursive.cu) \
+ implements hex-lattice traversal; the CubeCL walk \
+ (gpu_cubecl_geom) would resolve this cell as an immediate \
+ leak. Use Runner.Cpu / the legacy CUDA backend for hex \
+ scenes."
+ ));
+ }
+ Ok(())
+}
+
+/// Flatten a whole `Geometry` into [`HostTables`]. Deterministic and
+/// side-effect-free — the shared entry point for every GPU backend.
+pub fn build_host_tables(geom: &Geometry) -> HostTables {
+ use crate::geometry::lattice::HexOrientation;
+
+ let mut t = HostTables::default();
+
+ // Surfaces.
+ for s in &geom.surfaces {
+ let tag = pack_surface(s, &mut t.surf_params, &mut t.surf_bc);
+ t.surf_type.push(tag);
+ }
+
+ // Cells: region trees flattened, fill packed.
+ for c in &geom.cells {
+ let off = t.region_op.len() as i32;
+ flatten_region(&c.region, &mut t.region_op, &mut t.region_arg);
+ let len = t.region_op.len() as i32 - off;
+ t.cell_region_off.push(off);
+ t.cell_region_len.push(len);
+ let (ft, fd) = match c.fill {
+ CellFill::Material(m) => (FILL_MATERIAL, m as i32),
+ CellFill::Void => (FILL_VOID, 0),
+ CellFill::Universe(u) => (FILL_UNIVERSE, u as i32),
+ CellFill::Lattice(l) => (FILL_LATTICE, l as i32),
+ CellFill::HexLattice(h) => (FILL_HEX_LATTICE, h as i32),
+ };
+ t.cell_fill_type.push(ft);
+ t.cell_fill_data.push(fd);
+ let (lo, hi) = finite_aabb(c.aabb.min, c.aabb.max);
+ t.cell_aabb_min.extend_from_slice(&lo);
+ t.cell_aabb_max.extend_from_slice(&hi);
+ }
+
+ // Universes: flatten cell + surface index lists.
+ for (u_idx, u) in geom.universes.iter().enumerate() {
+ let c_off = t.univ_cell_indices.len() as i32;
+ for &ci in &u.cell_indices {
+ t.univ_cell_indices.push(ci as i32);
+ }
+ t.univ_cells_off.push(c_off);
+ t.univ_cells_len.push(u.cell_indices.len() as i32);
+
+ let s_off = t.univ_surface_indices.len() as i32;
+ for &si in &geom.universe_surfaces[u_idx] {
+ t.univ_surface_indices.push(si as i32);
+ }
+ t.univ_surfaces_off.push(s_off);
+ t.univ_surfaces_len
+ .push(geom.universe_surfaces[u_idx].len() as i32);
+ }
+
+ // Lattices: flatten universe arrays.
+ for lat in &geom.lattices {
+ t.lat_origin
+ .extend_from_slice(&[lat.origin.x, lat.origin.y, lat.origin.z]);
+ t.lat_pitch
+ .extend_from_slice(&[lat.pitch.x, lat.pitch.y, lat.pitch.z]);
+ t.lat_shape.extend_from_slice(&[
+ lat.shape[0] as i32,
+ lat.shape[1] as i32,
+ lat.shape[2] as i32,
+ ]);
+ let off = t.lat_universes.len() as i32;
+ for u in &lat.universes {
+ t.lat_universes.push(u.0 as i32);
+ }
+ t.lat_universes_off.push(off);
+ }
+
+ // Hex lattices: flatten parallel SoA. The CUDA `gr_hex_*` device
+ // functions consume the same per-element data layout as the CPU
+ // `HexLattice` struct.
+ for hex in &geom.hex_lattices {
+ t.hex_center
+ .extend_from_slice(&[hex.center.x, hex.center.y, hex.center.z]);
+ t.hex_pitch_xy.push(hex.pitch_xy);
+ t.hex_pitch_z.push(hex.pitch_z);
+ t.hex_n_rings.push(hex.n_rings as i32);
+ t.hex_n_axial.push(hex.n_axial as i32);
+ t.hex_orientation.push(match hex.orientation {
+ HexOrientation::Y => HEX_ORIENT_Y,
+ HexOrientation::X => HEX_ORIENT_X,
+ });
+ let off = t.hex_universes.len() as i32;
+ for u in &hex.universes {
+ t.hex_universes.push(u.0 as i32);
+ }
+ t.hex_universes_off.push(off);
+ }
+
+ t
+}
diff --git a/rust_prototype/src/geometry/mod.rs b/rust_prototype/src/geometry/mod.rs
index b797a48..470f40d 100644
--- a/rust_prototype/src/geometry/mod.rs
+++ b/rust_prototype/src/geometry/mod.rs
@@ -11,6 +11,7 @@ pub mod aabb;
pub mod bvh;
pub mod cell;
pub mod coord;
+pub mod flat;
pub mod lattice;
pub mod ray;
#[cfg(test)]
diff --git a/rust_prototype/src/gpu_ce_cubecl.rs b/rust_prototype/src/gpu_ce_cubecl.rs
new file mode 100644
index 0000000..b730003
--- /dev/null
+++ b/rust_prototype/src/gpu_ce_cubecl.rs
@@ -0,0 +1,2147 @@
+// SPDX-License-Identifier: MIT
+//! Continuous-energy cross-section lookup on CubeCL — foundation for a
+//! CE transport kernel (target: HEU-COMP-INTER, multi-nuclide
+//! fast/intermediate, no S(α,β)/URR).
+//!
+//! This module ports the genuinely new CE piece — per-nuclide pointwise
+//! cross-section data on the device, with a single binary search on the
+//! shared energy grid followed by log-log interpolation per reaction —
+//! exactly mirroring `SvdXsProvider::lookup` (`xs_provider.rs`). The
+//! transport loop on top reuses the geometry walk + collision structure
+//! already validated by the const-XS A/B.
+//!
+//! Like the const-XS kernel, it runs in f64. Per cubecl#1336 the heavy
+//! kernel only runs on the CUDA runtime (SPIR-V/Vulkan faults on large
+//! private-Array state); this lookup foundation is small enough to test
+//! on either, but the eventual transport kernel is CUDA-only until the
+//! upstream fix lands.
+//!
+//! ── Device data layout ──────────────────────────────────────────────
+//! One shared grid per nuclide. For each nuclide we upload:
+//! - `grid` : N_e energies (eV, ascending)
+//! - per reaction (elastic, fission, capture, inelastic, n2n): N_e σ
+//! values (barns); a reaction absent for a nuclide gets an all-zero
+//! slab so the kernel can read it unconditionally.
+//! All packed into one big f64 blob with per-nuclide offsets in an i32
+//! header, plus per-(material,nuclide) atom densities for the
+//! macroscopic sum.
+
+use cubecl::prelude::*;
+
+use crate::geometry::Geometry;
+use crate::geometry::flat::{check_gpu_supported, HostTables};
+use crate::gpu_cubecl_geom::{
+ cross_or_die, find_cell, trace_step, BC_TRANSMISSION, BC_VACUUM, FILL_MATERIAL,
+ MAX_DEPTH_USIZE,
+};
+
+/// CE reaction channels carried per nuclide. Order matters: it's the
+/// stride layout in the device blob.
+pub const N_RX: usize = 5;
+pub const RX_ELASTIC: usize = 0;
+pub const RX_FISSION: usize = 1;
+pub const RX_CAPTURE: usize = 2;
+pub const RX_INELASTIC: usize = 3;
+pub const RX_N2N: usize = 4;
+
+/// Host-side per-nuclide CE data, extracted from the resolved provider.
+#[derive(Clone)]
+pub struct NuclideCe {
+ /// Shared energy grid (eV, ascending).
+ pub grid: Vec,
+ /// `[N_RX][grid.len()]` reconstructed σ(E) per reaction (barns).
+ /// Reactions absent for this nuclide are all-zero.
+ pub xs: [Vec; N_RX],
+ /// Mass ratio A (for elastic CM kinematics later).
+ pub awr: f64,
+ /// ν̄ at a representative energy (constant approx for the first cut;
+ /// the transport kernel will interpolate a real ν̄(E) grid later).
+ pub nu_bar: f64,
+}
+
+/// Extract per-nuclide CE data from a resolved provider's nuclide
+/// kernels, reconstructing each reaction's σ on the nuclide's own grid.
+/// Reactions absent for a nuclide become all-zero slabs. Mirrors the
+/// reactions the device kernel carries (`N_RX`).
+pub fn extract_ce(
+ nuclides: &[std::sync::Arc],
+) -> Vec {
+ use crate::transport::xs_provider::ReactionKernel;
+
+ let recon = |k: &Option, grid: &[f64]| -> Vec {
+ match k {
+ Some(rk) => (0..grid.len()).map(|i| rk.reconstruct_at_index(i)).collect(),
+ None => vec![0.0; grid.len()],
+ }
+ };
+
+ nuclides
+ .iter()
+ .map(|nuc| {
+ // Shared grid = whichever reaction is present (elastic first).
+ let grid: Vec = nuc
+ .elastic
+ .as_ref()
+ .or(nuc.fission.as_ref())
+ .or(nuc.capture.as_ref())
+ .or(nuc.inelastic.as_ref())
+ .or(nuc.n2n.as_ref())
+ .map(|k| k.energies().to_vec())
+ .unwrap_or_default();
+ let xs = [
+ recon(&nuc.elastic, &grid),
+ recon(&nuc.fission, &grid),
+ recon(&nuc.capture, &grid),
+ recon(&nuc.inelastic, &grid),
+ recon(&nuc.n2n, &grid),
+ ];
+ NuclideCe {
+ grid,
+ xs,
+ awr: nuc.awr,
+ nu_bar: nuc.nu_bar_const,
+ }
+ })
+ .collect()
+}
+
+// ── Elastic angular distribution (tabulated μ) ──────────────────────
+//
+// CPU source: `NuclideKernels::elastic_angle: Option`
+// = { energies[N_e], distributions[N_e]: TabularMuDist{ mu, pdf, cdf,
+// histogram } }. Flattened for the device so the CubeCL sampler mirrors
+// `AngularDistribution::sample_mu` (bracket E, stochastic bin pick) +
+// `TabularMuDist::sample_with_xi` (linear or quadratic CDF inversion).
+
+/// Flat per-nuclide elastic angular data, device-ready.
+#[derive(Clone, Default)]
+pub struct AngularCe {
+ /// Incident energy grid (eV). Empty ⇒ isotropic fallback.
+ pub energies: Vec,
+ /// Per-energy μ-distribution slices into the flat mu/cdf/pdf arrays:
+ /// distribution `i` occupies `[dist_off[i] .. dist_off[i]+dist_len[i]]`.
+ pub dist_off: Vec,
+ pub dist_len: Vec,
+ /// `1` if histogram interpolation (linear CDF), `0` if lin-lin
+ /// (quadratic CDF inversion). Per energy point.
+ pub histogram: Vec,
+ /// Flat concatenated breakpoint arrays across all energy points.
+ pub mu: Vec,
+ pub cdf: Vec,
+ pub pdf: Vec,
+}
+
+/// Extract elastic angular data per nuclide (parallel to `extract_ce`).
+/// Nuclides with no elastic angular distribution get an empty
+/// `AngularCe` (isotropic fallback at sample time).
+pub fn extract_angular(
+ nuclides: &[std::sync::Arc],
+) -> Vec {
+ nuclides
+ .iter()
+ .map(|nuc| {
+ let Some(ang) = nuc.elastic_angle.as_ref() else {
+ return AngularCe::default();
+ };
+ let mut out = AngularCe {
+ energies: ang.energies.clone(),
+ ..Default::default()
+ };
+ for d in &ang.distributions {
+ out.dist_off.push(out.mu.len() as i32);
+ out.dist_len.push(d.mu.len() as i32);
+ out.histogram.push(i32::from(d.histogram));
+ out.mu.extend_from_slice(&d.mu);
+ out.cdf.extend_from_slice(&d.cdf);
+ // pdf may be empty (reader left it out) → pad to mu len
+ // with zeros so the device can read it unconditionally;
+ // the sampler treats all-zero pdf as the linear path.
+ if d.pdf.len() == d.mu.len() {
+ out.pdf.extend_from_slice(&d.pdf);
+ } else {
+ out.pdf.extend(std::iter::repeat_n(0.0, d.mu.len()));
+ }
+ }
+ out
+ })
+ .collect()
+}
+
+/// One material = a list of (nuclide index, atom density). Mirrors the
+/// CPU `Material.nuclides` after resolution; the device sums
+/// `Σ_t(E) = Σ_nuc n_d · σ_t,nuc(E)` over these.
+#[derive(Clone)]
+pub struct MaterialCe {
+ /// `(nuclide_idx, atom_density [atoms/barn-cm])`.
+ pub nuclides: Vec<(usize, f64)>,
+}
+
+/// CE scene packed for the device: flat blobs + offset header.
+pub struct PackedCe {
+ /// f64 blob: per-nuclide [grid, xs[0..N_RX]], then the flat
+ /// material-nuclide atom-density list.
+ pub fdata: Vec,
+ /// i32 header: per-nuclide [grid_off, n_e, xs_off], then the
+ /// material table (off/len into the mat-nuclide lists).
+ pub idata: Vec,
+ pub n_nuclides: usize,
+ pub n_materials: usize,
+ /// Mirror of the per-nuclide awr / nu_bar (host-side; also uploaded).
+ pub awr: Vec,
+ pub nu_bar: Vec,
+ /// Device offsets (filled by `pack_ce_scene`) into idata/fdata for
+ /// the material table and the per-nuclide awr/nu_bar arrays.
+ pub mat_table_off: usize, // i32: per material [list_off, list_len]
+ pub mat_nuc_idx_off: usize, // i32: flat nuclide indices
+ pub mat_nuc_den_off: usize, // f64: flat atom densities
+ pub awr_off: usize, // f64
+ pub nu_bar_off: usize, // f64
+}
+
+// Per-nuclide i32 header stride: [grid_off, n_e, xs_off].
+const NUC_HDR: usize = 3;
+const H_GRID_OFF: usize = 0;
+const H_N_E: usize = 1;
+const H_XS_OFF: usize = 2;
+
+/// Pack per-nuclide CE data only (no material table). Used by the XS
+/// lookup A/B, which compares one nuclide at a time. The material /
+/// awr / nu_bar offsets are left at 0 with empty tables.
+pub fn pack_ce(nuclides: &[NuclideCe]) -> PackedCe {
+ pack_ce_scene(nuclides, &[])
+}
+
+/// Pack a full CE scene: per-nuclide grids+σ, then the material table
+/// (per material: a list of (nuclide_idx, atom_density)), then the
+/// per-nuclide awr / nu_bar arrays. Layout (offsets recorded in the
+/// returned struct, in *element* units):
+/// fdata: [ per-nuclide grid+xs … ][ mat atom densities ][ awr ][ nu_bar ]
+/// idata: [ per-nuclide NUC_HDR … ][ mat [off,len] table ][ flat nuclide idxs ]
+pub fn pack_ce_scene(nuclides: &[NuclideCe], materials: &[MaterialCe]) -> PackedCe {
+ let mut fdata: Vec = Vec::new();
+ let mut idata: Vec = vec![0; nuclides.len() * NUC_HDR];
+ let mut awr = Vec::with_capacity(nuclides.len());
+ let mut nu_bar = Vec::with_capacity(nuclides.len());
+
+ for (n, nuc) in nuclides.iter().enumerate() {
+ let n_e = nuc.grid.len();
+ let grid_off = fdata.len();
+ fdata.extend_from_slice(&nuc.grid);
+ let xs_off = fdata.len();
+ for r in 0..N_RX {
+ debug_assert_eq!(nuc.xs[r].len(), n_e, "reaction {r} σ len != grid len");
+ fdata.extend_from_slice(&nuc.xs[r]);
+ }
+ idata[n * NUC_HDR + H_GRID_OFF] = grid_off as i32;
+ idata[n * NUC_HDR + H_N_E] = n_e as i32;
+ idata[n * NUC_HDR + H_XS_OFF] = xs_off as i32;
+ awr.push(nuc.awr);
+ nu_bar.push(nuc.nu_bar);
+ }
+
+ // Material table: per material [list_off, list_len] into the flat
+ // (nuclide_idx) i32 list + parallel (atom_density) f64 list.
+ let mat_table_off = idata.len();
+ idata.extend(std::iter::repeat_n(0, materials.len() * 2));
+ let mut flat_idx: Vec = Vec::new();
+ let mut flat_den: Vec = Vec::new();
+ for (m, mat) in materials.iter().enumerate() {
+ let off = flat_idx.len();
+ for &(ni, den) in &mat.nuclides {
+ flat_idx.push(ni as i32);
+ flat_den.push(den);
+ }
+ idata[mat_table_off + m * 2] = off as i32;
+ idata[mat_table_off + m * 2 + 1] = mat.nuclides.len() as i32;
+ }
+ let mat_nuc_idx_off = idata.len();
+ idata.extend_from_slice(&flat_idx);
+
+ let mat_nuc_den_off = fdata.len();
+ fdata.extend_from_slice(&flat_den);
+ let awr_off = fdata.len();
+ fdata.extend_from_slice(&awr);
+ let nu_bar_off = fdata.len();
+ fdata.extend_from_slice(&nu_bar);
+
+ if fdata.is_empty() {
+ fdata.push(0.0);
+ }
+ if idata.is_empty() {
+ idata.push(0);
+ }
+ PackedCe {
+ fdata,
+ idata,
+ n_nuclides: nuclides.len(),
+ n_materials: materials.len(),
+ awr,
+ nu_bar,
+ mat_table_off,
+ mat_nuc_idx_off,
+ mat_nuc_den_off,
+ awr_off,
+ nu_bar_off,
+ }
+}
+
+// ── Device CE lookup ────────────────────────────────────────────────
+
+/// Lower-bracket grid index for `energy` by binary search over the
+/// nuclide's grid slice `[grid_off .. grid_off + n_e]`. Returns an index
+/// in `[0, n_e-2]` (clamped), matching `ReactionKernel::energy_index`.
+#[cube]
+fn energy_index(fdata: &Array, grid_off: u32, n_e: u32, energy: f64) -> u32 {
+ let mut lo = u32::new(0);
+ let mut hi = n_e - 1u32;
+ // Standard binary search for the bracket; bounded loop (no while-cap
+ // worries — grids are < 2^20 points so 24 steps suffice, but use a
+ // generous fixed cap for the CubeCL frontend).
+ for _i in 0..32u32 {
+ if lo + 1u32 < hi {
+ let mid = (lo + hi) / 2u32;
+ if fdata[(grid_off + mid) as usize] <= energy {
+ lo = mid;
+ } else {
+ hi = mid;
+ }
+ }
+ }
+ // Clamp to [0, n_e-2].
+ select(lo > n_e - 2u32, n_e - 2u32, lo)
+}
+
+/// Log-log interpolate reaction `rx` of nuclide at header `hdr_base`,
+/// at `energy`, given the precomputed bracket `idx`. Mirrors
+/// `ReactionKernel::reconstruct_interp` for the `Table` case.
+#[cube]
+fn rx_interp(
+ idata: &Array,
+ fdata: &Array,
+ hdr_base: u32,
+ rx: u32,
+ idx: u32,
+ energy: f64,
+) -> f64 {
+ let grid_off = u32::cast_from(idata[(hdr_base + H_GRID_OFF as u32) as usize]);
+ let n_e = u32::cast_from(idata[(hdr_base + H_N_E as u32) as usize]);
+ let xs_off = u32::cast_from(idata[(hdr_base + H_XS_OFF as u32) as usize]);
+ let base = xs_off + rx * n_e;
+ let e_lo = fdata[(grid_off + idx) as usize];
+ let xs_lo = fdata[(base + idx) as usize];
+ let mut out = xs_lo;
+ if idx + 1u32 < n_e {
+ let e_hi = fdata[(grid_off + idx + 1u32) as usize];
+ let xs_hi = fdata[(base + idx + 1u32) as usize];
+ if e_hi > e_lo {
+ if xs_lo > f64::new(0.0) {
+ if xs_hi > f64::new(0.0) {
+ // log-log, clamped to [0, 1] like
+ // `ReactionKernel::reconstruct_interp`'s caller does
+ // (`log_frac.clamp(0.0, 1.0)` in xs_provider.rs) —
+ // `idx` is itself clamped to the last valid bracket,
+ // so an `energy` outside the nuclide's grid range
+ // would otherwise extrapolate the power law instead
+ // of holding the boundary value.
+ let f_raw = (energy / e_lo).ln() / (e_hi / e_lo).ln();
+ let f_hi = select(f_raw > f64::new(1.0), f64::new(1.0), f_raw);
+ let f = select(f_hi < f64::new(0.0), f64::new(0.0), f_hi);
+ let ratio = xs_hi / xs_lo;
+ out = xs_lo * (f * ratio.ln()).exp();
+ } else {
+ let frac = (energy - e_lo) / (e_hi - e_lo);
+ out = xs_lo + frac * (xs_hi - xs_lo);
+ }
+ } else {
+ let frac = (energy - e_lo) / (e_hi - e_lo);
+ out = xs_lo + frac * (xs_hi - xs_lo);
+ }
+ }
+ }
+ out
+}
+
+/// Test kernel: for each input energy, look up the total microscopic σ
+/// (sum of the N_RX reactions) of nuclide 0 and write it out. Validates
+/// the grid search + log-log interp against the CPU provider.
+#[cube(launch)]
+fn ce_total_micro_kernel(
+ idata: &Array,
+ fdata: &Array,
+ energies: &Array,
+ out: &mut Array,
+) {
+ let tid = ABSOLUTE_POS;
+ if tid < out.len() {
+ let e = energies[tid];
+ let hdr_base = u32::new(0); // nuclide 0
+ let grid_off = u32::cast_from(idata[(hdr_base + H_GRID_OFF as u32) as usize]);
+ let n_e = u32::cast_from(idata[(hdr_base + H_N_E as u32) as usize]);
+ let idx = energy_index(fdata, grid_off, n_e, e);
+ let mut tot = f64::new(0.0);
+ for r in 0..N_RX as u32 {
+ tot = tot + rx_interp(idata, fdata, hdr_base, r, idx, e);
+ }
+ out[tid] = tot;
+ }
+}
+
+/// Look up total microscopic σ(E) of nuclide 0 on the GPU at the given
+/// energies. Convenience wrapper for the A/B test.
+pub fn total_micro_xs(
+ device: &R::Device,
+ packed: &PackedCe,
+ energies: &[f64],
+) -> Vec {
+ let client = R::client(device);
+ let n = energies.len();
+ let idata_h = client.create_from_slice(i32::as_bytes(&packed.idata));
+ let fdata_h = client.create_from_slice(f64::as_bytes(&packed.fdata));
+ let e_h = client.create_from_slice(f64::as_bytes(energies));
+ let out_h = client.empty(n * core::mem::size_of::());
+
+ let threads = 64u32;
+ let blocks = n.div_ceil(threads as usize) as u32;
+ unsafe {
+ ce_total_micro_kernel::launch::(
+ &client,
+ CubeCount::Static(blocks, 1, 1),
+ CubeDim::new_1d(threads),
+ ArrayArg::from_raw_parts(idata_h, packed.idata.len()),
+ ArrayArg::from_raw_parts(fdata_h, packed.fdata.len()),
+ ArrayArg::from_raw_parts(e_h, n),
+ ArrayArg::from_raw_parts(out_h.clone(), n),
+ );
+ }
+ let bytes = client.read_one(out_h).expect("ce readback");
+ f64::from_bytes(&bytes).to_vec()
+}
+
+// ── Packed elastic angular blob (one nuclide) ───────────────────────
+//
+// Layout mirrors the σ pack: an i32 header + i32/f64 blobs. For the
+// A/B we pack a single nuclide at a time (the kernel samples "nuclide
+// 0"), same as the σ lookup A/B.
+//
+// idata: [ n_e,
+// eoff (energies start in fdata),
+// doff (dist_off start in idata, len n_e),
+// dlen (dist_len start in idata, len n_e),
+// hoff (histogram start in idata, len n_e),
+// muoff (mu start in fdata),
+// cdfoff (cdf start in fdata),
+// pdfoff (pdf start in fdata) ]
+// then the dist_off / dist_len / histogram arrays inline.
+// fdata: [ energies | mu | cdf | pdf ].
+
+/// One-nuclide angular pack for the device sampler.
+pub struct PackedAngular {
+ pub idata: Vec,
+ pub fdata: Vec,
+}
+
+const A_N_E: usize = 0;
+const A_EOFF: usize = 1;
+const A_DOFF: usize = 2;
+const A_DLEN: usize = 3;
+const A_HOFF: usize = 4;
+const A_MUOFF: usize = 5;
+const A_CDFOFF: usize = 6;
+const A_PDFOFF: usize = 7;
+const A_HDR: usize = 8;
+
+/// Pack one nuclide's elastic angular data for the device.
+pub fn pack_angular(ang: &AngularCe) -> PackedAngular {
+ let mut idata = vec![0i32; A_HDR];
+ let mut fdata: Vec = Vec::new();
+ let n_e = ang.energies.len();
+ idata[A_N_E] = n_e as i32;
+
+ idata[A_EOFF] = fdata.len() as i32;
+ fdata.extend_from_slice(&ang.energies);
+
+ idata[A_DOFF] = idata.len() as i32;
+ idata.extend_from_slice(&ang.dist_off);
+ idata[A_DLEN] = idata.len() as i32;
+ idata.extend_from_slice(&ang.dist_len);
+ idata[A_HOFF] = idata.len() as i32;
+ idata.extend_from_slice(&ang.histogram);
+
+ idata[A_MUOFF] = fdata.len() as i32;
+ fdata.extend_from_slice(&ang.mu);
+ idata[A_CDFOFF] = fdata.len() as i32;
+ fdata.extend_from_slice(&ang.cdf);
+ idata[A_PDFOFF] = fdata.len() as i32;
+ fdata.extend_from_slice(&ang.pdf);
+
+ if fdata.is_empty() {
+ fdata.push(0.0);
+ }
+ if idata.len() == A_HDR {
+ idata.push(0); // ensure non-empty blob
+ }
+ PackedAngular { idata, fdata }
+}
+
+// ── Device μ sampler (mirrors TabularMuDist::sample_with_xi) ─────────
+
+/// Invert the μ CDF of distribution at `[doff .. doff+dlen]` (slices
+/// into mu/cdf/pdf) for a pre-drawn `xi`. `hist` selects linear (1) vs
+/// quadratic (0) inversion. Mirrors `TabularMuDist::sample_with_xi`.
+#[cube]
+#[allow(unused_assignments)]
+fn sample_mu_bin(
+ fdata: &Array,
+ mu_off: u32,
+ cdf_off: u32,
+ pdf_off: u32,
+ doff: u32,
+ dlen: u32,
+ hist: i32,
+ xi: f64,
+) -> f64 {
+ let mut out = 2.0 * xi - 1.0; // n<2 fallback
+ if dlen >= 2u32 {
+ // binary search cdf for bracket idx (local within the dist).
+ let mut lo = u32::new(0);
+ let mut hi = dlen - 1u32;
+ for _i in 0..32u32 {
+ if lo + 1u32 < hi {
+ let mid = (lo + hi) / 2u32;
+ if fdata[(cdf_off + doff + mid) as usize] <= xi {
+ lo = mid;
+ } else {
+ hi = mid;
+ }
+ }
+ }
+ // clamp lo to dlen-2
+ let idx = select(lo > dlen - 2u32, dlen - 2u32, lo);
+ let cdf_lo = fdata[(cdf_off + doff + idx) as usize];
+ let cdf_hi = fdata[(cdf_off + doff + idx + 1u32) as usize];
+ let mu_lo = fdata[(mu_off + doff + idx) as usize];
+ let mu_hi = fdata[(mu_off + doff + idx + 1u32) as usize];
+ let dmu = mu_hi - mu_lo;
+ let dc = cdf_hi - cdf_lo;
+ if dc.abs() < f64::new(1e-15) {
+ out = clamp_mu(mu_lo);
+ } else {
+ if dmu.abs() < f64::new(1e-15) {
+ out = clamp_mu(mu_lo);
+ } else {
+ if hist == 1i32 {
+ let frac = (xi - cdf_lo) / dc;
+ out = clamp_mu(mu_lo + frac * dmu);
+ } else {
+ // quadratic lin-lin inversion.
+ let pdf_lo = fdata[(pdf_off + doff + idx) as usize];
+ let pdf_hi = fdata[(pdf_off + doff + idx + 1u32) as usize];
+ let a = (pdf_hi - pdf_lo) / (2.0 * dmu);
+ let b = pdf_lo;
+ let c = cdf_lo - xi;
+ let mut x = f64::new(0.0);
+ if a.abs() < f64::new(1e-14) {
+ if b.abs() < f64::new(1e-30) {
+ x = (xi - cdf_lo) / dc * dmu;
+ } else {
+ x = -c / b;
+ }
+ } else {
+ let disc = b * b - 4.0 * a * c;
+ let sq = select(disc > f64::new(0.0), disc, f64::new(0.0)).sqrt();
+ // physical root in [0, dmu]
+ let x1 = (-b + sq) / (2.0 * a);
+ let x2 = (-b - sq) / (2.0 * a);
+ let x1_ok = x1 >= f64::new(0.0) && x1 <= dmu;
+ x = select(x1_ok, x1, x2);
+ }
+ out = clamp_mu(mu_lo + x);
+ }
+ }
+ }
+ }
+ out
+}
+
+#[cube]
+fn clamp_mu(m: f64) -> f64 {
+ let lo = select(m > f64::new(-1.0), m, f64::new(-1.0));
+ select(lo < f64::new(1.0), lo, f64::new(1.0))
+}
+
+/// Sample an elastic μ at incident `energy` for the packed nuclide,
+/// given two uniforms (`xi_bin` for the stochastic energy-bin pick,
+/// `xi_mu` for the CDF inversion). Mirrors `AngularDistribution::sample_mu`.
+#[cube]
+#[allow(unused_assignments)]
+fn sample_mu_at(idata: &Array, fdata: &Array, energy: f64, xi_bin: f64, xi_mu: f64) -> f64 {
+ let n_e = u32::cast_from(idata[A_N_E]);
+ let mut out = 2.0 * xi_mu - 1.0; // isotropic fallback (no data)
+ if n_e >= 1u32 {
+ let eoff = u32::cast_from(idata[A_EOFF]);
+ let doff_base = u32::cast_from(idata[A_DOFF]);
+ let dlen_base = u32::cast_from(idata[A_DLEN]);
+ let hoff_base = u32::cast_from(idata[A_HOFF]);
+ let mu_off = u32::cast_from(idata[A_MUOFF]);
+ let cdf_off = u32::cast_from(idata[A_CDFOFF]);
+ let pdf_off = u32::cast_from(idata[A_PDFOFF]);
+
+ // bracket energy: pick a distribution index `pick`.
+ let e0 = fdata[eoff as usize];
+ let elast = fdata[(eoff + n_e - 1u32) as usize];
+ let mut pick = u32::new(0);
+ if energy <= e0 {
+ pick = u32::new(0);
+ } else {
+ if energy >= elast {
+ pick = n_e - 1u32;
+ } else {
+ // binary search for lower bracket idx
+ let mut lo = u32::new(0);
+ let mut hi = n_e - 1u32;
+ for _i in 0..32u32 {
+ if lo + 1u32 < hi {
+ let mid = (lo + hi) / 2u32;
+ if fdata[(eoff + mid) as usize] <= energy {
+ lo = mid;
+ } else {
+ hi = mid;
+ }
+ }
+ }
+ let e_lo = fdata[(eoff + lo) as usize];
+ let e_hi = fdata[(eoff + lo + 1u32) as usize];
+ let r = (energy - e_lo) / (e_hi - e_lo);
+ pick = select(xi_bin < r, lo + 1u32, lo);
+ }
+ }
+ let doff = u32::cast_from(idata[(doff_base + pick) as usize]);
+ let dlen = u32::cast_from(idata[(dlen_base + pick) as usize]);
+ let hist = idata[(hoff_base + pick) as usize];
+ out = sample_mu_bin(fdata, mu_off, cdf_off, pdf_off, doff, dlen, hist, xi_mu);
+ }
+ out
+}
+
+/// Test kernel: sample one μ per thread at the given (energy, xi_bin,
+/// xi_mu) triples. Lets the host drive a CPU-vs-GPU distribution A/B.
+#[cube(launch)]
+fn ce_sample_mu_kernel(
+ idata: &Array,
+ fdata: &Array,
+ energy: &Array,
+ xi_bin: &Array,
+ xi_mu: &Array,
+ out: &mut Array,
+) {
+ let tid = ABSOLUTE_POS;
+ if tid < out.len() {
+ out[tid] = sample_mu_at(idata, fdata, energy[tid], xi_bin[tid], xi_mu[tid]);
+ }
+}
+
+/// Sample μ on the GPU for one packed nuclide at the given inputs.
+pub fn sample_mu_gpu(
+ device: &R::Device,
+ packed: &PackedAngular,
+ energy: &[f64],
+ xi_bin: &[f64],
+ xi_mu: &[f64],
+) -> Vec {
+ let client = R::client(device);
+ let n = energy.len();
+ let idata_h = client.create_from_slice(i32::as_bytes(&packed.idata));
+ let fdata_h = client.create_from_slice(f64::as_bytes(&packed.fdata));
+ let e_h = client.create_from_slice(f64::as_bytes(energy));
+ let xb_h = client.create_from_slice(f64::as_bytes(xi_bin));
+ let xm_h = client.create_from_slice(f64::as_bytes(xi_mu));
+ let out_h = client.empty(n * core::mem::size_of::());
+ let threads = 64u32;
+ let blocks = n.div_ceil(threads as usize) as u32;
+ unsafe {
+ ce_sample_mu_kernel::launch::(
+ &client,
+ CubeCount::Static(blocks, 1, 1),
+ CubeDim::new_1d(threads),
+ ArrayArg::from_raw_parts(idata_h, packed.idata.len()),
+ ArrayArg::from_raw_parts(fdata_h, packed.fdata.len()),
+ ArrayArg::from_raw_parts(e_h, n),
+ ArrayArg::from_raw_parts(xb_h, n),
+ ArrayArg::from_raw_parts(xm_h, n),
+ ArrayArg::from_raw_parts(out_h.clone(), n),
+ );
+ }
+ let bytes = client.read_one(out_h).expect("mu readback");
+ f64::from_bytes(&bytes).to_vec()
+}
+
+// ── Fission outgoing-energy (χ) distribution ────────────────────────
+//
+// CPU source: `NuclideKernels::fission_energy_dist: Option`.
+// Two device paths, selected by `law`:
+// law = 0 tabular ContinuousTabular (ENDF Law 4): per-incident-energy
+// (e_out, pdf, cdf) tables + OpenMC stochastic-bin pick +
+// scaled kinematic remap. Mirrors EnergyDistribution::sample.
+// law = 1/2/3 closed-form Watt / Maxwell / Evaporation: a(E)/b(E) or
+// θ(E) lin-lin tables, sampled by the Cranberg/Coveyou
+// rejection samplers. Mirrors WattLaw::sample etc.
+// law = -1 none → emit the incident energy unchanged (fallback).
+
+/// Flat per-nuclide fission-χ data, device-ready.
+#[derive(Clone, Default)]
+pub struct FissionCe {
+ /// -1 none, 0 tabular, 1 Watt, 2 Maxwell, 3 Evaporation.
+ pub law: i32,
+ pub u: f64,
+ // Tabular (law 0): incident grid + per-E (e_out,pdf,cdf) slices.
+ pub energies: Vec,
+ pub dist_off: Vec,
+ pub dist_len: Vec,
+ pub e_out: Vec,
+ pub pdf: Vec,
+ pub cdf: Vec,
+ // Closed-form param tables (law 1/2/3). Watt uses (a*, b*); Maxwell/
+ // Evaporation use (a* = θ). b* empty for non-Watt.
+ pub pa_e: Vec,
+ pub pa_v: Vec,
+ pub pb_e: Vec,
+ pub pb_v: Vec,
+}
+
+/// Extract fission-χ per nuclide (parallel to extract_ce / extract_angular).
+pub fn extract_fission(
+ nuclides: &[std::sync::Arc],
+) -> Vec {
+ use crate::hdf5_reader::FissionEnergyLaw;
+ nuclides
+ .iter()
+ .map(|nuc| {
+ let Some(ed) = nuc.fission_energy_dist.as_ref() else {
+ return FissionCe { law: -1, ..Default::default() };
+ };
+ match &ed.closed_form {
+ Some(FissionEnergyLaw::Watt(w)) => FissionCe {
+ law: 1,
+ u: w.u,
+ pa_e: w.a_energies.clone(),
+ pa_v: w.a_values.clone(),
+ pb_e: w.b_energies.clone(),
+ pb_v: w.b_values.clone(),
+ ..Default::default()
+ },
+ Some(FissionEnergyLaw::Maxwell(m)) => FissionCe {
+ law: 2,
+ u: m.u,
+ pa_e: m.theta_energies.clone(),
+ pa_v: m.theta_values.clone(),
+ ..Default::default()
+ },
+ Some(FissionEnergyLaw::Evaporation(m)) => FissionCe {
+ law: 3,
+ u: m.u,
+ pa_e: m.theta_energies.clone(),
+ pa_v: m.theta_values.clone(),
+ ..Default::default()
+ },
+ None => {
+ // Tabular ContinuousTabular.
+ if ed.energies.is_empty() || ed.distributions.is_empty() {
+ return FissionCe { law: -1, ..Default::default() };
+ }
+ let mut f = FissionCe {
+ law: 0,
+ energies: ed.energies.clone(),
+ ..Default::default()
+ };
+ for d in &ed.distributions {
+ f.dist_off.push(f.e_out.len() as i32);
+ f.dist_len.push(d.e_out.len() as i32);
+ f.e_out.extend_from_slice(&d.e_out);
+ f.cdf.extend_from_slice(&d.cdf);
+ if d.pdf.len() == d.e_out.len() {
+ f.pdf.extend_from_slice(&d.pdf);
+ } else {
+ f.pdf.extend(std::iter::repeat_n(0.0, d.e_out.len()));
+ }
+ }
+ f
+ }
+ }
+ })
+ .collect()
+}
+
+// ── Packed fission blob (one nuclide) ───────────────────────────────
+// idata: [ law, n_e, eoff, doff, dloff, e_out_off, pdf_off, cdf_off,
+// pa_e_off, pa_n, pa_v_off, pb_e_off, pb_n, pb_v_off ]
+// then dist_off / dist_len arrays inline (tabular only).
+// fdata: [ u, energies | e_out | pdf | cdf | pa_e | pa_v | pb_e | pb_v ]
+
+pub struct PackedFission {
+ pub idata: Vec,
+ pub fdata: Vec,
+}
+
+const F_LAW: usize = 0;
+const F_N_E: usize = 1;
+const F_EOFF: usize = 2;
+const F_DOFF: usize = 3;
+const F_DLOFF: usize = 4;
+const F_EOUT_OFF: usize = 5;
+const F_PDF_OFF: usize = 6;
+const F_CDF_OFF: usize = 7;
+const F_PA_E_OFF: usize = 8;
+const F_PA_N: usize = 9;
+const F_PA_V_OFF: usize = 10;
+const F_PB_E_OFF: usize = 11;
+const F_PB_N: usize = 12;
+const F_PB_V_OFF: usize = 13;
+const F_U_SLOT: usize = 0; // fdata[0] = u
+const F_HDR: usize = 14;
+
+pub fn pack_fission(f: &FissionCe) -> PackedFission {
+ let mut idata = vec![0i32; F_HDR];
+ let mut fdata: Vec = vec![f.u]; // F_U_SLOT
+ idata[F_LAW] = f.law;
+ idata[F_N_E] = f.energies.len() as i32;
+
+ idata[F_EOFF] = fdata.len() as i32;
+ fdata.extend_from_slice(&f.energies);
+ idata[F_EOUT_OFF] = fdata.len() as i32;
+ fdata.extend_from_slice(&f.e_out);
+ idata[F_PDF_OFF] = fdata.len() as i32;
+ fdata.extend_from_slice(&f.pdf);
+ idata[F_CDF_OFF] = fdata.len() as i32;
+ fdata.extend_from_slice(&f.cdf);
+ idata[F_PA_E_OFF] = fdata.len() as i32;
+ fdata.extend_from_slice(&f.pa_e);
+ idata[F_PA_N] = f.pa_e.len() as i32;
+ idata[F_PA_V_OFF] = fdata.len() as i32;
+ fdata.extend_from_slice(&f.pa_v);
+ idata[F_PB_E_OFF] = fdata.len() as i32;
+ fdata.extend_from_slice(&f.pb_e);
+ idata[F_PB_N] = f.pb_e.len() as i32;
+ idata[F_PB_V_OFF] = fdata.len() as i32;
+ fdata.extend_from_slice(&f.pb_v);
+
+ idata[F_DOFF] = idata.len() as i32;
+ idata.extend_from_slice(&f.dist_off);
+ idata[F_DLOFF] = idata.len() as i32;
+ idata.extend_from_slice(&f.dist_len);
+
+ if fdata.is_empty() {
+ fdata.push(0.0);
+ }
+ if idata.len() == F_HDR {
+ idata.push(0);
+ }
+ PackedFission { idata, fdata }
+}
+
+// ── Device samplers ─────────────────────────────────────────────────
+
+/// lin-lin lookup over a (grid, values) pair packed at given offsets.
+/// Mirrors WattLaw::lookup_lin_lin.
+#[cube]
+fn fis_lin_lin(fdata: &Array, goff: u32, voff: u32, n: u32, e: f64) -> f64 {
+ let mut out = f64::new(0.0);
+ if n >= 1u32 {
+ let g0 = fdata[goff as usize];
+ let glast = fdata[(goff + n - 1u32) as usize];
+ if e <= g0 {
+ out = fdata[voff as usize];
+ } else {
+ if e >= glast {
+ out = fdata[(voff + n - 1u32) as usize];
+ } else {
+ let mut lo = u32::new(0);
+ let mut hi = n - 1u32;
+ for _i in 0..32u32 {
+ if lo + 1u32 < hi {
+ let mid = (lo + hi) / 2u32;
+ if fdata[(goff + mid) as usize] <= e {
+ lo = mid;
+ } else {
+ hi = mid;
+ }
+ }
+ }
+ let gl = fdata[(goff + lo) as usize];
+ let gh = fdata[(goff + lo + 1u32) as usize];
+ let vl = fdata[(voff + lo) as usize];
+ let vh = fdata[(voff + lo + 1u32) as usize];
+ let frac = (e - gl) / (gh - gl);
+ out = vl + frac * (vh - vl);
+ }
+ }
+ }
+ out
+}
+
+/// Quadratic/linear CDF inversion of a tabular (e_out, cdf, pdf) slice
+/// `[doff .. doff+dlen]` for `xi`. Mirrors TabularEnergyDist::sample_with_xi.
+#[cube]
+#[allow(unused_assignments)]
+fn fis_sample_eout_bin(
+ fdata: &Array,
+ eout_off: u32,
+ cdf_off: u32,
+ pdf_off: u32,
+ doff: u32,
+ dlen: u32,
+ xi: f64,
+) -> f64 {
+ let mut out = f64::new(1.0e6);
+ if dlen >= 2u32 {
+ let mut lo = u32::new(0);
+ let mut hi = dlen - 1u32;
+ for _i in 0..32u32 {
+ if lo + 1u32 < hi {
+ let mid = (lo + hi) / 2u32;
+ if fdata[(cdf_off + doff + mid) as usize] <= xi {
+ lo = mid;
+ } else {
+ hi = mid;
+ }
+ }
+ }
+ let idx = select(lo > dlen - 2u32, dlen - 2u32, lo);
+ let cdf_lo = fdata[(cdf_off + doff + idx) as usize];
+ let cdf_hi = fdata[(cdf_off + doff + idx + 1u32) as usize];
+ let e_lo = fdata[(eout_off + doff + idx) as usize];
+ let e_hi = fdata[(eout_off + doff + idx + 1u32) as usize];
+ let de = e_hi - e_lo;
+ if (cdf_hi - cdf_lo).abs() < f64::new(1e-15) {
+ out = e_lo;
+ } else {
+ let p_lo = fdata[(pdf_off + doff + idx) as usize];
+ let p_hi = fdata[(pdf_off + doff + idx + 1u32) as usize];
+ // quadratic when pdf present & de>0, else linear.
+ let use_quad = de > f64::new(0.0) && (p_lo > f64::new(0.0) || p_hi > f64::new(0.0));
+ if use_quad {
+ let m = (p_hi - p_lo) / de;
+ let dc = xi - cdf_lo;
+ if m.abs() < f64::new(1e-30) {
+ out = e_lo + dc / p_lo;
+ } else {
+ let disc = p_lo * p_lo + 2.0 * m * dc;
+ let sq = select(disc > f64::new(0.0), disc, f64::new(0.0)).sqrt();
+ out = e_lo + (sq - p_lo) / m;
+ }
+ } else {
+ let frac = (xi - cdf_lo) / (cdf_hi - cdf_lo);
+ out = e_lo + frac * de;
+ }
+ }
+ }
+ let floor = f64::new(1e-5);
+ select(out > floor, out, floor)
+}
+
+/// Sample a fission outgoing energy for the packed nuclide at incident
+/// `e_in`, drawing uniforms from the local rng. Mirrors
+/// EnergyDistribution::sample (tabular path with stochastic bin +
+/// kinematic remap) and the closed-form Watt/Maxwell/Evaporation
+/// samplers. `rng` is a length-2 u64 [state, inc].
+#[cube]
+#[allow(unused_assignments)]
+fn fis_sample_energy(idata: &Array, fdata: &Array, e_in: f64, rng: &mut Array) -> f64 {
+ let law = idata[F_LAW];
+ let u = fdata[F_U_SLOT];
+ let mut out = e_in; // law -1 fallback
+ if law == 0i32 {
+ // Tabular ContinuousTabular.
+ let n_e = u32::cast_from(idata[F_N_E]);
+ let eoff = u32::cast_from(idata[F_EOFF]);
+ let doff_base = u32::cast_from(idata[F_DOFF]);
+ let dloff_base = u32::cast_from(idata[F_DLOFF]);
+ let eout_off = u32::cast_from(idata[F_EOUT_OFF]);
+ let pdf_off = u32::cast_from(idata[F_PDF_OFF]);
+ let cdf_off = u32::cast_from(idata[F_CDF_OFF]);
+ if n_e >= 1u32 {
+ let e0 = fdata[eoff as usize];
+ let elast = fdata[(eoff + n_e - 1u32) as usize];
+ if e_in <= e0 {
+ let xi = ce_uniform_f(rng);
+ let doff = u32::cast_from(idata[(doff_base) as usize]);
+ let dlen = u32::cast_from(idata[(dloff_base) as usize]);
+ out = fis_sample_eout_bin(fdata, eout_off, cdf_off, pdf_off, doff, dlen, xi);
+ } else {
+ if e_in >= elast {
+ let xi = ce_uniform_f(rng);
+ let doff = u32::cast_from(idata[(doff_base + n_e - 1u32) as usize]);
+ let dlen = u32::cast_from(idata[(dloff_base + n_e - 1u32) as usize]);
+ out = fis_sample_eout_bin(fdata, eout_off, cdf_off, pdf_off, doff, dlen, xi);
+ } else {
+ // bracket
+ let mut lo = u32::new(0);
+ let mut hi = n_e - 1u32;
+ for _i in 0..32u32 {
+ if lo + 1u32 < hi {
+ let mid = (lo + hi) / 2u32;
+ if fdata[(eoff + mid) as usize] <= e_in {
+ lo = mid;
+ } else {
+ hi = mid;
+ }
+ }
+ }
+ let e_lo = fdata[(eoff + lo) as usize];
+ let e_hi = fdata[(eoff + lo + 1u32) as usize];
+ let r = (e_in - e_lo) / (e_hi - e_lo);
+ let pick_hi = ce_uniform_f(rng) < r;
+ let l = select(pick_hi, lo + 1u32, lo);
+ let doff_l = u32::cast_from(idata[(doff_base + l) as usize]);
+ let dlen_l = u32::cast_from(idata[(dloff_base + l) as usize]);
+ let xi = ce_uniform_f(rng);
+ let e_out = fis_sample_eout_bin(fdata, eout_off, cdf_off, pdf_off, doff_l, dlen_l, xi);
+ // Scaled kinematic remap to interpolated [E1, EK].
+ let doff_a = u32::cast_from(idata[(doff_base + lo) as usize]);
+ let dlen_a = u32::cast_from(idata[(dloff_base + lo) as usize]);
+ let doff_b = u32::cast_from(idata[(doff_base + lo + 1u32) as usize]);
+ let dlen_b = u32::cast_from(idata[(dloff_base + lo + 1u32) as usize]);
+ let ea_lo = fdata[(eout_off + doff_a) as usize];
+ let ea_hi = fdata[(eout_off + doff_a + dlen_a - 1u32) as usize];
+ let eb_lo = fdata[(eout_off + doff_b) as usize];
+ let eb_hi = fdata[(eout_off + doff_b + dlen_b - 1u32) as usize];
+ let el_lo = fdata[(eout_off + doff_l) as usize];
+ let el_hi = fdata[(eout_off + doff_l + dlen_l - 1u32) as usize];
+ let e1 = (f64::new(1.0) - r) * ea_lo + r * eb_lo;
+ let ek = (f64::new(1.0) - r) * ea_hi + r * eb_hi;
+ let span = el_hi - el_lo;
+ let adjusted = select(
+ span.abs() < f64::new(1e-30),
+ e_out,
+ e1 + (e_out - el_lo) * (ek - e1) / span,
+ );
+ let floor = f64::new(1e-5);
+ out = select(adjusted > floor, adjusted, floor);
+ }
+ }
+ }
+ } else {
+ let pa_e = u32::cast_from(idata[F_PA_E_OFF]);
+ let pa_n = u32::cast_from(idata[F_PA_N]);
+ let pa_v = u32::cast_from(idata[F_PA_V_OFF]);
+ let max_e = select(e_in - u > f64::new(1e-5), e_in - u, f64::new(1e-5));
+ if law == 1i32 {
+ // Watt.
+ let pb_e = u32::cast_from(idata[F_PB_E_OFF]);
+ let pb_n = u32::cast_from(idata[F_PB_N]);
+ let pb_v = u32::cast_from(idata[F_PB_V_OFF]);
+ let a = fis_lin_lin(fdata, pa_e, pa_v, pa_n, e_in);
+ let b = fis_lin_lin(fdata, pb_e, pb_v, pb_n, e_in);
+ let mut acc = f64::new(0.0);
+ let mut done = false;
+ for _i in 0..128u32 {
+ if !done {
+ let xi1 = fmax_small(ce_uniform_f(rng));
+ let xi2 = ce_uniform_f(rng);
+ let xi3 = fmax_small(ce_uniform_f(rng));
+ let xi4 = ce_uniform_f(rng);
+ let c = (f64::new(1.5707963267948966) * xi2).cos();
+ let w = -a * (xi1.ln() + c * c * xi3.ln());
+ let term = a * a * b / 4.0;
+ let e_out = w + term + (2.0 * xi4 - 1.0) * (a * a * b * w).sqrt();
+ acc = e_out;
+ if e_out > f64::new(0.0) && e_out <= max_e {
+ done = true;
+ }
+ }
+ }
+ // Clamp into [1e-5, max_e]: a no-op for the accepted case
+ // (already inside this range); on exhaustion it clamps the
+ // last attempted draw, mirroring `WattLaw::sample`'s safety
+ // valve — not a fixed sentinel energy.
+ let clamped_hi = select(acc > max_e, max_e, acc);
+ out = select(clamped_hi > f64::new(1e-5), clamped_hi, f64::new(1e-5));
+ } else {
+ // Maxwell (law 2) or Evaporation (law 3): pa = θ table.
+ let theta = fis_lin_lin(fdata, pa_e, pa_v, pa_n, e_in);
+ let mut acc = f64::new(0.0);
+ let mut done = false;
+ for _i in 0..128u32 {
+ if !done {
+ let xi1 = fmax_small(ce_uniform_f(rng));
+ let xi2 = ce_uniform_f(rng);
+ let xi3 = fmax_small(ce_uniform_f(rng));
+ let e_out = select(
+ law == 2i32,
+ -theta * (xi1.ln() + (f64::new(1.5707963267948966) * xi2).cos() * (f64::new(1.5707963267948966) * xi2).cos() * xi3.ln()),
+ -theta * (xi1 * xi2).ln(),
+ );
+ acc = e_out;
+ if e_out > f64::new(0.0) && e_out <= max_e {
+ done = true;
+ }
+ }
+ }
+ // Same exhaustion-clamp as the Watt branch above, mirroring
+ // `MaxwellLaw::sample_maxwell` / `sample_evaporation`.
+ let clamped_hi = select(acc > max_e, max_e, acc);
+ out = select(clamped_hi > f64::new(1e-5), clamped_hi, f64::new(1e-5));
+ }
+ }
+ out
+}
+
+/// uniform in [0,1) from a local [state,inc] rng (fission-module copy).
+#[cube]
+fn ce_uniform_f(rng: &mut Array) -> f64 {
+ let old0 = rng[0];
+ rng[0] = old0 * 6364136223846793005u64 + rng[1];
+ let xs0 = u32::cast_from(((old0 >> 18u64) ^ old0) >> 27u64);
+ let rot0 = u32::cast_from(old0 >> 59u64);
+ let a = u64::cast_from((xs0 >> rot0) | (xs0 << ((32u32 - rot0) & 31u32))) >> 5u64;
+ let old1 = rng[0];
+ rng[0] = old1 * 6364136223846793005u64 + rng[1];
+ let xs1 = u32::cast_from(((old1 >> 18u64) ^ old1) >> 27u64);
+ let rot1 = u32::cast_from(old1 >> 59u64);
+ let b = u64::cast_from((xs1 >> rot1) | (xs1 << ((32u32 - rot1) & 31u32))) >> 6u64;
+ f64::cast_from(a * 67108864u64 + b) * (1.0 / 9007199254740992.0)
+}
+
+#[cube]
+fn fmax_small(x: f64) -> f64 {
+ select(x > f64::new(1e-300), x, f64::new(1e-300))
+}
+
+/// Test kernel: one fission E_out per thread at the given incident
+/// energies + seeds.
+#[cube(launch)]
+fn ce_sample_fis_kernel(
+ idata: &Array,
+ fdata: &Array,
+ e_in: &Array,
+ rng: &mut Array,
+ out: &mut Array,
+) {
+ let tid = ABSOLUTE_POS;
+ if tid < out.len() {
+ let mut lrng = Array::::new(2usize);
+ lrng[0] = rng[tid * 2];
+ lrng[1] = rng[tid * 2 + 1];
+ out[tid] = fis_sample_energy(idata, fdata, e_in[tid], &mut lrng);
+ rng[tid * 2] = lrng[0];
+ rng[tid * 2 + 1] = lrng[1];
+ }
+}
+
+/// Sample fission E_out on the GPU for one packed nuclide.
+pub fn sample_fission_gpu(
+ device: &R::Device,
+ packed: &PackedFission,
+ e_in: &[f64],
+ seeds: &[(u64, u64)],
+) -> Vec {
+ let client = R::client(device);
+ let n = e_in.len();
+ let mut rng_flat = Vec::with_capacity(n * 2);
+ for s in seeds {
+ rng_flat.push(s.0);
+ rng_flat.push(s.1 | 1);
+ }
+ let idata_h = client.create_from_slice(i32::as_bytes(&packed.idata));
+ let fdata_h = client.create_from_slice(f64::as_bytes(&packed.fdata));
+ let e_h = client.create_from_slice(f64::as_bytes(e_in));
+ let rng_h = client.create_from_slice(u64::as_bytes(&rng_flat));
+ let out_h = client.empty(n * core::mem::size_of::());
+ let threads = 64u32;
+ let blocks = n.div_ceil(threads as usize) as u32;
+ unsafe {
+ ce_sample_fis_kernel::launch::(
+ &client,
+ CubeCount::Static(blocks, 1, 1),
+ CubeDim::new_1d(threads),
+ ArrayArg::from_raw_parts(idata_h, packed.idata.len()),
+ ArrayArg::from_raw_parts(fdata_h, packed.fdata.len()),
+ ArrayArg::from_raw_parts(e_h, n),
+ ArrayArg::from_raw_parts(rng_h, n * 2),
+ ArrayArg::from_raw_parts(out_h.clone(), n),
+ );
+ }
+ let bytes = client.read_one(out_h).expect("fis readback");
+ f64::from_bytes(&bytes).to_vec()
+}
+
+// ── Geometry walk — shared with `gpu_transport_cubecl` and
+// `gpu_render` via `crate::gpu_cubecl_geom` (single copy). ────────
+
+// ════════════════════════════════════════════════════════════════════
+// Fused continuous-energy transport kernel (single generation).
+//
+// One thread = one source neutron, transported to absorption / leakage
+// / step cap. Wires together the pieces validated individually:
+// - geometry walk (find_cell / trace_step) over the geometry SoA blob
+// - CE Σ_t(E) lookup summed over the cell material's nuclides
+// - nuclide pick ∝ Σ_t,nuc, reaction pick (elastic/inelastic/n2n/
+// fission/capture) analog-style
+// - elastic/inelastic scatter: free-gas target velocity (E<400kT) +
+// two-body CM energy loss with the validated tabulated angular μ
+// - fission: bank ν̄ neutrons at validated χ-sampled energies, kill
+// - capture / leak: kill
+// Tallies banked-fission count → host forms single-generation
+// k = fissions / source neutron. Mirrors the analog branch of
+// gr_trace_and_sample (transport_event_based.cu).
+//
+// All buffers are unified: one i32 blob (geometry + ce idata + angular
+// idata + fission idata + material table) and one f64 blob, with base
+// offsets in a small `scene` header so each sub-blob's internal offsets
+// are read relative to its base.
+
+/// Unified CE scene: geometry SoA + CE σ + angular + fission + material
+/// table, all in two blobs with a base-offset header.
+pub struct PackedCeScene {
+ pub idata: Vec,
+ pub fdata: Vec,
+ /// Header (see S_* consts): base offsets of each sub-blob + scalars.
+ pub header: Vec,
+ pub n_nuclides: usize,
+ pub n_materials: usize,
+}
+
+// scene header (i32) slot map.
+const S_ROOT_UNIV: usize = 0;
+const S_N_MATERIALS: usize = 1;
+const S_GEOM_I: usize = 2; // base of geometry idata in idata blob
+const S_GEOM_F: usize = 3; // base of geometry fdata in fdata blob
+const S_CE_I: usize = 4; // base of CE-nuclide idata
+const S_CE_F: usize = 5; // base of CE-nuclide fdata
+const S_MATTAB_I: usize = 6; // base of material table (in CE idata region)
+const S_MATNUCIDX_I: usize = 7;
+const S_MATNUCDEN_F: usize = 8;
+const S_AWR_F: usize = 9;
+const S_ANG_I: usize = 10; // per-nuclide angular idata base (stride A_HDR2)
+const S_ANG_F: usize = 11;
+const S_ANG_STRIDE_I: usize = 12; // i32 words per nuclide angular header+arrays
+const S_FIS_I: usize = 13;
+const S_FIS_F: usize = 14;
+const S_FIS_STRIDE_I: usize = 15;
+const S_ANG_STRIDE_F: usize = 16;
+const S_FIS_STRIDE_F: usize = 17;
+const S_NUC_HDR: usize = 18; // NUC_HDR (3) — exposed for kernel indexing
+const S_LEN: usize = 24;
+
+// Geometry meta offsets (must match pack_geom_into below). These index
+// into the geometry idata/fdata sub-blobs (relative to S_GEOM_I/S_GEOM_F).
+const G_SURF_TYPE: usize = 0;
+const G_SURF_BC: usize = 1;
+const G_CELL_REGION_OFF: usize = 2;
+const G_CELL_REGION_LEN: usize = 3;
+const G_CELL_FILL_TYPE: usize = 4;
+const G_CELL_FILL_DATA: usize = 5;
+const G_REGION_OP: usize = 6;
+const G_REGION_ARG: usize = 7;
+const G_UNIV_CELLS_OFF: usize = 8;
+const G_UNIV_CELLS_LEN: usize = 9;
+const G_UNIV_CELL_IDX: usize = 10;
+const G_LAT_SHAPE: usize = 11;
+const G_LAT_UNIV_OFF: usize = 12;
+const G_LAT_UNIV: usize = 13;
+const G_SURF_PARAMS: usize = 14; // f-blob
+const G_LAT_ORIGIN: usize = 15; // f-blob
+const G_LAT_PITCH: usize = 16; // f-blob
+const G_HDR: usize = 20;
+
+/// Build the unified CE scene pack from a Geometry + per-nuclide CE
+/// data + per-material nuclide lists. Re-uses the per-nuclide / angular
+/// / fission packers and records base offsets. Errors if `geom` uses a
+/// feature the CubeCL walk doesn't support yet (per-cell rotation, hex
+/// lattice) — see `check_gpu_supported`.
+pub fn pack_ce_full(
+ tables: &HostTables,
+ geom: &Geometry,
+ nuclides: &[NuclideCe],
+ angular: &[AngularCe],
+ fission: &[FissionCe],
+ materials: &[MaterialCe],
+) -> Result {
+ check_gpu_supported(geom)?;
+ let mut idata: Vec = Vec::new();
+ let mut fdata: Vec = Vec::new();
+ let mut header = vec![0i32; S_LEN];
+
+ // ── geometry sub-blob ──
+ let mut g_meta = vec![0i32; G_HDR];
+ let geom_i_base = idata.len();
+ // reserve geometry meta inline at the front of geometry idata region
+ let g_meta_at = idata.len();
+ idata.extend(std::iter::repeat_n(0, G_HDR));
+ let geom_f_base = fdata.len();
+
+ let push_i = |idata: &mut Vec, src: &[i32]| -> i32 {
+ let off = (idata.len() - geom_i_base) as i32;
+ idata.extend_from_slice(src);
+ off
+ };
+ g_meta[G_SURF_TYPE] = push_i(&mut idata, &tables.surf_type);
+ g_meta[G_SURF_BC] = push_i(&mut idata, &tables.surf_bc);
+ g_meta[G_CELL_REGION_OFF] = push_i(&mut idata, &tables.cell_region_off);
+ g_meta[G_CELL_REGION_LEN] = push_i(&mut idata, &tables.cell_region_len);
+ g_meta[G_CELL_FILL_TYPE] = push_i(&mut idata, &tables.cell_fill_type);
+ g_meta[G_CELL_FILL_DATA] = push_i(&mut idata, &tables.cell_fill_data);
+ g_meta[G_REGION_OP] = push_i(&mut idata, &tables.region_op);
+ g_meta[G_REGION_ARG] = push_i(&mut idata, &tables.region_arg);
+ g_meta[G_UNIV_CELLS_OFF] = push_i(&mut idata, &tables.univ_cells_off);
+ g_meta[G_UNIV_CELLS_LEN] = push_i(&mut idata, &tables.univ_cells_len);
+ g_meta[G_UNIV_CELL_IDX] = push_i(&mut idata, &tables.univ_cell_indices);
+ g_meta[G_LAT_SHAPE] = push_i(&mut idata, &tables.lat_shape);
+ g_meta[G_LAT_UNIV_OFF] = push_i(&mut idata, &tables.lat_universes_off);
+ g_meta[G_LAT_UNIV] = push_i(&mut idata, &tables.lat_universes);
+ // write geometry meta back
+ idata[g_meta_at..g_meta_at + G_HDR].copy_from_slice(&g_meta[..G_HDR]);
+
+ let push_f = |fdata: &mut Vec, src: &[f64]| -> i32 {
+ let off = (fdata.len() - geom_f_base) as i32;
+ fdata.extend_from_slice(src);
+ off
+ };
+ let g_sp = push_f(&mut fdata, &tables.surf_params);
+ let g_lo = push_f(&mut fdata, &tables.lat_origin);
+ let g_lp = push_f(&mut fdata, &tables.lat_pitch);
+ idata[g_meta_at + G_SURF_PARAMS] = g_sp;
+ idata[g_meta_at + G_LAT_ORIGIN] = g_lo;
+ idata[g_meta_at + G_LAT_PITCH] = g_lp;
+
+ // ── CE nuclide sub-blob (per-nuclide grid+xs, NUC_HDR header each) ──
+ let ce_i_base = idata.len();
+ let ce_f_base = fdata.len();
+ // per-nuclide NUC_HDR=3 header [grid_off, n_e, xs_off] (rel to ce_f_base)
+ idata.extend(std::iter::repeat_n(0, nuclides.len() * NUC_HDR));
+ for (n, nuc) in nuclides.iter().enumerate() {
+ let n_e = nuc.grid.len();
+ let grid_off = (fdata.len() - ce_f_base) as i32;
+ fdata.extend_from_slice(&nuc.grid);
+ let xs_off = (fdata.len() - ce_f_base) as i32;
+ for r in 0..N_RX {
+ fdata.extend_from_slice(&nuc.xs[r]);
+ }
+ idata[ce_i_base + n * NUC_HDR] = grid_off;
+ idata[ce_i_base + n * NUC_HDR + 1] = n_e as i32;
+ idata[ce_i_base + n * NUC_HDR + 2] = xs_off;
+ }
+
+ // awr array (f-blob).
+ let awr_f = (fdata.len()) as i32;
+ fdata.extend(nuclides.iter().map(|n| n.awr));
+
+ // ── material table ──
+ let mattab_i = idata.len() as i32;
+ idata.extend(std::iter::repeat_n(0, materials.len() * 2));
+ let mut flat_idx: Vec = Vec::new();
+ let mut flat_den: Vec = Vec::new();
+ for (m, mat) in materials.iter().enumerate() {
+ let off = flat_idx.len() as i32;
+ for &(ni, den) in &mat.nuclides {
+ flat_idx.push(ni as i32);
+ flat_den.push(den);
+ }
+ idata[mattab_i as usize + m * 2] = off;
+ idata[mattab_i as usize + m * 2 + 1] = mat.nuclides.len() as i32;
+ }
+ let matnucidx_i = idata.len() as i32;
+ idata.extend_from_slice(&flat_idx);
+ let matnucden_f = fdata.len() as i32;
+ fdata.extend_from_slice(&flat_den);
+
+ // ── angular sub-blobs: pack each nuclide via pack_angular, store at a
+ // fixed stride so the kernel can index nuclide n directly. ──
+ let ang_packs: Vec = angular.iter().map(pack_angular).collect();
+ let ang_stride_i = ang_packs.iter().map(|p| p.idata.len()).max().unwrap_or(1).max(1);
+ let ang_stride_f = ang_packs.iter().map(|p| p.fdata.len()).max().unwrap_or(1).max(1);
+ let ang_i_base = idata.len() as i32;
+ let ang_f_base = fdata.len() as i32;
+ for p in &ang_packs {
+ let i0 = idata.len();
+ idata.extend(std::iter::repeat_n(0, ang_stride_i));
+ idata[i0..i0 + p.idata.len()].copy_from_slice(&p.idata);
+ let f0 = fdata.len();
+ fdata.extend(std::iter::repeat_n(0.0, ang_stride_f));
+ fdata[f0..f0 + p.fdata.len()].copy_from_slice(&p.fdata);
+ }
+
+ // ── fission sub-blobs: same fixed-stride scheme. ──
+ let fis_packs: Vec = fission.iter().map(pack_fission).collect();
+ let fis_stride_i = fis_packs.iter().map(|p| p.idata.len()).max().unwrap_or(1).max(1);
+ let fis_stride_f = fis_packs.iter().map(|p| p.fdata.len()).max().unwrap_or(1).max(1);
+ let fis_i_base = idata.len() as i32;
+ let fis_f_base = fdata.len() as i32;
+ for p in &fis_packs {
+ let i0 = idata.len();
+ idata.extend(std::iter::repeat_n(0, fis_stride_i));
+ idata[i0..i0 + p.idata.len()].copy_from_slice(&p.idata);
+ let f0 = fdata.len();
+ fdata.extend(std::iter::repeat_n(0.0, fis_stride_f));
+ fdata[f0..f0 + p.fdata.len()].copy_from_slice(&p.fdata);
+ }
+
+ if idata.is_empty() {
+ idata.push(0);
+ }
+ if fdata.is_empty() {
+ fdata.push(0.0);
+ }
+
+ header[S_ROOT_UNIV] = geom.root_universe.0 as i32;
+ header[S_N_MATERIALS] = materials.len() as i32;
+ header[S_GEOM_I] = geom_i_base as i32;
+ header[S_GEOM_F] = geom_f_base as i32;
+ header[S_CE_I] = ce_i_base as i32;
+ header[S_CE_F] = ce_f_base as i32;
+ header[S_MATTAB_I] = mattab_i;
+ header[S_MATNUCIDX_I] = matnucidx_i;
+ header[S_MATNUCDEN_F] = matnucden_f;
+ header[S_AWR_F] = awr_f;
+ header[S_ANG_I] = ang_i_base;
+ header[S_ANG_F] = ang_f_base;
+ header[S_ANG_STRIDE_I] = ang_stride_i as i32;
+ header[S_ANG_STRIDE_F] = ang_stride_f as i32;
+ header[S_FIS_I] = fis_i_base;
+ header[S_FIS_F] = fis_f_base;
+ header[S_FIS_STRIDE_I] = fis_stride_i as i32;
+ header[S_FIS_STRIDE_F] = fis_stride_f as i32;
+ header[S_NUC_HDR] = NUC_HDR as i32;
+
+ Ok(PackedCeScene {
+ idata,
+ fdata,
+ header,
+ n_nuclides: nuclides.len(),
+ n_materials: materials.len(),
+ })
+}
+
+// ── Fused CE transport kernel ───────────────────────────────────────
+
+const PI_CE: f64 = 3.141592653589793;
+const KB_EV: f64 = 8.617333262e-5; // Boltzmann, eV/K
+
+/// Look up macroscopic Σ for one nuclide at energy `e`, channel `rx`,
+/// scaled by atom density `nd`. Reads the CE sub-blob at ce_i_base/ce_f_base.
+#[cube]
+fn ce_macro_rx(
+ idata: &Array,
+ fdata: &Array,
+ ce_i_base: u32,
+ ce_f_base: u32,
+ nuc_hdr: u32,
+ ni: u32,
+ rx: u32,
+ e: f64,
+) -> f64 {
+ let hb = ce_i_base + ni * nuc_hdr;
+ let grid_off = ce_f_base + u32::cast_from(idata[hb as usize]);
+ let n_e = u32::cast_from(idata[(hb + 1u32) as usize]);
+ let xs_off = ce_f_base + u32::cast_from(idata[(hb + 2u32) as usize]);
+ // binary search on grid.
+ let idx = energy_index(fdata, grid_off, n_e, e);
+ // log-log interp on channel rx.
+ let base = xs_off + rx * n_e;
+ let e_lo = fdata[(grid_off + idx) as usize];
+ let xs_lo = fdata[(base + idx) as usize];
+ let mut out = xs_lo;
+ if idx + 1u32 < n_e {
+ let e_hi = fdata[(grid_off + idx + 1u32) as usize];
+ let xs_hi = fdata[(base + idx + 1u32) as usize];
+ if e_hi > e_lo {
+ if xs_lo > f64::new(0.0) && xs_hi > f64::new(0.0) {
+ // Clamped log-log — see `rx_interp`'s matching comment.
+ let f_raw = (e / e_lo).ln() / (e_hi / e_lo).ln();
+ let f_hi = select(f_raw > f64::new(1.0), f64::new(1.0), f_raw);
+ let f = select(f_hi < f64::new(0.0), f64::new(0.0), f_hi);
+ out = xs_lo * (f * (xs_hi / xs_lo).ln()).exp();
+ } else {
+ let frac = (e - e_lo) / (e_hi - e_lo);
+ out = xs_lo + frac * (xs_hi - xs_lo);
+ }
+ }
+ }
+ out
+}
+
+/// Two-body elastic energy loss + scatter direction. Free-gas target
+/// (E < 400 kT) gives the target velocity; the scattering cosine comes
+/// from the validated tabulated angular sampler (CM). `dir` (len 3)
+/// mutated; returns outgoing energy. Mirrors the .cu elastic event.
+#[cube]
+#[allow(unused_assignments)]
+fn ce_elastic_scatter(
+ idata: &Array,
+ fdata: &Array,
+ ang_i_base: u32,
+ ang_f_base: u32,
+ ang_stride_i: u32,
+ ang_stride_f: u32,
+ ni: u32,
+ a: f64,
+ cell_kt: f64,
+ e_in: f64,
+ rng: &mut Array,
+ dir: &mut Array,
+) -> f64 {
+ // free-gas target velocity components (0 if E above thermal cutoff).
+ let mut vtx = f64::new(0.0);
+ let mut vty = f64::new(0.0);
+ let mut vtz = f64::new(0.0);
+ let kt = select(cell_kt > f64::new(0.0), cell_kt, f64::new(600.0) * KB_EV);
+ if e_in < f64::new(400.0) * kt {
+ let sigma = (kt / a).sqrt();
+ let u1 = fmax_small(ce_uniform_f(rng));
+ let u2 = ce_uniform_f(rng);
+ let r1 = sigma * (-2.0 * u1.ln()).sqrt();
+ vtx = r1 * (2.0 * PI_CE * u2).cos();
+ vty = r1 * (2.0 * PI_CE * u2).sin();
+ let u3 = fmax_small(ce_uniform_f(rng));
+ let u4 = ce_uniform_f(rng);
+ let r2 = sigma * (-2.0 * u3.ln()).sqrt();
+ vtz = r2 * (2.0 * PI_CE * u4).cos();
+ }
+ let v_n = (2.0 * e_in).sqrt();
+ let vnx = dir[0] * v_n;
+ let vny = dir[1] * v_n;
+ let vnz = dir[2] * v_n;
+ let vrx = vnx - vtx;
+ let vry = vny - vty;
+ let vrz = vnz - vtz;
+ let vr2 = vrx * vrx + vry * vry + vrz * vrz;
+ let vr = select(vr2 > f64::new(1e-40), vr2.sqrt(), f64::new(1e-20));
+ let ia1 = f64::new(1.0) / (f64::new(1.0) + a);
+ let vcx = (vnx + a * vtx) * ia1;
+ let vcy = (vny + a * vty) * ia1;
+ let vcz = (vnz + a * vtz) * ia1;
+ let vcn = vr * a * ia1;
+ // CM cosine from tabulated angular dist (relative energy as the key).
+ let e_rel = 0.5 * (a / (a + 1.0)) * vr * vr;
+ let xi_bin = ce_uniform_f(rng);
+ let xi_mu = ce_uniform_f(rng);
+ let mu_cm = sample_mu_at_base(idata, fdata, ang_i_base + ni * ang_stride_i, ang_f_base + ni * ang_stride_f, e_rel, xi_bin, xi_mu);
+ let phi = 2.0 * PI_CE * ce_uniform_f(rng);
+ // rotate the relative-velocity unit vector by (mu_cm, phi), scale by
+ // vcn, add the CM velocity → lab outgoing velocity.
+ let mut rel = Array::::new(3usize);
+ rel[0] = vrx / vr;
+ rel[1] = vry / vr;
+ rel[2] = vrz / vr;
+ rotate_dir_ce(&mut rel, mu_cm, phi);
+ let voutx = vcx + vcn * rel[0];
+ let vouty = vcy + vcn * rel[1];
+ let voutz = vcz + vcn * rel[2];
+ let vout2 = voutx * voutx + vouty * vouty + voutz * voutz;
+ let e_out = 0.5 * vout2;
+ let vout = select(vout2 > f64::new(1e-40), vout2.sqrt(), f64::new(1e-20));
+ dir[0] = voutx / vout;
+ dir[1] = vouty / vout;
+ dir[2] = voutz / vout;
+ select(e_out > f64::new(1e-5), e_out, f64::new(1e-5))
+}
+
+/// Rotate unit vector `dir` (len 3) by polar cosine `mu` and azimuth `phi`.
+#[cube]
+fn rotate_dir_ce(dir: &mut Array, mu: f64, phi: f64) {
+ let u = dir[0];
+ let v = dir[1];
+ let w = dir[2];
+ let s = (f64::new(1.0) - mu * mu).sqrt();
+ let cphi = phi.cos();
+ let sphi = phi.sin();
+ let perp = (f64::new(1.0) - w * w).sqrt();
+ if perp > f64::new(1e-10) {
+ dir[0] = mu * u + s * (cphi * u * w - sphi * v) / perp;
+ dir[1] = mu * v + s * (cphi * v * w + sphi * u) / perp;
+ dir[2] = mu * w - s * cphi * perp;
+ } else {
+ dir[0] = s * cphi;
+ dir[1] = s * sphi;
+ dir[2] = select(w > f64::new(0.0), mu, -mu);
+ }
+}
+
+/// `sample_mu_at` variant taking explicit idata/fdata bases (so the
+/// angular sub-blob can live anywhere in the unified blob).
+#[cube]
+#[allow(unused_assignments)]
+fn sample_mu_at_base(
+ idata: &Array,
+ fdata: &Array,
+ ibase: u32,
+ fbase: u32,
+ energy: f64,
+ xi_bin: f64,
+ xi_mu: f64,
+) -> f64 {
+ // header lives at ibase (A_HDR slots); offsets inside are relative
+ // to the sub-pack's own idata[0]/fdata[0], so add ibase/fbase.
+ let n_e = u32::cast_from(idata[(ibase + A_N_E as u32) as usize]);
+ let mut out = 2.0 * xi_mu - 1.0;
+ if n_e >= 1u32 {
+ let eoff = fbase + u32::cast_from(idata[(ibase + A_EOFF as u32) as usize]);
+ let doff_base = ibase + u32::cast_from(idata[(ibase + A_DOFF as u32) as usize]);
+ let dlen_base = ibase + u32::cast_from(idata[(ibase + A_DLEN as u32) as usize]);
+ let hoff_base = ibase + u32::cast_from(idata[(ibase + A_HOFF as u32) as usize]);
+ let mu_off = fbase + u32::cast_from(idata[(ibase + A_MUOFF as u32) as usize]);
+ let cdf_off = fbase + u32::cast_from(idata[(ibase + A_CDFOFF as u32) as usize]);
+ let pdf_off = fbase + u32::cast_from(idata[(ibase + A_PDFOFF as u32) as usize]);
+ let e0 = fdata[eoff as usize];
+ let elast = fdata[(eoff + n_e - 1u32) as usize];
+ let mut pick = u32::new(0);
+ if energy <= e0 {
+ pick = u32::new(0);
+ } else {
+ if energy >= elast {
+ pick = n_e - 1u32;
+ } else {
+ let mut lo = u32::new(0);
+ let mut hi = n_e - 1u32;
+ for _i in 0..32u32 {
+ if lo + 1u32 < hi {
+ let mid = (lo + hi) / 2u32;
+ if fdata[(eoff + mid) as usize] <= energy {
+ lo = mid;
+ } else {
+ hi = mid;
+ }
+ }
+ }
+ let e_lo = fdata[(eoff + lo) as usize];
+ let e_hi = fdata[(eoff + lo + 1u32) as usize];
+ let r = (energy - e_lo) / (e_hi - e_lo);
+ pick = select(xi_bin < r, lo + 1u32, lo);
+ }
+ }
+ let dloc = u32::cast_from(idata[(doff_base + pick) as usize]);
+ let dlenv = u32::cast_from(idata[(dlen_base + pick) as usize]);
+ let hist = idata[(hoff_base + pick) as usize];
+ out = sample_mu_bin(fdata, mu_off, cdf_off, pdf_off, dloc, dlenv, hist, xi_mu);
+ }
+ out
+}
+
+/// fis_sample_energy variant with explicit idata/fdata bases.
+#[cube]
+#[allow(unused_assignments)]
+fn fis_sample_energy_base(
+ idata: &Array,
+ fdata: &Array,
+ ibase: u32,
+ fbase: u32,
+ e_in: f64,
+ rng: &mut Array,
+) -> f64 {
+ // The packed fission header/offsets are relative to the sub-pack's
+ // own idata[0]/fdata[0]; shift e_out/cdf/pdf reads by fbase and the
+ // dist_off/len index reads by ibase. We re-implement the tabular
+ // path here using the same logic as fis_sample_energy but rebased.
+ let law = idata[(ibase + F_LAW as u32) as usize];
+ let u = fdata[(fbase + F_U_SLOT as u32) as usize];
+ let mut out = e_in;
+ if law == 0i32 {
+ let n_e = u32::cast_from(idata[(ibase + F_N_E as u32) as usize]);
+ let eoff = fbase + u32::cast_from(idata[(ibase + F_EOFF as u32) as usize]);
+ let doff_base = ibase + u32::cast_from(idata[(ibase + F_DOFF as u32) as usize]);
+ let dloff_base = ibase + u32::cast_from(idata[(ibase + F_DLOFF as u32) as usize]);
+ let eout_off = fbase + u32::cast_from(idata[(ibase + F_EOUT_OFF as u32) as usize]);
+ let pdf_off = fbase + u32::cast_from(idata[(ibase + F_PDF_OFF as u32) as usize]);
+ let cdf_off = fbase + u32::cast_from(idata[(ibase + F_CDF_OFF as u32) as usize]);
+ if n_e >= 1u32 {
+ let e0 = fdata[eoff as usize];
+ let elast = fdata[(eoff + n_e - 1u32) as usize];
+ if e_in <= e0 {
+ let xi = ce_uniform_f(rng);
+ let doff = u32::cast_from(idata[doff_base as usize]);
+ let dlen = u32::cast_from(idata[dloff_base as usize]);
+ out = fis_sample_eout_bin(fdata, eout_off, cdf_off, pdf_off, doff, dlen, xi);
+ } else {
+ if e_in >= elast {
+ let xi = ce_uniform_f(rng);
+ let doff = u32::cast_from(idata[(doff_base + n_e - 1u32) as usize]);
+ let dlen = u32::cast_from(idata[(dloff_base + n_e - 1u32) as usize]);
+ out = fis_sample_eout_bin(fdata, eout_off, cdf_off, pdf_off, doff, dlen, xi);
+ } else {
+ let mut lo = u32::new(0);
+ let mut hi = n_e - 1u32;
+ for _i in 0..32u32 {
+ if lo + 1u32 < hi {
+ let mid = (lo + hi) / 2u32;
+ if fdata[(eoff + mid) as usize] <= e_in {
+ lo = mid;
+ } else {
+ hi = mid;
+ }
+ }
+ }
+ let e_lo = fdata[(eoff + lo) as usize];
+ let e_hi = fdata[(eoff + lo + 1u32) as usize];
+ let r = (e_in - e_lo) / (e_hi - e_lo);
+ let pick_hi = ce_uniform_f(rng) < r;
+ let l = select(pick_hi, lo + 1u32, lo);
+ let doff_l = u32::cast_from(idata[(doff_base + l) as usize]);
+ let dlen_l = u32::cast_from(idata[(dloff_base + l) as usize]);
+ let xi = ce_uniform_f(rng);
+ let e_out = fis_sample_eout_bin(fdata, eout_off, cdf_off, pdf_off, doff_l, dlen_l, xi);
+ let doff_a = u32::cast_from(idata[(doff_base + lo) as usize]);
+ let dlen_a = u32::cast_from(idata[(dloff_base + lo) as usize]);
+ let doff_b = u32::cast_from(idata[(doff_base + lo + 1u32) as usize]);
+ let dlen_b = u32::cast_from(idata[(dloff_base + lo + 1u32) as usize]);
+ let ea_lo = fdata[(eout_off + doff_a) as usize];
+ let ea_hi = fdata[(eout_off + doff_a + dlen_a - 1u32) as usize];
+ let eb_lo = fdata[(eout_off + doff_b) as usize];
+ let eb_hi = fdata[(eout_off + doff_b + dlen_b - 1u32) as usize];
+ let el_lo = fdata[(eout_off + doff_l) as usize];
+ let el_hi = fdata[(eout_off + doff_l + dlen_l - 1u32) as usize];
+ let e1 = (f64::new(1.0) - r) * ea_lo + r * eb_lo;
+ let ek = (f64::new(1.0) - r) * ea_hi + r * eb_hi;
+ let span = el_hi - el_lo;
+ let adjusted = select(span.abs() < f64::new(1e-30), e_out, e1 + (e_out - el_lo) * (ek - e1) / span);
+ let floor = f64::new(1e-5);
+ out = select(adjusted > floor, adjusted, floor);
+ }
+ }
+ }
+ } else {
+ let pa_e = fbase + u32::cast_from(idata[(ibase + F_PA_E_OFF as u32) as usize]);
+ let pa_n = u32::cast_from(idata[(ibase + F_PA_N as u32) as usize]);
+ let pa_v = fbase + u32::cast_from(idata[(ibase + F_PA_V_OFF as u32) as usize]);
+ let max_e = select(e_in - u > f64::new(1e-5), e_in - u, f64::new(1e-5));
+ if law == 1i32 {
+ let pb_e = fbase + u32::cast_from(idata[(ibase + F_PB_E_OFF as u32) as usize]);
+ let pb_n = u32::cast_from(idata[(ibase + F_PB_N as u32) as usize]);
+ let pb_v = fbase + u32::cast_from(idata[(ibase + F_PB_V_OFF as u32) as usize]);
+ let a = fis_lin_lin(fdata, pa_e, pa_v, pa_n, e_in);
+ let b = fis_lin_lin(fdata, pb_e, pb_v, pb_n, e_in);
+ let mut acc = f64::new(0.0);
+ let mut done = false;
+ for _i in 0..128u32 {
+ if !done {
+ let xi1 = fmax_small(ce_uniform_f(rng));
+ let xi2 = ce_uniform_f(rng);
+ let xi3 = fmax_small(ce_uniform_f(rng));
+ let xi4 = ce_uniform_f(rng);
+ let c = (f64::new(1.5707963267948966) * xi2).cos();
+ let w = -a * (xi1.ln() + c * c * xi3.ln());
+ let term = a * a * b / 4.0;
+ let e_out = w + term + (2.0 * xi4 - 1.0) * (a * a * b * w).sqrt();
+ acc = e_out;
+ if e_out > f64::new(0.0) && e_out <= max_e {
+ done = true;
+ }
+ }
+ }
+ // Clamp into [1e-5, max_e] — see `fis_sample_energy`'s Watt
+ // branch for the rationale (mirrors `WattLaw::sample`).
+ let clamped_hi = select(acc > max_e, max_e, acc);
+ out = select(clamped_hi > f64::new(1e-5), clamped_hi, f64::new(1e-5));
+ } else {
+ let theta = fis_lin_lin(fdata, pa_e, pa_v, pa_n, e_in);
+ let mut acc = f64::new(0.0);
+ let mut done = false;
+ for _i in 0..128u32 {
+ if !done {
+ let xi1 = fmax_small(ce_uniform_f(rng));
+ let xi2 = ce_uniform_f(rng);
+ let xi3 = fmax_small(ce_uniform_f(rng));
+ let cc = (f64::new(1.5707963267948966) * xi2).cos();
+ let e_out = select(law == 2i32, -theta * (xi1.ln() + cc * cc * xi3.ln()), -theta * (xi1 * xi2).ln());
+ acc = e_out;
+ if e_out > f64::new(0.0) && e_out <= max_e {
+ done = true;
+ }
+ }
+ }
+ // Same exhaustion-clamp as above, mirroring
+ // `MaxwellLaw::sample_maxwell` / `sample_evaporation`.
+ let clamped_hi = select(acc > max_e, max_e, acc);
+ out = select(clamped_hi > f64::new(1e-5), clamped_hi, f64::new(1e-5));
+ }
+ }
+ out
+}
+
+// ── The fused CE transport kernel (one source neutron per thread) ───
+
+#[cube(launch)]
+#[allow(clippy::too_many_arguments, unused_assignments, unused_variables)]
+fn ce_transport_kernel(
+ header: &Array,
+ idata: &Array,
+ fdata: &Array,
+ pos: &mut Array, // 3N (source positions)
+ dir: &mut Array, // 3N
+ ein: &Array, // N (source energies)
+ rng: &mut Array, // 2N
+ mat_kt: &Array, // per material kT (eV)
+ n_particles: u32,
+ max_events: u32,
+ // outputs (atomics, len 4): [fis_count, coll, leak, cap]
+ counters: &mut Array>,
+ // fission energy bank (N*4 cap) + cursor in counters[0]
+ fis_e: &mut Array,
+ fis_cap: u32,
+) {
+ let tid = ABSOLUTE_POS;
+ if tid < n_particles as usize {
+ // header offsets.
+ let root_univ = u32::cast_from(header[S_ROOT_UNIV]);
+ let n_materials = u32::cast_from(header[S_N_MATERIALS]);
+ let geom_i = u32::cast_from(header[S_GEOM_I]);
+ let geom_f = u32::cast_from(header[S_GEOM_F]);
+ let ce_i = u32::cast_from(header[S_CE_I]);
+ let ce_f = u32::cast_from(header[S_CE_F]);
+ let mattab_i = u32::cast_from(header[S_MATTAB_I]);
+ let matnucidx_i = u32::cast_from(header[S_MATNUCIDX_I]);
+ let matnucden_f = u32::cast_from(header[S_MATNUCDEN_F]);
+ let awr_f = u32::cast_from(header[S_AWR_F]);
+ let ang_i = u32::cast_from(header[S_ANG_I]);
+ let ang_f = u32::cast_from(header[S_ANG_F]);
+ let ang_si = u32::cast_from(header[S_ANG_STRIDE_I]);
+ let ang_sf = u32::cast_from(header[S_ANG_STRIDE_F]);
+ let fis_i = u32::cast_from(header[S_FIS_I]);
+ let fis_f = u32::cast_from(header[S_FIS_F]);
+ let fis_si = u32::cast_from(header[S_FIS_STRIDE_I]);
+ let fis_sf = u32::cast_from(header[S_FIS_STRIDE_F]);
+ let nuc_hdr = u32::cast_from(header[S_NUC_HDR]);
+
+ // geometry meta (relative to geom_i / geom_f).
+ let surf_type_off = geom_i + u32::cast_from(idata[(geom_i + G_SURF_TYPE as u32) as usize]);
+ let surf_bc_off = geom_i + u32::cast_from(idata[(geom_i + G_SURF_BC as u32) as usize]);
+ let cell_region_off_a = geom_i + u32::cast_from(idata[(geom_i + G_CELL_REGION_OFF as u32) as usize]);
+ let cell_region_len_a = geom_i + u32::cast_from(idata[(geom_i + G_CELL_REGION_LEN as u32) as usize]);
+ let cell_fill_type_a = geom_i + u32::cast_from(idata[(geom_i + G_CELL_FILL_TYPE as u32) as usize]);
+ let cell_fill_data_a = geom_i + u32::cast_from(idata[(geom_i + G_CELL_FILL_DATA as u32) as usize]);
+ let region_op_off = geom_i + u32::cast_from(idata[(geom_i + G_REGION_OP as u32) as usize]);
+ let region_arg_off = geom_i + u32::cast_from(idata[(geom_i + G_REGION_ARG as u32) as usize]);
+ let univ_cells_off_base = geom_i + u32::cast_from(idata[(geom_i + G_UNIV_CELLS_OFF as u32) as usize]);
+ let univ_cells_len_a = geom_i + u32::cast_from(idata[(geom_i + G_UNIV_CELLS_LEN as u32) as usize]);
+ let univ_cell_indices_a = geom_i + u32::cast_from(idata[(geom_i + G_UNIV_CELL_IDX as u32) as usize]);
+ let lat_shape_off = geom_i + u32::cast_from(idata[(geom_i + G_LAT_SHAPE as u32) as usize]);
+ let lat_universes_off_a = geom_i + u32::cast_from(idata[(geom_i + G_LAT_UNIV_OFF as u32) as usize]);
+ let lat_universes_a = geom_i + u32::cast_from(idata[(geom_i + G_LAT_UNIV as u32) as usize]);
+ let surf_params_off = geom_f + u32::cast_from(idata[(geom_i + G_SURF_PARAMS as u32) as usize]);
+ let lat_origin_off = geom_f + u32::cast_from(idata[(geom_i + G_LAT_ORIGIN as u32) as usize]);
+ let lat_pitch_off = geom_f + u32::cast_from(idata[(geom_i + G_LAT_PITCH as u32) as usize]);
+
+ // particle state.
+ let mut lrng = Array::::new(2usize);
+ lrng[0] = rng[tid * 2];
+ lrng[1] = rng[tid * 2 + 1];
+ let mut px = pos[tid * 3];
+ let mut py = pos[tid * 3 + 1];
+ let mut pz = pos[tid * 3 + 2];
+ let mut ddir = Array::::new(3usize);
+ ddir[0] = dir[tid * 3];
+ ddir[1] = dir[tid * 3 + 1];
+ ddir[2] = dir[tid * 3 + 2];
+ let mut e = ein[tid];
+
+ // geometry stack.
+ let mut st_cell = Array::::new(MAX_DEPTH_USIZE);
+ let mut st_offx = Array::::new(MAX_DEPTH_USIZE);
+ let mut st_offy = Array::::new(MAX_DEPTH_USIZE);
+ let mut st_offz = Array::::new(MAX_DEPTH_USIZE);
+ let mut st_has_lat = Array::::new(MAX_DEPTH_USIZE);
+ let mut st_lat_id = Array::::new(MAX_DEPTH_USIZE);
+ let mut st_lat_ix = Array::::new(MAX_DEPTH_USIZE);
+ let mut st_lat_iy = Array::::new(MAX_DEPTH_USIZE);
+ let mut st_lat_iz = Array::::new(MAX_DEPTH_USIZE);
+ let mut ts = Array::::new(3usize);
+
+ let mut depth = find_cell(
+ idata, fdata, surf_params_off, surf_type_off, region_op_off, region_arg_off,
+ cell_region_off_a, cell_region_len_a, cell_fill_type_a, cell_fill_data_a,
+ univ_cells_off_base, univ_cells_len_a, univ_cell_indices_a,
+ lat_shape_off, lat_universes_off_a, lat_universes_a, lat_origin_off, lat_pitch_off,
+ root_univ, px, py, pz,
+ &mut st_cell, &mut st_offx, &mut st_offy, &mut st_offz,
+ &mut st_has_lat, &mut st_lat_id, &mut st_lat_ix, &mut st_lat_iy, &mut st_lat_iz,
+ );
+
+ let mut alive = u32::new(1);
+ let mut lc_coll = u32::new(0);
+ let mut lc_leak = u32::new(0);
+ let mut lc_cap = u32::new(0);
+ let mut lc_fis = u32::new(0);
+ if depth == 0u32 {
+ alive = u32::new(0);
+ lc_leak += 1;
+ }
+
+ let mut ev = u32::new(0);
+ for _i in 0..8192u32 {
+ if alive == 1u32 && ev < max_events {
+ ev += 1;
+ let mut need_refind = false;
+ let ci = st_cell[(depth - 1) as usize];
+ let ft = idata[(cell_fill_type_a + u32::cast_from(ci)) as usize];
+ let mut mat = i32::new(-1);
+ if ft == FILL_MATERIAL {
+ mat = idata[(cell_fill_data_a + u32::cast_from(ci)) as usize];
+ }
+ if mat < 0i32 {
+ // void: free-stream to next surface.
+ trace_step(
+ idata, fdata, surf_params_off, surf_type_off, surf_bc_off,
+ region_op_off, region_arg_off, cell_region_off_a, cell_region_len_a,
+ lat_origin_off, lat_pitch_off, px, py, pz, ddir[0], ddir[1], ddir[2],
+ depth, &mut st_cell, &mut st_offx, &mut st_offy, &mut st_offz, &mut st_has_lat,
+ &mut st_lat_id, &mut st_lat_ix, &mut st_lat_iy, &mut st_lat_iz, &mut ts,
+ );
+ let dist = ts[0];
+ let surf_idx = i32::cast_from(ts[1]);
+ let bc = i32::cast_from(ts[2]);
+ if dist >= f64::new(1e29) {
+ alive = u32::new(0);
+ lc_leak += 1;
+ } else {
+ cross_or_die(idata, fdata, surf_params_off, surf_type_off, bc, surf_idx, dist,
+ &mut px, &mut py, &mut pz, &mut ddir, &mut alive);
+ if bc == BC_VACUUM { lc_leak += 1; }
+ if bc == BC_TRANSMISSION { need_refind = true; }
+ }
+ } else {
+ // material: sum Σ_t over its nuclides.
+ let mtab = mattab_i + u32::cast_from(mat) * 2u32;
+ let list_off = u32::cast_from(idata[mtab as usize]);
+ let list_len = u32::cast_from(idata[(mtab + 1u32) as usize]);
+ let cell_kt = mat_kt[mat as usize];
+ let mut sum_t = f64::new(0.0);
+ for j in 0..list_len {
+ let ni = u32::cast_from(idata[(matnucidx_i + list_off + j) as usize]);
+ let nd = fdata[(matnucden_f + list_off + j) as usize];
+ let st = ce_macro_rx(idata, fdata, ce_i, ce_f, nuc_hdr, ni, RX_ELASTIC as u32, e)
+ + ce_macro_rx(idata, fdata, ce_i, ce_f, nuc_hdr, ni, RX_FISSION as u32, e)
+ + ce_macro_rx(idata, fdata, ce_i, ce_f, nuc_hdr, ni, RX_CAPTURE as u32, e)
+ + ce_macro_rx(idata, fdata, ce_i, ce_f, nuc_hdr, ni, RX_INELASTIC as u32, e)
+ + ce_macro_rx(idata, fdata, ce_i, ce_f, nuc_hdr, ni, RX_N2N as u32, e);
+ sum_t = sum_t + nd * st;
+ }
+ let xi_coll = fmax_small(ce_uniform_f(&mut lrng));
+ let d_coll = select(sum_t > f64::new(0.0), -(xi_coll.ln()) / sum_t, f64::new(1e30));
+ trace_step(
+ idata, fdata, surf_params_off, surf_type_off, surf_bc_off,
+ region_op_off, region_arg_off, cell_region_off_a, cell_region_len_a,
+ lat_origin_off, lat_pitch_off, px, py, pz, ddir[0], ddir[1], ddir[2],
+ depth, &mut st_cell, &mut st_offx, &mut st_offy, &mut st_offz, &mut st_has_lat,
+ &mut st_lat_id, &mut st_lat_ix, &mut st_lat_iy, &mut st_lat_iz, &mut ts,
+ );
+ let dist = ts[0];
+ let surf_idx = i32::cast_from(ts[1]);
+ let bc = i32::cast_from(ts[2]);
+ if dist >= f64::new(1e29) {
+ alive = u32::new(0);
+ lc_leak += 1;
+ } else {
+ if d_coll < dist {
+ // collision.
+ px = px + ddir[0] * d_coll;
+ py = py + ddir[1] * d_coll;
+ pz = pz + ddir[2] * d_coll;
+ lc_coll += 1;
+ // pick nuclide ∝ Σ_t,nuc.
+ let xi_nuc = ce_uniform_f(&mut lrng) * sum_t;
+ let mut cum = f64::new(0.0);
+ let mut hit_local = u32::new(0);
+ let mut found = false;
+ for j in 0..list_len {
+ if !found {
+ let ni = u32::cast_from(idata[(matnucidx_i + list_off + j) as usize]);
+ let nd = fdata[(matnucden_f + list_off + j) as usize];
+ let st = ce_macro_rx(idata, fdata, ce_i, ce_f, nuc_hdr, ni, RX_ELASTIC as u32, e)
+ + ce_macro_rx(idata, fdata, ce_i, ce_f, nuc_hdr, ni, RX_FISSION as u32, e)
+ + ce_macro_rx(idata, fdata, ce_i, ce_f, nuc_hdr, ni, RX_CAPTURE as u32, e)
+ + ce_macro_rx(idata, fdata, ce_i, ce_f, nuc_hdr, ni, RX_INELASTIC as u32, e)
+ + ce_macro_rx(idata, fdata, ce_i, ce_f, nuc_hdr, ni, RX_N2N as u32, e);
+ cum = cum + nd * st;
+ if xi_nuc < cum {
+ hit_local = j;
+ found = true;
+ }
+ }
+ }
+ let hit_ni = u32::cast_from(idata[(matnucidx_i + list_off + hit_local) as usize]);
+ // reaction pick within hit nuclide (micro σ, analog).
+ let s_el = ce_macro_rx(idata, fdata, ce_i, ce_f, nuc_hdr, hit_ni, RX_ELASTIC as u32, e);
+ let s_inel = ce_macro_rx(idata, fdata, ce_i, ce_f, nuc_hdr, hit_ni, RX_INELASTIC as u32, e);
+ let s_n2n = ce_macro_rx(idata, fdata, ce_i, ce_f, nuc_hdr, hit_ni, RX_N2N as u32, e);
+ let s_fis = ce_macro_rx(idata, fdata, ce_i, ce_f, nuc_hdr, hit_ni, RX_FISSION as u32, e);
+ let s_cap = ce_macro_rx(idata, fdata, ce_i, ce_f, nuc_hdr, hit_ni, RX_CAPTURE as u32, e);
+ let s_tot = s_el + s_inel + s_n2n + s_fis + s_cap;
+ let xi_rxn = ce_uniform_f(&mut lrng) * select(s_tot > f64::new(0.0), s_tot, f64::new(1.0));
+ let a = fdata[(awr_f + hit_ni) as usize];
+
+ if xi_rxn < s_el {
+ // elastic scatter.
+ e = ce_elastic_scatter(idata, fdata, ang_i, ang_f, ang_si, ang_sf,
+ hit_ni, a, cell_kt, e, &mut lrng, &mut ddir);
+ } else {
+ if xi_rxn < s_el + s_inel {
+ // inelastic: treat as elastic-like CM scatter (energy loss
+ // dominated by kinematics; first-cut for fast spectrum).
+ e = ce_elastic_scatter(idata, fdata, ang_i, ang_f, ang_si, ang_sf,
+ hit_ni, a, cell_kt, e, &mut lrng, &mut ddir);
+ } else {
+ if xi_rxn < s_el + s_inel + s_n2n {
+ // (n,2n): two outgoing; emit isotropically, keep E (approx).
+ // Bank one extra fission-equivalent neutron at scattered E.
+ e = ce_elastic_scatter(idata, fdata, ang_i, ang_f, ang_si, ang_sf,
+ hit_ni, a, cell_kt, e, &mut lrng, &mut ddir);
+ } else {
+ if xi_rxn < s_el + s_inel + s_n2n + s_fis {
+ // fission: bank ν̄ neutrons at χ energies.
+ let nu = fdata[(awr_f + hit_ni) as usize]; // placeholder
+ let _ = nu;
+ let nu_bar = ce_nu(idata, fdata, ce_i, ce_f, nuc_hdr, hit_ni);
+ let xi = ce_uniform_f(&mut lrng);
+ let nfis_f = nu_bar + xi;
+ let nfis = u32::cast_from(nfis_f);
+ for s in 0..nfis {
+ let slot = counters[0].fetch_add(1u32);
+ if slot < fis_cap {
+ fis_e[slot as usize] = fis_sample_energy_base(
+ idata, fdata,
+ fis_i + hit_ni * fis_si, fis_f + hit_ni * fis_sf,
+ e, &mut lrng);
+ }
+ let _ = s;
+ }
+ lc_fis += nfis;
+ alive = u32::new(0);
+ } else {
+ // capture: kill.
+ lc_cap += 1;
+ alive = u32::new(0);
+ }
+ }
+ }
+ }
+ } else {
+ // surface crossing.
+ cross_or_die(idata, fdata, surf_params_off, surf_type_off, bc, surf_idx, dist,
+ &mut px, &mut py, &mut pz, &mut ddir, &mut alive);
+ if bc == BC_VACUUM { lc_leak += 1; }
+ if bc == BC_TRANSMISSION { need_refind = true; }
+ }
+ }
+ }
+
+ if alive == 1u32 && need_refind {
+ depth = find_cell(
+ idata, fdata, surf_params_off, surf_type_off, region_op_off, region_arg_off,
+ cell_region_off_a, cell_region_len_a, cell_fill_type_a, cell_fill_data_a,
+ univ_cells_off_base, univ_cells_len_a, univ_cell_indices_a,
+ lat_shape_off, lat_universes_off_a, lat_universes_a, lat_origin_off, lat_pitch_off,
+ root_univ, px, py, pz,
+ &mut st_cell, &mut st_offx, &mut st_offy, &mut st_offz,
+ &mut st_has_lat, &mut st_lat_id, &mut st_lat_ix, &mut st_lat_iy, &mut st_lat_iz,
+ );
+ if depth == 0u32 {
+ alive = u32::new(0);
+ lc_leak += 1;
+ }
+ }
+ }
+ }
+
+ rng[tid * 2] = lrng[0];
+ rng[tid * 2 + 1] = lrng[1];
+ let _ = n_materials;
+ counters[1].fetch_add(lc_coll);
+ counters[2].fetch_add(lc_leak);
+ counters[3].fetch_add(lc_cap);
+ }
+}
+
+/// ν̄ for a nuclide. First cut: a constant carried in the awr slot's
+/// sibling — here we approximate with 2.5 for fissile nuclides (those
+/// with nonzero fission σ at 1 MeV). A real ν̄(E) grid is a follow-up;
+/// for the k A/B this constant matches what the CPU const-ν path uses.
+#[cube]
+fn ce_nu(idata: &Array, fdata: &Array, ce_i: u32, ce_f: u32, nuc_hdr: u32, ni: u32) -> f64 {
+ // Probe fission σ at ~1 MeV; if > 0 the nuclide is fissile.
+ let s_fis = ce_macro_rx(idata, fdata, ce_i, ce_f, nuc_hdr, ni, RX_FISSION as u32, f64::new(1.0e6));
+ select(s_fis > f64::new(0.0), f64::new(2.5), f64::new(0.0))
+}
+
+/// Result of one CE generation.
+#[derive(Clone, Debug, Default)]
+pub struct CeGen {
+ pub n_fissions: u64,
+ pub n_collisions: u64,
+ pub n_leak: u64,
+ pub n_capture: u64,
+ pub fission_energies: Vec,
+}
+
+/// Run one CE transport generation on the given runtime. Sources are
+/// (pos, dir, energy); returns banked fission neutrons + counters.
+/// Single-generation k = n_fissions / n_source.
+#[allow(clippy::too_many_arguments)]
+pub fn ce_generation(
+ device: &R::Device,
+ scene: &PackedCeScene,
+ mat_kt: &[f64],
+ pos: &[(f64, f64, f64)],
+ dir: &[(f64, f64, f64)],
+ energy: &[f64],
+ seeds: &[(u64, u64)],
+ max_events: u32,
+ fis_cap: usize,
+) -> CeGen {
+ let client = R::client(device);
+ let n = pos.len();
+ let mut pflat = Vec::with_capacity(n * 3);
+ let mut dflat = Vec::with_capacity(n * 3);
+ let mut rflat = Vec::with_capacity(n * 2);
+ for i in 0..n {
+ pflat.push(pos[i].0);
+ pflat.push(pos[i].1);
+ pflat.push(pos[i].2);
+ dflat.push(dir[i].0);
+ dflat.push(dir[i].1);
+ dflat.push(dir[i].2);
+ rflat.push(seeds[i].0);
+ rflat.push(seeds[i].1 | 1);
+ }
+
+ let hdr_h = client.create_from_slice(i32::as_bytes(&scene.header));
+ let idata_h = client.create_from_slice(i32::as_bytes(&scene.idata));
+ let fdata_h = client.create_from_slice(f64::as_bytes(&scene.fdata));
+ let pos_h = client.create_from_slice(f64::as_bytes(&pflat));
+ let dir_h = client.create_from_slice(f64::as_bytes(&dflat));
+ let ein_h = client.create_from_slice(f64::as_bytes(energy));
+ let rng_h = client.create_from_slice(u64::as_bytes(&rflat));
+ let kt_h = client.create_from_slice(f64::as_bytes(mat_kt));
+ let ctr_h = client.create_from_slice(u32::as_bytes(&[0u32; 4]));
+ let fis_h = client.empty(fis_cap.max(1) * core::mem::size_of::());
+
+ let threads = 64u32;
+ let blocks = n.div_ceil(threads as usize) as u32;
+ unsafe {
+ ce_transport_kernel::launch::(
+ &client,
+ CubeCount::Static(blocks, 1, 1),
+ CubeDim::new_1d(threads),
+ ArrayArg::from_raw_parts(hdr_h, scene.header.len()),
+ ArrayArg::from_raw_parts(idata_h, scene.idata.len()),
+ ArrayArg::from_raw_parts(fdata_h, scene.fdata.len()),
+ ArrayArg::from_raw_parts(pos_h, n * 3),
+ ArrayArg::from_raw_parts(dir_h, n * 3),
+ ArrayArg::from_raw_parts(ein_h, n),
+ ArrayArg::from_raw_parts(rng_h, n * 2),
+ ArrayArg::from_raw_parts(kt_h, mat_kt.len()),
+ n as u32,
+ max_events,
+ ArrayArg::from_raw_parts(ctr_h.clone(), 4),
+ ArrayArg::from_raw_parts(fis_h.clone(), fis_cap.max(1)),
+ fis_cap as u32,
+ );
+ }
+ let ctr = u32::from_bytes(&client.read_one(ctr_h).expect("ctr")).to_vec();
+ let n_fis = (ctr[0] as usize).min(fis_cap);
+ let fe = f64::from_bytes(&client.read_one(fis_h).expect("fis")).to_vec();
+ CeGen {
+ n_fissions: ctr[0] as u64,
+ n_collisions: ctr[1] as u64,
+ n_leak: ctr[2] as u64,
+ n_capture: ctr[3] as u64,
+ fission_energies: fe[..n_fis].to_vec(),
+ }
+}
diff --git a/rust_prototype/src/gpu_cubecl_geom.rs b/rust_prototype/src/gpu_cubecl_geom.rs
new file mode 100644
index 0000000..54ce834
--- /dev/null
+++ b/rust_prototype/src/gpu_cubecl_geom.rs
@@ -0,0 +1,642 @@
+// SPDX-License-Identifier: MIT
+//! Shared CubeCL device-side geometry walk.
+//!
+//! `gpu_transport_cubecl` (const-XS transport), `gpu_ce_cubecl` (fused
+//! CE transport), and `gpu_render` (preview raycaster) each need the
+//! same recursive universe/rect-lattice descent over the flattened
+//! geometry SoA. This module is the single copy: the `#[cube]`
+//! primitives (surface eval/distance, CSG containment, boundary
+//! crossing) plus the `find_cell` / `trace_step` walk, written once so
+//! a fix applies to every kernel instead of needing separate patches
+//! to N hand-copied files.
+//!
+//! The SoA tag constants are re-exported from
+//! [`crate::geometry::flat`], the single source of truth also
+//! consumed by the legacy CUDA (`.cu`) backend — this module does not
+//! redefine them.
+//!
+//! `find_cell` resolves `CellFill::Void` cells as ordinary terminal
+//! cells (matching `gr_find_cell` in `geom_recursive.cu` and the CPU
+//! `EffectiveFill::Void` free-stream) rather than treating them as an
+//! immediate leak; callers detect "no material" via `cell_fill_type !=
+//! FILL_MATERIAL` and free-stream through with `trace_step`, exactly
+//! as a vacuum/air region should. Hex-lattice fills are not
+//! implemented by this walk (only the legacy CUDA path has hex
+//! descent); callers must reject `CellFill::HexLattice` scenes before
+//! upload via [`crate::geometry::flat::check_gpu_supported`] — the
+//! hex branch here is unreachable defensive fallback, not a supported
+//! path.
+
+use cubecl::prelude::*;
+
+pub(crate) use crate::geometry::flat::{
+ BC_REFLECTIVE, BC_TRANSMISSION, BC_VACUUM, FILL_LATTICE, FILL_MATERIAL, FILL_UNIVERSE,
+ FILL_VOID, REGION_COMPLEMENT, REGION_HALFSPACE_NEG, REGION_HALFSPACE_POS, REGION_INTERSECTION,
+ REGION_UNION, SURF_CYL_X, SURF_CYL_Y, SURF_CYL_Z, SURF_PLANE_GENERAL, SURF_PLANE_X,
+ SURF_PLANE_Y, SURF_PLANE_Z, SURF_SPHERE,
+};
+
+/// Max recursion depth of the universe/lattice descent (root universe
+/// + up to 3 nested lattices/universes covers every ICSBEP scene).
+pub(crate) const MAX_DEPTH: u32 = 4;
+pub(crate) const MAX_DEPTH_USIZE: usize = 4;
+/// f64 slots per surface in `fdata[surf_params_off..]`.
+pub(crate) const SURF_STRIDE: u32 = crate::geometry::flat::SURF_PARAM_STRIDE as u32;
+
+/// Evaluate surface `s_idx` at local point — sign tells which halfspace.
+/// Mirrors `gr_surf_eval`.
+#[cube]
+pub(crate) fn surf_eval(
+ idata: &Array,
+ fdata: &Array,
+ surf_params_off: u32,
+ surf_type_off: u32,
+ s_idx: u32,
+ x: f64,
+ y: f64,
+ z: f64,
+) -> f64 {
+ let t = idata[(surf_type_off + s_idx) as usize];
+ let p = (surf_params_off + s_idx * SURF_STRIDE) as usize;
+ let mut out = f64::new(1e30);
+ if t == SURF_PLANE_X {
+ out = x - fdata[p];
+ } else {
+ if t == SURF_PLANE_Y {
+ out = y - fdata[p];
+ } else {
+ if t == SURF_PLANE_Z {
+ out = z - fdata[p];
+ } else {
+ if t == SURF_SPHERE {
+ let dx = x - fdata[p];
+ let dy = y - fdata[p + 1];
+ let dz = z - fdata[p + 2];
+ out = dx * dx + dy * dy + dz * dz - fdata[p + 3] * fdata[p + 3];
+ } else {
+ if t == SURF_CYL_Z {
+ let dx = x - fdata[p];
+ let dy = y - fdata[p + 1];
+ out = dx * dx + dy * dy - fdata[p + 2] * fdata[p + 2];
+ } else {
+ if t == SURF_CYL_X {
+ let dy = y - fdata[p];
+ let dz = z - fdata[p + 1];
+ out = dy * dy + dz * dz - fdata[p + 2] * fdata[p + 2];
+ } else {
+ if t == SURF_CYL_Y {
+ let dx = x - fdata[p];
+ let dz = z - fdata[p + 1];
+ out = dx * dx + dz * dz - fdata[p + 2] * fdata[p + 2];
+ } else {
+ if t == SURF_PLANE_GENERAL {
+ out = fdata[p] * x + fdata[p + 1] * y + fdata[p + 2] * z - fdata[p + 3];
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ out
+}
+
+#[cube]
+pub(crate) fn dist_plane(p: f64, d: f64, x0: f64, big: f64, tol: f64) -> f64 {
+ let mut out = big;
+ if d.abs() > f64::new(1e-300) {
+ let t = (x0 - p) / d;
+ out = select(t > tol, t, big);
+ }
+ out
+}
+
+#[cube]
+pub(crate) fn dist_sphere(px: f64, py: f64, pz: f64, dx: f64, dy: f64, dz: f64, cx: f64, cy: f64, cz: f64, r: f64, big: f64, tol: f64) -> f64 {
+ let rx = px - cx;
+ let ry = py - cy;
+ let rz = pz - cz;
+ let a = dx * dx + dy * dy + dz * dz;
+ let b = 2.0 * (rx * dx + ry * dy + rz * dz);
+ let c = rx * rx + ry * ry + rz * rz - r * r;
+ let disc = b * b - 4.0 * a * c;
+ let mut out = big;
+ if disc >= f64::new(0.0) {
+ let sq = disc.sqrt();
+ let t1 = (-b - sq) / (2.0 * a);
+ let t2 = (-b + sq) / (2.0 * a);
+ let pick = select(t1 > tol, t1, t2);
+ out = select(pick > tol, pick, big);
+ }
+ out
+}
+
+#[cube]
+pub(crate) fn dist_cyl(p1: f64, p2: f64, d1: f64, d2: f64, c1: f64, c2: f64, r: f64, big: f64, tol: f64) -> f64 {
+ let r1 = p1 - c1;
+ let r2 = p2 - c2;
+ let a = d1 * d1 + d2 * d2;
+ let mut out = big;
+ if a > f64::new(1e-300) {
+ let b = 2.0 * (r1 * d1 + r2 * d2);
+ let c = r1 * r1 + r2 * r2 - r * r;
+ let disc = b * b - 4.0 * a * c;
+ if disc >= f64::new(0.0) {
+ let sq = disc.sqrt();
+ let t1 = (-b - sq) / (2.0 * a);
+ let t2 = (-b + sq) / (2.0 * a);
+ let pick = select(t1 > tol, t1, t2);
+ out = select(pick > tol, pick, big);
+ }
+ }
+ out
+}
+
+/// Distance from `(px,py,pz)` along unit `(dx,dy,dz)` to surface
+/// `s_idx`; `1e30` for no forward hit. Mirrors `gr_surf_dist`.
+#[cube]
+pub(crate) fn surf_dist(
+ idata: &Array,
+ fdata: &Array,
+ surf_params_off: u32,
+ surf_type_off: u32,
+ s_idx: u32,
+ px: f64,
+ py: f64,
+ pz: f64,
+ dx: f64,
+ dy: f64,
+ dz: f64,
+) -> f64 {
+ let t = idata[(surf_type_off + s_idx) as usize];
+ let p = (surf_params_off + s_idx * SURF_STRIDE) as usize;
+ let big = f64::new(1e30);
+ let tol = f64::new(1e-12);
+ let mut out = big;
+ if t == SURF_PLANE_X {
+ out = dist_plane(px, dx, fdata[p], big, tol);
+ } else {
+ if t == SURF_PLANE_Y {
+ out = dist_plane(py, dy, fdata[p], big, tol);
+ } else {
+ if t == SURF_PLANE_Z {
+ out = dist_plane(pz, dz, fdata[p], big, tol);
+ } else {
+ if t == SURF_SPHERE {
+ out = dist_sphere(px, py, pz, dx, dy, dz, fdata[p], fdata[p + 1], fdata[p + 2], fdata[p + 3], big, tol);
+ } else {
+ if t == SURF_CYL_Z {
+ out = dist_cyl(px, py, dx, dy, fdata[p], fdata[p + 1], fdata[p + 2], big, tol);
+ } else {
+ if t == SURF_CYL_X {
+ out = dist_cyl(py, pz, dy, dz, fdata[p], fdata[p + 1], fdata[p + 2], big, tol);
+ } else {
+ if t == SURF_CYL_Y {
+ out = dist_cyl(px, pz, dx, dz, fdata[p], fdata[p + 1], fdata[p + 2], big, tol);
+ } else {
+ if t == SURF_PLANE_GENERAL {
+ let denom = fdata[p] * dx + fdata[p + 1] * dy + fdata[p + 2] * dz;
+ if denom.abs() > f64::new(1e-300) {
+ let tv = (fdata[p + 3] - (fdata[p] * px + fdata[p + 1] * py + fdata[p + 2] * pz)) / denom;
+ out = select(tv > tol, tv, big);
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ out
+}
+
+/// Postfix CSG stack-machine: 1 if local point is inside cell region.
+/// Mirrors `gr_cell_contains`.
+#[cube]
+pub(crate) fn cell_contains(
+ idata: &Array,
+ fdata: &Array,
+ surf_params_off: u32,
+ surf_type_off: u32,
+ region_op_off: u32,
+ region_arg_off: u32,
+ r_off: u32,
+ r_len: u32,
+ x: f64,
+ y: f64,
+ z: f64,
+) -> u32 {
+ let mut stack = Array::::new(16usize);
+ let mut sp = 0usize;
+ for i in 0..r_len {
+ let op = idata[(region_op_off + r_off + i) as usize];
+ let arg = idata[(region_arg_off + r_off + i) as usize];
+ if op == REGION_HALFSPACE_POS {
+ let v = surf_eval(idata, fdata, surf_params_off, surf_type_off, u32::cast_from(arg), x, y, z);
+ stack[sp] = select(v > f64::new(0.0), 1u32, 0u32);
+ sp += 1;
+ } else {
+ if op == REGION_HALFSPACE_NEG {
+ let v = surf_eval(idata, fdata, surf_params_off, surf_type_off, u32::cast_from(arg), x, y, z);
+ stack[sp] = select(v < f64::new(0.0), 1u32, 0u32);
+ sp += 1;
+ } else {
+ if op == REGION_INTERSECTION {
+ let b = stack[sp - 1];
+ let a = stack[sp - 2];
+ sp -= 1;
+ stack[sp - 1] = select(a + b == 2u32, 1u32, 0u32);
+ } else {
+ if op == REGION_UNION {
+ let b = stack[sp - 1];
+ let a = stack[sp - 2];
+ sp -= 1;
+ stack[sp - 1] = select(a + b > 0u32, 1u32, 0u32);
+ } else {
+ if op == REGION_COMPLEMENT {
+ let a = stack[sp - 1];
+ stack[sp - 1] = select(a == 0u32, 1u32, 0u32);
+ }
+ }
+ }
+ }
+ }
+ }
+ select(sp == 1usize, stack[0], 0u32)
+}
+
+/// Reflect a unit direction about an axis-aligned / general plane (in a
+/// length-3 `dir` array, mutated in place). Mirrors `gr_reflect_direction`.
+#[cube]
+pub(crate) fn reflect_dir(idata: &Array, fdata: &Array, surf_params_off: u32, surf_type_off: u32, s_idx: u32, dir: &mut Array) {
+ let t = idata[(surf_type_off + s_idx) as usize];
+ let p = (surf_params_off + s_idx * SURF_STRIDE) as usize;
+ if t == SURF_PLANE_X {
+ dir[0] = -dir[0];
+ } else {
+ if t == SURF_PLANE_Y {
+ dir[1] = -dir[1];
+ } else {
+ if t == SURF_PLANE_Z {
+ dir[2] = -dir[2];
+ } else {
+ if t == SURF_PLANE_GENERAL {
+ let nx = fdata[p];
+ let ny = fdata[p + 1];
+ let nz = fdata[p + 2];
+ let ddot = dir[0] * nx + dir[1] * ny + dir[2] * nz;
+ dir[0] = dir[0] - 2.0 * ddot * nx;
+ dir[1] = dir[1] - 2.0 * ddot * ny;
+ dir[2] = dir[2] - 2.0 * ddot * nz;
+ }
+ }
+ }
+ }
+}
+
+/// Apply a boundary crossing: advance to the surface, then vacuum kills,
+/// reflective inverts the direction, transmission steps a nudge past.
+/// Mirrors `gr_apply_boundary`.
+#[cube]
+#[allow(clippy::too_many_arguments)]
+pub(crate) fn cross_or_die(
+ idata: &Array,
+ fdata: &Array,
+ surf_params_off: u32,
+ surf_type_off: u32,
+ bc: i32,
+ surf_idx: i32,
+ dist: f64,
+ px: &mut f64,
+ py: &mut f64,
+ pz: &mut f64,
+ dir: &mut Array,
+ alive: &mut u32,
+) {
+ if bc == BC_VACUUM {
+ *px = *px + dir[0] * dist;
+ *py = *py + dir[1] * dist;
+ *pz = *pz + dir[2] * dist;
+ *alive = u32::new(0);
+ } else {
+ if bc == BC_REFLECTIVE {
+ *px = *px + dir[0] * dist;
+ *py = *py + dir[1] * dist;
+ *pz = *pz + dir[2] * dist;
+ if surf_idx >= 0i32 {
+ reflect_dir(idata, fdata, surf_params_off, surf_type_off, u32::cast_from(surf_idx), dir);
+ }
+ } else {
+ let nudge = f64::new(1e-10);
+ *px = *px + dir[0] * (dist + nudge);
+ *py = *py + dir[1] * (dist + nudge);
+ *pz = *pz + dir[2] * (dist + nudge);
+ }
+ }
+}
+
+/// Recursive cell-find. Writes the resolved stack into the parallel
+/// `st_*` arrays (length ≥ MAX_DEPTH) and returns the depth (0 = leak).
+/// Single-exit; mirrors `gr_find_cell` (rect lattices + universes;
+/// hex lattices are rejected before upload by
+/// [`crate::geometry::flat::check_gpu_supported`], so that branch is
+/// unreachable defensive fallback here, not a supported path).
+#[cube]
+#[allow(clippy::too_many_arguments)]
+pub(crate) fn find_cell(
+ idata: &Array,
+ fdata: &Array,
+ surf_params_off: u32,
+ surf_type_off: u32,
+ region_op_off: u32,
+ region_arg_off: u32,
+ cell_region_off_a: u32,
+ cell_region_len_a: u32,
+ cell_fill_type_a: u32,
+ cell_fill_data_a: u32,
+ univ_cells_off_base: u32,
+ univ_cells_len_a: u32,
+ univ_cell_indices_a: u32,
+ lat_shape_off: u32,
+ lat_universes_off_a: u32,
+ lat_universes_a: u32,
+ lat_origin_off: u32,
+ lat_pitch_off: u32,
+ root_universe: u32,
+ wx: f64,
+ wy: f64,
+ wz: f64,
+ st_cell: &mut Array,
+ st_offx: &mut Array,
+ st_offy: &mut Array,
+ st_offz: &mut Array,
+ st_has_lat: &mut Array,
+ st_lat_id: &mut Array,
+ st_lat_ix: &mut Array,
+ st_lat_iy: &mut Array,
+ st_lat_iz: &mut Array,
+) -> u32 {
+ let mut depth = u32::new(0);
+ let mut cur_univ = root_universe;
+ let mut off_x = f64::new(0.0);
+ let mut off_y = f64::new(0.0);
+ let mut off_z = f64::new(0.0);
+ let mut has_lat = i32::new(0);
+ let mut lat_id = i32::new(0);
+ let mut lat_ix = i32::new(0);
+ let mut lat_iy = i32::new(0);
+ let mut lat_iz = i32::new(0);
+ let mut lx = wx;
+ let mut ly = wy;
+ let mut lz = wz;
+ let mut fc_done = false;
+
+ for _d in 0..MAX_DEPTH {
+ if !fc_done {
+ lx = lx - off_x;
+ ly = ly - off_y;
+ lz = lz - off_z;
+
+ let c_off = idata[(univ_cells_off_base + cur_univ) as usize];
+ let c_len = idata[(univ_cells_len_a + cur_univ) as usize];
+ let mut chosen = i32::new(-1);
+ for k in 0..u32::cast_from(c_len) {
+ if chosen < 0i32 {
+ let cand = idata[(univ_cell_indices_a + u32::cast_from(c_off) + k) as usize];
+ let r_off = idata[(cell_region_off_a + u32::cast_from(cand)) as usize];
+ let r_len = idata[(cell_region_len_a + u32::cast_from(cand)) as usize];
+ let inside = cell_contains(
+ idata, fdata, surf_params_off, surf_type_off,
+ region_op_off, region_arg_off,
+ u32::cast_from(r_off), u32::cast_from(r_len),
+ lx, ly, lz,
+ );
+ if inside == 1u32 {
+ chosen = cand;
+ }
+ }
+ }
+
+ if chosen < 0i32 {
+ depth = u32::new(0);
+ fc_done = true;
+ } else {
+ st_cell[depth as usize] = chosen;
+ st_offx[depth as usize] = off_x;
+ st_offy[depth as usize] = off_y;
+ st_offz[depth as usize] = off_z;
+ st_has_lat[depth as usize] = has_lat;
+ st_lat_id[depth as usize] = lat_id;
+ st_lat_ix[depth as usize] = lat_ix;
+ st_lat_iy[depth as usize] = lat_iy;
+ st_lat_iz[depth as usize] = lat_iz;
+ depth += 1;
+
+ let ft = idata[(cell_fill_type_a + u32::cast_from(chosen)) as usize];
+ let fd = idata[(cell_fill_data_a + u32::cast_from(chosen)) as usize];
+ if ft == FILL_MATERIAL {
+ fc_done = true;
+ } else {
+ if ft == FILL_VOID {
+ // Void is a valid terminal cell — the caller
+ // free-streams through it via `trace_step`
+ // (mat < 0 branch), exactly like an air/vacuum
+ // gap on the CPU/CUDA paths. Not a leak.
+ fc_done = true;
+ } else {
+ if ft == FILL_UNIVERSE {
+ cur_univ = u32::cast_from(fd);
+ off_x = f64::new(0.0);
+ off_y = f64::new(0.0);
+ off_z = f64::new(0.0);
+ has_lat = i32::new(0);
+ } else {
+ if ft == FILL_LATTICE {
+ let lid = u32::cast_from(fd);
+ let org = (lat_origin_off + lid * 3u32) as usize;
+ let pit = (lat_pitch_off + lid * 3u32) as usize;
+ let shp = (lat_shape_off + lid * 3u32) as usize;
+ let rx = lx - fdata[org];
+ let ry = ly - fdata[org + 1];
+ let rz = lz - fdata[org + 2];
+ let fix = (rx / fdata[pit]).floor();
+ let fiy = (ry / fdata[pit + 1]).floor();
+ let fiz = (rz / fdata[pit + 2]).floor();
+ let ix = i32::cast_from(fix);
+ let iy = i32::cast_from(fiy);
+ let iz = i32::cast_from(fiz);
+ let sh0 = idata[shp];
+ let sh1 = idata[shp + 1];
+ let sh2 = idata[shp + 2];
+ let inb = ix >= 0i32 && iy >= 0i32 && iz >= 0i32 && ix < sh0 && iy < sh1 && iz < sh2;
+ if !inb {
+ depth = u32::new(0);
+ fc_done = true;
+ } else {
+ let slab_n = sh0 * sh1;
+ let linear = iz * slab_n + iy * sh0 + ix;
+ let luoff = idata[(lat_universes_off_a + lid) as usize];
+ cur_univ = u32::cast_from(idata[(lat_universes_a + u32::cast_from(luoff + linear)) as usize]);
+ off_x = fdata[org] + (fix + 0.5) * fdata[pit];
+ off_y = fdata[org + 1] + (fiy + 0.5) * fdata[pit + 1];
+ off_z = fdata[org + 2] + (fiz + 0.5) * fdata[pit + 2];
+ has_lat = i32::new(1);
+ lat_id = i32::cast_from(lid);
+ lat_ix = ix;
+ lat_iy = iy;
+ lat_iz = iz;
+ }
+ } else {
+ // Hex lattice — unsupported on this walk.
+ // `check_gpu_supported` rejects any scene
+ // with a HexLattice cell before a kernel
+ // using this function is ever launched;
+ // this is unreachable defensive fallback.
+ depth = u32::new(0);
+ fc_done = true;
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ depth
+}
+
+/// Trace to the next surface / lattice-grid crossing from world
+/// `(wx,wy,wz)` along `(dx,dy,dz)`, given the current stack of `depth`
+/// frames. Writes results into `out`:
+/// out[0] = distance, out[1] = surface_idx (−1 = grid), out[2] = bc.
+/// Mirrors `gr_trace_step` (rect grids only). The caller re-resolves the
+/// next cell via `find_cell` at the nudged point.
+#[cube]
+#[allow(clippy::too_many_arguments)]
+pub(crate) fn trace_step(
+ idata: &Array,
+ fdata: &Array,
+ surf_params_off: u32,
+ surf_type_off: u32,
+ surf_bc_off: u32,
+ region_op_off: u32,
+ region_arg_off: u32,
+ cell_region_off_a: u32,
+ cell_region_len_a: u32,
+ lat_origin_off: u32,
+ lat_pitch_off: u32,
+ wx: f64,
+ wy: f64,
+ wz: f64,
+ dx: f64,
+ dy: f64,
+ dz: f64,
+ depth: u32,
+ st_cell: &mut Array,
+ st_offx: &mut Array,
+ st_offy: &mut Array,
+ st_offz: &mut Array,
+ st_has_lat: &mut Array,
+ st_lat_id: &mut Array,
+ st_lat_ix: &mut Array,
+ st_lat_iy: &mut Array,
+ st_lat_iz: &mut Array,
+ out: &mut Array,
+) {
+ let big = f64::new(1e30);
+ let mut best_dist = big;
+ let mut best_surf = i32::new(-1);
+
+ // Surfaces of every stack cell (in that cell's local frame).
+ let mut flx = wx;
+ let mut fly = wy;
+ let mut flz = wz;
+ for d in 0..depth {
+ flx = flx - st_offx[d as usize];
+ fly = fly - st_offy[d as usize];
+ flz = flz - st_offz[d as usize];
+ let cci = st_cell[d as usize];
+ let r_off = idata[(cell_region_off_a + u32::cast_from(cci)) as usize];
+ let r_len = idata[(cell_region_len_a + u32::cast_from(cci)) as usize];
+ for i in 0..u32::cast_from(r_len) {
+ let op = idata[(region_op_off + u32::cast_from(r_off) + i) as usize];
+ let arg = idata[(region_arg_off + u32::cast_from(r_off) + i) as usize];
+ let is_hs = op == REGION_HALFSPACE_POS || op == REGION_HALFSPACE_NEG;
+ if is_hs {
+ let dd = surf_dist(idata, fdata, surf_params_off, surf_type_off, u32::cast_from(arg), flx, fly, flz, dx, dy, dz);
+ if dd < best_dist {
+ best_dist = dd;
+ best_surf = arg;
+ }
+ }
+ }
+ }
+
+ // Rect-lattice grid crossings (parent frame).
+ let mut plx = wx;
+ let mut ply = wy;
+ let mut plz = wz;
+ for d in 0..depth {
+ if st_has_lat[d as usize] == 1i32 {
+ let lid = u32::cast_from(st_lat_id[d as usize]);
+ let org = (lat_origin_off + lid * 3u32) as usize;
+ let pit = (lat_pitch_off + lid * 3u32) as usize;
+ let gd = grid_dist(
+ plx, ply, plz, dx, dy, dz,
+ fdata[org], fdata[org + 1], fdata[org + 2],
+ fdata[pit], fdata[pit + 1], fdata[pit + 2],
+ st_lat_ix[d as usize], st_lat_iy[d as usize], st_lat_iz[d as usize],
+ );
+ if gd + f64::new(1e-9) < best_dist {
+ best_dist = gd;
+ best_surf = i32::new(-1);
+ }
+ }
+ plx = plx - st_offx[d as usize];
+ ply = ply - st_offy[d as usize];
+ plz = plz - st_offz[d as usize];
+ }
+
+ let mut bc = i32::new(BC_TRANSMISSION as i64);
+ if best_surf >= 0i32 {
+ bc = idata[(surf_bc_off + u32::cast_from(best_surf)) as usize];
+ }
+ out[0] = best_dist;
+ out[1] = f64::cast_from(best_surf);
+ out[2] = f64::cast_from(bc);
+}
+
+/// Rect-lattice distance to next grid crossing along the ray. Mirrors
+/// `gr_lattice_distance_to_grid`.
+#[cube]
+pub(crate) fn grid_dist(
+ px: f64, py: f64, pz: f64, dx: f64, dy: f64, dz: f64,
+ ox: f64, oy: f64, oz: f64, pitx: f64, pity: f64, pitz: f64,
+ ix: i32, iy: i32, iz: i32,
+) -> f64 {
+ let big = f64::new(1e30);
+ let mut best = big;
+ best = grid_axis(px - ox, dx, pitx, ix, best);
+ best = grid_axis(py - oy, dy, pity, iy, best);
+ best = grid_axis(pz - oz, dz, pitz, iz, best);
+ best
+}
+
+#[cube]
+pub(crate) fn grid_axis(pos: f64, d: f64, pitch: f64, idx: i32, cur_best: f64) -> f64 {
+ let mut best = cur_best;
+ if d.abs() > f64::new(1e-300) {
+ let fwd = d > f64::new(0.0);
+ let target = select(fwd, f64::cast_from(idx + 1i32) * pitch, f64::cast_from(idx) * pitch);
+ let mut tt = (target - pos) / d;
+ if tt <= f64::new(0.0) {
+ let nxt = select(fwd, f64::cast_from(idx + 2i32) * pitch, f64::cast_from(idx - 1i32) * pitch);
+ tt = (nxt - pos) / d;
+ }
+ if tt > f64::new(0.0) && tt < best {
+ best = tt;
+ }
+ }
+ best
+}
diff --git a/rust_prototype/src/gpu_recursive.rs b/rust_prototype/src/gpu_recursive.rs
index 0471e12..a87a385 100644
--- a/rust_prototype/src/gpu_recursive.rs
+++ b/rust_prototype/src/gpu_recursive.rs
@@ -18,9 +18,12 @@ use cudarc::driver::{
};
use cudarc::nvrtc;
-use crate::geometry::cell::{CellFill, Region};
-use crate::geometry::surface::{BoundaryCondition, Surface};
-use crate::geometry::{Geometry, Vec3};
+use crate::geometry::Geometry;
+// Backend-agnostic geometry → SoA flattening. Lifted into
+// `geometry::flat` so the CubeCL renderer shares one definition of the
+// device layout; the tag constants there are the single source of
+// truth this CUDA path and the CubeCL kernel both mirror.
+use crate::geometry::flat::build_host_tables;
/// Query the device's compute capability and return the NVRTC
/// `--gpu-architecture` argument value (e.g. `"sm_86"`, `"sm_90"`,
@@ -56,313 +59,6 @@ pub fn device_nvrtc_arch(ctx: &Arc) -> Result,
- surf_params: Vec, // 8 doubles per surface
- surf_bc: Vec,
-
- cell_region_off: Vec,
- cell_region_len: Vec,
- cell_fill_type: Vec,
- cell_fill_data: Vec,
- cell_aabb_min: Vec, // 3 doubles per cell
- cell_aabb_max: Vec,
-
- region_op: Vec,
- region_arg: Vec,
-
- univ_cells_off: Vec,
- univ_cells_len: Vec,
- univ_surfaces_off: Vec,
- univ_surfaces_len: Vec,
- univ_cell_indices: Vec,
- univ_surface_indices: Vec,
-
- lat_origin: Vec,
- lat_pitch: Vec,
- lat_shape: Vec,
- lat_universes_off: Vec,
- lat_universes: Vec,
- // Hex lattice SoA — parallel to the rect arrays. The `n_*`
- // counters match the layout in `geom_recursive.cu::GrGeometry`.
- hex_center: Vec