Skip to content

Benchmark evidence

Evidence provenance

Each row is one set of measurements. Dates, commits, hosts and case counts come from the linked summary files; "not recorded" means the value is missing. Commits are the revisions of the original runs; history-provenance.json maps them to this repository's history. The linked files identify each build with SHA-256 hashes of the native library, Python sources and benchmark scripts. Files under benchmarks/results/ keep their recorded measurements; identifying data and archive metadata were redacted (redacted paths).

Measurement set Date Source commit Host Cases Outcome Summary / manifest
CPU improvements after 0.1.0 2026-10-04 8bbcb87 plus archived candidate patch AMD Ryzen 9 9950X, Linux, Python 3.13.1 36 targeted and 92 broad four-thread comparisons, plus thread and memory screens Target timings pass; original broad outliers, rechecks and four periodic memory failures retained; predates dense-product scheduling change cpu-speedups-20261004.json, CPU report
Dense-product scheduling and final CPU controls 2026-10-04 8bbcb87 plus archived reconstructed candidate AMD Ryzen 9 9950X, Linux, Python 3.13.1 12 paired 32-thread cases, four-thread controls and six bitwise workflow checks Final comparisons with 0.1.0 pass; intermediate regressions and discarded policies retained cpu-scheduling-20261004.json, scheduling report
Linux core correctness 2026-09-19 2843a70 AMD Ryzen 9 9950X, Linux, Python 3.13.1 615 All pass linux-core-qualification.json, summary
Linux core performance 2026-09-19 to 2026-09-20 2470fbe AMD Ryzen 9 9950X, Linux, Python 3.13.1 1,300 planned, 769 timed All measured; broad-grid median 5.12×; 4 slower cases (2 recorded internal illuminations at a solver switch point, 1 scalar recorded tl_vcw_r call, 1 matrix-free illumination case at 32 columns) linux-core-performance.json, summary
Reference replay grid 2026-09-12 not recorded AMD Ryzen 9 9950X, Linux, Python 3.13.1, CPUs 8–11 527 527 runtime and 525 RSS checks pass; minimum 1.02× complete-qualification.json
macOS dispatch grid 2026-09-12 not recorded Apple M3, macOS 26.6.2, Python 3.13.12 30 All pass; minimum 1.30×, largest RSS ratio 0.695 mac-qualification.json
Mac and Linux comparison not recorded (by 2026-09-12) 7421ca9 (Linux) Apple M3 and AMD Ryzen 9 9950X 736 per platform Every outcome kept; median 5.17× (M3) and 5.04× (Ryzen); 11 slower Mac cases comparison-plan.json, platform comparison
Requested illumination 2026-09-12 not recorded AMD Ryzen 9 9950X (CPUs 8–11) and Apple M3 9 All complete; matrix-free gradient memory 30× below the full T-matrix at 512 spheres illumination-linux-n512-l1-p1.json and the other illumination-*.json, large problems
Published applications sources retrieved 2026-09-12 not recorded macOS 26.6.2 arm64, Python 3.13.12 5 spectra, 300 thermal frequencies Within the stated tolerances; thermal absorption within 0.577% of the author data papers/qualification.json, papers/thermal-result.json, published applications
Public cluster recheck (macOS) not recorded 43bb7fe macOS 26.6.2 arm64, Python 3.13.12 2 Both pass; spherical cluster 7.20×, cylindrical cluster 1.56×, lower peak memory (RSS ratios 0.729 and 0.701) agent-usability/qualification.json
Fractional Legendre check not recorded not recorded not recorded 532 530 finite cases within 2.17e-13 relative of 70-digit values; 2 expected overflows fractional-legendre-physical.json
LU scheduling probe not recorded not recorded AMD Ryzen 9950X, 16 Rayon workers on CPUs 0–15 35 configurations Bounded worker counts 1.99× to 83.9× faster per stage than the full pool, in 7 selected sizes cpu-parallelism.json, numerics
Pre-release speed comparison (not a validation run) 2026-09-26 to 2026-09-30 baseline 5dadf1d shared 4-CPU cloud VM, one thread 556 cases of 203 workloads Geometric-mean time ratio against 5dadf1d: 0.77 forward and 0.68 reverse without lattice sums, 0.48 and 0.35 for lattice sums; recsumcw1d about 4% slower not archived; workloads in benchmark_cluster.py

The retained report does not record run dates for both platforms in the Mac and Linux comparison. correction-plan.json, created on 2026-09-12, uses it as its baseline, so the comparison ran on or before that day.

The pre-release comparison alternated optimized builds of a later revision and 5dadf1d, then rechecked suspected slowdowns by CPU time and instruction counts. Among the largest gains (forward / reverse) are field evaluation 17× / 8.6×, EBCM 8.4× / 5.0×, cylindrical expansion 6.3× / 5.4×, cylindrical periodic arrays 4.4× / 3.2×, Wigner 3j 2.3× and intkambe pullbacks 1.9×. The single-point 1D cylindrical reciprocal sum (recsumcw1d) is the only slowdown beyond noise: the lattice accuracy checks require extra Ewald shell, sheet and method tracking. Explicit lattice splits below the automatic one can take longer because they sum more shells and check their cancellation (last section).

Automatic-dispatch overhead

A 2026-10-03 check compared eight NumPy calls before and after automatic autodiff selection: sphere scattering and cross sections at degrees 1 and 10, one-point fields at degree 3, a two-sphere solve, and two-channel interface and slab construction. Both versions used the same native library.

Frameworks imported before timing Added time in the seven smallest calls Calls exceeding the 10% timing limit
None 0.3–2.8 µs 3 of 8
Advect, JAX, PyTorch and Autograd 2.0–9.7 µs 7 of 8

