Skip to content

Numeric comparison

Configure tolerance strategies for scalar, array, and sample-point comparisons, and register them as a pipeline layer.

What it does — The numeric comparison layer lets you compare floating-point results using configurable tolerance strategies: absolute, relative, mixed, ULP, and stochastic. Array comparisons include top-K diagnostic reporting.
Use this when — You need to compare numerical results (not symbolic) where exact structural equality is too strict.
Prerequisites — Comparison pipeline for how layers work.
Outcome — Know how to configure tolerance strategies, create a numeric layer, register it in the pipeline, and interpret diagnostic results.

Tolerance strategies

All strategies implement the ToleranceStrategy protocol — a single method is_close(a, b, **kwargs) → bool. Choose the strategy that matches your quantity's error characteristics.

Strategy When to use Key parameter
AbsoluteTolerance Error is bounded by a fixed magnitude atol
RelativeTolerance Error scales with value magnitude rtol
MixedTolerance Both absolute and relative components atol + rtol
ULPTolerance Bit-level precision (floating-point round-off) n_ulps
StochasticTolerance Monte Carlo / noisy estimates with standard error k (sigma multiplier)

Absolute tolerance

Passes when |a - b| <= atol. Use for quantities with a fixed error budget.

from elegua.numeric.tolerance import AbsoluteTolerance

tol = AbsoluteTolerance(atol=1e-6)
assert tol.is_close(1.000001, 1.0)    # True  (difference 1e-6)
assert not tol.is_close(1.00001, 1.0)  # False (difference 1e-5)

Relative tolerance

Passes when |a - b| <= rtol * max(|a|, |b|, 1.0). The floor at 1.0 prevents division by near-zero. Use for quantities where error scales with magnitude.

from elegua.numeric.tolerance import RelativeTolerance

tol = RelativeTolerance(rtol=1e-4)
assert tol.is_close(1.0001, 1.0)      # True
assert tol.is_close(1000.1, 1000.0)   # True  (relative error 1e-4)

Mixed tolerance

Passes when |a - b| <= atol + rtol * max(|a|, |b|). Combines both scales — useful for values that range from near-zero to large.

from elegua.numeric.tolerance import MixedTolerance

tol = MixedTolerance(atol=1e-6, rtol=1e-3)
assert tol.is_close(0.0, 1e-7)        # True  (absolute difference 1e-7)

ULP tolerance

Passes when the integer representations of the two floats differ by at most n_ulps. This is the most precise comparison — it catches only floating-point round-off, not semantic differences.

from elegua.numeric.tolerance import ULPTolerance

tol = ULPTolerance(n_ulps=2)
assert tol.is_close(1.0, 1.0 + 2**-52)  # True  (1 ULP apart)

Stochastic tolerance

Passes when |a - b| <= k * sqrt(se_a² + se_b²), where se is the standard error of each estimate. Use for Monte Carlo or noisy results where each value has an associated uncertainty.

from elegua.numeric.tolerance import StochasticTolerance

tol = StochasticTolerance(k=3.0)  # 3-sigma threshold
# The standard_error values come from the token result payload
assert tol.is_close(1.0, 1.05, se_a=0.012, se_b=0.012)
# => True: |1.0 - 1.05| = 0.05 <= 3.0 * sqrt(0.012² + 0.012²) ≈ 0.051

Scalar comparison

Direct comparison with a fixed strategy

from elegua.numeric.tolerance import AbsoluteTolerance, make_scalar_comparator
from elegua.models import ValidationToken
from elegua.task import TaskStatus

compare = make_scalar_comparator(strategy=AbsoluteTolerance(atol=1e-6))

token_a = ValidationToken(
    adapter_id="oracle",
    status=TaskStatus.OK,
    result={"value": 1.000001},
)
token_b = ValidationToken(
    adapter_id="iut",
    status=TaskStatus.OK,
    result={"value": 1.0},
)

status = compare(token_a, token_b)
assert status == TaskStatus.OK  # within 1e-6 tolerance

Profile-based resolution by quantity label

Use a ToleranceProfile to apply different strategies to different quantities, resolved by the quantity_label field in the token result:

from elegua.numeric.tolerance import (
    AbsoluteTolerance,
    RelativeTolerance,
    ToleranceProfile,
    make_scalar_comparator,
)

profile = ToleranceProfile()
profile.register("energy", AbsoluteTolerance(atol=1e-8))
profile.register("rate", RelativeTolerance(rtol=1e-3))

compare = make_scalar_comparator(profile=profile)

token_a = ValidationToken(
    adapter_id="oracle",
    status=TaskStatus.OK,
    result={"value": 1.0, "quantity_label": "energy"},
)
token_b = ValidationToken(
    adapter_id="iut",
    status=TaskStatus.OK,
    result={"value": 1.00000001, "quantity_label": "energy"},
)

