From a86529b03209e5b3f908e6f7efa9595a259ff373 Mon Sep 17 00:00:00 2001 From: Mingli Yuan Date: Thu, 27 Aug 2026 19:44:33 +0800 Subject: [PATCH] research: split native AMP evaluation from matrix transport --- sonnet/README.md | 6 +- .../00-problem-frontier.md | 36 ++- .../02-benchmark-results.md | 75 ++++-- .../03-disposition.md | 24 +- .../04-native-process-evaluator.md | 133 +++++++++++ .../amp-polynomial-matrix-compiler/README.md | 14 +- .../amp_escape_compiler.py | 221 +++++++++++++++++- .../test_amp_polynomial_matrix_compiler.py | 122 ++++++++++ 8 files changed, 588 insertions(+), 43 deletions(-) create mode 100644 sonnet/amp-polynomial-matrix-compiler/04-native-process-evaluator.md diff --git a/sonnet/README.md b/sonnet/README.md index 8f0f3fc4..0afe0a37 100644 --- a/sonnet/README.md +++ b/sonnet/README.md @@ -196,7 +196,11 @@ basis turns composition into an exact sparse nilpotent matrix and the long-horizon coordinate into a triangular linear solve. It eliminates exponential symbolic support and supplies exact residual certificates, while a strong logarithmic recurrence prevents any claim of universal numerical -speedup. The result is `EXPAND-NARROW` and remains Sonnet-local. +speedup. Its second phase separates an exact inverse-state AMP evaluator from +the coefficient compiler: scalar numerical limits now use a constant-width +process recurrence with an analytic tail bound, while sparse matrices remain +offline coefficient/replay certificates. The result is `EXPAND-NARROW` and +remains Sonnet-local. ## Research-local calibration — the \(S^6\) complex structure claim diff --git a/sonnet/amp-polynomial-matrix-compiler/00-problem-frontier.md b/sonnet/amp-polynomial-matrix-compiler/00-problem-frontier.md index fbf8f931..b3948002 100644 --- a/sonnet/amp-polynomial-matrix-compiler/00-problem-frontier.md +++ b/sonnet/amp-polynomial-matrix-compiler/00-problem-frontier.md @@ -1,7 +1,8 @@ # Problem frontier: do AMP polynomials and matrices simplify an algorithm? -Status: frozen contract for issue -[#152](https://github.com/mountain/process-geometry/issues/152). +Status: frozen contract for issues +[#152](https://github.com/mountain/process-geometry/issues/152) and +[#154](https://github.com/mountain/process-geometry/issues/154). ## 1. The question @@ -43,27 +44,39 @@ F(y)=dy+\log(1+tq^d), g(q)=e^{-F(y)}=\frac{q^d}{1+tq^d}. \] -The main observer is the normalized long-horizon quantity +The scalar readout is the normalized long-horizon quantity \[ G_N(y)=d^{-N}F^{\circ N}(y) \] -and its limit when the asymptotic coordinate converges. This is deliberately +and its limit when the escape coordinate converges. This is deliberately narrower than reconstructing the full iterate or orbit. +Two computational tasks must not be conflated: + +1. **numerical limit:** evaluate one scalar escape coordinate to a requested + analytic-tail tolerance; +2. **coefficient readout:** recover a finite polynomial-like coordinate and an + exact transport/residual certificate for reuse or symbolic inspection. + +The first task does not require coefficients. The second task does. + ## 3. Same-information baselines -Three paths receive separate ledgers. +Four paths receive separate ledgers. 1. **Expanded symbolic baseline:** form `f^[N](x)` as one ordinary expanded polynomial. 2. **Strong numerical baseline:** update `F(y)` directly in the logarithmic chart, accumulate the normalized correction, and stop when floating-point correction is zero. It is forbidden to expand the polynomial. -3. **AMP compiler:** compile a finite polynomial-like escape coordinate once +3. **Native AMP evaluator:** update the inverse state `q` directly, accumulate + the exact process-level decomposition, and stop from an analytic tail bound. + It may not call the series or matrix compiler. +4. **AMP compiler:** compile a finite polynomial-like escape coordinate once from a sparse substitution matrix, then evaluate it for the declared - observer. + readout. Its online phase uses the degree-ray variable `z=q^d`. The expanded baseline measures symbolic support only. It may not be used as the sole numerical competitor. @@ -74,8 +87,9 @@ the sole numerical competitor. - observer order and nonzero polynomial-like terms; - dense versus sparse matrix entries; - exact compilation and residual certificate; +- native process levels, state width, primitive evaluations, and tail bound; - strong-baseline executed steps; -- compile-once/evaluate-many online work; +- compile-once/evaluate-many Horner work; - numerical error across observer orders; - chart failure outside the asymptotic domain; - decoder and output scope. @@ -100,13 +114,17 @@ Narrow or stop a claim if: - one scalar long-time observer is presented as full-orbit reconstruction; - a classical Böttcher/Koopman result is claimed as new. +Here “Koopman observable” means a function on state used by the classical +composition operator. It is not identified with a Process Geometry observer, +which also carries task, information, chart, and certification semantics. + ## 6. Claim ceiling This phase does not claim a new Böttcher theorem, generic Koopman solver, complexity-class improvement, Ising solver, or Public API. ```text -Epistemic maturity: T1 exact finite compiler + bounded numerical calibration +Epistemic maturity: T1 exact finite compiler + certified native calibration Engineering status: Sonnet-local Python Mathematical Core: unchanged ``` diff --git a/sonnet/amp-polynomial-matrix-compiler/02-benchmark-results.md b/sonnet/amp-polynomial-matrix-compiler/02-benchmark-results.md index a617f258..cfbfc53d 100644 --- a/sonnet/amp-polynomial-matrix-compiler/02-benchmark-results.md +++ b/sonnet/amp-polynomial-matrix-compiler/02-benchmark-results.md @@ -1,7 +1,7 @@ # Benchmark results -Status: exact support/certificate measurements plus bounded floating-point -calibration. +Status: exact support/certificate measurements plus certified native and +bounded compiled floating-point calibration. ## 1. Frozen instance @@ -27,6 +27,15 @@ It detects that the interaction correction has underflowed to zero after nine executed steps in binary64 arithmetic. This early stop is retained as a positive baseline result. +The native inverse-state evaluator reaches the same binary64 value after four +process levels and reports the analytic tail bound + +\[ +4.03\times10^{-23}<10^{-15}. +\] + +This is a truncation-tail statement, not a total floating-point error bound. + ## 2. Symbolic support For positive `t`, the fully expanded iterate has @@ -49,7 +58,26 @@ polynomial; its observer state remains `K` ray coefficients plus the affine This is a real symbolic and storage simplification. It does not imply the same factor against direct numerical recurrence. -## 3. Sparse compilation and accuracy +## 3. Native process calibration + +The native evaluator uses one initial exponential, one `log1p` and one inverse +state update per retained process level, four persistent working scalars, and +no series or matrix construction. It fails closed if binary64 inverse-state +or tail arithmetic underflows before a trustworthy bound can be reported. + +| Initial `y` | Process levels | Reported analytic tail bound | Binary64 value | +|---:|---:|---:|---:| +| 0.0 | 6 | `3.54e-25` | `0.4073545227394800` | +| 0.5 | 5 | `5.53e-21` | `0.6746578808175746` | +| 1.0 | 4 | `9.27e-17` | `1.0670158740022506` | +| 1.5 | 4 | `4.03e-23` | `1.5248559772600594` | +| 2.0 | 4 | `7.48e-30` | `2.0091558398904660` | + +The point `y=0`, where the finite asymptotic series becomes unstable, remains +well behaved for the exact process recurrence. This is a domain separation, +not evidence that every AMP chart is global. + +## 4. Sparse compilation and accuracy | Order `K` | Dense entries | Sparse entries | Nonzero `h_k` | First residual | Absolute error | |---:|---:|---:|---:|---:|---:| @@ -62,7 +90,20 @@ same factor against direct numerical recurrence. All coefficients, sparse entries, and residuals are exact rationals. Only the final evaluation/error comparison uses floating point. -## 4. Strong-baseline red team +## 5. Same-accuracy and strong-baseline red team + +At `y=1.5`, choosing the smallest tested compiled order that meets each target +gives the following structural online comparison. `K/2` is the number of +degree-ray Horner slots; the native column is the number of `log1p` process +levels. + +| Target | Native levels | Compiled `K` | Compiled error | Horner slots | Sparse compile entries | +|---:|---:|---:|---:|---:|---:| +| `1e-6` | 3 | 8 | `2.60e-7` | 4 | 10 | +| `1e-8` | 3 | 12 | `3.73e-10` | 6 | 21 | +| `1e-10` | 3 | 14 | `4.82e-11` | 7 | 28 | +| `1e-12` | 4 | 18 | `7.17e-13` | 9 | 45 | +| `1e-15` | 4 | 22 | `6.66e-16` | 11 | 66 | For 100 queries at `K=20`: @@ -71,16 +112,20 @@ compile once: 55 sparse entries 20 triangular divisions online compiled proxy: - 9 nonzero series terms per query + 10 degree-ray Horner slots (9 nonzero coefficients) per query +native process: + 4 log1p levels per query with a 4.03e-23 analytic tail strong recurrence: 9 executed correction steps per query in binary64 ``` -The online structural counts are comparable. Compilation overhead means the -AMP path does **not** earn a universal single-query floating-point speedup. -At `K=10`, four nonzero terms give about `1.5e-8` absolute error and can be an -economical batch approximation, but the tradeoff depends on tolerance, -initial chart, numeric backend, and number of queries. +The compiled Horner path may be faster after enough repeated same-chart +queries, because its online operations are simple multiply-adds. The native +path has no coefficient build, accepts parameter variation directly, retains +constant-width state, and supplies a stopping certificate. Therefore neither +path earns a universal runtime win: the crossover depends on tolerance, +initial chart, backend, validation policy, and reuse count. Wall-clock timing +is deliberately not a CI assertion. The main earned advantages are instead: @@ -90,7 +135,7 @@ The main earned advantages are instead: - a reusable compile-once coordinate for many states or parameter sweeps; - exposing the support geometry and failure boundary. -## 5. Negative chart control +## 6. Negative chart control At `y_0=0`, the asymptotic series is outside its safe region. The errors are @@ -102,9 +147,10 @@ K=20: more than 4.0 Increasing observer order makes the answer worse. A finite AMP truncation is therefore not a globally convergent numerical method. The compiler must carry a chart/domain certificate or use residual-driven adaptation; order -alone is not safety. +alone is not safety. The native process path is the default for this scalar +numerical task in its certified `y>=0, t>0` domain. -## 6. Replay +## 7. Replay The executable tests independently verify: @@ -113,6 +159,9 @@ The executable tests independently verify: - the exact eigenrelation and first omitted residual; - expanded support counts against explicit small iterates; - convergence against the strong logarithmic recurrence; +- native evaluation without coefficient or matrix compiler calls; +- analytic-tail, domain, process-budget, and primitive-cost reporting; +- degree-ray Horner replay with a generic-support fallback; - sparse/dense cost separation; - failure outside the asymptotic chart; - typed refusal for invalid or cancellation-prone frozen tasks. diff --git a/sonnet/amp-polynomial-matrix-compiler/03-disposition.md b/sonnet/amp-polynomial-matrix-compiler/03-disposition.md index 6fea4614..7dd98bf6 100644 --- a/sonnet/amp-polynomial-matrix-compiler/03-disposition.md +++ b/sonnet/amp-polynomial-matrix-compiler/03-disposition.md @@ -1,6 +1,6 @@ # Disposition: selective algorithmic simplification -Status: issue #152 result. +Status: issues #152 and #154 result. ## 1. Verdict by layer @@ -9,8 +9,9 @@ Status: issue #152 result. | AMP polynomial-like basis | **EXPAND** | compresses degree-`d^N` expanded support into a fixed observer ray and exposes the correct asymptotic coordinate | | AMP matrix-like transport | **EXPAND** | composition becomes an exact sparse nilpotent matrix and the conjugacy becomes a triangular linear solve | | Exact certificate/replay | **EXPAND** | rational coefficients, finite eigenrelation, and first omitted residual replay independently | -| Single-query floating-point speed | **NARROW** | strong logarithmic recurrence stops early and can equal or beat compiled evaluation | -| Global numerical method | **STOP outside chart** | higher truncation can diverge near the non-asymptotic region | +| Native inverse-state evaluator | **EXPAND** | evaluates the scalar limit without Taylor coefficients or matrices and returns a tail/cost ledger | +| Compiled repeated-query speed | **NARROW** | degree-ray Horner can reduce online arithmetic, but compilation and crossover remain task dependent | +| Finite series outside chart | **STOP** | higher truncation can diverge near the non-asymptotic region | | Generic interacting dynamics | **OPEN** | one power-dominant scalar family is not a general AMP solver | The overall disposition is @@ -19,12 +20,12 @@ The overall disposition is \boxed{\texttt{EXPAND-NARROW}}. \] -Both proposed layers earned real algorithmic roles, but only for specified -observers and charts. +The native and compiled layers earn different algorithmic roles. Neither is +promoted as a generic AMP runtime. ## 2. What was actually simplified -The representation changes the algorithmic structure: +For coefficient readout, the representation changes the algorithmic structure: \[ \text{nonlinear repeated state map} @@ -48,6 +49,17 @@ the result alone: This is the first exact example in the AMP line where the two sides form one algorithm rather than two analogies. +For scalar numerical evaluation, however, linearization is unnecessary: + +\[ +q_{n+1}=\frac{q_n^d}{1+tq_n^d},\qquad +H(y)=y+\sum_{n\ge0}d^{-n-1}\log(1+tq_n^d). +\] + +This native recurrence is now the default path. The matrix is an offline +compiler/certificate for a different task, not the ontology of the nonlinear +process and not a required runtime intermediate. + ## 3. What remains classical and what is programme-specific The Böttcher conjugacy, escape-rate function, and Koopman composition operator diff --git a/sonnet/amp-polynomial-matrix-compiler/04-native-process-evaluator.md b/sonnet/amp-polynomial-matrix-compiler/04-native-process-evaluator.md new file mode 100644 index 00000000..5017867a --- /dev/null +++ b/sonnet/amp-polynomial-matrix-compiler/04-native-process-evaluator.md @@ -0,0 +1,133 @@ +# Native AMP process evaluator + +Status: executable T1 calibration for issue +[#154](https://github.com/mountain/process-geometry/issues/154). + +## 1. Why split the evaluator from the compiler? + +The first compiler solved a coefficient problem: find a finite +polynomial-like escape coordinate and replay its conjugacy relation exactly. +That legitimately uses a sparse linear action on coefficients. + +The numerical question is smaller: evaluate one scalar escape limit. Forcing +that question through a coefficient basis introduces Taylor order, chart +truncation, and a matrix that the original nonlinear process does not require. +The software now keeps these tasks separate. + +```text +scalar numerical limit -> native inverse-state recurrence +fixed-chart repeated readout -> polynomial-like degree-ray Horner evaluator +exact coefficient witness -> sparse matrix-like offline compiler/replay +``` + +This is a split/refinement of the Sonnet, not removal of the matrix result. + +## 2. Exact process identity + +For + +\[ +F(y)=dy+\log(1+t e^{-dy}), +\qquad q_n=e^{-F^{\circ n}(y)}, +\] + +the inverse state evolves exactly by + +\[ +q_0=e^{-y}, +\qquad +q_{n+1}=\frac{q_n^d}{1+tq_n^d}. +\] + +Unfolding the normalized recurrence, without expanding a series, gives + +\[ +d^{-N}F^{\circ N}(y) +=y+\sum_{n=0}^{N-1}d^{-n-1}\log(1+tq_n^d). +\] + +The native evaluator accumulates this identity directly. It never calls the +coefficient source, substitution matrix, or triangular compiler. + +## 3. First certified domain and tail + +Freeze + +\[ +d\ge2,\qquad t>0,\qquad y\ge0. +\] + +Then `0 <= q_0 <= 1` and + +\[ +0\le q_{n+1}\le q_n^d\le q_n. +\] + +After retaining `R` process levels, use +`log(1+u) <= u` and monotonicity of `q_n`: + +\[ +\begin{aligned} +0\le T_R +&=\sum_{n=R}^{\infty}d^{-n-1}\log(1+tq_n^d)\\ +&\le tq_R^d\sum_{n=R}^{\infty}d^{-n-1}\\ +&=\frac{d^{-R}tq_R^d}{d-1}. +\end{aligned} +\] + +The implementation stops only when this a posteriori analytic tail, evaluated +on the represented inverse-state recurrence, is at most the requested +tolerance. It rejects an invalid chart, exhausted level budget, or binary64 +underflow with typed errors. The certificate intentionally excludes roundoff +and is not advertised as an interval enclosure of the total error. + +## 4. Cost regimes + +The implementation retains four persistent working scalars (`q`, cached +`q^d`, accumulated value, and inverse degree weight), one initial exponential, +and per level one `log1p`, one inverse-state update, and degree powers. Storage +is `O(1)`. + +For fixed positive `y`, `q_R <= exp(-d^R y)`, so the required process depth has +the double-logarithmic scale + +\[ +R=O(\log\log(1/\varepsilon)) +\] + +for fixed `d,t`; this is a domain-specific upper-scale statement, not a generic +AMP complexity theorem. + +The compiled path has different economics. Its current exact compiler stores +a sparse triangular action and costs quadratically in observer order in the +worst case of this implementation. Once compiled, support lies on +`q^(dj)`, so evaluation uses Horner in + +\[ +z=q^d=e^{-dy} +\] + +with `floor(K/d)` coefficient slots and no matrix at runtime. It can win on a +large batch with the same degree, interaction, chart, and tolerance. It loses +its premise when parameters vary or the chart fails. Compilation amortization +is therefore reported separately rather than hidden in an online timing. + +## 5. Architectural consequence + +The classical Koopman composition operator remains useful for compiling a +function-space action. It is not treated as a claim that the nonlinear AMP +process is fundamentally a linear observer. In Process Geometry terminology, +an observer also specifies task-visible information, chart, decoder, and +certificate; it is not synonymous with a scalar Koopman observable. + +The resulting boundary is deliberately local: + +```text +Mathematical Core: unchanged +Research Programme: split numerical evaluation from coefficient compilation +Engineering Architecture: Sonnet-local dual path +Theory Map: no promoted node +Experimental/Public API: none +``` + +Transfer to mixed-log or coupled systems remains the next evidence gate. diff --git a/sonnet/amp-polynomial-matrix-compiler/README.md b/sonnet/amp-polynomial-matrix-compiler/README.md index fe16a738..ed585a27 100644 --- a/sonnet/amp-polynomial-matrix-compiler/README.md +++ b/sonnet/amp-polynomial-matrix-compiler/README.md @@ -1,7 +1,8 @@ # AMP polynomial/matrix compiler -This research-local Sonnet answers issue -[#152](https://github.com/mountain/process-geometry/issues/152). +This research-local Sonnet answers issues +[#152](https://github.com/mountain/process-geometry/issues/152) and +[#154](https://github.com/mountain/process-geometry/issues/154). Read in order: @@ -13,6 +14,8 @@ Read in order: sparse cost, numerical error, and the strong-baseline red team. 4. [`03-disposition.md`](03-disposition.md) states where algorithmic simplification was and was not earned. +5. [`04-native-process-evaluator.md`](04-native-process-evaluator.md) separates + direct AMP process evaluation from coefficient and matrix compilation. The executable certificate is [`amp_escape_compiler.py`](amp_escape_compiler.py); its independent tests are @@ -21,9 +24,10 @@ The executable certificate is Current result: ```text -polynomial-like basis: EXPAND for symbolic support and observer coordinates -matrix-like transport: EXPAND for exact sparse compilation and replay -generic numerical acceleration: NARROW / task and tolerance dependent +native inverse-state process: EXPAND as the default scalar numerical path +polynomial-like basis: EXPAND for fixed-chart coefficient/readout compilation +matrix-like transport: EXPAND for offline exact compilation and replay +compiled numerical acceleration: NARROW / task, tolerance, and reuse dependent overall issue disposition: EXPAND-NARROW ``` diff --git a/sonnet/amp-polynomial-matrix-compiler/amp_escape_compiler.py b/sonnet/amp-polynomial-matrix-compiler/amp_escape_compiler.py index 25864552..9b786af8 100644 --- a/sonnet/amp-polynomial-matrix-compiler/amp_escape_compiler.py +++ b/sonnet/amp-polynomial-matrix-compiler/amp_escape_compiler.py @@ -14,16 +14,20 @@ is obtained from the *linear* equation ``(d I - C) h = u``. This is the research-local matrix-like realization of an AMP polynomial-like chart. -The implementation deliberately uses exact ``Fraction`` arithmetic and keeps -symbolic expansion, strong numerical recurrence, and compiled evaluation as -separate cost baselines. It is not a public Koopman or Bottcher API. +The implementation deliberately keeps two AMP paths separate. The native +inverse-state process evaluates the scalar escape limit without coefficients +or matrices. The exact ``Fraction`` compiler constructs a fixed-chart +polynomial-like readout and sparse replay certificate. Symbolic expansion and +strong numerical recurrence remain independent cost baselines. This is not a +public Koopman or Bottcher API. """ from __future__ import annotations from dataclasses import asdict, dataclass from fractions import Fraction -from math import comb, exp, log1p +from functools import cached_property +from math import comb, exp, isfinite, log1p def _fraction(value: int | Fraction) -> Fraction: @@ -157,14 +161,43 @@ class EscapeCoordinate: def nonzero_term_count(self) -> int: return sum(value != 0 for value in self.coefficients) + @cached_property + def _degree_ray_coefficients(self) -> tuple[Fraction, ...] | None: + if not all( + degree % self.degree == 0 or coefficient == 0 + for degree, coefficient in enumerate(self.coefficients, start=1) + ): + return None + return self.coefficients[self.degree - 1 :: self.degree] + + @property + def uses_degree_ray_horner(self) -> bool: + """Whether support is confined to ``q**(degree*j)``.""" + + return self._degree_ray_coefficients is not None + + @property + def horner_step_count(self) -> int: + """Number of coefficient slots visited by :meth:`evaluate`.""" + + if self._degree_ray_coefficients is not None: + return len(self._degree_ray_coefficients) + return len(self.coefficients) + def evaluate(self, log_state: float) -> float: + ray_coefficients = self._degree_ray_coefficients + if ray_coefficients is not None: + z = exp(-self.degree * log_state) + correction = 0.0 + for coefficient in reversed(ray_coefficients): + correction = correction * z + float(coefficient) + return log_state + z * correction + q = exp(-log_state) - power = q correction = 0.0 - for coefficient in self.coefficients: - correction += float(coefficient) * power - power *= q - return log_state + correction + for coefficient in reversed(self.coefficients): + correction = correction * q + float(coefficient) + return log_state + q * correction @dataclass(frozen=True) @@ -287,6 +320,157 @@ class DirectIterationResult: executed_steps: int +class NativeProcessDomainError(ValueError): + """The certified native evaluator does not cover the requested task.""" + + +class NativeProcessBudgetError(RuntimeError): + """The native evaluator exhausted its process-level budget.""" + + def __init__(self, max_levels: int, tail_bound: float) -> None: + self.max_levels = max_levels + self.tail_bound = tail_bound + super().__init__( + "native AMP evaluation exhausted " + f"{max_levels} process levels with tail bound {tail_bound!r}" + ) + + +@dataclass(frozen=True) +class NativeProcessCost: + """Per-evaluation primitive ledger, excluding validation.""" + + process_levels: int + initial_exponential_evaluations: int + degree_power_evaluations: int + log1p_evaluations: int + inverse_state_updates: int + state_scalars: int + + +@dataclass(frozen=True) +class NativeEscapeEvaluation: + """Certified truncation of the exact inverse-state process.""" + + degree: int + interaction: Fraction + initial_log_state: float + tolerance: float + value: float + tail_bound: float + final_inverse_state: float + cost: NativeProcessCost + + @property + def certifies_tail_tolerance(self) -> bool: + """The analytic truncation tail, not roundoff, meets the request.""" + + return self.tail_bound <= self.tolerance + + +def evaluate_escape_process( + degree: int, + interaction: int | Fraction, + initial_log_state: float, + *, + tolerance: float = 1e-15, + max_levels: int = 64, +) -> NativeEscapeEvaluation: + r"""Evaluate the escape coordinate by the native AMP recurrence. + + With ``q_0=exp(-y)`` and + + ``q_(n+1) = q_n**d / (1+t*q_n**d)``, + + the exact coordinate is + + ``y + sum(d**(-n-1) * log1p(t*q_n**d), n >= 0)``. + + The first certified domain is ``y >= 0`` and ``t > 0``. There + ``0 <= q_(n+1) <= q_n <= 1``, so after ``R`` retained levels the + remaining analytic tail is at most + + ``d**(-R) * t*q_R**d / (d-1)``. + + No polynomial coefficients, Taylor series, or substitution matrix are + constructed. The returned bound excludes floating-point roundoff. + """ + + if degree < 2: + raise NativeProcessDomainError("degree must be at least two") + t_exact = _fraction(interaction) + try: + t = float(t_exact) + except OverflowError as error: + raise NativeProcessDomainError( + "interaction is outside the binary64 evaluator range" + ) from error + if t_exact <= 0 or not isfinite(t): + raise NativeProcessDomainError( + "the certified native chart requires a finite positive interaction" + ) + y = float(initial_log_state) + if not isfinite(y) or y < 0: + raise NativeProcessDomainError( + "the first certified native chart requires finite y >= 0" + ) + tolerance = float(tolerance) + if not isfinite(tolerance) or tolerance <= 0: + raise NativeProcessDomainError("tolerance must be finite and positive") + if max_levels < 1: + raise NativeProcessDomainError("max_levels must be positive") + + q = exp(-y) + powered = q**degree + if q == 0.0 or powered == 0.0: + raise NativeProcessDomainError( + "inverse-state underflow prevents a trustworthy binary64 tail bound" + ) + value = y + weight = 1.0 / degree + tail_bound = float("inf") + + for level in range(1, max_levels + 1): + interaction_argument = t * powered + if not isfinite(interaction_argument): + raise NativeProcessDomainError( + "interaction correction overflowed the binary64 evaluator" + ) + value += weight * log1p(interaction_argument) + q = powered / (1.0 + interaction_argument) + powered = q**degree + if q == 0.0 or powered == 0.0: + raise NativeProcessDomainError( + "inverse-state underflow prevents a trustworthy binary64 tail bound" + ) + tail_bound = weight * t * powered / (degree - 1) + if tail_bound == 0.0: + raise NativeProcessDomainError( + "tail-bound underflow requires a higher-precision backend" + ) + if tail_bound <= tolerance: + return NativeEscapeEvaluation( + degree=degree, + interaction=t_exact, + initial_log_state=y, + tolerance=tolerance, + value=value, + tail_bound=tail_bound, + final_inverse_state=q, + cost=NativeProcessCost( + process_levels=level, + initial_exponential_evaluations=1, + degree_power_evaluations=level + 1, + log1p_evaluations=level, + inverse_state_updates=level, + state_scalars=4, + ), + ) + weight /= degree + + raise NativeProcessBudgetError(max_levels, tail_bound) + + def direct_normalized_log_iteration_result( degree: int, interaction: int | Fraction, @@ -366,6 +550,11 @@ class BenchmarkReport: expanded_symbolic_terms: int direct_recurrence_steps: int compiled_online_series_terms: int + compiled_horner_steps: int + native_process_value: float + native_process_tail_bound: float + native_process_levels: int + native_process_log1p_evaluations: int compilation_cost: dict[str, int] first_omitted_residual: tuple[int, str] | None @@ -378,6 +567,7 @@ def benchmark_report( horizon: int = 100, queries: int = 1, initial_log_state: float = 1.5, + tolerance: float = 1e-15, ) -> BenchmarkReport: if queries < 1: raise ValueError("query count must be positive") @@ -390,6 +580,12 @@ def benchmark_report( ) direct = direct_result.value compiled = certificate.coordinate.evaluate(initial_log_state) + native = evaluate_escape_process( + degree, + interaction, + initial_log_state, + tolerance=tolerance, + ) residual = certificate.first_omitted_residual return BenchmarkReport( degree=degree, @@ -410,6 +606,13 @@ def benchmark_report( compiled_online_series_terms=( queries * certificate.coordinate.nonzero_term_count ), + compiled_horner_steps=queries * certificate.coordinate.horner_step_count, + native_process_value=native.value, + native_process_tail_bound=native.tail_bound, + native_process_levels=queries * native.cost.process_levels, + native_process_log1p_evaluations=( + queries * native.cost.log1p_evaluations + ), compilation_cost=asdict(certificate.cost), first_omitted_residual=( (residual.degree, str(residual.coefficient)) if residual else None diff --git a/tests/research/test_amp_polynomial_matrix_compiler.py b/tests/research/test_amp_polynomial_matrix_compiler.py index 80678aad..521470e6 100644 --- a/tests/research/test_amp_polynomial_matrix_compiler.py +++ b/tests/research/test_amp_polynomial_matrix_compiler.py @@ -4,6 +4,7 @@ from fractions import Fraction import importlib.util +from math import exp from pathlib import Path import sys @@ -119,6 +120,122 @@ def test_compiled_coordinate_converges_to_the_strong_log_recurrence_baseline(): assert errors == sorted(errors, reverse=True) +def test_native_process_evaluator_matches_the_strong_recurrence_with_a_tail_bound(): + expected_levels = {0.0: 6, 0.5: 5, 1.0: 4, 1.5: 4, 2.0: 4} + + for initial_log_state, levels in expected_levels.items(): + native = module.evaluate_escape_process( + 2, + 1, + initial_log_state, + tolerance=1e-15, + ) + direct = module.direct_normalized_log_iteration( + 2, + 1, + initial_log_state, + 200, + ) + + assert abs(native.value - direct) < 2e-15 + assert native.certifies_tail_tolerance + assert native.tail_bound <= 1e-15 + assert native.cost.process_levels == levels + assert native.cost.log1p_evaluations == levels + assert native.cost.degree_power_evaluations == levels + 1 + assert native.cost.initial_exponential_evaluations == 1 + assert native.cost.state_scalars == 4 + + +def test_native_process_evaluator_does_not_compile_a_series_or_matrix(): + forbidden_names = ( + "interaction_log_coefficients", + "build_substitution_matrix", + "substitution_coefficient", + ) + saved = {name: getattr(module, name) for name in forbidden_names} + + def forbidden(*_args, **_kwargs): + raise AssertionError("the native evaluator called the coefficient compiler") + + try: + for name in forbidden_names: + setattr(module, name, forbidden) + result = module.evaluate_escape_process(2, 1, 1.5, tolerance=1e-15) + finally: + for name, value in saved.items(): + setattr(module, name, value) + + assert result.certifies_tail_tolerance + assert result.cost.process_levels == 4 + + +def test_native_process_evaluator_fails_closed_on_domain_and_budget(): + invalid_tasks = ( + (1, 1, 1.0, 1e-15, 64), + (2, 0, 1.0, 1e-15, 64), + (2, 1, -0.1, 1e-15, 64), + (2, 1, 1.0, 0.0, 64), + (2, 1, 1.0, 1e-15, 0), + (2, 1, 1_000.0, 1e-15, 64), + ) + for degree, interaction, state, tolerance, budget in invalid_tasks: + try: + module.evaluate_escape_process( + degree, + interaction, + state, + tolerance=tolerance, + max_levels=budget, + ) + except module.NativeProcessDomainError: + pass + else: # pragma: no cover + raise AssertionError("an invalid native process task was accepted") + + try: + module.evaluate_escape_process( + 2, + 1, + 0.0, + tolerance=1e-100, + max_levels=1, + ) + except module.NativeProcessBudgetError as error: + assert error.max_levels == 1 + assert error.tail_bound > 1e-100 + else: # pragma: no cover + raise AssertionError("an exhausted native process budget was hidden") + + +def test_compiled_evaluation_uses_the_amp_degree_ray_horner_structure(): + coordinate = module.compile_escape_coordinate(2, 1, 20).coordinate + initial_log_state = 1.5 + q = exp(-initial_log_state) + direct_series = initial_log_state + sum( + float(coefficient) * q**degree + for degree, coefficient in enumerate(coordinate.coefficients, start=1) + ) + + assert coordinate.uses_degree_ray_horner + assert coordinate.horner_step_count == 10 + assert abs(coordinate.evaluate(initial_log_state) - direct_series) < 3e-16 + + generic = module.EscapeCoordinate( + degree=2, + interaction=Fraction(1), + order=3, + coefficients=(Fraction(1, 3), Fraction(1, 2), Fraction(-2, 5)), + ) + generic_direct = initial_log_state + sum( + float(coefficient) * q**degree + for degree, coefficient in enumerate(generic.coefficients, start=1) + ) + assert not generic.uses_degree_ray_horner + assert generic.horner_step_count == 3 + assert abs(generic.evaluate(initial_log_state) - generic_direct) < 3e-16 + + def test_sparse_cost_is_reported_without_hiding_the_strong_baseline(): report = module.benchmark_report( degree=2, @@ -138,6 +255,11 @@ def test_sparse_cost_is_reported_without_hiding_the_strong_baseline(): } assert report.expanded_symbolic_terms == 2**99 + 1 assert report.compiled_online_series_terms == 900 + assert report.compiled_horner_steps == 1_000 + assert report.native_process_levels == 400 + assert report.native_process_log1p_evaluations == 400 + assert report.native_process_tail_bound <= 1e-15 + assert abs(report.native_process_value - report.direct_normalized_value) < 2e-15 # The strong numerical baseline detects underflow of the correction and # stops early; the compiler therefore does not receive a false O(100) # per-query advantage on this floating-point task.