For example, slab construction changed from 19.3 to 22.0 µs without framework imports, and from 19.1 to 28.8 µs with all four imported. The two-sphere solve stayed within the timing limit in both conditions. All recorded values agreed exactly. These timings cover small forward calls, not gradient computation or large workloads; memory was not measured.

The check used compare_builds.py on Linux, an AMD Ryzen 9 9950X and Python 3.13.1: five paired rounds, three samples of at least 20 ms per call, alternating measurement order, one native/BLAS thread and CPU affinity 4–5. Imports and input setup were outside the timed calls. Reported times are medians of each round's fastest sample; the limit uses the median paired time ratio. The raw rounds and source snapshots are retained privately. Earlier benchmark results above remain unchanged.

Measured source and library identities

The baseline was e3e759f1bc46d1df35bb1dbb6d9c99a91fb691be; the candidate was the automatic-dispatch source snapshot, including its traversal optimization and reviewed fixes. SHA-256 fingerprints identify these measured builds, independently of later source changes:

Baseline Python: 5fbd9d6ef41fbc04633607ab473ed5f1753d1759e1013f42a611c684fb9e0c08
Candidate Python: 839a44d639e7c3aa01004915066df57c9cc6a84e34938c766197e30c6781d596
Shared native: 9e8a39cb2fc8459b5069a65ffc349c9eee71024ced6464524e55f52f9b8ed3c5

Redacted paths

The distributed evidence has privacy-only redactions documented in benchmarks/privacy-provenance.json. Numerical values, timings, outcomes and numerical array bytes are unchanged; archive owner metadata is normalized and distributed-file checksums are updated. Original commit identifiers and measured source/library fingerprints describe the historical runs. Sanitized source snapshots report separate content hashes. No measurement was rerun for this cleanup.

Archived per-kernel measurements

These measurements time individual numerical operations. Each belongs to the build named in its result file, which may differ from the current source. Unless a section says otherwise, the host is an AMD Ryzen 9 9950X (16 physical cores) with Linux x86-64, Python 3.13.1, treams 0.4.5 and an optimized treams-rs build with faer and Rayon. Both packages get the same thread budget, each runs in its own process, and the result is compared with treams before timing at rtol=2e-9, atol=1e-12. Result file names refer to benchmarks/results/. Sections without a result file summarize timings that were not archived.

Reproduce a workload with uv run --no-sync python scripts/benchmark_cluster.py --workload <name> ... after just build-ext-release.

Axisymmetric EBCM

For r(theta)=0.3(1+0.23 cos²(theta)), k0=1.3, inner eps=3.1+0.2i, mu=1.2+0.1i, kappa=0.07 and vacuum outside, the complete singular Q integral is compared with treams at rtol=2e-9, atol=1e-12, using the treams surface integral, which omits a radial area factor (radial_area_factor=False). treams-rs uses 96 Gauss-Legendre nodes; treams uses adaptive SciPy quadrature. Four threads, seven samples after warmup. The reverse pass covers every sampled radius and slope, and all complex wavenumbers and impedances; its peak RSS stays at the forward peak. Timings include the Python call and saving the residual (the data kept for the reverse pass), not shape and basis setup. The timings were not archived. On a shared four-CPU host, parallel evaluation at quadrature nodes with shared radial functions and solid harmonics brings the degree-4 forward from 5.4 to 0.74 ms and forward plus reverse from 8.5 to 1.8 ms; the table shows the build without them.

Degree Modes treams ms Rust ms Speedup Rust reverse ms treams / Rust peak MiB
3 30 284.26 3.54 80.3x 1.99 83.87 / 42.18
4 48 727.40 5.90 123.4x 3.00 84.93 / 42.45

At degree 6 the strict comparison fails on six of 9,216 entries, by up to 4.98e-10. All six have m=0 and odd l_out+l_in, so their integrands are odd under theta -> pi-theta on this equatorially symmetric surface and their exact integrals vanish (below 8e-82 at 80 digits). The largest integral of the absolute integrand is about 7.31e5, and double-precision epsilon times it is 1.62e-10: treams-rs residuals at 48, 96 and 192 nodes lie between 0.04 and 2.43 times this floor, and treams leaves up to 6.62e-10. This is a rounding floor in both packages, so the tolerance stays and no degree-6 speedup is claimed. Reproduce it with uv run --no-sync --with mpmath python scripts/qualify_ebcm_cancellation.py; the cancellation diagnostic holds all eight m=0 entries, their high-precision values, quadrature samples and condition estimates. Workload: --workload ebcm --particles 1 --lmax 4 --samples 96 --threads 4.

Finite sphere clusters

Chains of spheres with radii 0.15–0.25, spacing 0.8, relative permittivity 4+0.1i and vacuum wavenumber 1.3. Timings cover particle coefficients, translations and the full dense interacting T-matrix, and treams-rs keeps data for the pullback; they exclude imports. Peak RSS includes imports, memory retained for reuse and the benchmark's own memory. Seven samples after warmup, without fixed CPU placement or frequency. Raw samples: benchmarks/results/release-*.json. With packed in-place LU and four threads, dimension 240 takes 2.19 ms against 89.95 ms (41.0x; 46.6 against 75.0 MiB) and dimension 960 takes 35.63 ms against 1641.15 ms (46.1x; 100.9 against 159.0 MiB), in benchmarks/results/packed-lu-cluster-d{240,960}.json. Dense output and LU need O(D²) memory and the factorization O(D³) time. Workload: --particles 16 --lmax 3 --threads 4.