assert compare(token_a, token_b) == TaskStatus.OK  # uses AbsoluteTolerance(atol=1e-8)

Array comparison with top-K diagnostics

Compare two arrays elementwise and report the worst disagreements:

from elegua.numeric.tolerance import AbsoluteTolerance
from elegua.numeric.array import make_array_comparator
from elegua.models import ValidationToken
from elegua.task import TaskStatus

comparator = make_array_comparator(
    AbsoluteTolerance(atol=1e-6),
    k=5,                           # report top 5 disagreements
    return_diagnostics=True,
)

token_a = ValidationToken(
    adapter_id="oracle",
    status=TaskStatus.OK,
    result={"values": [1.0, 2.0, 3.0, 4.0, 5.0]},
)
token_b = ValidationToken(
    adapter_id="iut",
    status=TaskStatus.OK,
    result={"values": [1.0, 2.05, 3.0, 4.1, 5.0]},
)

result = comparator(token_a, token_b)
print(result.status)              # TaskStatus.MATH_MISMATCH
print(result.max_disagreement)    # 0.1 (at index 3)
print(result.argmax_location)     # 3
print(result.failing_count)       # 2
for entry in result.top_k_disagreements:
    print(f"  index {entry.index}: expected {entry.expected}, got {entry.actual}")
# => index 3: expected 4.0, got 4.1
# => index 1: expected 2.0, got 2.05

Sample-point comparison (L4)

The sample-point comparator checks that two adapters produce matching values at shared evaluation points. This is the layer 4 comparison in the pipeline — it catches equivalent-but-different expressions by evaluating them numerically.

from elegua.compare_numeric import make_numeric_comparator

# toler = 1e-6, min_samples = 1
compare = make_numeric_comparator(tol=1e-6, min_samples=1)

token_a = ValidationToken(
    adapter_id="oracle",
    status=TaskStatus.OK,
    result={
        "numeric_samples": [
            {"vars": {"x": 0.0}, "value": 1.0},
            {"vars": {"x": 1.0}, "value": 2.0},
        ]
    },
)
token_b = ValidationToken(
    adapter_id="iut",
    status=TaskStatus.OK,
    result={
        "numeric_samples": [
            {"vars": {"x": 0.0}, "value": 1.000001},
            {"vars": {"x": 1.0}, "value": 2.0},
        ]
    },
)

assert compare(token_a, token_b) == TaskStatus.OK  # within 1e-6 tolerance

Registering the numeric layer in the pipeline

The make_numeric_layer factory creates a single LayerFn that routes by payload type. Register it at layer 3 (after L1 identity and L2 structural) with exclude_keys=NUMERIC_KEYS so that L1/L2 do not compare numeric fields:

from elegua.comparison import ComparisonPipeline
from elegua.numeric.layer import make_numeric_layer, NUMERIC_KEYS
from elegua.numeric.tolerance import MixedTolerance

pipeline = ComparisonPipeline()  # includes L1 + L2
pipeline.register(
    3, "numeric",
    make_numeric_layer(
        strategy=MixedTolerance(atol=1e-6, rtol=1e-3),
        array_strategy=MixedTolerance(atol=1e-6, rtol=1e-3),
        return_diagnostics=True,
    ),
    exclude_keys=NUMERIC_KEYS,
)

The layer routes by payload key: - value → scalar comparator - values → array comparator - numeric_samples → sample-point comparator - No numeric fields → MATH_MISMATCH (non-opt-in fixtures pass through to L1/L2)

Profile-based pipeline

For pipelines that need different tolerances per quantity:

from elegua.numeric.tolerance import ToleranceProfile, AbsoluteTolerance, RelativeTolerance

profile = ToleranceProfile()
profile.register("energy", AbsoluteTolerance(atol=1e-8))
profile.register("rate", RelativeTolerance(rtol=1e-3))

pipeline.register(
    3, "numeric",
    make_numeric_layer(profile=profile),
    exclude_keys=NUMERIC_KEYS,
)

Summary

Need Use
Simple fixed tolerance AbsoluteTolerance(atol=1e-6)
Error scales with magnitude RelativeTolerance(rtol=1e-3)
Both MixedTolerance(atol=1e-6, rtol=1e-3)
Bit-level precision ULPTolerance(n_ulps=2)
Noisy / Monte Carlo results StochasticTolerance(k=3.0)
Different tolerances per quantity ToleranceProfile with make_numeric_layer(profile=...)
Array comparison make_array_comparator(tolerance, k=5, return_diagnostics=True)
Sample-point L4 check make_numeric_comparator(tol=1e-6, min_samples=1)