This development adapter keeps Python dependencies outside the Rust oneloop
core. The recommended expression-native installation is the small shared-kernel
host in host/. It offers two distinct build modes:
- A standalone
oneloop_nativeextension accepting Python numbers. - A community binding linked into the host's single
symbolica.coreextension, also accepting and returning that host's Symbolica expressions.
Matching Symbolica revisions in two independently loaded extension binaries does
not establish shared expression state. The standalone extension deliberately
does not exchange Symbolica Expression objects. This is not a published package.
The full local community checkout is also integrated; see
local host instructions.
Both build modes enable prebuilt and omit generated-evaluators by default.
Run maturin build --release in this directory for the standalone wheel or in
host/ for the development host. These builds use the expression backend for
backend="auto" and numeric master hooks, retain all numeric domains, and reject
explicit backend="native" requests with a feature-required error.
To enable the faster, larger generated evaluators in either build, add
--features generated-evaluators. See the
build-size documentation
for Cargo feature unification in community hosts.
Use an isolated Python virtual environment with Maturin installed, without a
separate symbolica/symbolica-community distribution:
cd python/host
maturin develop --releaseThis development distribution provides symbolica.core itself and links the
OneLOop adapter into that same extension. It must not be installed over another
distribution providing symbolica.core. It does not clone or modify the full
Symbolica community repository. The dependency revisions and Rust/Python
requirements described below apply to this host as well; its own lockfile and
Cargo manifest form the build root. NumPy is installed for Symbolica's evaluator
API, even though OneLOop's manual batched evaluations accept ordinary lists.
from symbolica import E, N, Replacement, S
from symbolica.community.hepkit import oneloop as olo
psq = S("psq", is_real=True)
m2 = S("m2", is_positive=True)
master = olo.B0(psq, m2, m2, 1)
all_branches = olo.get_expression(master)
selected = olo.select_branch(all_branches, [
Replacement(m2, N(1)), Replacement(psq, N("3.23")),
])
print(selected[0]) # No ifs; still a parametric function of psq and m2.
assert all("if(" not in str(c) for c in selected)
# The manual evaluator also takes a native Symbol or variable Expression.
evaluator = olo.Evaluator(olo.B0)
values = evaluator.evaluate_batch([[3.23, 1, 1, 1]] * 1024)The E, S, and N constructors belong to Symbolica; OneLOop never parses
symbolic strings. Attributes and facts about composite expressions are managed
by Symbolica. For example, an argument m2 + E("12/3") is accepted directly.
The literal oneloop::B0(...) spelling is also accepted by inspection; use the
canonical oneloopmaster::B0 for evaluatable master symbols.
In the community module, olo.A0, olo.B0, olo.dB0, olo.C0, and olo.D0
are callable Symbolica Expressions supplied by the corresponding Rust master
accessors. They carry the same oneloopmaster:: identities and native hooks
used by reduction results. No explicit S or E construction is needed for
these master heads.
m2, mu2 = S("m2", "mu2")
master = olo.A0(m2, mu2) # Untagged call for exact symbolic inspection.
finite = olo.A0(0, m2, mu2) # Leading tag selects the finite coefficient.
print(finite.evaluate({m2: 2, mu2: 1})) # Existing direct native Rust hook.
print(olo.master_coefficients(master)) # Tags 0, -1, -2, in that order.The squared scale is always the last positional argument in symbolic calls.
Use tags 0, -1, and -2 for finite, simple-pole, and double-pole numerical
evaluation. Untagged calls go to get_expression or master_coefficients.
Uppercase exports are resolved and cached on first attribute access; this
registers lightweight Symbols without preparing formulas or numerical backends.
dir(olo) and from ...oneloop import A0 expose the same primitive exports.
For direct numerical coefficient triples with optional scale and precision
arguments, use the lowercase olo.a0/b0/db0/c0/d0 functions.
Complete tagged calls automatically normalize to numerical Expressions when
all kinematic arguments are numbers and at least one is floating point. For
example, olo.A0(0, 2.0, 1) evaluates the finite coefficient during construction.
Mixed exact and floating inputs use the maximum supplied floating-point
precision, including complex components and the scale, without a binary64
conversion of exact inputs. All-exact calls such as olo.A0(0, 2, 1) retain
their symbolic meaning; request .evaluate({}) to choose numerical evaluation.
Untagged calls, partially symbolic calls, and invalid tags, arities, or numerical
domains remain symbolic. Nonfinite numerical results also leave the call intact.
Use a Python virtual environment with Maturin installed:
cd python
maturin develop --releasePython 3.9 or newer and Rust edition 2024 are required. The manifests use Symbolica and Numerica 3.0.1 from crates.io, with PyO3 0.28. Libraries and hosts share versioned dependencies without Symbolica source overrides. The supplied lockfiles record the standalone and shared-host dependency resolutions.
The core and adapter dependencies use default-features = false for Symbolica,
explicitly retaining tracing_max_level_info, integer-gmp, float-mpfr,
native_code_generation, and bincode. Symbolica's optional faster_alloc
global mimalloc allocator is excluded; this standalone build uses Rust's system
allocator. The earlier sequential-thread crash was traced to mimalloc during
PyO3 constructor-string extraction, before evaluator construction. With this
supported feature configuration, the unchanged reproducer and full API suite
pass; no JIT-clone fix or extra cache/thread-lifetime restriction was required.
Import registers Python classes and functions only. Symbolica state initialization
attaches lightweight master callbacks; it does not build OneLOop formulas or
prepare numerical backends. The selected backend and family are prepared on
first use. With prebuilt, requesting SymJIT loads that family's numerical
instructions and compiles them for this machine. Without prebuilt, it builds
that family's evaluator from expressions. Cache/configuration errors are raised
when SymJIT is requested, not during import or native evaluation.
Ordinary calls use the direct Rust backend. backend="symjit", rebuild=True
explicitly recompiles the requested evaluator. Native rebuilding prepares fresh
constants and a numeric workspace without JIT compilation. The read-only
is_initialized() reports explicit Rust oneloop::initialize() full warmup;
it remains false during ordinary lazy Python use.
On 2026-09-22, fresh imports of the local community host took 10–41 ms across
three staged processes, down from 13.16 s before lazy loading. A fresh import
after installation into the existing host took 0.10 s. First native A0 calls
took 0.2–1.4 ms; first SymJIT A0 calls took 4–8 ms. These are local development
build measurements, not a platform-independent latency guarantee. Formula
construction also now resolves the exact log(1)/(1-1) limit before building
the discarded singular expression, eliminating the startup infinity warnings.
The core enables its five embedded cache assets by default. The 2026-09-08 release
extension with the system allocator and native binary64 Li2 passed all eleven
standalone tests on CPython 3.13.12: seven API tests and four pure performance-
harness tests. These include cold import, mixed batches, rebuilding, and
sequential fresh test threads. Batch performance has also been measured below;
the overall 1.5× Fortran target is not met. The in-repository host makes shared-kernel expression tests directly runnable;
the older standalone results below are not host validation results.
For these portable scalar APIs, unset SYMJIT_TOML and use a working directory
without a symjit.toml override. The wrapper rejects either override rather than
silently changing the cached compiler configuration.
import oneloop_native as olo
assert not olo.is_initialized() # Import does not warm up numerical backends.
finite, simple_pole, double_pole = olo.a0(2.0, mu_squared=4.0)
value = olo.b0(-1.0, 1.0, 1.0, mu_squared=1.0)
evaluator = olo.Evaluator("B0")
value = evaluator.evaluate([-1.0, 1.0, 1.0, 1.0])
values = evaluator.evaluate_batch([
[-1.0, 1.0, 1.0, 1.0],
[7.0, 1.0, 2.0, 1.0],
[3.0, 0.7 - 0.03j, 1.4 - 0.08j, 4.0],
])Every call returns three coefficients ordered as
(finite, simple_pole, double_pole), corresponding to Laurent tags (0, -1, -2).
Ordinary numeric inputs with the default prec=16 use binary64 components and
return Python complex values through the expression interpreter by default,
or direct Rust arithmetic when built with generated-evaluators.
Select backend="symjit" for the prepared SymJIT O2 path, or
backend="expression" for the transparent Symbolica expression interpreter.
Higher precision or Decimal inputs use the arbitrary-precision path below.
The direct functions have these argument orders:
| Function | Ordered arguments before optional mu_squared=1.0 |
|---|---|
a0 |
mass_squared |
b0, db0 |
momentum_squared, mass_0_squared, mass_1_squared |
c0 |
p1_squared, p2_squared, p3_squared, mass_0_squared, mass_1_squared, mass_2_squared |
d0 |
p1_squared, p2_squared, p3_squared, p4_squared, s12, s23, mass_0_squared, mass_1_squared, mass_2_squared, mass_3_squared |
Here s12 and s23 are squared channel invariants, not their square roots. All
momenta and masses are already squared quantities. External invariants must be
real; complex squared masses must have nonpositive imaginary parts; mu_squared
must be positive and real. Nonfinite input components are rejected. These checks
do not establish that every singular kinematic point has a finite integral or
that every analytic region has been validated.
The reusable Evaluator accepts the same ordered arguments with
mu_squared always supplied last, giving arities 2, 4, 4, 7, and 11. An empty
batch returns []; ragged rows or incorrect argument counts raise ValueError.
In the shared-kernel host, construct this object with
Evaluator(olo.B0) (the primitive master Expression), not a
family-name string. Only the numeric-only standalone compatibility API retains
its historical Evaluator("B0") selector. Numeric Symbolica Expressions, including
rational constants and composite constant expressions, are also accepted in host
numeric argument rows; they use requested-precision evaluation and return
DecimalComplex, avoiding a lossy intermediate binary64 conversion.
Rows are passed together to the core's batched evaluator. List conversion and
Python result allocation remain part of Python call overhead and are included
in the measured Python/Fortran ratios below. NumPy is not needed in standalone mode.
Numerical functions a0, b0, db0, c0, and d0 use the core's shared,
mutex-protected family caches. The standalone numeric-only build also retains
uppercase aliases; community uppercase exports are primitive Expressions. Each reusable object
clones a warmed backend into its own evaluator workspace:
evaluator = olo.Evaluator("B0", rebuild=True) # Explicit fresh construction.
evaluator.rebuild() # Replace this object's evaluator only after success.
value = olo.b0(-1.0, 1.0, 1.0, rebuild=True) # One uncached rebuilt call.
assert evaluator.family == "B0"
assert evaluator.arity == 4All five direct functions accept prec=16, prec=32, prec=1000, or another
positive integer number of decimal digits. The reusable evaluator accepts
the same keyword, and its methods can override it for one call:
from decimal import Decimal, localcontext
import oneloop_native as olo
finite, pole, double_pole = olo.a0(Decimal("2"),
mu_squared=Decimal("4"), prec=1000)
assert isinstance(finite.real, Decimal)
assert isinstance(finite.imag, Decimal)
print(finite.real) # Approximately 3.386294361119890618834464242916...
bubble = olo.Evaluator("B0", prec=32)
row = [Decimal("-1"), Decimal("1"), Decimal("1"), Decimal("4")]
values = bubble.evaluate_batch([row] * 1024)
more_digits = bubble.evaluate(row, prec=1000) # Does not change bubble.prec.
assert bubble.prec == 32
# Decimal arithmetic after evaluation follows Python's decimal context.
with localcontext() as ctx:
ctx.prec = 1000
twice_real_part = finite.real * 2Decimal("...") is parsed directly at the requested working precision, including
mu_squared: there is no intermediate float conversion. Creating a Decimal
from a string and creating the output Decimal objects do not depend on the
global decimal context. In contrast, Decimal(0.1) already contains the Python
float's rounding error; use Decimal("0.1") to specify that decimal value.
Python float/complex inputs at higher prec retain their original binary64
values exactly, but cannot supply missing input digits.
Python's built-in complex cannot hold Decimal components. Use
DecimalComplex(real, imag) for arbitrary-precision complex squared masses:
mass = olo.DecimalComplex(Decimal("0.70000000000000000000000000000001"),
Decimal("-0.03000000000000000000000000000002"))
finite, _, _ = olo.b0(Decimal("3"), mass, Decimal("1.4"),
mu_squared=Decimal("4"), prec=1000)
print(finite.real, finite.imag)
rounded = complex(finite) # Explicitly lossy conversion to binary64.High-precision calls return three DecimalComplex instances with standard-library
Decimal .real and .imag properties, including for real-valued results.
They are precision-preserving value containers, not a replacement complex
arithmetic package; operate on their Decimal components under a suitable context.
No additional Python numerical dependency is required.
Selection rules are consistent across scalar and batch calls:
prec != 16always selects arbitrary precision.- At
prec=16, anyDecimal,DecimalComplex, or integer beyond the exact binary64 integer range selects arbitrary precision too. - A mixed batch uses arbitrary precision for the entire batch if any input requires it. Otherwise the default batch uses binary64 arithmetic.
- Arbitrary-precision results use DecimalComplex; ordinary default calls retain Python complex outputs. Precision must be a positive integer, not a Boolean.
The core uses Symbolica's expression interpreter with Complex<Float> and fixed
guard bits by default. With generated-evaluators enabled, it defaults to the
generated generic Rust implementation instead. backend="expression" explicitly
selects the interpreter; "symjit" is binary64-only.
The reusable object keeps a precision-specific workspace for reuse; changing
precision prepares its constants again. Both sets of five binary64 backends are
prepared on first use, as are arbitrary precisions.
Arbitrary-precision batches amortize Python overhead; they do not use binary64
SymJIT. Its current binary64 batches also use scalar code because upstream
SIMD fails zero-input batches. rebuild=True rebuilds the selected backend, and no custom
automatic precision-escalation policy is added.
Requested precision is a working-precision choice, not a certified error bound. Cancellation, exact singularities, or unverified analytic limits can still affect results. Returned decimal strings are limited by the computed Float precision, not padded to imply that lost digits were recovered. The historical binary64 performance ratios do not apply to this backend. See the core precision contract and checks.
The adapter performs all work on its calling thread. It never silently spawns a
worker, changes numerical precision, or changes Symbolica's license policy.
An Evaluator must be used on the thread where it was created.
Eager import includes expression-map construction and must run on an adequately sized stack, as must explicit rebuilding. Most regression checks use 128 MiB; the two ordinary-main-thread startup regressions passed with an 8 MiB limit in the final system-allocator release run. For explicit rebuilding in a new standalone application, arrange an adequate stack before importing the extension or otherwise starting Symbolica work; for example:
import threading
def work():
import oneloop_native as olo
print(olo.Evaluator("B0", rebuild=True).evaluate([-1, 1, 1, 1]))
threading.stack_size(128 * 1024 * 1024)
worker = threading.Thread(target=work)
worker.start()
worker.join()Applications already using Symbolica should arrange an adequate calling-thread stack and observe its licensing constraints. The installed restricted license permits only one active Symbolica thread per user; do not run this concurrently with other Symbolica tests or applications under that license. A valid Symbolica setup is still required.
This API is available in the shared-kernel host only. It accepts a complete
Symbolica master call with physical arguments, including mu_squared last,
and returns three native Symbolica Expression objects:
from symbolica import E, N, Replacement, S
from symbolica.community.hepkit import oneloop as olo
psq = S("inspection_psq", is_real=True)
m2 = S("inspection_m2", is_positive=True)
master = olo.B0(psq, m2, m2, 1)
finite, pole, double_pole = olo.get_expression(master)
assert olo.get_expression(master, coefficient=0) == finite
selected = olo.select_branch((finite, pole, double_pole), [
Replacement(m2, N(1)), Replacement(psq, N("3.23")),
])
assert all("if(" not in str(c) for c in selected)
assert selected[1:] == (N(1), N(0))
# Integer and exact-fraction sample points are supported as well.
integer_region = olo.select_branch(finite, [Replacement(m2, N(1)), Replacement(psq, N(3))])
fraction_region = olo.select_branch(finite, [Replacement(m2, N(1)), Replacement(psq, E("1/2"))])The unselected finite body still repeats roots and continuation guards; it is exact but not yet compact. Selection removes guards decidable at the supplied sample, not the variables inside the retained values. Thus its output is a parametric expression valid in the selected analytic region, not a global replacement for the original expression across branch boundaries. See the inspection guide for expansion budgets.
For a large generic C0/D0 expression, use shared=True to store every branch
with native Symbolica common-subexpression bindings, then call select_branch.
The case C0(0,-a,a,a,a,b,mu2) also works as an ordinary complete expression:
mass2, mass2B, mu2 = S("mass2", "mass2B", "mu2")
master = S("oneloopmaster::C0")(0, -mass2, mass2, mass2, mass2, mass2B, mu2)
probes = [Replacement(mass2, N(2)), Replacement(mass2B, N(1)),
Replacement(mu2, N(1))]
complete = olo.get_expression(master)
selected = olo.select_branch(complete, probes)
assert olo.select_branch(selected, probes) == selectedFor a fully generic master, complete = olo.get_expression(master, shared=True)
returns three SharedExpression objects. Each exposes a native Expression
.root, a list of (alias, defining Expression) pairs in .definitions, and
cheap .num_definitions metadata. Every branch is stored, without opaque
OneLOop helpers. select_branch(complete, probes) returns ordinary parametric
Expressions; .to_expression(max_nodes=...) explicitly unfolds all bindings
when feasible. Do not evaluate a shared root alone without its definitions.
The Rust representation is Symbolica's native AliasedAtom.
branch_rules=probes remains available for early selection into ordinary
Expressions; do not combine it with shared=True. Fully duplicated generic
C0/D0 trees can be enormous even though the complete shared form is practical.
Variable probes with standard matching settings are evaluated directly on the
shared graph; compound patterns and custom matching settings use bounded
predicate expansion to preserve native matching semantics.
Return order is (finite, simple_pole, double_pole); coefficient=0, -1, or
-2 selects one Expression. select_branch preserves a single Expression,
tuple, or list as the same kind of result. It visits nested native ifs at any
depth, including those inside conditions, functions and powers. Rules are native
Symbolica Replacement objects, applied in one simultaneous replacement pass
per tested condition. Duplicate matching rules obey Symbolica's first-match
priority. Unresolved predicates remain symbolic and retain their original
variables; condition samples never leak into the kept branch's arithmetic.
Reuse the original variable objects in rules, or their fully qualified Symbolica
names, such as S("mz_masses::m2"); an unqualified name uses the default namespace.
Strings, name-based assumption lists, and (name, value) replacement tuples are
not accepted. The standalone numeric extension no longer exposes inspection.
This mode implements SymbolicaCommunityModule; it does not edit or register
anything in an existing community checkout. A compatible host can depend on the
adapter with:
oneloop-python = { path = "../oneloopmaster/python", default-features = false, features = ["community"] }The host must resolve a single compatible Symbolica/PyO3 dependency graph and
select Numerica from the pinned Symbolica revision at its own Cargo build root. Link this Rust library into that host; do not also load a standalone
OneLOop wheel as an expression bridge. Cargo unifies dependency features:
another host dependency enabling Symbolica defaults or faster_alloc can
re-enable its global mimalloc allocator despite this adapter's manifest. The
host must audit its resolved feature graph, preserve the intended allocator
configuration, and rerun the thread-handoff/API checks in that actual host.
In the host's existing registration code:
register_module!(m, oneloop_native::CommunityModule);The corresponding Python facade follows the host's existing pattern:
from symbolica.community.oneloop_native import *
initialize_module()Registration adds Python classes and functions without constructing formulas.
initialize_module() is a no-op for OneLOop. Symbolica's initialize! inventory
attaches only lightweight master and JIT callbacks. Evaluators remain lazy;
is_initialized() reports explicit full warmup without doing any work.
In this single-kernel configuration, compact master expressions can be combined with other host expressions and then compiled with their transparent definitions:
from symbolica import S
from symbolica.community.hepkit import oneloop as olo
x = S("x")
finite, pole, double_pole = olo.master_coefficients(olo.A0(x, 1))
evaluator = olo.compile_native([finite + pole], [x])
result = evaluator.evaluate_complex([2 + 0j])master_coefficients(master) includes the explicit squared scale last in the call
and returns three genuine host Expression objects. Use compile_native for
combined expressions so OneLOop's native function definitions remain visible to
the evaluator compiler; simply returning a compact master call is not a substitute
for supplying that map. compile_native returns the host's Evaluator, initially
using its complex interpreter, with exact rational constants retained. Its
optional JIT settings start with the core's branch-preserving O2 configuration;
enabling JIT is explicit through the host evaluator API. The host's array output
and numerical precision interfaces are separate from the standalone list API.
On 2026-09-09, the release-built shared-kernel host completed the full Python
suite in 56.759 seconds: 27 passed and two standalone-only checks skipped.
Five passing tests exercise expression inspection and branch selection, including
the equal-mass B0 with floating, integer and rational samples across its real
regions, namespaced attributes and composite masses, 1,200 nested conditionals,
native conditional/wildcard replacement rules, and a 1e-1000 floating sample
in a cancellation-sensitive condition. Selected B0 coefficients have no ifs,
retain their parameters and match the native numerical backend at the samples.
The suite also covers the shared Symbolica expression compiler, manual batch
evaluation, exact rational Expression inputs and all-five-family 1,000-digit
checks. Five tests are pure performance-harness checks, not integral evaluations.
The freshly rebuilt standalone numeric extension passed the same suite in
66.447 seconds: 24 passed and five shared-kernel inspection checks skipped.
Its cold-start and portable-environment guard subprocess checks passed, and it
confirms that symbolic inspection entry points are absent in standalone mode.
Release Clippy with -D warnings also passed for both Python build roots.
The adapter tests cover elementary values, all 31 shared benchmark fixtures, single/batch agreement for 3/4/5/8-row B0 layouts, heterogeneous family batches of size 4/5/8 including partial tails, explicit rebuilding, and argument errors. The shared-kernel expression tests run with the host in this repository. The standalone run checks that symbolic entry points are absent instead. These tests are not evidence of exhaustive analytic-region coverage or of a Python performance target.
On 2026-09-08, all eleven standalone tests passed in 19.740 seconds with
CPython 3.13.12, the final release extension, the system allocator, native
binary64 Li2, and the original fresh-worker-per-test mode
(ONELOOP_PYTHON_THREAD_HANDOFF=1). Seven are actual API tests; the other four
are fake-evaluator performance-harness tests that do not run Symbolica. This
supersedes the earlier 107.040-second seven-test debug result.
The standalone suite first tests lazy import in two fresh subprocesses, one
entering through a direct function and one through a reusable evaluator after
import. Each checks readiness before its first master call, all 31 fixtures
across all five families, and nine mixed batch rows per family, with no rebuild
fallback or worker thread. On POSIX each child caps its inherited main-thread
stack at 8 MiB without increasing a smaller limit. Separate children verify rejection of
SYMJIT_TOML and a working-directory symjit.toml, before cache loading or
source construction. They use private temporary directories and child-only
environment changes. Both cold children and both environment guards passed in
the full system-allocator run. This establishes those specific startup cases,
not a general stack-size bound for arbitrary expressions or rebuilding.
After building and installing the extension, run from the Rust crate root
(.. relative to this directory):
python -m unittest discover -s python/tests -vTo force explicit rebuilding in the numerical checks, use:
ONELOOP_PYTHON_REBUILD=1 python -m unittest discover -s python/tests -vFor a fresh shared-kernel host build and full suite in an isolated virtual
environment, run bash python/host/test.sh from the repository root. It builds
the host with Maturin and sets the shared-kernel module selection automatically.
This test mode passes explicit rebuild=True to numerical constructors and
direct calls. It does not change lazy startup or the core's enabled
features: import still loads embedded caches in the default build. Batch sizes
make the same tests applicable when SIMD is enabled, but do not assert that it
is enabled. Explicit-rebuild mode skips the two positive cold subprocesses;
portable-environment rejection is still checked at import in separate children.
Community mode skips the standalone subprocess tests entirely.
To test a prepared full community host, set
ONELOOP_PYTHON_MODULE=symbolica.community.hepkit.oneloop instead.
After the sequential subprocess checks, the remaining test harness explicitly
creates one persistent, adequately stacked test thread and does not require any
third-party Python testing dependency. The unchanged separate
tests/thread_handoff.py regression also passes: after the initializing worker
exits, a new worker constructs and drops all five family evaluators. The complete
API suite also passes with a fresh worker per test. The native allocator trace
and configuration repair are retained in tests/thread_handoff_backtrace.txt.
These results do not impose a lifetime restriction on the initializing thread;
individual Python Evaluator objects still use their documented creating-thread
policy. Test the new shared-kernel host using the module selection above.
To rerun the confirmed sequential-thread checks separately:
python -X faulthandler python/tests/thread_handoff.py
ONELOOP_PYTHON_THREAD_HANDOFF=1 python -X faulthandler -m unittest \
discover -s python/tests -p test_api.py -vThe separate standard-library survey defaults to 1024 rows per batch, 20,000 scalar points per repetition, seven repetitions, and a 256-point untimed scalar warmup per workload. It uses the same 31 named fixtures and explicit squared scale arguments as the Rust and original-Fortran surveys:
python python/performance_survey.py \
--batch 1024 --iterations 20000 --repetitions 7 \
--build-label release --output /tmp/oneloop-python.json \
--tsv /tmp/oneloop-python.tsvRun from the Rust crate root with the desired extension already installed.
--build-label is a caller-supplied description, not proof of compiler flags;
the report also records the module path and binary hash. Import and all numeric
work stay on the main thread. No source-rebuild fallback is used.
SAME repeats each fixture separately; HETERO cycles a family's fixtures in
file order. The report records exact point counts, batch counts and partial
tails, raw repetitions and medians, Python/CPU metadata, and fixture hashes.
All three coefficients are validated both before measurement and for every
measured output. Python input lists are built before timing. The timed interval
includes calls to Evaluator.evaluate_batch, adapter list-to-Rust conversions,
result allocation, and retention of each returned batch. Checksums, validation,
and final output destruction are outside timing. In contrast, the native Rust
and Fortran measurements include their finite-coefficient checksum accumulation
inside timing; reports explicitly preserve that difference.
Supply --fortran /tmp/oneloop-fortran.json for per-workload ratios with exact
matching fixture inputs, call counts, repetitions and warmup. Exit status 2 then
means at least one selected Python/Fortran median ratio exceeds
--maximum-ratio (default 1.5); it is not a correctness failure. Its four
fake-evaluator unit tests check counts, tails, output validation and report generation without
loading Symbolica:
python -m unittest discover -s python/tests -p test_performance_survey.py -vThe final survey used 4,097 points per repetition, seven repetitions, and 1,024-row batches: four complete batches plus a one-row tail. All three Laurent outputs passed validation for all 31 uniform-fixture workloads and five mixed family workloads. The measured build uses the system allocator, native binary64 Li2, regenerated strict-v3 assets, and the four-entry coefficient grouping cache, with warmed O2 evaluators. Each timed process was pinned to CPU 160 on a shared AMD EPYC 9754 Linux host; other users' work was not stopped.
The overall ≤1.5× Fortran target was not met: six of the 36 workloads met it and 30 missed it. All five mixed-family workloads missed that target. Ratios below compare median time per scalar point, with all three outputs:
| Mixed family | Python / original Fortran | Python / prepared Rust, same batch |
|---|---|---|
| A0 | 7.0092× | 5.4296× |
| B0 | 4.4102× | 2.5419× |
| dB0 | 5.3069× | 1.8572× |
| C0 | 3.5930× | 1.1003× |
| D0 | 10.1065× | 1.0387× |
The prepared-Rust column measures the same cached scalar evaluator route and batch size, not automatic symbolic callbacks or expression-map compilation. Small Python/Rust timing differences do not mean either is near the original Fortran speed, and shared-host variation limits fine-grained comparisons. All ratios use the final, matching Fortran baseline, not the earlier snapshot. Startup, explicit rebuilding, arbitrary precision, and an integrated community host are outside these measurements. The timing-boundary differences described above remain relevant; these are sampled throughput results, not universal guarantees. See the full report and retained evidence.
The minimal shared host exposes symbolica.community.hepkit.oneloop; the original
symbolica.community.oneloop import remains an alias with identical classes.
A full community host can call oneloop_native::register_hep_module on its
existing HEP module after Feynkit registration. See HEP integration
for data ownership, the single-kernel dependency requirement and initialization.
The minimal host does not include Feynkit's graph or momentum classes.
Positive epsilon coefficients are available as A0(1, m2, mu2) and
B0(1, p2, m02, m12, mu2) in the common 1/r_Gamma normalization.
The existing lowercase evaluators retain their three-coefficient output.