Spheres lmax Matrix dimension Threads treams ms Rust ms Speedup treams / Rust peak MiB
8 3 240 1 88.23 3.01 29.3× 71.8 / 44.2
16 3 480 1 389.64 13.91 28.0× 90.3 / 57.2
8 5 560 1 686.49 22.88 30.0× 98.4 / 65.1
32 3 960 1 1677.50 88.36 19.0× 154.9 / 113.2
8 3 240 4 90.63 1.96 46.2× 73.6 / 42.7
16 3 480 4 391.17 9.35 41.9× 91.6 / 57.8
8 5 560 4 691.96 12.69 54.5× 100.2 / 64.5
32 3 960 4 1636.14 43.41 37.7× 156.4 / 113.6

Batched spherical fields

Weighted singular electric fields at four positions, order 3 (120 modes), 2,048 samples and four threads, against treams.efield(...) @ coefficients. treams-rs contracts the amplitudes at each sample, so it never builds the sample-by-component-by-mode matrix; its reverse pass recomputes local wave derivatives and keeps no dense Jacobian. Basis setup is outside the timings.

Workload treams ms Rust ms Speedup Rust reverse ms treams / Rust peak MiB Result
Weighted field 822.27 40.82 20.14x — 96.94 / 40.98 benchmarks/results/fields-n4-l3-p2048-t4.json
Weighted field, shared spherical/cylindrical path — 40.64 20.24x 71.11 — / 41.28 shared-fields-n4-l3-p2048-t4.json
Full operator, (2048, 3, 120) array 856.08 45.47 18.83x 75.11 97.68 / 64.20 field-operator-n4-l3-p2048-t4.json
Weighted field, shared operator code — 41.86 19.60x 70.65 — / 41.63 field-geometry-n4-l3-p2048-t4.json
Cylindrical, 112 modes 502.92 47.57 10.57x 64.24 94.11 / 41.64 cylindrical-fields*-n4-l3-p2048-t4.json

Post-import baselines of the first row are 64.00 MiB for treams and 40.22 MiB for treams-rs. The cylindrical case uses orders -3 to 3, axial labels 0.2 and -0.3 and both helicities; without sharing the radial value and its first two derivatives between adjacent orders it takes 138.96 ms. On a shared four-core machine (16 spheres at order 4, 4,000 samples, one thread), evaluating each scaled radial function once per sample, position, wavenumber and order brings weighted fields from 4.61 s to 0.26 s forward and from 12.3 s to 1.08 s forward plus reverse, with values equal bit for bit. Workloads: --workload field --samples 2048, --workload field-operator, --workload cylindrical-field.

Periodic sphere arrays and adjoints

A square grid of spheres with spacing 0.8, the particle parameters above and Bloch vector (0.1, 0.15). Timings cover local coefficients, the 2D Ewald coupling and the full interacting response matrix, keeping the data for every pullback; basis setup is excluded. The complete reverse pass (dense adjoint solve, particle pullbacks, positions, wavenumbers, Bloch vector and lattice) takes 146.59 and 749.60 ms for four and nine spheres at four threads, peaking at 43.4 and 60.0 MiB; this build computes the Ewald derivatives per polarization. Raw results: periodic-n4-l3-t1.json, periodic-adjoint-n4-l3-t4.json and periodic-adjoint-n9-l3-t4.json. Workload: --workload periodic --particles 4 --lmax 3 --threads 4.

Spheres lmax Threads treams ms Rust ms Speedup treams / Rust peak MiB
4 3 1 103.57 49.24 2.10x 89.2 / 41.6
4 3 4 118.94 14.06 8.46x 89.5 / 40.7
9 3 4 571.90 70.05 8.16x 184.5 / 49.8

Complete periodic S matrices

The array workload adds plane-wave incidence and radiation into ten ports (the zeroth and four first diffraction orders, both polarizations); the full S matrix is compared with treams. Reverse covers the channel and radiation pullbacks, the periodic solve and all particle pullbacks. Sharing Ewald derivatives between polarizations of equal wavenumber brings the four-sphere, four-thread reverse from 141.24 to 71.30 ms, about 4.7 forward evaluations, with no mode-by-parameter Jacobian. The nine-sphere case peaks at 60.5 MiB through reverse. Raw samples: array-before-n4-l3-t4.json and array-n*-l3-t*.json. Workload: --workload array --particles 9 --lmax 3 --threads 4.

Spheres lmax Threads treams ms Rust ms Speedup Rust reverse ms treams / Rust peak MiB
4 3 1 105.41 50.35 2.09x 254.38 90.6 / 43.3
4 3 4 117.41 15.27 7.69x 71.30 90.8 / 42.8
9 3 4 570.15 73.00 7.81x 351.90 187.9 / 52.4

Multipole rotations

Two spherical positions and all degrees through 8 (320 modes), four threads: the complete rotation, including the angular blocks, takes 0.510 ms against 5.597 ms in treams (10.98x). The reverse pass takes 0.753 ms using the kept small-d blocks; forward peak RSS is 42.27 against 67.03 MiB. Raw samples: rotation-n2-l8-t4.json. Workload: --workload rotation --particles 2 --lmax 8 --threads 4.

Cylindrical-to-spherical conversion

One common position, spherical lmax=12 (336 modes), 32 axial wavenumbers between -0.7 and 0.7 and cylindrical mmax=12 (1,600 modes), four threads, seven samples. The full 336-by-1,600 matrix takes 3.34 ms against 23.13 ms in treams (6.92x); the pullback for positions and complex wavenumbers takes 5.80 ms. Forward peak RSS is 73.9 MiB for treams and 55.9 MiB for treams-rs. treams-rs skips the azimuthal orders that vanish at coincident transverse positions and keeps adjacent orders for the position derivatives; without the skip, the forward takes 20.48 ms and the reverse 23.63 ms. This covers coincident positions only. The timings were not archived.

Plane fields and direct NumPy buffer transfer

