For developers
Symbolica evaluation-domain bug and fix
Historical thermal callback-dispatch fix for GammaLoop and Spenso numeric wrappers, including precision and original validation.
This investigation records the callback-dispatch fix “fix symbolica callback resolution for numeric wrappers”, authored on 2026-09-09. Its validation ran before the feature branch was rebased onto Symbolica 3, so the test results are historical. The registry records that original validation revision, which is outside the current branch history. The subject and callback forwarding described below identify the fix independently of hashes rewritten by rebasing.
After replacing exponential workarounds with Symbolica’s native hyperbolic functions, GammaLoop’s interpreted evaluator could panic during thermal CFF evaluation, including thermal_bugblatter:
External function 'symbolica_coth' does not have an implementation for gammalooprs::utils::F<f64>Symbolica resolves these functions through numeric callbacks registered for an exact Rust type. A callback for f64 therefore does not automatically apply to GammaLoop’s F<f64>. The wrappers implemented constant conversion through try_from_complex_float, but lacked callback forwarding through EvaluationDomain::resolve_function. Implementing numeric methods such as tanh on a wrapper was insufficient for this dispatch path. The panic came from missing callback forwarding in GammaLoop and its bundled Spenso wrappers.
The fix adds callback resolution to the existing domain implementations:
F<T>: Unwrap arguments, resolve throughT, and wrap the result.QuadFloat: Delegate through Symbolica’sDoubleFloatdomain.VarFloat<N>: Convert through Symbolica’s arbitrary-precisionFloat.- Spenso
Complex<T>: Prefer the corresponding Symbolica complex callback; otherwise evaluate throughComplex<Float>.
Callbacks registered directly for a wrapper retain priority. Symbolic tags and all numeric arguments are forwarded, and higher-precision values avoid an intermediate conversion through f64. Complex callbacks evaluate the complete complex value. The Spenso implementation adds an Into<Float> bound, satisfied by GammaLoop’s existing numeric wrappers. This fix uses the pinned Symbolica API.
GammaLoop’s eager complex domain is Complex<F<f64>>, which does not match Symbolica’s native Complex<f64> callback registrations. Built-in external functions such as the hyperbolic functions therefore use the Complex<Float> fallback: each call allocates argument storage, converts both components to 53-bit MPFR values, evaluates in MPFR, and converts back. Higher-precision wrappers use this fallback at their own precision. This cost affects eager complex evaluation; SymJIT resolves native callbacks, while C++ and assembly use generated code. Real F<f64> callbacks already forward to native f64.
The fallback preserves wrapper-specific callback priority and numeric range. A 53-bit mantissa alone does not justify conversion to f64: arbitrary-precision types can have the same mantissa precision and a wider exponent range. A native fast path would need an explicit conversion capability that preserves both.
Eight new regression tests cover native tanh, coth, sech, and csch evaluation, complex values, precision beyond f64, callback precedence, tags, multiple arguments, and unresolved functions. Together with nine thermal CFF tests, all 17 tests passed with retries disabled. Formatting, cargo check, and Clippy also passed.
Testing exposed an intermittent thermal_ring relative error of approximately 2.02e-11, just above its previous 2e-11 limit. With user approval, that tolerance was increased to 3e-11. Cancellation and varying evaluation order are plausible contributors; the underlying numerical sensitivity remains to be investigated.
The separate Symbolica C++ export/header issue was outside this runtime fix. Switching compilation to symjit does not supply callbacks for GammaLoop’s interpreted numeric domains.
The targeted test run was:
cargo nextest run -p gammalooprs -p spenso --lib \
--cargo-profile dev-optim --locked --offline -P test_gammaloop \
--retries 0 \
-E 'test(evaluation_domain) or (test(cff::generation::tests_cff::thermal) and not test(failing))'