from dataclasses import dataclass import math import numpy as np VALIDATION_POLICY = "pointwise-and-relative-l2-v1" @dataclass(frozen=True) class OutputComparison: passed: bool max_abs_diff: float relative_l2_error: float | None pointwise_passed: bool def compare_output_arrays(actual, expected, *, atol, rtol): """Require pointwise tolerance and relative L2 tolerance for nonzero outputs.""" if any(not math.isfinite(value) or value < 0 for value in (atol, rtol)): raise ValueError("output tolerances must be finite and non-negative") actual, expected = np.asarray(actual), np.asarray(expected) if actual.dtype.kind not in "biuf" or expected.dtype.kind not in "biuf": raise ValueError("output comparison requires real numeric arrays") if actual.shape != expected.shape: raise ValueError(f"output shapes differ: {actual.shape} != {expected.shape}") if not actual.size: return OutputComparison(True, 0.0, None, True) if not np.all(np.isfinite(actual)) or not np.all(np.isfinite(expected)): return OutputComparison(False, math.inf, None, False) if expected.dtype.kind in "biu": difference = np.abs(actual.astype(object) - expected.astype(object)) passed = bool(np.array_equal(actual, expected)) return OutputComparison(passed, float(difference.max()), None, passed) actual, expected = actual.astype(np.float64), expected.astype(np.float64) with np.errstate(over="ignore", invalid="ignore", divide="ignore"): difference = np.abs(actual - expected) pointwise = bool(np.all(difference <= atol + rtol * np.abs(expected))) scale = float(np.max(np.abs(expected))) relative = (float(np.linalg.norm(difference / scale) / np.linalg.norm(expected / scale)) if scale else None) # A zero reference has no relative scale. rtol=0 explicitly requests absolute-only checks. relative_passed = relative is None or rtol == 0 or relative <= rtol return OutputComparison(pointwise and relative_passed, float(difference.max()), relative, pointwise)