128 plane modes (64 transverse vectors, both polarizations), 4,096 Cartesian samples with propagating and evanescent waves at k0=1.3, four threads, seven samples. Timings include input conversion, output transfer and saving data for the pullback; no output Jacobian is kept. Contracting polarization cotangents (the gradients with respect to the outputs) over all samples before differentiating each mode brings the weighted reverse from 6.10 to 3.58 ms. Handing the Rust buffer to NumPy without a copy brings the full operator from 13.32 ms and 89.4 MiB to the row below. The reverse pass accepts any cotangent memory layout, using one copy to make the data contiguous. For multipole field operators (four positions, lmax=3, 2,048 points) the same transfer gives 823.02 / 42.83 ms (19.2x), 75.81 ms reverse and 97.1 / 53.5 MiB forward peak RSS, against 64.2 MiB for treams-rs with a copy. The timings were not archived.

Output treams forward Rust forward Speedup Rust reverse Forward peak RSS, treams / Rust
Weighted plane field 139.37 ms 1.95 ms 71.5x 3.58 ms 88.3 / 42.1 MiB
Full plane field operator 137.13 ms 2.70 ms 50.7x 9.76 ms 88.1 / 65.7 MiB

Plane-to-spherical illumination

Two spherical positions, lmax=8 (320 modes), and 64 transverse vectors with both polarizations (128 plane modes) at k0=1.3, including evanescent directions; four threads, seven samples. treams takes 14.07 ms and treams-rs 0.598 ms (23.6x); the reverse pass for all positions and complex wavevectors takes 1.23 ms. Forward peak RSS is 67.5 / 40.2 MiB. treams-rs shares the normalized direction across multipoles, runs incident modes in parallel and hands the output to NumPy without keeping it in the residual. Reusing the phase of each position across its multipoles gives 0.494 ms against 14.143 ms (28.6x) with 1.163 ms reverse. The timings were not archived.

Complete cylindrical arrays

One-dimensional arrays along x with kz=0.2, k0=1.3, radii 0.15–0.25, permittivity 4+0.1j, spacing 0.8 and period 0.8N; four threads, seven samples. Each evaluation covers particle T-matrices, Ewald coupling, the periodic solve and all four plane-wave blocks for ten ports; reverse takes arbitrary complex cotangents. Both packages use the Ewald split eta=0.7: at its automatic split, treams differs from the converged nine-cylinder answer by about 8.4e-8 in the S matrix, while treams-rs stays stable as eta changes, and a regression test checks it against the converged reference. Sharing Ewald evaluations between polarizations of equal wavenumber brings the four-cylinder case at the automatic split from 18.42 to 9.38 ms forward and from 45.84 to 22.99 ms reverse. The timings were not archived. Workload: --workload cylindrical-array --particles 9 --lmax 3 --threads 4 --repeats 7.

Cylinders mmax Modes treams ms Rust ms Speedup Rust reverse ms treams / Rust peak MiB
4 5 88 2101.29 8.26 254.4x 21.13 69.1 / 41.6
9 3 126 2982.63 16.64 179.2x 45.15 70.9 / 43.8

Cylindrical plane illumination and phase reuse

Four positions, mmax=12 (200 cylindrical modes), kz=0.2, and 128 transverse vectors with both polarizations (256 plane modes); four threads, seven samples. treams-rs takes 0.220 ms against 6.376 ms (28.9x); the reverse pass for positions and transverse wavevectors takes 0.446 ms, with axial labels fixed. Forward peak RSS is 39.9 against 67.7 MiB. Without reusing the phase of each position across its multipoles, the forward takes 0.377 ms and the reverse 0.537 ms. The timings were not archived. Workload: --workload cylindrical-plane-expansion --particles 4 --lmax 12 --samples 128 --threads 4 --repeats 7.

Compact planar multilayers

Four interior layers with alternating permittivities 2.3+0.1j and 1.7+0.05j in vacuum, thicknesses 0.1–0.4, k0=1.3, transverse qx=0.1–0.8 and qy=0.2; four threads, seven samples. Both forward timings call the public slab constructor and return the complete four-block dense array. Reverse timings start from the compact diff.layer_stack cotangent and cover all medium wavenumbers, impedances, transverse components and thicknesses. treams-rs solves each channel on its own and composes each layer's reflectionless propagation in closed form, as a diagonal phase scaling of the stack below; the forward blocks remain equal bit for bit to the general composition. Its solve and residual grow linearly with the channel count, while the dense output still costs quadratic time and memory. Slab bases with partial polarization use the general projected composition. The timings were not archived. Workload: --workload slab --layers 4 --channels 512 --threads 4 --repeats 7.

Channels Plane modes treams ms Rust ms Speedup Compact Rust reverse ms treams / Rust peak MiB
128 256 84.37 2.81 30.0x 1.16 88.9 / 52.0
512 1024 2152.16 23.58 91.3x 3.28 378.2 / 187.9

Periodic spherical-to-cylindrical conversion

One common position, lmax=mmax=12, 336 spherical inputs and 1,600 cylindrical outputs across 32 axial diffraction orders, wavenumber 1.3, period 200 and Bloch component 0.2; four threads, seven samples. treams-rs takes 1.158 ms against 24.266 ms (21.0x). Its reverse pass takes 6.114 ms and covers both positions, complex medium wavenumbers, per-output axial wavenumbers and the period. Forward peak RSS is 48.5 MiB against 73.6 MiB, and forward plus reverse peaks at 65.1 MiB; the residual holds geometry only. The timing covers conversion only and was not archived. Workload: --workload periodic-conversion --particles 1 --lmax 12 --samples 32 --threads 4 --repeats 7.

Dense internal illumination

Two general dense S matrices, four threads, seven samples. Random complex reflections scale as 0.1/sqrt(N), with identity transmission plus similar perturbations; seed 81. All four fields are compared with treams; building the S matrices is outside the timings. Each sample is a batch of at least 20 ms that includes freeing the returned fields and residuals. Ordinary illumination uses its input blocks without copies and factors the operator in one LU buffer. Recorded illumination also copies the inputs, so the pullback survives later changes to the Python arrays; at 1,024 modes this costs about 332 MiB against 259 MiB for treams' forward-only call. Raw results: benchmarks/results/internal-{forward,adjoint}-l{128,512}-p{1,8}.json. just bench-performance reruns these eight cases and the two plane permutation cases below into benchmarks/results/local/geometry-recheck-*.json. Here lmax is only a size argument: the dense matrix has 2*lmax modes.

Modes RHS columns treams ms Rust forward ms Speedup treams / Rust peak MiB
256 1 3.54 0.87 4.07x 82.6 / 56.6
256 8 4.19 1.51 2.79x 83.0 / 57.5
1024 1 62.09 34.04 1.82x 259.2 / 234.0
1024 8 63.52 30.49 2.08x 259.4 / 235.1
Modes RHS columns treams forward ms Rust recorded forward ms Speedup Rust reverse ms Rust forward / through-reverse peak MiB
256 1 3.94 0.93 4.23x 0.30 65.6 / 85.6
256 8 3.82 1.11 3.43x 0.50 65.2 / 85.2
1024 1 61.40 40.97 1.50x 29.96 332.1 / 420.6
1024 8 66.26 41.26 1.61x 33.34 332.4 / 423.8

Compact plane permutations

Cyclic Cartesian-axis transforms return both output polarizations for each input mode, shape (2, N), against treams.pw.permute_xyz broadcast to the same shape; this is not a dense N-by-N operator. Four threads, seven batched samples including output and residual cleanup. The residual keeps only the vectors and discrete labels. Raw results: benchmarks/results/plane-permutation-l{64,512}.json. The full plane-field operator (128 modes, 4,096 samples) with the same shared polarization arithmetic takes 1.81 ms against 138.43 ms (76.55x), with 9.77 ms reverse and 67.5 against 89.5 MiB forward peak RSS (plane-transform-check-field-n128.json).

Modes treams us Rust us Speedup Rust reverse us treams / Rust forward peak MiB
128 7.52 5.41 1.39x 16.78 65.0 / 42.2
1024 170.23 34.68 4.91x 77.78 65.2 / 43.4

Plane translation phases

Compact exp(i k.r) tables at 4,096 displacements with propagating and evanescent wavevectors, against the treams pw.translate ufunc; both packages receive precomputed vectors. The context keeps only positions and wavevectors, and the table goes to NumPy without a copy. Reading contiguous cotangents without copying brings the large forward-plus-reverse peak from 235.1 to 172.3 MiB and reverse from 79.33 to 64.42 ms; noncontiguous inputs are copied. This covers only the phase calculation. The timings were not archived. Workload: --workload plane-phases --particles 16 --lmax 32 --samples 4096 --threads 4 (plane modes = 2·particles·lmax; the smaller case uses 8 and 8).

Plane modes treams ms Rust ms Speedup Rust reverse ms treams / Rust forward peak MiB
128 7.81 1.81 4.31x 7.95 90.5 / 50.2
1024 72.69 22.53 3.23x 64.42 258.9 / 108.0

Oriented chirality forms

Compact signed-helicity up/down/cross coefficients averaged from -0.2 to 0.7 along x, with propagating and evanescent waves. The reference builds Cartesian polarizations with treams.special.vpw_A and contracts their inner products and exponential averages; it is the equivalent Cartesian calculation, not treams' xy-only chirality method. Outputs have shape (3, N). Reverse covers both real transverse components, complex normal components and the interval ends; the residual keeps only the input geometry. Raw results: benchmarks/results/oriented-chirality-l{64,512}.json. Workload: --workload oriented-chirality --particles 1 --lmax 512 --threads 4.

Modes Reference us Rust us Speedup Rust reverse us Reference / Rust forward peak MiB
128 54.72 24.45 2.24x 114.67 65.3 / 42.8
1024 619.42 65.88 9.40x 280.72 65.3 / 43.4

Heterogeneous particle clusters and block adjoints

Spheres with alternating cutoffs 3 and 4, radii 0.15–0.25, permittivity 4+0.1j, vacuum wavenumber 1.3 and spacing 0.8; local particle construction is outside the timings, and both packages return the complete interacting matrix. The Rust calculation takes the local arrays separately and keeps their block structure. Its reverse pass returns only the local diagonal-block gradients plus every position and both embedding-wavenumber cotangents; the 624-mode process peaks at 107.1 MiB through reverse. For homogeneous clusters with lmax=3, 240 modes take 2.48 ms against 90.32 ms (36.42x), 2.34 ms reverse and 46.1/50.9 MiB; 960 modes take 37.50 ms against 1678.25 ms (44.75x), 63.31 ms reverse and 102.6/177.5 MiB. Raw results: particle-cluster{-public,}-n{4,16}-l3.json and block-adjoint-cluster-n{8,32}-l3.json. Workloads: --workload particle-cluster or --workload particle-cluster-public with --particles 16 --lmax 3 --threads 4.

Particles Modes Path treams ms Rust ms Speedup Rust reverse ms treams / Rust forward peak MiB
4 156 Native block path 35.43 3.24 10.95x 0.91 71.3 / 44.0
16 624 Native block path 734.82 17.51 41.96x 22.74 109.2 / 68.5
4 156 Public cluster + solve 35.67 3.97 8.99x — 71.7 / 45.1
16 624 Public cluster + solve 737.35 23.76 31.03x — 107.9 / 86.3

Broadcast Bessel functions

Cylindrical Hankel H1 of order 3 at 128 or 4,096 complex arguments with real part 0.6–8.0 and imaginary part 0.2; values and first derivatives are compared with treams.special. Four threads, seven batched samples including cleanup. Parallel evaluation starts at 64 values. Ordinary forward calls use their inputs without copying and save no pullback data; a 128-value run without these changes fails the runtime check (65.59 against 61.08 us). treams-rs peaks at 39.8–41.0 MiB against 64.9–65.4 MiB. Small differences between recorded and ordinary forward times come from run-to-run scheduling. The ufunc table in the next section measures a later build of the same functions. Raw results: benchmarks/results/bessel{,-derivative}{,-forward}-n{128,4096}.json. Workload: --workload bessel-forward --samples 4096 --particles 1 --lmax 3 --threads 4; the other workload names match the file names.

Values Operation Path treams us Rust us Speedup Rust reverse us
128 H1 Public forward 60.86 32.13 1.89x —
4096 H1 Public forward 1905.60 550.44 3.46x —
128 H1 derivative Public forward 93.62 39.13 2.39x —
4096 H1 derivative Public forward 3035.05 936.35 3.24x —
128 H1 Recorded 61.54 28.52 2.16x 36.16
4096 H1 Recorded 1911.90 554.59 3.45x 935.10
128 H1 derivative Recorded 93.36 48.76 1.91x 62.43
4096 H1 derivative Recorded 3034.26 882.62 3.44x 1389.10

Heterogeneous cylindrical clusters

Alternating azimuthal cutoffs 3 and 4, two fixed axial channels (0.2, 0.4) and four threads; local cylinder construction is outside the timings, and the public path includes cluster construction and the interaction solve. The 512-mode native run peaks at 83.0 MiB through reverse. Parallel cylindrical translation contractions bring its reverse from 59.38 to 25.60 ms and the 128-mode reverse from 3.01 to 1.30 ms. Raw data: benchmarks/results/cylindrical-particle-cluster{,-public}-n{4,16}-l3.json; just bench-performance reruns all four into benchmarks/results/local/.

Particles / modes Path Upstream ms Rust ms Speedup Reverse ms Upstream / Rust forward RSS MiB
4 / 128 Native blocks 4.161 1.373 3.03x 1.298 70.5 / 42.5
16 / 512 Native blocks 67.254 20.640 3.26x 25.596 96.1 / 59.3
4 / 128 Public API 4.255 1.336 3.18x — 70.2 / 43.9
16 / 512 Public API 67.142 23.677 2.84x — 96.8 / 70.2

Scalar overhead and NumPy ufuncs

Scalar Python arguments at z=1.3+0.2i take a direct native path without temporary arrays; arrays (Re z = 0.6..8, Im z = 0.2) use NumPy ufunc loops, which keep NumPy's output allocation, masks, broadcasting and overlap handling. Order is 3. The complex Hankel derivative gets its adjacent orders from one sequence evaluation, using the Bessel derivative recurrence. All twelve cases pass their correctness, runtime and forward-RSS checks; treams-rs peaks at 39.2–41.3 MiB against 64.2–65.2 MiB. The recorded scalar value is faster by only 1.06x, so recheck it on the target host. Raw files: benchmarks/results/ufunc-bessel{,-derivative}{,-forward}-n{1,128,4096}.json; just bench-performance reruns all twelve into benchmarks/results/local/.

Values Operation Path Upstream us Rust us Speedup Reverse us
1 Value Ordinary 0.957 0.596 1.61x —
128 Value Ordinary 61.576 35.080 1.76x —
4096 Value Ordinary 1901.426 553.620 3.43x —
1 Derivative Ordinary 1.067 1.004 1.06x —
128 Derivative Ordinary 95.156 24.925 3.82x —
4096 Derivative Ordinary 3091.247 554.712 5.57x —
1 Value Recorded 0.910 0.859 1.06x 0.796
128 Value Recorded 61.387 29.670 2.07x 26.041
4096 Value Recorded 2009.643 564.970 3.56x 575.693
1 Derivative Recorded 1.081 0.876 1.23x 0.812
128 Derivative Recorded 94.985 33.888 2.80x 31.131
4096 Derivative Recorded 3216.664 567.114 5.67x 582.907

Public angular functions

Integer-degree Legendre, pi and tau functions at degree 6 and order 2, with a scalar argument 0.3+0.1j and arrays over [-0.8,0.8]+0.1j; four threads. They use NumPy loops, a direct scalar path and the same factored recurrences forward and in reverse; tau shares one sine-power factor between adjacent Legendre orders. Recording includes saving the pullback data and freeing the result; each reverse pass uses a fresh record. All 50 runtime checks of this just bench-performance run pass, and the ordinary forward and recorded special-function and particle paths pass their peak-RSS checks. treams-rs peaks at 40.3–41.7 MiB against 64.1–65.2 MiB. The 18 angular-*.json files in raw results record every sample, native-extension hash, Python and NumPy version, thread setting and reverse peak RSS. Units are microseconds.

Operation Count Upstream forward Rust forward Speedup Rust reverse
Legendre ordinary 1 0.802 0.290 2.76x —
Legendre ordinary 128 6.192 3.545 1.75x —
Legendre ordinary 4096 362.485 46.219 7.84x —
Legendre recorded 1 0.780 0.430 1.82x 0.402
Legendre recorded 128 6.267 4.643 1.35x 6.061
Legendre recorded 4096 363.316 71.860 5.06x 98.387
Pi ordinary 1 0.837 0.335 2.50x —
Pi ordinary 128 9.155 6.732 1.36x —
Pi ordinary 4096 652.243 69.679 9.36x —
Pi recorded 1 0.815 0.487 1.67x 0.465
Pi recorded 128 9.161 7.882 1.16x 11.552
Pi recorded 4096 651.226 71.144 9.15x 111.898
Tau ordinary 1 0.879 0.362 2.43x —
Tau ordinary 128 12.946 8.792 1.47x —
Tau ordinary 4096 856.314 86.509 9.90x —
Tau recorded 1 0.850 0.492 1.73x 0.455
Tau recorded 128 12.955 9.976 1.30x 15.271
Tau recorded 4096 855.324 88.387 9.68x 142.593

Wigner elements, Euler pullbacks and public Ewald integrals

Wigner degree 6, row 1, column -2, outer Euler angles 0.2 and -0.1; the polar argument is 0.7+0.1j for scalars and spans [0.3,1.3]+0.1j for arrays. The 3j case uses degrees (6,6,6) and orders (1,-2,1); the incomplete gamma degree is 1.5; the Kambe integral has order -2 and eta=0.7+0.1j. Individual Wigner elements use the Jacobi recurrence without a full angular matrix; the ufuncs run large strided arrays in parallel and keep masked and in-place behavior. Wigner residuals keep only labels and Euler arguments, and the scalar 3j path skips NumPy dispatch. All 68 runtime checks of this run pass (50 angular and baseline workloads and 18 Wigner and Ewald cases). The angular table above records the run of commit 4f41708; this 68-case run refreshed the raw JSON files. treams-rs peaks at 39.9–41.5 MiB against 64.2–65.8 MiB. Native hashes, samples and reverse measurements are in the raw results. Units are microseconds.

Operation Count Upstream forward Rust forward Speedup Rust reverse
wigner-forward 1 1.510 1.236 1.22x —
wigner-forward 128 40.153 14.328 2.80x —
wigner-forward 4096 2256.407 135.234 16.69x —
wigner-small-forward 1 1.124 0.970 1.16x —
wigner-small-forward 128 39.366 12.470 3.16x —
wigner-small-forward 4096 2127.653 119.326 17.83x —
wigner3j-forward 1 1.302 0.292 4.47x —
wigner3j-forward 128 28.319 16.130 1.76x —
wigner3j-forward 4096 1003.028 141.485 7.09x —
incgamma-forward 1 0.672 0.314 2.14x —
incgamma-forward 128 20.111 15.654 1.28x —
incgamma-forward 4096 1085.018 164.622 6.59x —
intkambe-forward 1 0.835 0.374 2.23x —
intkambe-forward 128 24.098 22.111 1.09x —
intkambe-forward 4096 1358.198 204.507 6.64x —
wigner 1 1.428 0.718 1.99x 0.742
wigner 128 39.724 24.048 1.65x 22.380
wigner 4096 2357.991 144.681 16.30x 274.425

Cylindrical axial field derivatives

Four particles at order 3, four threads. The ordinary reverse and the optional per-mode axial reverse both pass the accuracy, forward-runtime and forward-RSS checks. The forward is the same for both, and recording adds no axial derivative table; the axial reverse computes one more derivative per mode in about the same time. The build at ce0bde4 measured 3.172/47.935 ms forward and 4.197/62.601 ms reverse at 128/2,048 samples; successive short runs vary by a few percent, so these differences show no change. These four cases ran separately from the 68-case run above. Times in milliseconds:

Reverse requested Samples Upstream forward Rust forward Speedup Rust reverse Rust / upstream RSS MiB
Existing 128 32.071 3.120 10.28x 4.045 44.8 / 67.7
Existing 2048 503.039 49.549 10.15x 66.882 44.9 / 96.4
All + axial 128 33.642 3.094 10.87x 4.180 43.8 / 67.7
All + axial 2048 504.578 48.038 10.50x 65.684 44.3 / 96.3

Shared axial derivatives of finite and periodic cylindrical expansions

Four or sixteen particles, two axial groups, order 3 and four threads; every case passes its accuracy, forward-runtime and peak-RSS checks. Periodic cases use the Ewald split eta=0.7 in both packages: at period 12.8 the automatic treams split differs from the converged result by up to 0.007466 in this matrix, while explicit eta=0.5/0.8/1.0 and the treams-rs automatic split agree with treams-rs to about 1.5e-11. A scalar regression test covers periods 7.2 and 12.8. The -axial cases run the same forward and also request the grouped axial gradient in reverse, which reuses the Ewald derivatives without an axial Jacobian. These eight cases ran separately from the other runs. The complete four-cylinder array without and with the grouped axial derivative takes 4.729 and 4.511 ms forward, 12.138 and 11.760 ms reverse, and peaks at 42.5 and 43.9 MiB, in one short run on one host (cylindrical-array-{before,after}-axial.json).

Workload Particles Upstream forward ms Rust forward ms Speedup Rust reverse ms Rust / upstream RSS MiB
expansion 4 2.319 0.583 3.98x 0.606 41.9 / 67.1
expansion 16 38.088 10.762 3.54x 10.599 48.2 / 74.8
expansion-axial 4 2.313 0.592 3.91x 0.640 42.4 / 66.8
expansion-axial 16 37.895 10.523 3.60x 10.716 48.0 / 74.8
periodic 4 1522.719 8.786 173.32x 19.316 43.5 / 67.6
periodic 16 16303.387 88.316 184.60x 238.705 50.4 / 86.0
periodic-axial 4 1510.388 9.131 165.41x 20.026 43.4 / 66.8
periodic-axial 16 16334.604 88.576 184.41x 238.965 50.3 / 85.8

Coordinate transformations

All eight point and eight vector-frame conversions use NumPy gufuncs with analytic pullbacks, support component strides, broadcasting and output buffers, and apply factored rotations without temporary matrices. The run passes 128 performance checks, 48 of them coordinate comparisons at 1, 128 and 65,536 points; each requires a forward speedup ≥ 1 and no higher forward peak RSS. The forward-only helpers of both packages return the result directly and free it inside the timing; recorded operations also return data for the pullback, and the JSON field forward_records_adjoint says which. Sub-microsecond scalar differences are sensitive to machine noise. Raw samples, RSS and binary identities: coordinate-*-n*.json.

Transform 1 point 128 points 65,536 points
car2sph 1.08x 1.21x 2.31x
sph2car 1.04x 1.33x 2.87x
car2pol 1.08x 1.51x 4.23x
vsph2car 1.01x 1.31x 2.96x
vpol2car 1.07x 1.43x 5.04x

Local wave functions

The run passes all 191 runtime checks and all 187 applicable forward-RSS checks; the four recorded internal-illumination cases keep owned input copies and are not compared with the forward-only treams memory. It used taskset -c 8-11 just bench-performance, giving both packages the same four physical cores and four BLAS/Rayon threads; diagnostic runs on busier cores are kept next to the final results. When both isolated medians are below 1 ms, the two packages also alternate in one process over 14 paired samples of at least 20 ms each, and the speedup is the median paired ratio. The smallest speedup is 1.013x (scalar plane M wave). The wave kernels share normalized angular recurrences, fetch adjacent cylindrical Bessel values together, reuse plane polarizations for constant directions and normalize with an algebraic complex square root, with scaled fallbacks for extreme magnitudes. Recorded illumination copies its eight input blocks as independent tasks from 128 modes when Rayon has more than one thread (on one thread: serially below 512 modes, on the worker from there) and finishes the copies before solving; with two tasks from 512 modes, the 510-mode (order 255) case copied 33 MB serially at 0.85x of the treams forward-only call (snapshot-*, serial-* and parallel-copy-* files). Final results: wave-<function>-n<size>.json and wave-adjoint-final-<function>-n<size>.json, each with the native binary hash.

Forward function 1 sample 128 samples 4,096 samples
sph_harm 1.32x 3.94x 15.05x
vsw_rA 1.73x 4.59x 20.36x
vcw_rA 1.30x 2.33x 9.90x
vpw_A 1.06x 3.35x 21.75x
Recorded function 128 samples 4,096 samples Reverse at 4,096 samples
vsw_rA 4.40x 26.88x 0.644 ms
vcw_rA 2.20x 10.58x 0.677 ms
vpw_A 2.08x 18.83x 0.191 ms

Coefficient namespaces and integral adjoints

The run passes all 257 runtime checks and all 253 applicable forward-RSS checks on the same four-core Linux setup, with the four recorded internal-illumination cases exempt from the memory check. The smallest speedup is 1.006x for scalar cw.rotate. Cylindrical coefficient arrays switch to Rayon at 64 elements, scalar Python numbers skip NumPy dispatch, and plane phases evaluate one exponential and one sine/cosine pair. Paired timing calibrates each package's batch to at least 20 ms on its own, so the slower package is not oversampled; 91 cases kept the batch sizes of the preceding calibration at the same native SHA-256 and 166 used independent sizes, as the manifest records. Failed baselines and targeted rechecks stay next to the passing results. The result manifest and native SHA-256 are in benchmarks/coefficient-qualification.json.

Forward function 1 sample 128 samples 4,096 samples
tl_vcw 1.822x 1.299x 2.569x
tl_vcw_r 2.427x 1.368x 4.606x
cw.translate 2.195x 1.510x 4.272x
pw.permute_xyz 3.592x 1.367x 10.028x
pw.translate 2.223x 1.226x 1.236x

Lattice geometry and material branches

The run passes 42 geometry cases and 257 regression cases: 299 runtime checks and 295 applicable forward-RSS checks, on CPUs 8–11 with four BLAS/Rayon threads. The smallest margin is 1.005x for scalar cw.rotate, and the largest applicable RSS ratio is 0.912. Circular diffraction enumeration runs 7.915x, 64.034x and 256.860x faster at radius 1, 4 and 16; tests check complete enumeration in skew cells separately. The geometry results are geometry-<operation>-n<size>.json, and the 257 rechecks of the coefficient cases use the geometry-recheck- prefix; benchmarks/geometry-qualification.json records all 299 paths and their native binary hash. Mac diagnostic results keep their own file names and platform metadata.

Forward operation 1 sample 128 samples 4,096 samples
volume2 1.033x 2.978x 44.687x
volume3 1.047x 2.647x 25.899x
reciprocal2 1.026x 6.004x 8.677x
reciprocal3 1.034x 3.121x 3.577x
refractive_index 3.213x 3.466x 1.955x
wave_vec_z 1.904x 3.197x 1.607x

Lattice sums at explicit small splits (pre-release timings)

These timings compare 5dadf1d, a pre-release commit without the cancellation checks of explicit Ewald splits below every automatic split, with the later pre-release commit that adds them (numerical limits). They were not rerun.

Timed as a whole against 5dadf1d (release, one thread, unit cells), 1D spherical sums on the axis of degree 0 to 8 at k = 0.42, eta = 0.14 and 0.2 and Bloch vectors of 0, 0.1 and 0.25 take 0.07 to 0.52 ms each, 0.8 to 2.6 times as long, and up to 9 times at degrees 6 to 8 at the shift 0.1, where 5dadf1d returned some in 0.02 ms and up to 7e-8 of max(|S|, 1) off; at a zero Bloch vector and the shift 0.5, odd degrees vanish exactly at once. 2D spherical sums of degree up to 6 at k = 1.3 and eta = 0.25 take 0.8 times as long in all. 3D sums of degree up to 3 at k = 1.3 and eta = 0.25 and 0.2, and a quarter of the 2D sums at 0.2, failed with "did not converge" at 5dadf1d; with the checks, the 3D sums converge in about 0.1 and 0.24 s each on average.

Sums and Ewald parts that fail to converge sum to the grown shell limit first (release): 3D sums fail within about 2.4 s, the 3D real-space part of degree 3 at k = 0.7 + 0.1i after 28 s at eta = 0.03, 2D sums and parts within 2.5 s.