diff --git a/.github/workflows/validation-rustfmt-evidence.yml b/.github/workflows/validation-rustfmt-evidence.yml new file mode 100644 index 000000000..ab28bba6c --- /dev/null +++ b/.github/workflows/validation-rustfmt-evidence.yml @@ -0,0 +1,49 @@ +name: Validation Rustfmt Evidence + +on: + push: + branches: + - fix/validation-bias-overflow-safe-mean + paths: + - "crates/**" + - ".github/workflows/validation-rustfmt-evidence.yml" + +permissions: + contents: read + +concurrency: + group: validation-rustfmt-evidence-${{ github.ref }} + cancel-in-progress: true + +jobs: + format: + runs-on: ubuntu-latest + timeout-minutes: 10 + steps: + - name: Checkout exact branch head + uses: actions/checkout@631c942040754b6e095e929c1677c07e10ed4f87 + with: + persist-credentials: false + - name: Install pinned Rust formatter + run: rustup toolchain install 1.98.0 --profile minimal --component rustfmt + - name: Materialize canonical rustfmt output + run: | + set -euo pipefail + cargo +1.98.0 fmt --all + git diff --check + git diff --binary > "${RUNNER_TEMP}/rustfmt.patch" + git diff --name-only --diff-filter=ACM -z | tar --null -T - -cf "${RUNNER_TEMP}/rustfmt-tree.tar" + git diff --name-only --diff-filter=ACM > "${RUNNER_TEMP}/rustfmt-files.txt" + test -s "${RUNNER_TEMP}/rustfmt.patch" + test -s "${RUNNER_TEMP}/rustfmt-files.txt" + - name: Upload immutable formatting evidence + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 + with: + name: validation-rustfmt-${{ github.sha }} + path: | + ${{ runner.temp }}/rustfmt.patch + ${{ runner.temp }}/rustfmt-tree.tar + ${{ runner.temp }}/rustfmt-files.txt + retention-days: 1 + if-no-files-found: error + compression-level: 0 diff --git a/CHANGELOG.d/validation-bias-cancellation-roundoff.md b/CHANGELOG.d/validation-bias-cancellation-roundoff.md new file mode 100644 index 000000000..456fe2e7c --- /dev/null +++ b/CHANGELOG.d/validation-bias-cancellation-roundoff.md @@ -0,0 +1,3 @@ +### Fixed + +- Validation Evidence mean-bias accumulation now retains error-free low terms from opposite-sign cancellation before scale reduction. Repeated sub-ULP residuals can therefore contribute when their combined represented mass changes the final bias, while exact cancellation and fail-closed unrepresentable results keep their existing semantics. diff --git a/CHANGELOG.d/validation-bias-compensated-division.md b/CHANGELOG.d/validation-bias-compensated-division.md new file mode 100644 index 000000000..7d3236905 --- /dev/null +++ b/CHANGELOG.d/validation-bias-compensated-division.md @@ -0,0 +1,3 @@ +- Preserve mixed-sign Validation Evidence mean-bias compensation through the original scientific denominator so a correctly retained low term is not rounded into the numerator and lost again before division. +- Add a public mirrored contract for the represented residual boundary `[2^-53, -2^-52, -(1 + 2^-52)]`, whose exact mean rounds to `±0x1.5555555555557p-2`. +- Keep same-sign arithmetic, reusable psychometric estimation ownership, and `bias_standard_error` unchanged. diff --git a/CHANGELOG.d/validation-bias-exact-proof-budget-characterization.md b/CHANGELOG.d/validation-bias-exact-proof-budget-characterization.md new file mode 100644 index 000000000..6a2d7b22b --- /dev/null +++ b/CHANGELOG.d/validation-bias-exact-proof-budget-characterization.md @@ -0,0 +1,17 @@ +# Validation bias-SE exact-proof budget characterization + +- Correct the pair-record resource evidence for 3,162 observations to 4,997,541 records and lock exact pair counts in a Rust characterization contract. +- Distinguish the current minimum-shifted O(n) `u128` intermediate envelope (`n=2,047` at aligned diameter `2^53`) from the wider exact pair-square numerator envelope (`n=4,095` for the same diameter); neither is a production sample-count budget. +- Normalize the shared power-of-two dyadic unit before judging checked O(n) intermediates. The former `D=2^58, n=65` refusal was a characterization artifact: factoring the common `2^58` unit reduces the aligned coefficients to zero/one and preserves the exact restored numerator `2^122` without pair fallback. +- Preserve the non-equivalence finding with a normalized counterexample rather than the raw-scale artifact. With one zero and the remaining coefficients at odd diameter `D=2^58+1`, the common dyadic unit is one; both pair and narrow O(n) kernels fit at `n=64`, while at `n=65` the exact pair numerator still fits `u128` but the narrow O(n) products require 129-bit intermediates before cancellation. +- Record the canonical accumulator bound `Σc_i <= Σc_i² <= Σ(i Wide256 O(n) -> buffered pair fail-closed fallback`. RED `3136739460ef0c8e13c044a7e5b04891e4f4e23d` requires the missing route; repair `ce4ed2722e160eb0ca0ee2d636a5eca55e3ff2d5` implements it and records both wide-product selection and pairwise fallback separately in CSV. +- The corrected harness keeps the existing narrow-to-pair hybrid for comparison. On odd `D=2^58+1, n=65`, that predecessor hybrid still allocates the pair buffer, while the new narrow-to-wide-to-pair candidate must recover the same exact numerator through `Wide256` with `used_wide_product=true` and `used_pairwise_fallback=false`. Power-of-two-normalized `n=65` and odd `n=64` remain narrow-path admissions. +- Represented-input equivalence distinguishes proof admission from public numerical correctness. Characterization `2bc1d2284d75154e020640adb573c1cfadf005fb` uses residuals `[0,1,2^-54,2]`: every neutral-zero coordinate is exact, but `1 - 2^-54` rounds in binary64, so the O(n²) pairwise-f64 proof refuses. On common unit `2^-54`, integer coordinates `[0,2^54,1,2^55]` give exact pair numerator `3569704090242693886528325169446915`; the generic fallback happens to return the correct public value. +- Add source-level RED `fd9f9ff2c5c395e4cc13042232f4deef018adb48` for residuals `[0,1,2,-2^53]`. The minimum residual cannot exactly translate `1`, while neutral zero preserves every coordinate. Signed unit-one coordinates give `P=243388915243820099130562543878155`, so `SE(mean)^2=P/48`; the predecessor translated floating-moment path returns `0x4320000000000000` while exact rounding requires adjacent `0x4320000000000001`. Its Actions runs were cancelled by the immediate repair push and are not hosted RED evidence. +- Initial production repair `81ba770cc4812c8fbeb4b3529f0a73b41abbed0f` kept pairwise exact accumulation first and searched represented residual anchors. Follow-up RED `9f403194a2ec1636531c2dfe9229cfb34b73d747` showed that observed anchors alone are incomplete: residuals `[1,2^-54,2,3]` have no observed universal exact anchor, while neutral dyadic anchor `0` preserves all coordinates. On unit `2^-54`, `P=6490371073168534319490338297741315`; exact rounding requires `0x3fe4a7e9cb8a3491`, while the predecessor translated floating-moment fallback returns adjacent `0x3fe4a7e9cb8a3492`. +- Repair `6cf30eeb549c0df0377bda1111cf46396e8282a3` temporarily included neutral zero plus represented residuals in a deterministic exact-anchor set and reused signed dyadic/Wide256 cancellation plus the exact midpoint rounder. That closed the demonstrated correctness defects but retained O(n²) anchor scanning. +- Source-level route-order RED `e0b324864e48a503e2aba0d2a487a0b95f5276ed` required production to attempt `neutral_zero_linear` before quadratic proofs. Repair `2b62bd46eb0c391327d2285c2244a76f5a1e0449` added a two-pass neutral-zero signed-dyadic/Wide256 kernel, O(n) in time with O(1) proof storage after the residual vector. The superseded RED did not finish a hosted failing run and is not claimed as hosted RED evidence. +- A later retention criterion required the conditioned observed-anchor fallback to prove unique bounded admission after neutral-zero refusal. The checked-in corpus supplied no such represented fixture. RED `d40bfbf98b36562164d15d505f8f8825fd1c1349` and repair `14e7862f4ddccce54f3b93d4dac89adbf047ba77` therefore remove that unsupported O(n²) route. Production order is now `neutral_zero_linear -> pairwise_reference -> generic_fallback`; this is evidence-based consolidation, not a theorem that pairwise is globally redundant. +- Intermediate commit `da703fc50e136ed79e12262909c9f400a3945621` replaced the stale minimum-anchor helper but copied production `Wide256`, dyadic parsing, scaling, and neutral-zero arithmetic into the integration characterization and described `n=17..2050` as production-aligned despite the production `n=4..=16` admission. Repair `43d208383b765a3424019de45530af77ca484d78` removes that source copy: admitted `n=4` and `n=16` families now use an independent `i128/u128` direct pair-distance oracle plus audited output bits, forward/reversed/rotated order invariance is checked through the public API, and `[-2^53,0,1,2^53]` remains the neutral-zero-only admission witness with `P=2^109+3` and bits `0x432a20bd700c2c3e`. Wider `n>16` evidence remains characterization-only rather than a production-route test. +- Keep production `bias_standard_error` sample admission unchanged at `n=4..=16`. A budget change beyond 16 still requires broad pair/neutral-zero same-domain equivalence, truthful production route evidence, recorded Rust 1.98.0 release-mode CPU/raw CSV and allocator/RSS evidence, applicable buyer-path p95 evidence, exact-head Rust/rustdoc/100% line+branch/security/documentation GREEN, current TRACEABILITY/research/operator doctoring, and qualifying independent current-head review. diff --git a/CHANGELOG.d/validation-bias-overflow-safe-mean.md b/CHANGELOG.d/validation-bias-overflow-safe-mean.md new file mode 100644 index 000000000..6aea47d11 --- /dev/null +++ b/CHANGELOG.d/validation-bias-overflow-safe-mean.md @@ -0,0 +1,5 @@ +### Fixed + +- `validation_core::mean_bias` now computes finite signed residual means without allowing either raw-sum overflow or largest-scale normalization to erase a representable low-order bias after extreme mixed-sign cancellation. Opposite signs cancel at represented magnitude first; the remaining one-sign mass is power-of-two normalized and divided by the original recovery denominator before scale restoration. +- `validation_core::bias_standard_error` reuses the same stable bias mean and scales deviations before squaring, forming the SEM directly rather than materializing an avoidably overflowing raw square sum or sample variance. Constant extreme finite bias retains its exact zero standard error, and representable non-constant extreme cases remain measurable. +- Exact cancellation remains zero; a mathematically non-zero mean or standard error that would become false zero only because it falls below binary64 range fails closed rather than being reported as perfect recovery. diff --git a/CHANGELOG.d/validation-bias-overflowing-residual-cancellation.md b/CHANGELOG.d/validation-bias-overflowing-residual-cancellation.md new file mode 100644 index 000000000..ccd2bebad --- /dev/null +++ b/CHANGELOG.d/validation-bias-overflowing-residual-cancellation.md @@ -0,0 +1,3 @@ +### Fixed + +- Preserve a representable mean signed bias when individual `recovered - truth` residuals overflow but opposing represented input terms cancel. The fallback keeps the original paired-observation denominator, preserves minimum-subnormal results after extreme cancellation, and still fails closed when the final mean bias itself is unrepresentable. diff --git a/CHANGELOG.d/validation-bias-pairwise-subtraction-roundoff.md b/CHANGELOG.d/validation-bias-pairwise-subtraction-roundoff.md new file mode 100644 index 000000000..61855c405 --- /dev/null +++ b/CHANGELOG.d/validation-bias-pairwise-subtraction-roundoff.md @@ -0,0 +1,3 @@ +### Fixed + +- Preserve represented low-order mass when a finite `recovered - truth` subtraction rounds before mean-bias aggregation. `mean_bias` now detects nonzero error-free subtraction roundoff and switches that case to the existing recovered-plus-negated-truth numerator, while mixed-sign mean restoration retains a second-order compensation tail through the scientific denominator and final binary64 midpoint decision. This prevents pairwise subtraction and compensation rounding from moving a representable bias by one ULP without changing the exact-subtraction fast path, overflow refusal policy, or `bias_standard_error` contract. diff --git a/CHANGELOG.d/validation-bias-same-sign-remainder-division.md b/CHANGELOG.d/validation-bias-same-sign-remainder-division.md new file mode 100644 index 000000000..2a444034b --- /dev/null +++ b/CHANGELOG.d/validation-bias-same-sign-remainder-division.md @@ -0,0 +1,3 @@ +### Validation + +- Preserve Neumaier compensation through the scientific count division when exact mixed-sign cancellation leaves a same-sign remainder. This prevents a represented-input mean bias from moving by one ULP because `sum + correction` was rounded before division. The public regression contract covers the sign-mirrored boundary as well. diff --git a/CHANGELOG.d/validation-bias-same-sign-subnormal-double-rounding.md b/CHANGELOG.d/validation-bias-same-sign-subnormal-double-rounding.md new file mode 100644 index 000000000..798189142 --- /dev/null +++ b/CHANGELOG.d/validation-bias-same-sign-subnormal-double-rounding.md @@ -0,0 +1,5 @@ +# Validation bias same-sign subnormal single rounding + +- `validation_core::mean_bias` now rounds same-sign all-subnormal represented residuals once at the final binary64 subnormal grid when the scientific divisor is at least the surviving term count. +- The public contract fixes the three-residual case `(3 * 2^52 - 64) / 3`, which must round to subnormal units `2^52 - 21` rather than the adjacent `2^52 - 22` produced by normalize-then-rescale double rounding. +- Halfway cases use IEEE 754 round-to-nearest, ties-to-even on exact represented subnormal units; a companion fixture prevents the discarded direct-float repair from reintroducing a one-ULP halfway error. diff --git a/CHANGELOG.d/validation-bias-standard-error-anchor-conditioning.md b/CHANGELOG.d/validation-bias-standard-error-anchor-conditioning.md new file mode 100644 index 000000000..c6c8f04ee --- /dev/null +++ b/CHANGELOG.d/validation-bias-standard-error-anchor-conditioning.md @@ -0,0 +1,3 @@ +### Fixed + +- Preserve exact translated-residual `bias_standard_error` conditioning when several exact anchors exist. The Validation Evidence path now chooses the exact anchor with the smallest maximum translated magnitude and uses canonical represented `(high, low)` ordering only as a tie-breaker, preventing an avoidable one-ULP square/square-root drift while retaining permutation invariance and the existing exactness admission boundary. diff --git a/CHANGELOG.d/validation-bias-standard-error-anchor-permutation.md b/CHANGELOG.d/validation-bias-standard-error-anchor-permutation.md new file mode 100644 index 000000000..08ceac571 --- /dev/null +++ b/CHANGELOG.d/validation-bias-standard-error-anchor-permutation.md @@ -0,0 +1,4 @@ +### Fixed + +- `validation_core::bias_standard_error` now searches exact translated-residual anchors in canonical represented `(high, low)` order instead of making the first observation authoritative. This preserves permutation invariance when one represented anchor admits exact deltas but another anchor would force the rounded-residual fallback. +- The public regression contract fixes a three-residual boundary whose low- and high-anchor permutations previously returned the adjacent lower binary64 standard error while the middle anchor returned the correctly rounded represented-input result. Sign-mirrored orderings are covered as well. diff --git a/CHANGELOG.d/validation-bias-standard-error-common-high-mean-roundoff.md b/CHANGELOG.d/validation-bias-standard-error-common-high-mean-roundoff.md new file mode 100644 index 000000000..c8be802e3 --- /dev/null +++ b/CHANGELOG.d/validation-bias-standard-error-common-high-mean-roundoff.md @@ -0,0 +1,5 @@ +# Validation bias standard error preserves common-high low-term dispersion + +- When every rounded signed residual collapses to the same binary64 high part, evaluate the subtraction low-term dispersion with the exact translated-residual second moment whenever its anchor-relative deltas are exactly representable. +- Prevent low terms such as `[-2^-54, 0, 0]` from being re-centered on a rounded low-term mean and moving the final standard error one ULP upward. +- Retain the predecessor scaled low-term path only when exact translation cannot be established; this is a bounded represented-input repair, not a global correct-rounding claim. diff --git a/CHANGELOG.d/validation-bias-standard-error-distinct-high-roundoff.md b/CHANGELOG.d/validation-bias-standard-error-distinct-high-roundoff.md new file mode 100644 index 000000000..15935d5a0 --- /dev/null +++ b/CHANGELOG.d/validation-bias-standard-error-distinct-high-roundoff.md @@ -0,0 +1,5 @@ +# Validation bias standard error retains distinct residual roundoff + +- Preserve error-free subtraction low terms when represented bias residual high parts differ but their anchor-relative exact deltas are representable. +- Evaluate the translation-invariant second moment of those exact deltas in O(n), avoiding a rounded-residual standard error that can materially overstate uncertainty. +- Keep the predecessor rounded-residual path when exact translated deltas cannot be established without another rounding step; this change does not claim globally correctly rounded standard errors. diff --git a/CHANGELOG.d/validation-bias-standard-error-dyadic-two-level-count.md b/CHANGELOG.d/validation-bias-standard-error-dyadic-two-level-count.md new file mode 100644 index 000000000..5074948c3 --- /dev/null +++ b/CHANGELOG.d/validation-bias-standard-error-dyadic-two-level-count.md @@ -0,0 +1,4 @@ +### Fixed + +- `validation_core::bias_standard_error` now preserves an exactly translated two-level sample's count geometry when its standard-error factor reduces to a reciprocal power of two. For the 6/10 split of 16 observations, `SE(mean)` is exactly `|level_gap| / 8`; the represented `next_down(1.0)` gap therefore remains `0x3fbf_ffff_ffff_ffff` instead of being rounded up to `0.125` by the generic sum/square/square-root path. +- The dyadic shortcut is admitted only after exact residual translation and exact integer count verification. Other non-singleton two-level samples retain the existing translated second-moment path, and a mathematically nonzero dyadic result that falls below binary64 range still fails closed. diff --git a/CHANGELOG.d/validation-bias-standard-error-eight-observation-pair-distance.md b/CHANGELOG.d/validation-bias-standard-error-eight-observation-pair-distance.md new file mode 100644 index 000000000..9fb2ca898 --- /dev/null +++ b/CHANGELOG.d/validation-bias-standard-error-eight-observation-pair-distance.md @@ -0,0 +1,2 @@ +- Validation Evidence now admits the exact pair-distance identity for bounded eight-observation mean-bias standard errors when represented residuals and pairwise differences are proven exact and the reduced dyadic ratio fits the existing checked `u128` midpoint proof. +- This prevents the translated floating moment/`sqrt` fallback from moving a demonstrated exact represented-input eight-observation standard error one ULP high, while retaining the established fallback outside the bounded proof. diff --git a/CHANGELOG.d/validation-bias-standard-error-eleven-observation-pair-distance.md b/CHANGELOG.d/validation-bias-standard-error-eleven-observation-pair-distance.md new file mode 100644 index 000000000..d5cf5ed90 --- /dev/null +++ b/CHANGELOG.d/validation-bias-standard-error-eleven-observation-pair-distance.md @@ -0,0 +1,2 @@ +- Validation Evidence now admits the exact pair-distance identity for bounded eleven-observation mean-bias standard errors when represented residuals and pairwise differences are proven exact and the reduced dyadic ratio fits the existing checked `u128` midpoint proof. +- This prevents the translated floating moment/`sqrt` fallback from moving a demonstrated exact represented-input eleven-observation standard error one ULP low, while retaining the established fallback outside the bounded proof. diff --git a/CHANGELOG.d/validation-bias-standard-error-exact-proof-budget.md b/CHANGELOG.d/validation-bias-standard-error-exact-proof-budget.md new file mode 100644 index 000000000..9a5984186 --- /dev/null +++ b/CHANGELOG.d/validation-bias-standard-error-exact-proof-budget.md @@ -0,0 +1,4 @@ +- Add exact-proof budget characterization for mean-bias standard error: a seventeen-observation represented-input fixture, O(n²) versus algebraically equivalent O(n) checked-`u128` numerator evidence, and the compact-dyadic `n<=2_047` worst-case arithmetic envelope. +- Add a reproducible standard-library timing harness for the buffered O(n²), allocation-free two-pass O(n²), canonical-dyadic checked O(n), and O(n)→pair-fallback kernels while keeping production exact pair-distance admission bounded at `n<=16` until release-mode CPU/allocation and applicable buyer-path p95 evidence establish a resource budget. +- Add a dependency-free two-limb wider-product O(n) reference for characterization. On the canonical odd boundary `D=2^58+1,n=65`, the narrow checked-`u128` O(n) intermediates require 129 bits and refuse, while the exact pair numerator remains 123 bits; the wider-product reference performs the cancellation without overflow and recovers the same exact pair numerator. This is evidence about intermediate width, not production admission or a new arithmetic owner. +- Extend the timing harness to emit the wider-product reference beside the existing kernels so a recorded Rust 1.98.0 release-mode run can compare CPU cost without adding a production dependency or weakening pairwise fallback. \ No newline at end of file diff --git a/CHANGELOG.d/validation-bias-standard-error-exact-residual-mean.md b/CHANGELOG.d/validation-bias-standard-error-exact-residual-mean.md new file mode 100644 index 000000000..588714ec2 --- /dev/null +++ b/CHANGELOG.d/validation-bias-standard-error-exact-residual-mean.md @@ -0,0 +1,5 @@ +# Validation bias standard error avoids rounded-mean dispersion drift + +- Evaluate larger-sample bias standard errors from exact anchor-relative residual deltas when those deltas are provably representable, even when the original pairwise residual subtractions were exact. +- Prevent an exactly represented residual vector such as `[1, 1-2^-52, 1]` from being turned into a different dispersion geometry by rounding its mean before centering. +- Keep the predecessor rounded-mean path when exact translated deltas cannot be established; this remains a bounded binary64 repair rather than a global correct-rounding claim. diff --git a/CHANGELOG.d/validation-bias-standard-error-fifteen-observation-pair-distance.md b/CHANGELOG.d/validation-bias-standard-error-fifteen-observation-pair-distance.md new file mode 100644 index 000000000..a66d459f6 --- /dev/null +++ b/CHANGELOG.d/validation-bias-standard-error-fifteen-observation-pair-distance.md @@ -0,0 +1,2 @@ +- Validation Evidence now admits the exact pair-distance identity for bounded fifteen-observation mean-bias standard errors when represented residuals and pairwise differences are proven exact and the reduced dyadic ratio fits the existing checked `u128` midpoint proof. +- This prevents the translated floating moment/`sqrt` fallback from moving a demonstrated exact represented-input fifteen-observation standard error one ULP low, while retaining the established fallback outside the bounded proof. diff --git a/CHANGELOG.d/validation-bias-standard-error-five-observation-pair-distance.md b/CHANGELOG.d/validation-bias-standard-error-five-observation-pair-distance.md new file mode 100644 index 000000000..ec2dfc839 --- /dev/null +++ b/CHANGELOG.d/validation-bias-standard-error-five-observation-pair-distance.md @@ -0,0 +1,2 @@ +- Validation Evidence now admits the exact pair-distance identity for bounded five-observation mean-bias standard errors when every represented residual and pairwise difference is proven exact and the reduced dyadic ratio fits the existing `u128` midpoint proof. +- This prevents the translated floating moment/`sqrt` fallback from moving an exact represented-input five-observation standard error by one ULP while preserving fail-closed fallback outside the bounded proof. diff --git a/CHANGELOG.d/validation-bias-standard-error-four-observation-large-reduced-ratio.md b/CHANGELOG.d/validation-bias-standard-error-four-observation-large-reduced-ratio.md new file mode 100644 index 000000000..cb906dd60 --- /dev/null +++ b/CHANGELOG.d/validation-bias-standard-error-four-observation-large-reduced-ratio.md @@ -0,0 +1,3 @@ +### Fixed + +- Preserve the exact four-observation pair-distance standard-error proof when the GCD-reduced `u128` numerator exceeds `2^53`. The binary64 numerator conversion is now only a candidate seed; exact dyadic-square and midpoint comparisons remain authoritative for the returned rounding, so representable exact ratios do not fall back solely because the seed integer is not exactly representable as binary64. diff --git a/CHANGELOG.d/validation-bias-standard-error-four-observation-ratio-sqrt-rounding.md b/CHANGELOG.d/validation-bias-standard-error-four-observation-ratio-sqrt-rounding.md new file mode 100644 index 000000000..94987cda3 --- /dev/null +++ b/CHANGELOG.d/validation-bias-standard-error-four-observation-ratio-sqrt-rounding.md @@ -0,0 +1,5 @@ +# Validation bias standard error: exact four-observation ratio/sqrt rounding + +- For four observations whose represented residuals and all pairwise residual differences are exact, `bias_standard_error` now uses the identity `SE(mean)^2 = Σ_{i= 0` with sample mean `m`, the sample standard deviation satisfies `SD <= sqrt(n) * m`, so the standard error of the replication mean cannot exceed `m`. The generic `MonteCarloSummary` remains sign-neutral for metrics such as bias; this support check is applied only when the summary occupies the RMSE-specific report slot. diff --git a/CHANGELOG.d/validation-report-rmse-summary-percentile-support.md b/CHANGELOG.d/validation-report-rmse-summary-percentile-support.md new file mode 100644 index 000000000..a0017c08e --- /dev/null +++ b/CHANGELOG.d/validation-report-rmse-summary-percentile-support.md @@ -0,0 +1,3 @@ +### Fixed + +- Validation Evidence now rejects `monte_carlo_rmse` summaries whose empirical nearest-rank percentile endpoint exceeds the finite support available to `n` nonnegative RMSE replications with the recorded mean. The typed report boundary evaluates the support as `percentile_upper / mean <= replication_count` with a small binary64 tolerance, preserving the attainable `[0, ..., 0, n*mean]` boundary without materializing an overflow-prone sample sum. Generic `MonteCarloSummary` remains sign-neutral for metrics such as bias. diff --git a/CHANGELOG.d/validation-report-scientific-invariants.md b/CHANGELOG.d/validation-report-scientific-invariants.md new file mode 100644 index 000000000..6af64f089 --- /dev/null +++ b/CHANGELOG.d/validation-report-scientific-invariants.md @@ -0,0 +1,5 @@ +### Fixed + +- `validation_core::ValidationReport` now rejects finite but scientifically impossible metric payloads: negative RMSE/standard errors, coverage or temporal-order accuracy outside `[0, 1]`, invalid Wilson endpoints, Wilson intervals that do not contain the empirical coverage recorded in the same report, and negative mean/percentile values when a generic Monte Carlo summary is specifically embedded as RMSE evidence. +- Explicit validation, canonical JSON helpers, direct serde serialization/deserialization, and human-readable summary projection now enforce the same report invariants, so alternate ingress or egress paths cannot bypass the durable Validation Evidence contract. +- Direct serde serialization of `MonteCarloSummary` now applies its existing count/finiteness/nonnegative-uncertainty/percentile-order invariants before writing fields, matching its fail-closed deserialization contract; the generic summary remains sign-neutral so it can still represent signed metrics such as bias. diff --git a/CHANGELOG.d/validation-report-wilson-all-covered-positive-lower.md b/CHANGELOG.d/validation-report-wilson-all-covered-positive-lower.md new file mode 100644 index 000000000..6c0542d5d --- /dev/null +++ b/CHANGELOG.d/validation-report-wilson-all-covered-positive-lower.md @@ -0,0 +1,4 @@ +### Fixed + +- `validation_core::ValidationReport` now rejects exact all-covered Wilson evidence whose stored lower endpoint is numeric zero. The canonical producer returns `n / (n + z²)` for `p = 1`; with a non-empty sample and finite represented `z²`, that lower endpoint is strictly positive even when positive `z` squares to zero. This closes a durable-evidence state that passed the degenerate eliminated-root identity but could not be emitted by the producer. +- Added a public regression contract covering an extreme finite `z`, positive canonical lower support, `+0.0`/`-0.0` rejection, JSON egress, human projection, and serde ingress. diff --git a/CHANGELOG.d/validation-report-wilson-boundary-peer-root.md b/CHANGELOG.d/validation-report-wilson-boundary-peer-root.md new file mode 100644 index 000000000..75443cac9 --- /dev/null +++ b/CHANGELOG.d/validation-report-wilson-boundary-peer-root.md @@ -0,0 +1,3 @@ +### Validation Evidence + +`ValidationReport` now rejects a stored Wilson lower endpoint of exact zero, and the complement-symmetric exact-one upper endpoint, when the peer endpoint implies that the omitted Wilson root remains representable in binary64. This closes a boundary-admission hole where the eliminated endpoint-pair residual could cancel to zero even though the stored pair could not come from one Wilson interval, while preserving genuinely unrepresentable extreme boundary roots. diff --git a/CHANGELOG.d/validation-report-wilson-pair-coherence.md b/CHANGELOG.d/validation-report-wilson-pair-coherence.md new file mode 100644 index 000000000..02495f5da --- /dev/null +++ b/CHANGELOG.d/validation-report-wilson-pair-coherence.md @@ -0,0 +1,4 @@ +### Fixed + +- `ValidationReport` now rejects interval-coverage evidence whose stored Wilson lower and upper endpoints merely contain the empirical coverage but cannot be the two roots of one Wilson score interval for that same proportion. Admission uses the Wilson root identities after eliminating the unrecorded `z² / n` term, with the complementary uncovered-proportion identity below `p = 0.5` and a probability-scale binary64 tolerance. +- Canonical Wilson output remains admissible, including asymmetric interior coverage; JSON serialization, serde ingress, and human projection share the same fail-closed scientific boundary. diff --git a/CHANGELOG.d/validation-report-zero-mean-rmse-summary.md b/CHANGELOG.d/validation-report-zero-mean-rmse-summary.md new file mode 100644 index 000000000..f4a052cdd --- /dev/null +++ b/CHANGELOG.d/validation-report-zero-mean-rmse-summary.md @@ -0,0 +1,3 @@ +### Fixed + +- Reject `ValidationReport::monte_carlo_rmse` payloads whose exact-zero mean is paired with nonzero spread, standard error, or empirical percentile endpoints; for nonnegative RMSE replications, a zero mean is exact perfect recovery in every retained replication. The report fixture now also uses the canonical `SD / sqrt(n)` Monte Carlo standard error required by the generic summary contract. diff --git a/CHANGELOG.d/validation-report-zero-rmse-standard-error.md b/CHANGELOG.d/validation-report-zero-rmse-standard-error.md new file mode 100644 index 000000000..4e8530697 --- /dev/null +++ b/CHANGELOG.d/validation-report-zero-rmse-standard-error.md @@ -0,0 +1,3 @@ +# Validation Evidence: zero RMSE standard-error coherence + +`ValidationReport` now rejects an exact-zero point RMSE paired with a positive RMSE standard error. Under TEPP's squared-residual delta-method definition, exact-zero RMSE means every residual is exactly zero, so its RMSE standard error is also exactly zero. Signed zero remains one numerical zero-valued scientific state. diff --git a/CHANGELOG.d/validation-rmse-overflow-safe.md b/CHANGELOG.d/validation-rmse-overflow-safe.md new file mode 100644 index 000000000..9ea06bdc8 --- /dev/null +++ b/CHANGELOG.d/validation-rmse-overflow-safe.md @@ -0,0 +1,5 @@ +### Fixed + +- `validation_core::root_mean_square_error` now normalizes finite absolute residuals before squaring, so representable extreme RMSE values such as constant `f64::MAX` residuals are no longer rejected only because an intermediate square overflows. +- Minimum-subnormal recovery error is preserved when the final RMSE remains representable. A mathematically non-zero RMSE that would round to exact zero at the final binary64 boundary fails closed rather than being reported as perfect recovery. +- `validation_core::rmse_standard_error` computes squared-residual variation in the same normalized domain and restores the residual scale only once, preserving exact zero uncertainty for constant extreme residuals and finite uncertainty when raw squared deviations would overflow. diff --git a/CHANGELOG.d/validation-standard-error-acceptance-both-overflow.md b/CHANGELOG.d/validation-standard-error-acceptance-both-overflow.md new file mode 100644 index 000000000..e767cca8c --- /dev/null +++ b/CHANGELOG.d/validation-standard-error-acceptance-both-overflow.md @@ -0,0 +1,3 @@ +### Fixed + +- `validation_core::accept_within_standard_errors` now compares the exact represented binary64 residual magnitude with the exact represented `k × SE` product when both direct operations overflow. This prevents independently rounded scale normalization from turning a strict rejection into a false acceptance at the full-range boundary while preserving the adjacent admissible multiplier. diff --git a/CHANGELOG.d/validation-standard-error-acceptance-equal-nonzero-correction-projection.md b/CHANGELOG.d/validation-standard-error-acceptance-equal-nonzero-correction-projection.md new file mode 100644 index 000000000..bb2f43dcb --- /dev/null +++ b/CHANGELOG.d/validation-standard-error-acceptance-equal-nonzero-correction-projection.md @@ -0,0 +1,3 @@ +### Fixed + +- Validation Evidence SE-aware acceptance now resolves finite ties whose error-free subtraction correction and FMA product correction project to the same nonzero binary64 value. The decision compares the represented subtraction correction with the exact dyadic roundoff of represented `k * standard_error`, preventing a strict represented-input rejection or acceptance from being collapsed into rounded equality. The change is boundary-local to equal finite nonzero ties and does not alter ordinary unequal finite decisions, zero-bound exact recovery, or the existing both-overflow comparator. diff --git a/CHANGELOG.d/validation-standard-error-acceptance-finite-rounding-tie.md b/CHANGELOG.d/validation-standard-error-acceptance-finite-rounding-tie.md new file mode 100644 index 000000000..e43026926 --- /dev/null +++ b/CHANGELOG.d/validation-standard-error-acceptance-finite-rounding-tie.md @@ -0,0 +1,3 @@ +### Fixed + +- Preserve the represented-input inequality in `accept_within_standard_errors` when a finite residual and finite `k * SE` round to the same binary64 value: if subtraction is exact and FMA exposes a nonzero product residual, that residual now disambiguates the tie instead of defaulting to acceptance. This closes the concrete `(1 - 2^-27) * (1 + 2^-27) = 1 - 2^-54` false-accept boundary while leaving inexact-subtraction ties on the existing conservative rounded path. diff --git a/CHANGELOG.d/validation-standard-error-acceptance-scale-underflow.md b/CHANGELOG.d/validation-standard-error-acceptance-scale-underflow.md new file mode 100644 index 000000000..bdc4f0be3 --- /dev/null +++ b/CHANGELOG.d/validation-standard-error-acceptance-scale-underflow.md @@ -0,0 +1,3 @@ +### Fixed + +- Preserved finite positive `k · SE` acceptance bounds before scale normalization in `accept_within_standard_errors`. A large estimate/target scale could previously make `SE / scale` underflow to zero even when the represented direct residual and represented `k · SE` bound were both finite, falsely rejecting a scientifically admissible recovery result. Direct finite residual/bound comparison now precedes the overflow-only normalized fallback; zero-SE and zero-multiplier exact-recovery semantics are unchanged. diff --git a/CHANGELOG.d/validation-standard-error-acceptance-subnormal-product-rounding.md b/CHANGELOG.d/validation-standard-error-acceptance-subnormal-product-rounding.md new file mode 100644 index 000000000..c51918b98 --- /dev/null +++ b/CHANGELOG.d/validation-standard-error-acceptance-subnormal-product-rounding.md @@ -0,0 +1,5 @@ +# Validation: preserve standard-error ties below correction resolution + +- Reject SE-aware recovery when an exact finite residual equals a rounded `k * SE` bound but the exact represented product is smaller and the FMA correction itself falls below binary64 resolution. +- Cover both the minimum-subnormal rounded bound and the minimum-normal boundary: when subtraction is exact and both finite-tie correction projections are zero, compare the represented residual magnitude with the exact dyadic product of represented `k` and `SE`. +- Keep exact minimum-subnormal/minimum-normal equality accepted and preserve the existing finite direct path, nonzero low-term tie discriminator, one-sided overflow behavior, and both-overflow exact comparator outside this boundary. diff --git a/CHANGELOG.d/validation-standard-error-acceptance-subtraction-rounding.md b/CHANGELOG.d/validation-standard-error-acceptance-subtraction-rounding.md new file mode 100644 index 000000000..c2d12be18 --- /dev/null +++ b/CHANGELOG.d/validation-standard-error-acceptance-subtraction-rounding.md @@ -0,0 +1,5 @@ +# Validation: preserve subtraction-rounded standard-error decisions + +- Reject SE-aware recovery when a finite subtraction rounds onto the same binary64 value as an exact finite `k * SE` bound even though the exact dyadic residual represented by the inputs is larger. +- Compare the error-free subtraction correction, sign-adjusted for the absolute residual, with the FMA product correction only on nonzero finite rounded ties; retain the prior rounded decision when those correction projections are equal. +- Keep the finite direct path, scale-underflow repair, and both-overflow exact significand/exponent comparator unchanged outside this tie boundary. diff --git a/CHANGELOG.d/validation-summary-support-and-extreme-match.md b/CHANGELOG.d/validation-summary-support-and-extreme-match.md new file mode 100644 index 000000000..443c9f3e4 --- /dev/null +++ b/CHANGELOG.d/validation-summary-support-and-extreme-match.md @@ -0,0 +1,4 @@ +### Fixed + +- `validation_core::MonteCarloSummary` now rejects zero-sample-spread artifacts whose empirical percentile support is not degenerate at the represented mean. Zero spread means every retained replication is identical, so a durable summary cannot simultaneously claim `SD = 0` and percentile support away from its mean; signed zero remains one numeric zero-valued state. +- `validation_core::match_count` now decides finite absolute-tolerance matches without requiring an unrepresentable residual magnitude. Opposite-sign finite extremes whose subtraction overflows are deterministically mismatches for every finite tolerance, while `absolute_residuals` continues to fail closed when the residual value itself is requested. diff --git a/CHANGELOG.d/validation-wilson-all-covered-extreme-z-scale.md b/CHANGELOG.d/validation-wilson-all-covered-extreme-z-scale.md new file mode 100644 index 000000000..f88979e5e --- /dev/null +++ b/CHANGELOG.d/validation-wilson-all-covered-extreme-z-scale.md @@ -0,0 +1,3 @@ +### Fixed + +- Preserved the positive all-covered Wilson lower endpoint for durable `u64` sample counts that are not exactly representable in binary64 when a large but finite standard-normal critical value makes `z² / n > 1`. The canonical count-based producer now uses the complementary-miss form only on the small-`z² / n` side and the algebraically equivalent direct reciprocal form on the large side. On this exact-count all-covered path, represented `z²` is decomposed into its binary significand and power-of-two scale before division by the durable count, so a rounded `1 / n` product cannot shift the exposed endpoint by one ULP. This avoids false exact `1.0`, false exact `0.0`, and the RED case's reciprocal-product rounding error without changing the Wilson estimand. diff --git a/CHANGELOG.d/validation-wilson-count-precision.md b/CHANGELOG.d/validation-wilson-count-precision.md new file mode 100644 index 000000000..37433fdd0 --- /dev/null +++ b/CHANGELOG.d/validation-wilson-count-precision.md @@ -0,0 +1,4 @@ +### Fixed + +- `validation_core` now preserves distinct covered/sample counts when Wilson coverage evidence exceeds binary64's exact-integer range. Near the all-covered boundary it forms empirical coverage from the smaller uncovered complement and evaluates the Wilson interval by complement symmetry, so one retained miss cannot be rounded away into an all-covered numerical path. +- `WilsonCoverageEvidenceV1` stores `sample_count` and `covered_count` as fixed-width `u64` values, making the versioned JSON count contract independent of Rust pointer width while retaining exact integer provenance. diff --git a/CHANGELOG.d/validation-wilson-coverage-evidence-provenance.md b/CHANGELOG.d/validation-wilson-coverage-evidence-provenance.md new file mode 100644 index 000000000..2c1dfebe7 --- /dev/null +++ b/CHANGELOG.d/validation-wilson-coverage-evidence-provenance.md @@ -0,0 +1,7 @@ +# Validation Evidence: versioned Wilson coverage provenance + +- Add `WilsonCoverageEvidenceV1`, a schema-tagged durable carrier that retains empirical `sample_count`, `covered_count`, the caller-supplied standard-normal `z`, represented coverage, and canonical Wilson endpoints. +- Fix the interval interpretation as `critical_value_kind = "standard_normal_z"` and `interval_sidedness = "two_sided"`; v1 records the producer input and sidedness rather than inventing a nominal confidence-level label that the API never received. +- Recompute coverage and Wilson bounds from stored counts and `z` during artifact admission so denominator, critical-value, projection, or endpoint tampering fails closed. +- Add `ValidationEvidenceV1` as the versioned durable envelope that binds the existing `ValidationReport` projection to the recomputable Wilson coverage provenance; legacy report callers remain source-compatible. +- Keep one crate-private count-based Wilson numeric authority shared by the existing interval API and the versioned carrier; no estimator target, Longitudinal Modeling semantics, fast-mlsirm ownership, or contextual-orchestrator routing contract changes. diff --git a/CHANGELOG.d/validation-wilson-exact-count-large-z-rounding.md b/CHANGELOG.d/validation-wilson-exact-count-large-z-rounding.md new file mode 100644 index 000000000..61a2824d2 --- /dev/null +++ b/CHANGELOG.d/validation-wilson-exact-count-large-z-rounding.md @@ -0,0 +1,3 @@ +### Fixed + +- `validation_core::wilson_coverage_interval` now preserves the exactly represented finite sample-count contribution when an all-covered Wilson endpoint has an exactly representable `n` but large finite `z²` completely absorbs `n` in the rounded denominator `n + z²`. The exact-count path recovers the addition residual and applies a fused quotient-residual correction. A later partial-denominator repair extends the same compensated denominator mechanism to ordinary inexact `n + z²` sums; the near-one complementary repair and the inexact-`u64` path remain separate boundary contracts. diff --git a/CHANGELOG.d/validation-wilson-exact-count-partial-denominator-rounding.md b/CHANGELOG.d/validation-wilson-exact-count-partial-denominator-rounding.md new file mode 100644 index 000000000..8dcf6d7b8 --- /dev/null +++ b/CHANGELOG.d/validation-wilson-exact-count-partial-denominator-rounding.md @@ -0,0 +1,3 @@ +### Fixed + +- `validation_core::wilson_coverage_interval` now compensates the exact TwoSum residual whenever an exactly representable all-covered sample count forms an inexact binary64 denominator `n + z²`. This preserves the represented-input Wilson lower endpoint when ordinary partial denominator rounding moves the direct quotient by one ULP, while exact denominator sums and the existing near-one/inexact-`u64` boundary contracts remain unchanged. diff --git a/CHANGELOG.d/validation-wilson-exact-count-residual-midpoint.md b/CHANGELOG.d/validation-wilson-exact-count-residual-midpoint.md new file mode 100644 index 000000000..9120cd2cb --- /dev/null +++ b/CHANGELOG.d/validation-wilson-exact-count-residual-midpoint.md @@ -0,0 +1,3 @@ +### Fixed + +- Exact-count all-covered Wilson evidence now compares the represented-input quotient residual against the adjacent binary64 midpoint before changing the direct quotient. This prevents residual compensation from forcing a one-ULP move when the finite-count correction is real but below the final endpoint's rounding resolution, while preserving earlier cases where denominator rounding genuinely changes the correctly rounded endpoint. diff --git a/CHANGELOG.d/validation-wilson-exact-count-small-z-rounding.md b/CHANGELOG.d/validation-wilson-exact-count-small-z-rounding.md new file mode 100644 index 000000000..f9d3e006f --- /dev/null +++ b/CHANGELOG.d/validation-wilson-exact-count-small-z-rounding.md @@ -0,0 +1,3 @@ +### Fixed + +- `validation_core::wilson_coverage_interval` now preserves a representable all-covered Wilson miss mass when an exactly representable sample count and small positive `z²` make the direct `n / (n + z²)` denominator round back to `n`. The repair is boundary-local: it subtracts the algebraically equivalent `z² / (n + z²)` only when the direct lower endpoint has spuriously collapsed to exact `1.0`, while smaller uncertainty that is genuinely below binary64 resolution remains `1.0`. diff --git a/CHANGELOG.d/validation-wilson-extreme-endpoint.md b/CHANGELOG.d/validation-wilson-extreme-endpoint.md new file mode 100644 index 000000000..35f8a6170 --- /dev/null +++ b/CHANGELOG.d/validation-wilson-extreme-endpoint.md @@ -0,0 +1,5 @@ +### Fixed + +- `validation_core::wilson_coverage_interval` now evaluates the all-covered Wilson lower endpoint as `n / (n + z²)` instead of subtracting nearly equal `O(z²)` center and margin terms, preserving a positive binary64-representable lower bound for large finite critical values rather than collapsing it to exact zero. +- Nonzero strict-interior coverage now evaluates the Wilson lower endpoint through the algebraically rationalized positive root instead of trusting `center - margin`. This also rejects nonzero floating-point cancellation residues that can be many orders of magnitude larger than the represented Wilson endpoint even when the subtraction does not collapse to exact zero. The rationalized implementation switches between natural-scale and `z²`-normalized forms at `z² = 1` so small finite critical values do not introduce division overflow. +- All-uncovered and strict-interior coverage use the same rationalized positive-lower calculation on the complementary uncovered proportion when the generic `center + margin` path falsely rounds a Wilson upper endpoint to exact `1.0`. Ordinary upper endpoints remain on the direct path when no false-one collapse occurs. diff --git a/CHANGELOG.d/validation-wilson-ratio-rounding.md b/CHANGELOG.d/validation-wilson-ratio-rounding.md new file mode 100644 index 000000000..5daa05fb7 --- /dev/null +++ b/CHANGELOG.d/validation-wilson-ratio-rounding.md @@ -0,0 +1,3 @@ +### Fixed + +- `validation_core` now rounds durable covered/sample count ratios directly from their exact `u64` provenance instead of first rounding each integer to binary64. This removes one-ULP empirical-coverage errors above binary64's exact-integer range and feeds the Wilson score producer the correctly rounded represented proportion while retaining complement-symmetric evaluation near the all-covered boundary. diff --git a/CHANGELOG.d/validation-wilson-sample-count-scale.md b/CHANGELOG.d/validation-wilson-sample-count-scale.md new file mode 100644 index 000000000..c1483b6ce --- /dev/null +++ b/CHANGELOG.d/validation-wilson-sample-count-scale.md @@ -0,0 +1,6 @@ +# Validation: preserve exact Wilson sample-count scale + +- `WilsonCoverageEvidenceV1` no longer pre-rounds a fixed-width `u64` sample count before Wilson score projection when the count is not exactly representable in binary64. +- The inexact-large-count path evaluates Wilson scale terms through the correctly rounded reciprocal `1 / n`; exact binary64-representable counts keep the existing path. +- The all-covered large-count branch evaluates the complementary Wilson miss mass before subtracting from one, so representable uncertainty immediately below `1.0` is not erased. +- The prior non-boundary large-count fixture is corrected because its Wilson lower endpoint had inherited the rounded-denominator calculation even after empirical `covered_count / sample_count` was fixed. diff --git a/CHANGELOG.d/validation-zero-multiplier-acceptance.md b/CHANGELOG.d/validation-zero-multiplier-acceptance.md new file mode 100644 index 000000000..14d008bfb --- /dev/null +++ b/CHANGELOG.d/validation-zero-multiplier-acceptance.md @@ -0,0 +1,4 @@ +## Fixed + +- Validation Evidence now treats `k = 0` in the standard-error acceptance gate as exact recovery before any binary64 scale reduction, preventing a huge standard error from erasing a nonzero residual into a false acceptance. +- Exact-recovery acceptance now uses finite numeric equality rather than IEEE total-order identity, so `-0.0` and `+0.0` are one zero-valued recovery result while every nonzero finite residual remains distinct. diff --git a/crates/validation_core/examples/bias_se_exact_proof_budget.rs b/crates/validation_core/examples/bias_se_exact_proof_budget.rs new file mode 100644 index 000000000..d63f0a32f --- /dev/null +++ b/crates/validation_core/examples/bias_se_exact_proof_budget.rs @@ -0,0 +1,462 @@ +//! Reproducible integer-kernel timing and layout harness for bias-SE exact-proof budgeting. +//! +//! This example compares checked-integer proof kernels on deterministic dyadic +//! coefficients: a production-layout-shaped buffered O(n²) pair proof, an +//! allocation-free two-pass O(n²) variant, an algebraically equivalent O(n) +//! sufficient accumulator, a two-limb wider-product O(n) reference, the existing +//! narrow-to-pair hybrid, and a candidate narrow-to-wide-to-pair hybrid. The O(n) +//! paths first remove the shared power-of-two unit from anchor-relative +//! coefficients so checked-intermediate refusal is evaluated on the canonical +//! dyadic grid rather than on an arbitrary raw integer scale. This is +//! characterization tooling, not production admission and not buyer-path latency +//! evidence by itself. + +use std::hint::black_box; +use std::mem::size_of; +use std::time::{Duration, Instant}; + +#[derive(Clone, Copy)] +struct KernelObservation { + aligned_pair_square_sum: u128, + unit_exponent: i32, + scratch_records: usize, + scratch_payload_bytes: usize, + used_wide_product: bool, + used_pairwise_fallback: bool, +} + +#[derive(Clone, Copy)] +struct Wide256 { + high: u128, + low: u128, +} + +impl Wide256 { + fn multiply_u128(left: u128, right: u128) -> Self { + let mask = u128::from(u64::MAX); + let left_limbs = [ + u64::try_from(left & mask).expect("masked low limb fits u64"), + u64::try_from(left >> 64).expect("high limb fits u64"), + ]; + let right_limbs = [ + u64::try_from(right & mask).expect("masked low limb fits u64"), + u64::try_from(right >> 64).expect("high limb fits u64"), + ]; + let mut limbs = [0_u64; 4]; + + for (left_index, left_limb) in left_limbs.iter().copied().enumerate() { + let mut carry = 0_u128; + for (right_index, right_limb) in right_limbs.iter().copied().enumerate() { + let limb_index = left_index + right_index; + let accumulator = u128::from(left_limb) + .checked_mul(u128::from(right_limb)) + .expect("64-bit limb product fits u128") + .checked_add(u128::from(limbs[limb_index])) + .expect("schoolbook partial sum fits u128") + .checked_add(carry) + .expect("schoolbook carry sum fits u128"); + limbs[limb_index] = + u64::try_from(accumulator & mask).expect("masked schoolbook limb fits u64"); + carry = accumulator >> 64; + } + limbs[left_index + 2] = + u64::try_from(carry).expect("schoolbook multiplication carry fits u64"); + } + + Self { + high: u128::from(limbs[2]) | (u128::from(limbs[3]) << 64), + low: u128::from(limbs[0]) | (u128::from(limbs[1]) << 64), + } + } + + fn checked_sub(self, right: Self) -> Option { + let (low, borrow) = self.low.overflowing_sub(right.low); + let high = self + .high + .checked_sub(right.high)? + .checked_sub(u128::from(u8::from(borrow)))?; + Some(Self { high, low }) + } + + fn as_u128(self) -> Option { + (self.high == 0).then_some(self.low) + } +} + +type Kernel = fn(&[u128]) -> Option; + +fn fixture(sample_count: usize) -> Vec { + (0..sample_count) + .map(|index| { + let value = u128::try_from(index).expect("fixture index fits u128"); + (value * 1_000_003 + value * value * 97 + 17) % 4_000_000_001 + }) + .collect() +} + +fn boundary_fixture(sample_count: usize, diameter: u128) -> Vec { + let mut values = Vec::with_capacity(sample_count); + values.push(0); + values.extend((1..sample_count).map(|_| diameter)); + values +} + +fn multiply_by_power_of_two(value: u128, shift: u32) -> Option { + let factor = 1_u128.checked_shl(shift)?; + value.checked_mul(factor) +} + +fn compact_dyadic(value: u128) -> Option<(u128, i32)> { + if value == 0 { + return None; + } + let trailing = value.trailing_zeros(); + Some((value >> trailing, i32::try_from(trailing).ok()?)) +} + +fn accumulate_aligned_pair_square_sum( + records: impl IntoIterator>, + unit_exponent: i32, +) -> Option { + let mut sum = 0_u128; + for (significand, exponent) in records.into_iter().flatten() { + let shift = exponent.checked_sub(unit_exponent)?.unsigned_abs(); + let coefficient = multiply_by_power_of_two(significand, shift)?; + sum = sum.checked_add(coefficient.checked_mul(coefficient)?)?; + } + Some(sum) +} + +fn pair_square_sum_quadratic_buffered(values: &[u128]) -> Option { + let pair_count = values + .len() + .checked_mul(values.len().checked_sub(1)?)? + .checked_div(2)?; + let mut records: Vec> = Vec::with_capacity(pair_count); + let mut unit_exponent = i32::MAX; + for left in 0..values.len() { + for right in left + 1..values.len() { + let record = compact_dyadic(values[left].abs_diff(values[right])); + if let Some((_, exponent)) = record { + unit_exponent = unit_exponent.min(exponent); + } + records.push(record); + } + } + let scratch_records = records.capacity(); + let scratch_payload_bytes = scratch_records.checked_mul(size_of::>())?; + if unit_exponent == i32::MAX { + return Some(KernelObservation { + aligned_pair_square_sum: 0, + unit_exponent: 0, + scratch_records, + scratch_payload_bytes, + used_wide_product: false, + used_pairwise_fallback: false, + }); + } + let aligned_pair_square_sum = accumulate_aligned_pair_square_sum(records, unit_exponent)?; + Some(KernelObservation { + aligned_pair_square_sum, + unit_exponent, + scratch_records, + scratch_payload_bytes, + used_wide_product: false, + used_pairwise_fallback: false, + }) +} + +fn pair_square_sum_quadratic_two_pass(values: &[u128]) -> Option { + let mut unit_exponent = i32::MAX; + for left in 0..values.len() { + for right in left + 1..values.len() { + if let Some((_, exponent)) = compact_dyadic(values[left].abs_diff(values[right])) { + unit_exponent = unit_exponent.min(exponent); + } + } + } + if unit_exponent == i32::MAX { + return Some(KernelObservation { + aligned_pair_square_sum: 0, + unit_exponent: 0, + scratch_records: 0, + scratch_payload_bytes: 0, + used_wide_product: false, + used_pairwise_fallback: false, + }); + } + + let records = (0..values.len()).flat_map(|left| { + (left + 1..values.len()) + .map(move |right| compact_dyadic(values[left].abs_diff(values[right]))) + }); + let aligned_pair_square_sum = accumulate_aligned_pair_square_sum(records, unit_exponent)?; + Some(KernelObservation { + aligned_pair_square_sum, + unit_exponent, + scratch_records: 0, + scratch_payload_bytes: 0, + used_wide_product: false, + used_pairwise_fallback: false, + }) +} + +fn normalized_linear_terms(values: &[u128]) -> Option<(u128, u128, u32)> { + let minimum = *values.iter().min()?; + let common_shift = values + .iter() + .filter_map(|value| { + let coefficient = value.checked_sub(minimum)?; + (coefficient != 0).then_some(coefficient.trailing_zeros()) + }) + .min() + .unwrap_or(0); + + let mut coefficient_sum = 0_u128; + let mut square_sum = 0_u128; + for value in values { + let coefficient = value.checked_sub(minimum)? >> common_shift; + coefficient_sum = coefficient_sum.checked_add(coefficient)?; + square_sum = square_sum.checked_add(coefficient.checked_mul(coefficient)?)?; + } + Some((coefficient_sum, square_sum, common_shift)) +} + +fn pair_square_sum_linear(values: &[u128]) -> Option { + let sample_count = u128::try_from(values.len()).ok()?; + let (coefficient_sum, square_sum, common_shift) = normalized_linear_terms(values)?; + let pair_square_sum = sample_count + .checked_mul(square_sum)? + .checked_sub(coefficient_sum.checked_mul(coefficient_sum)?)?; + Some(KernelObservation { + aligned_pair_square_sum: pair_square_sum, + unit_exponent: i32::try_from(common_shift).ok()?, + scratch_records: 0, + scratch_payload_bytes: 0, + used_wide_product: false, + used_pairwise_fallback: false, + }) +} + +fn pair_square_sum_linear_wide_product(values: &[u128]) -> Option { + let sample_count = u128::try_from(values.len()).ok()?; + let (coefficient_sum, square_sum, common_shift) = normalized_linear_terms(values)?; + let pair_square_sum = Wide256::multiply_u128(sample_count, square_sum) + .checked_sub(Wide256::multiply_u128(coefficient_sum, coefficient_sum))? + .as_u128()?; + Some(KernelObservation { + aligned_pair_square_sum: pair_square_sum, + unit_exponent: i32::try_from(common_shift).ok()?, + scratch_records: 0, + scratch_payload_bytes: 0, + used_wide_product: true, + used_pairwise_fallback: false, + }) +} + +fn pair_square_sum_hybrid(values: &[u128]) -> Option { + if let Some(observation) = pair_square_sum_linear(values) { + return Some(observation); + } + let mut observation = pair_square_sum_quadratic_buffered(values)?; + observation.used_pairwise_fallback = true; + Some(observation) +} + +fn pair_square_sum_wide_hybrid(values: &[u128]) -> Option { + if let Some(observation) = pair_square_sum_linear(values) { + return Some(observation); + } + if let Some(observation) = pair_square_sum_linear_wide_product(values) { + return Some(observation); + } + let mut observation = pair_square_sum_quadratic_buffered(values)?; + observation.used_pairwise_fallback = true; + Some(observation) +} + +fn restored_pair_square_sum(observation: KernelObservation) -> Option { + let shift = observation.unit_exponent.checked_mul(2)?.unsigned_abs(); + multiply_by_power_of_two(observation.aligned_pair_square_sum, shift) +} + +fn percentile_95(mut durations: Vec) -> Duration { + durations.sort_unstable(); + let rank = durations.len().saturating_mul(95).div_ceil(100); + durations[rank.saturating_sub(1)] +} + +fn measure(values: &[u128], samples: usize, kernel: Kernel) -> (Duration, KernelObservation) { + for _ in 0..3 { + black_box(kernel(black_box(values)).expect("fixture must remain within u128")); + } + let mut durations = Vec::with_capacity(samples); + for _ in 0..samples { + let started = Instant::now(); + black_box(kernel(black_box(values)).expect("fixture must remain within u128")); + durations.push(started.elapsed()); + } + let observation = kernel(values).expect("fixture must remain within u128"); + (percentile_95(durations), observation) +} + +fn emit( + geometry: &str, + sample_count: usize, + kernel_name: &str, + p95: Duration, + samples: usize, + observation: KernelObservation, +) { + println!( + "{geometry},{sample_count},{kernel_name},{},{samples},{},{},{},{},{},{}", + p95.as_nanos(), + observation.unit_exponent, + observation.scratch_records, + observation.scratch_payload_bytes, + size_of::>(), + observation.used_wide_product, + observation.used_pairwise_fallback + ); +} + +fn assert_and_measure_geometry( + geometry: &str, + values: &[u128], + samples: usize, + expect_linear_admission: bool, + expect_hybrid_fallback: bool, + expect_wide_hybrid_wide_product: bool, + expect_wide_hybrid_pair_fallback: bool, +) { + let buffered = pair_square_sum_quadratic_buffered(values) + .expect("buffered quadratic result stays within u128"); + let two_pass = pair_square_sum_quadratic_two_pass(values) + .expect("two-pass quadratic result stays within u128"); + let wide_product = pair_square_sum_linear_wide_product(values) + .expect("wider-product linear reference stays within its declared budget"); + let hybrid = pair_square_sum_hybrid(values).expect("hybrid result stays within u128"); + let wide_hybrid = pair_square_sum_wide_hybrid(values) + .expect("narrow-wide-pair hybrid result stays within u128"); + let exact_pair_square_sum = restored_pair_square_sum(buffered) + .expect("buffered result restores to exact pair-square sum"); + assert_eq!( + restored_pair_square_sum(two_pass), + Some(exact_pair_square_sum), + "quadratic kernels must agree" + ); + assert_eq!( + restored_pair_square_sum(wide_product), + Some(exact_pair_square_sum), + "wider-product linear reference must preserve the exact pair numerator" + ); + assert_eq!( + restored_pair_square_sum(hybrid), + Some(exact_pair_square_sum), + "narrow-pair hybrid must preserve the exact pair numerator" + ); + assert_eq!( + restored_pair_square_sum(wide_hybrid), + Some(exact_pair_square_sum), + "narrow-wide-pair hybrid must preserve the exact pair numerator" + ); + assert_eq!( + hybrid.used_pairwise_fallback, expect_hybrid_fallback, + "narrow-pair fallback observation must match the declared geometry" + ); + assert_eq!( + wide_hybrid.used_wide_product, expect_wide_hybrid_wide_product, + "wide-route observation must match the declared geometry" + ); + assert_eq!( + wide_hybrid.used_pairwise_fallback, expect_wide_hybrid_pair_fallback, + "wide-hybrid pair fallback observation must match the declared geometry" + ); + + match pair_square_sum_linear(values) { + Some(linear) => { + assert!(expect_linear_admission, "linear admission was not expected"); + assert_eq!( + restored_pair_square_sum(linear), + Some(exact_pair_square_sum), + "linear identity must agree with pair reference" + ); + } + None => assert!(!expect_linear_admission, "linear refusal was not expected"), + } + + let mut kernels: Vec<(&str, Kernel)> = vec![ + ("quadratic_buffered", pair_square_sum_quadratic_buffered), + ("quadratic_two_pass", pair_square_sum_quadratic_two_pass), + ( + "linear_wide_product_reference", + pair_square_sum_linear_wide_product, + ), + ("hybrid_narrow_pair", pair_square_sum_hybrid), + ("hybrid_narrow_wide_pair", pair_square_sum_wide_hybrid), + ]; + if expect_linear_admission { + kernels.push(("linear", pair_square_sum_linear)); + } + for (kernel_name, kernel) in kernels { + let (p95, observation) = measure(values, samples, kernel); + emit( + geometry, + values.len(), + kernel_name, + p95, + samples, + observation, + ); + } +} + +fn main() { + let samples = std::env::args() + .nth(1) + .and_then(|value| value.parse::().ok()) + .unwrap_or(25) + .max(1); + + println!( + "geometry,sample_count,kernel,p95_ns,timing_samples,unit_exponent,scratch_records,scratch_payload_bytes,pair_record_size_bytes,used_wide_product,used_pairwise_fallback" + ); + for sample_count in [16_usize, 64, 256, 1_024, 2_047] { + let values = fixture(sample_count); + assert_and_measure_geometry("compact_admit", &values, samples, true, false, false, false); + } + + let power_of_two_values = boundary_fixture(65, 1_u128 << 58); + assert_and_measure_geometry( + "power_of_two_normalized_admit", + &power_of_two_values, + samples, + true, + false, + false, + false, + ); + + let odd_diameter = (1_u128 << 58) + 1; + let odd_64 = boundary_fixture(64, odd_diameter); + assert_and_measure_geometry( + "odd_boundary_admit", + &odd_64, + samples, + true, + false, + false, + false, + ); + + let odd_65 = boundary_fixture(65, odd_diameter); + assert_and_measure_geometry( + "odd_boundary_wide_recovery", + &odd_65, + samples, + false, + true, + true, + false, + ); +} diff --git a/crates/validation_core/src/bias.rs b/crates/validation_core/src/bias.rs index d7ca438b9..0863e03e8 100644 --- a/crates/validation_core/src/bias.rs +++ b/crates/validation_core/src/bias.rs @@ -1,66 +1,669 @@ //! Signed mean bias recovery metric. use crate::ValidationError; -use crate::input::{require_finite, require_paired_finite}; +use crate::input::require_paired_finite; +use crate::numeric::{ + deterministic_compensated_sum, deterministic_representable_mean, + deterministic_representable_sum_over_count, exact_power_of_two_scale, +}; + +fn signed_residuals(truth: &[f64], recovered: &[f64]) -> Result, ValidationError> { + require_paired_finite(truth, recovered)?; + truth + .iter() + .zip(recovered) + .map(|(truth_value, recovered_value)| { + let residual = recovered_value - truth_value; + if residual.is_finite() { + Ok(residual) + } else { + Err(ValidationError::InvalidInput) + } + }) + .collect() +} + +fn subtraction_roundoff(recovered: f64, truth: f64, residual: f64) -> f64 { + let negated_truth = -truth; + let truth_virtual = residual - recovered; + let recovered_virtual = residual - truth_virtual; + let recovered_roundoff = recovered - recovered_virtual; + let truth_roundoff = negated_truth - truth_virtual; + recovered_roundoff + truth_roundoff +} + +fn subtraction_has_roundoff(recovered: f64, truth: f64, residual: f64) -> bool { + subtraction_roundoff(recovered, truth, residual) != 0.0 +} + +fn standard_error_from_deviations(deviations: &[f64]) -> Result { + let scale = deviations + .iter() + .map(|deviation| deviation.abs()) + .fold(0.0, f64::max); + if scale == 0.0 { + return Ok(0.0); + } + + let normalized_squares: Vec<_> = deviations + .iter() + .map(|deviation| { + let normalized = *deviation / scale; + normalized * normalized + }) + .collect(); + let square_sum = deterministic_compensated_sum(normalized_squares); + let sample_count = deviations.len() as f64; + // Form SE directly as sqrt(sum(d²) / (n * (n - 1))). Separately + // rounding sqrt(sample_variance) and sqrt(n) can move the final binary64 + // standard error by one ULP even when the represented squared-deviation + // ratio is exact. + let normalized_standard_error = + (square_sum / (sample_count * (sample_count - 1.0))).sqrt(); + let standard_error = scale * normalized_standard_error; + if !standard_error.is_finite() + || (standard_error == 0.0 && normalized_standard_error != 0.0) + { + Err(ValidationError::InvalidInput) + } else if standard_error == 0.0 { + Ok(0.0) + } else { + Ok(standard_error) + } +} + +fn scaled_standard_error(values: &[f64], mean: f64) -> Result { + let direct_deviations: Option> = values + .iter() + .map(|value| { + let deviation = *value - mean; + deviation.is_finite().then_some(deviation) + }) + .collect(); + if let Some(deviations) = direct_deviations { + return standard_error_from_deviations(&deviations); + } + + let outer_scale = values.iter().map(|value| value.abs()).fold(0.0, f64::max); + if outer_scale == 0.0 { + return Ok(0.0); + } + let normalized_values: Vec<_> = values.iter().map(|value| *value / outer_scale).collect(); + let normalized_mean = deterministic_representable_mean(&normalized_values)?; + let normalized_deviations: Vec<_> = normalized_values + .iter() + .map(|value| *value - normalized_mean) + .collect(); + let normalized_standard_error = standard_error_from_deviations(&normalized_deviations)?; + let standard_error = outer_scale * normalized_standard_error; + if !standard_error.is_finite() + || (standard_error == 0.0 && normalized_standard_error != 0.0) + { + Err(ValidationError::InvalidInput) + } else if standard_error == 0.0 { + Ok(0.0) + } else { + Ok(standard_error) + } +} + +fn greatest_common_divisor(mut left: u128, mut right: u128) -> u128 { + while right != 0 { + let remainder = left % right; + left = right; + right = remainder; + } + left +} + +/// Return the exact rational scale implied by two-level sample counts. +/// +/// For two exact residual levels with counts `m` and `n - m`, +/// `SE(mean)^2 = gap^2 * m(n-m) / (n^2(n-1))`. If the reduced count-only +/// factor is exactly the square of a rational `numerator / denominator` whose +/// roots fit the platform's exact integer count representation, callers can +/// preserve that algebraic scale without reconstructing it through rounded +/// sums, squares and sqrt. +fn exact_two_level_rational_scale( + first_count: usize, + second_count: usize, +) -> Option<(usize, usize)> { + let sample_count = (first_count as u128).checked_add(second_count as u128)?; + let count_product = (first_count as u128).checked_mul(second_count as u128)?; + if count_product == 0 { + return None; + } + let target = sample_count + .checked_mul(sample_count)? + .checked_mul(sample_count.checked_sub(1)?)?; + + let divisor = greatest_common_divisor(count_product, target); + let reduced_numerator = count_product / divisor; + let reduced_denominator = target / divisor; + let numerator = reduced_numerator.isqrt(); + let denominator = reduced_denominator.isqrt(); + if numerator.checked_mul(numerator)? != reduced_numerator + || denominator.checked_mul(denominator)? != reduced_denominator + || numerator == 0 + || denominator == 0 + || numerator > usize::MAX as u128 + || denominator > usize::MAX as u128 + { + return None; + } + Some((numerator as usize, denominator as usize)) +} + +/// Round `|gap| * numerator / denominator` directly in minimum-subnormal units +/// when the exact rational result lies at or below the normal/subnormal boundary. +/// +/// A normalized binary64 quotient can be correctly rounded in its working +/// binade and still move by one ULP when an exact power-of-two restoration enters +/// the subnormal range. On supported targets a represented significand is at +/// most 53 bits and a `usize` factor at most 64 bits. The only potentially large +/// operation is the exponent shift; if it cannot fit `u128`, the exact result is +/// necessarily above this bounded subnormal path and the caller keeps its normal +/// overflow-safe implementation. +fn exact_subnormal_rational_scale( + gap: f64, + numerator: usize, + denominator: usize, +) -> Option> { + if !gap.is_finite() || gap == 0.0 || numerator == 0 || denominator == 0 { + return None; + } + + let magnitude_bits = gap.abs().to_bits(); + let exponent = ((magnitude_bits >> 52) & 0x7ff) as u32; + let fraction = magnitude_bits & 0x000f_ffff_ffff_ffff; + let significand = if exponent == 0 { + fraction as u128 + } else { + ((1_u64 << 52) | fraction) as u128 + }; + let product = significand * numerator as u128; + let unit_shift = if exponent == 0 { 0 } else { exponent - 1 }; + let scaled_numerator = product.checked_shl(unit_shift)?; + let denominator = denominator as u128; + + let mut rounded_units = scaled_numerator / denominator; + let remainder = scaled_numerator % denominator; + let twice_remainder = remainder * 2; + if twice_remainder > denominator + || (twice_remainder == denominator && rounded_units & 1 == 1) + { + rounded_units += 1; + } + + if rounded_units == 0 { + return Some(Err(ValidationError::InvalidInput)); + } + let minimum_normal_units = 1_u128 << 52; + if rounded_units > minimum_normal_units { + return None; + } + if rounded_units == minimum_normal_units { + return Some(Ok(f64::MIN_POSITIVE)); + } + Some(Ok(f64::from_bits(rounded_units as u64))) +} + +fn exact_three_level_standard_error( + first_offset: f64, + second_offset: f64, +) -> Result, ValidationError> { + // For a translated three-observation sample `[0, x, y]`, + // `SE(mean)^2 = (x^2 + y^2 - xy) / 9`. Admit the direct identity only when + // every binary64 product/addition is proven error-free and the numerator is + // itself an exact represented square. If raw products overflow or underflow + // to zero, retry the same proof after an exactly reversible power-of-two + // normalization; this preserves the represented geometry instead of making + // proof admission depend on magnitude alone. Other failed proofs stay on the + // general translated path. + let first_square = first_offset * first_offset; + let second_square = second_offset * second_offset; + let cross_product = first_offset * second_offset; + let products_leave_represented_range = !first_square.is_finite() + || !second_square.is_finite() + || !cross_product.is_finite() + || (first_offset != 0.0 && first_square == 0.0) + || (second_offset != 0.0 && second_square == 0.0) + || (first_offset != 0.0 && second_offset != 0.0 && cross_product == 0.0); + if products_leave_represented_range { + let max_magnitude = first_offset.abs().max(second_offset.abs()); + if max_magnitude == 0.0 || !max_magnitude.is_finite() { + return Ok(None); + } + let scale = exact_power_of_two_scale(max_magnitude); + let normalized_first = first_offset / scale; + let normalized_second = second_offset / scale; + if !normalized_first.is_finite() + || !normalized_second.is_finite() + || (first_offset != 0.0 && normalized_first == 0.0) + || (second_offset != 0.0 && normalized_second == 0.0) + || normalized_first * scale != first_offset + || normalized_second * scale != second_offset + { + return Ok(None); + } + + let normalized_first_square = normalized_first * normalized_first; + let normalized_second_square = normalized_second * normalized_second; + let normalized_cross_product = normalized_first * normalized_second; + if !normalized_first_square.is_finite() + || !normalized_second_square.is_finite() + || !normalized_cross_product.is_finite() + || normalized_first.mul_add(normalized_first, -normalized_first_square) != 0.0 + || normalized_second.mul_add(normalized_second, -normalized_second_square) != 0.0 + || normalized_first.mul_add(normalized_second, -normalized_cross_product) != 0.0 + { + return Ok(None); + } + + let normalized_square_sum = normalized_first_square + normalized_second_square; + if !normalized_square_sum.is_finite() + || subtraction_roundoff( + normalized_first_square, + -normalized_second_square, + normalized_square_sum, + ) != 0.0 + { + return Ok(None); + } + let normalized_radicand = normalized_square_sum - normalized_cross_product; + if !normalized_radicand.is_finite() + || subtraction_roundoff( + normalized_square_sum, + normalized_cross_product, + normalized_radicand, + ) != 0.0 + { + return Ok(None); + } + + let normalized_exact_root = normalized_radicand.sqrt(); + if normalized_exact_root.mul_add(normalized_exact_root, -normalized_radicand) != 0.0 { + return Ok(None); + } + let normalized_standard_error = + deterministic_representable_sum_over_count(&[normalized_exact_root], 3)?; + let standard_error = scale * normalized_standard_error; + if !standard_error.is_finite() + || (standard_error == 0.0 && normalized_standard_error != 0.0) + { + return Err(ValidationError::InvalidInput); + } + return Ok(Some(standard_error)); + } + + if first_offset.mul_add(first_offset, -first_square) != 0.0 + || second_offset.mul_add(second_offset, -second_square) != 0.0 + || first_offset.mul_add(second_offset, -cross_product) != 0.0 + { + return Ok(None); + } + + let square_sum = first_square + second_square; + if !square_sum.is_finite() + || subtraction_roundoff(first_square, -second_square, square_sum) != 0.0 + { + return Ok(None); + } + let radicand = square_sum - cross_product; + if !radicand.is_finite() || subtraction_roundoff(square_sum, cross_product, radicand) != 0.0 { + return Ok(None); + } + + let exact_root = radicand.sqrt(); + if exact_root.mul_add(exact_root, -radicand) != 0.0 { + return Ok(None); + } + + let standard_error = + if let Some(subnormal_result) = exact_subnormal_rational_scale(exact_root, 1, 3) { + subnormal_result? + } else { + deterministic_representable_sum_over_count(&[exact_root], 3)? + }; + Ok(Some(standard_error)) +} + +fn translated_residuals_from_anchor( + diffs: &[f64], + roundoffs: &[f64], + anchor_index: usize, +) -> Option> { + let anchor_high = diffs[anchor_index]; + let anchor_low = roundoffs[anchor_index]; + let mut translated = Vec::with_capacity(diffs.len()); + + for (&high, &low) in diffs.iter().zip(roundoffs) { + let high_delta = high - anchor_high; + if !high_delta.is_finite() + || subtraction_roundoff(high, anchor_high, high_delta) != 0.0 + { + return None; + } + + let low_delta = low - anchor_low; + if !low_delta.is_finite() + || subtraction_roundoff(low, anchor_low, low_delta) != 0.0 + { + return None; + } + + let delta = high_delta + low_delta; + if !delta.is_finite() || subtraction_roundoff(high_delta, -low_delta, delta) != 0.0 { + return None; + } + translated.push(delta); + } + + Some(translated) +} + +fn canonical_exact_translated_residuals(diffs: &[f64], roundoffs: &[f64]) -> Option> { + let mut best: Option<(usize, f64, Vec)> = None; + + for anchor_index in 0..diffs.len() { + let Some(translated) = translated_residuals_from_anchor(diffs, roundoffs, anchor_index) else { + continue; + }; + let max_magnitude = translated + .iter() + .map(|value| value.abs()) + .fold(0.0, f64::max); + + let should_replace = match &best { + None => true, + Some((best_index, best_max_magnitude, _)) => max_magnitude + .total_cmp(best_max_magnitude) + .then_with(|| diffs[anchor_index].total_cmp(&diffs[*best_index])) + .then_with(|| roundoffs[anchor_index].total_cmp(&roundoffs[*best_index])) + .is_lt(), + }; + if should_replace { + best = Some((anchor_index, max_magnitude, translated)); + } + } + + best.map(|(_, _, translated)| translated) +} + +fn exact_translated_residual_standard_error( + diffs: &[f64], + roundoffs: &[f64], +) -> Result, ValidationError> { + // A translated second moment is order-invariant, so admission must not depend + // on whichever observation happened to arrive first. Search every candidate + // anchor that preserves exact high, low and recombined deltas, prefer the one + // with the smallest maximum translated magnitude, then break ties in canonical + // represented `(high, low)` order. This keeps the choice permutation-invariant + // while minimizing the dynamic range exposed to the later square/sqrt path. + let Some(translated) = canonical_exact_translated_residuals(diffs, roundoffs) else { + return Ok(None); + }; + + let max_magnitude = translated + .iter() + .map(|value| value.abs()) + .fold(0.0, f64::max); + if max_magnitude == 0.0 { + return Ok(Some(0.0)); + } + + let mut zero_count = 0_usize; + let mut repeated_gap = None; + let mut gap_count = 0_usize; + let mut exactly_two_levels = true; + for &value in &translated { + if value == 0.0 { + zero_count += 1; + } else if let Some(gap) = repeated_gap { + if value != gap { + exactly_two_levels = false; + break; + } + gap_count += 1; + } else { + repeated_gap = Some(value); + gap_count = 1; + } + } + if exactly_two_levels && let Some(gap) = repeated_gap { + let rational_scale = exact_two_level_rational_scale(zero_count, gap_count); + let standard_error = if zero_count == 1 || gap_count == 1 { + // For an exactly translated two-level sample where either level + // occurs once, SE(mean) simplifies to |gap| / n. + gap.abs() / translated.len() as f64 + } else if let Some((numerator, denominator)) = rational_scale { + // A normalized quotient can double-round when its exact power-of-two + // restoration lands in the subnormal range. Round exact represented + // minimum-subnormal units first when that bounded case applies; + // otherwise retain the existing overflow-safe sum-over-count path. + if let Some(subnormal_result) = + exact_subnormal_rational_scale(gap, numerator, denominator) + { + subnormal_result? + } else { + let scaled_gap = vec![gap.abs(); numerator]; + deterministic_representable_sum_over_count(&scaled_gap, denominator)? + } + } else { + 0.0 + }; + + if standard_error != 0.0 { + return Ok(Some(standard_error)); + } + if (zero_count == 1 || gap_count == 1 || rational_scale.is_some()) && gap != 0.0 { + return Err(ValidationError::InvalidInput); + } + } + + if translated.len() == 3 { + let nonzero_offsets: Vec<_> = translated + .iter() + .copied() + .filter(|value| *value != 0.0) + .collect(); + if nonzero_offsets.len() == 2 + && let Some(standard_error) = + exact_three_level_standard_error(nonzero_offsets[0], nonzero_offsets[1])? + { + return Ok(Some(standard_error)); + } + } + + // Keep the translated binary64 geometry on an exact dyadic scale. Using the + // largest translated value itself can turn an exactly represented gap d into + // rounded(1/3) * d after the square-root stage and move the final SE by one + // ULP. A power-of-two scale changes only exponents, so restoring it is exact. + let scale = exact_power_of_two_scale(max_magnitude); + + let normalized: Vec<_> = translated.iter().map(|value| *value / scale).collect(); + if translated + .iter() + .zip(&normalized) + .any(|(value, normalized_value)| *value != 0.0 && *normalized_value == 0.0) + { + return Ok(None); + } + + let normalized_sum = deterministic_compensated_sum(normalized.clone()); + let normalized_square_sum = deterministic_compensated_sum( + normalized + .iter() + .map(|value| value * value) + .collect(), + ); + let sample_count = translated.len() as f64; + let scaled_square_sum = sample_count * normalized_square_sum; + let dispersion_numerator = + (-normalized_sum).mul_add(normalized_sum, scaled_square_sum); + if !dispersion_numerator.is_finite() || dispersion_numerator <= 0.0 { + return Ok(None); + } + + let denominator = sample_count * sample_count * (sample_count - 1.0); + let normalized_standard_error = (dispersion_numerator / denominator).sqrt(); + let standard_error = scale * normalized_standard_error; + if !standard_error.is_finite() + || (standard_error == 0.0 && normalized_standard_error != 0.0) + { + Err(ValidationError::InvalidInput) + } else { + Ok(Some(standard_error)) + } +} /// Mean signed bias `mean(recovered − truth)`. /// +/// Exactly representable finite signed residuals use the canonical +/// cancellation-safe mean directly. If a finite pairwise subtraction discards +/// represented low-order mass, or if the subtraction overflows even though the +/// final mean bias can remain representable, TEPP expands the same algebraic +/// numerator into recovered values plus negated truth values. The canonical +/// cancellation path then carries represented input mass through the original +/// paired-observation denominator instead of making a rounded pairwise residual +/// authoritative. This preserves the existing fast path when every subtraction +/// is exact while avoiding subtraction-rounding and overflow artifacts. +/// /// # Errors /// -/// Returns [`ValidationError::InvalidInput`] for empty, unequal-length, -/// non-finite inputs, or arithmetic overflow to a non-finite mean. +/// Returns [`ValidationError::InvalidInput`] for empty, unequal-length, or +/// non-finite inputs, or for an unrepresentable nonzero final mean bias. pub fn mean_bias(truth: &[f64], recovered: &[f64]) -> Result { require_paired_finite(truth, recovered)?; - let mut sum = 0.0_f64; - for (t, r) in truth.iter().zip(recovered) { - let diff = r - t; - if !diff.is_finite() { - return Err(ValidationError::InvalidInput); - } - sum += diff; - if !sum.is_finite() { - return Err(ValidationError::InvalidInput); + + let mut residuals = Vec::with_capacity(truth.len()); + let mut requires_expanded_numerator = false; + for (truth_value, recovered_value) in truth.iter().zip(recovered) { + let residual = recovered_value - truth_value; + if !residual.is_finite() { + requires_expanded_numerator = true; + break; } + requires_expanded_numerator |= + subtraction_has_roundoff(*recovered_value, *truth_value, residual); + residuals.push(residual); + } + if !requires_expanded_numerator { + return deterministic_representable_mean(&residuals); } - require_finite(sum / truth.len() as f64) + + let mut expanded_terms = Vec::with_capacity(truth.len().saturating_mul(2)); + expanded_terms.extend_from_slice(recovered); + expanded_terms.extend(truth.iter().map(|value| -*value)); + deterministic_representable_sum_over_count(&expanded_terms, truth.len()) } /// Standard error of the mean signed bias under independent observations. /// +/// The signed-difference mean uses the same cancellation-safe deterministic +/// reference as [`mean_bias`]. Squared deviations are accumulated only after +/// scaling by their largest magnitude, and the standard error is formed from +/// `sqrt(sum(d²) / (n * (n - 1)))` so an avoidable intermediate variance or +/// separately rounded `sqrt(n)` cannot change the final represented result. +/// For any two finite represented residuals, the exact two-observation identity +/// `SE = |r₁-r₂| / 2` is evaluated before a rounded residual mean can distort +/// dispersion. If either pairwise residual subtraction discarded represented +/// low-order input mass, the same identity is evaluated from the expanded +/// recovered/truth inputs instead of the rounded residuals. For larger samples +/// whose rounded residuals all collapse to the same binary64 value, the common +/// high part cannot contribute to dispersion. TEPP therefore first evaluates a +/// translation-invariant second moment directly from the error-free subtraction +/// low terms when their anchor-relative differences are exactly representable; +/// only cases that cannot prove that exact translation retain the predecessor +/// rounded-low-term mean path. For other larger samples, including exact pairwise +/// residuals, TEPP uses the `high + low` decomposition and the same translated +/// second moment whenever every anchor-relative high delta, low delta, and +/// combined residual delta is exactly representable. Exact candidate translation +/// anchors are compared by their maximum translated magnitude first and canonical +/// represented `(high, low)` order second, preserving permutation invariance while +/// avoiding an unnecessarily wide square/sqrt working range. For an exactly +/// translated two-level sample where either level occurs once, the algebraic +/// identity `SE(mean) = |level_gap| / n` is evaluated directly. Non-singleton +/// two-level count geometries whose reduced count factor is an exact rational +/// square are likewise applied as a represented rational scale before moment +/// reconstruction; if that exact rational result is subnormal, TEPP rounds once +/// in represented minimum-subnormal units instead of normalizing and restoring +/// through a second binary64 rounding boundary. An exactly translated three-level +/// sample also uses `SE(mean)^2 = (x² + y² - xy) / 9` when every product and +/// addition is proven error-free and that numerator is itself an exact represented +/// square; if those raw products overflow or underflow to zero, the same proof is +/// retried on an exactly reversible power-of-two normalization so the identity is +/// scale-invariant and a representable nonzero dispersion cannot be mistaken for +/// zero. The exact root is divided by three once instead of reconstructing the +/// same rational square through rounded normalized moments. The general translated +/// path avoids making a rounded residual mean authoritative before dispersion is +/// evaluated. Its normalization uses an exact power-of-two scale so the translated +/// geometry is not re-rounded through an arbitrary magnitude before the final SE +/// is restored. Cases that cannot prove those translated deltas or exact three-level +/// identity representable retain the predecessor translated or rounded-residual +/// path. Individual signed residuals must still be representable because their +/// dispersion is itself part of the requested scientific result. +/// /// # Errors /// -/// Returns [`ValidationError::InvalidInput`] for invalid pairs, `n < 2`, or -/// non-finite intermediate bias arithmetic. +/// Returns [`ValidationError::InvalidInput`] for invalid pairs, `n < 2`, an +/// unrepresentable signed residual or mean, or an unrepresentable final +/// standard error. pub fn bias_standard_error(truth: &[f64], recovered: &[f64]) -> Result { if truth.len() < 2 { return Err(ValidationError::InvalidInput); } - require_paired_finite(truth, recovered)?; - let mut diffs = Vec::with_capacity(truth.len()); - for (t, r) in truth.iter().zip(recovered) { - let diff = r - t; - if !diff.is_finite() { - return Err(ValidationError::InvalidInput); + let diffs = signed_residuals(truth, recovered)?; + let subtraction_roundoffs: Vec<_> = truth + .iter() + .zip(recovered) + .zip(&diffs) + .map(|((truth_value, recovered_value), residual)| { + subtraction_roundoff(*recovered_value, *truth_value, *residual) + }) + .collect(); + let has_subtraction_roundoff = subtraction_roundoffs.iter().any(|roundoff| *roundoff != 0.0); + + if diffs.len() == 2 { + let half_difference = if has_subtraction_roundoff { + let expanded_difference = [recovered[0], -truth[0], -recovered[1], truth[1]]; + deterministic_representable_sum_over_count(&expanded_difference, 2)? + } else { + let represented_difference = [diffs[0], -diffs[1]]; + deterministic_representable_sum_over_count(&represented_difference, 2)? + }; + return Ok(half_difference.abs()); + } + + if has_subtraction_roundoff && diffs.iter().all(|residual| *residual == diffs[0]) { + let zero_roundoffs = vec![0.0; subtraction_roundoffs.len()]; + if let Some(standard_error) = + exact_translated_residual_standard_error(&subtraction_roundoffs, &zero_roundoffs)? + { + return Ok(standard_error); } - diffs.push(diff); + let roundoff_mean = deterministic_representable_mean(&subtraction_roundoffs)?; + return scaled_standard_error(&subtraction_roundoffs, roundoff_mean); } - let mean = require_finite(diffs.iter().sum::() / diffs.len() as f64)?; - let mut variance_sum = 0.0_f64; - for diff in &diffs { - let delta = diff - mean; - let square = delta * delta; - if !square.is_finite() { - return Err(ValidationError::InvalidInput); + + if diffs.len() > 2 { + if let Some(standard_error) = + exact_translated_residual_standard_error(&diffs, &subtraction_roundoffs)? + { + return Ok(standard_error); } - variance_sum += square; } - let variance = variance_sum / (diffs.len() as f64 - 1.0); - require_finite(require_finite(variance.sqrt())? / (diffs.len() as f64).sqrt()) + + let mean = deterministic_representable_mean(&diffs)?; + scaled_standard_error(&diffs, mean) } #[cfg(test)] mod tests { - use super::{bias_standard_error, mean_bias}; + use super::{ + bias_standard_error, exact_subnormal_rational_scale, exact_three_level_standard_error, + mean_bias, + }; use crate::ValidationError; #[test] @@ -68,8 +671,9 @@ mod tests { let truth = [1.0, 2.0, 3.0]; let recovered = [2.0, 3.0, 4.0]; assert!((mean_bias(&truth, &recovered).expect("bias") - 1.0).abs() < 1e-12); + assert_eq!(mean_bias(&[1.0], &[1.0]), Ok(0.0)); let se = bias_standard_error(&truth, &recovered).expect("se"); - assert!((se - 0.0).abs() < 1e-12); + assert_eq!(se, 0.0); assert_eq!(mean_bias(&[], &[]), Err(ValidationError::InvalidInput)); assert_eq!( mean_bias(&[1.0], &[1.0, 2.0]), @@ -84,7 +688,7 @@ mod tests { Err(ValidationError::InvalidInput) ); let se_var = bias_standard_error(&[0.0, 0.0], &[1.0, -1.0]).expect("se"); - assert!(se_var > 0.0); + assert_eq!(se_var, 1.0); assert_eq!( mean_bias(&[f64::MAX], &[-f64::MAX]), Err(ValidationError::InvalidInput) @@ -96,24 +700,129 @@ mod tests { } #[test] - fn overflow_and_nonfinite_intermediates_fail_closed() { + fn representable_extreme_constant_bias_does_not_fail_on_raw_sum_overflow() { assert_eq!( mean_bias(&[0.0, 0.0], &[f64::MAX, f64::MAX]), - Err(ValidationError::InvalidInput) + Ok(f64::MAX) + ); + assert_eq!( + bias_standard_error(&[0.0, 0.0], &[f64::MAX, f64::MAX]), + Ok(0.0) ); + } + + #[test] + fn exact_zero_and_unrepresentable_nonzero_bias_are_distinct() { + let ulp = f64::from_bits(1); + assert_eq!(mean_bias(&[0.0, 0.0], &[ulp, -ulp]), Ok(0.0)); assert_eq!( - mean_bias(&[-f64::MAX], &[f64::MAX]), + mean_bias(&[0.0, 0.0], &[ulp, 0.0]), Err(ValidationError::InvalidInput) ); + } + + #[test] + fn representable_standard_error_avoids_square_and_variance_overflow() { assert_eq!( bias_standard_error(&[0.0, 0.0], &[f64::MAX, -f64::MAX]), - Err(ValidationError::InvalidInput) + Ok(f64::MAX) ); - // Squared deviation overflows for extreme residuals. let huge = 1e200; assert_eq!( bias_standard_error(&[0.0, 0.0], &[huge, -huge]), - Err(ValidationError::InvalidInput) + Ok(huge) + ); + let square_sum_overflows = 1e154; + assert_eq!( + bias_standard_error(&[0.0, 0.0], &[square_sum_overflows, -square_sum_overflows]), + Ok(square_sum_overflows) + ); + let three_point = bias_standard_error( + &[0.0, 0.0, 0.0], + &[square_sum_overflows, -square_sum_overflows, 0.0], + ) + .expect("representable three-point standard error"); + let expected = square_sum_overflows / 3.0_f64.sqrt(); + assert!(((three_point - expected) / expected).abs() <= f64::EPSILON); + } + + #[test] + fn overflowing_direct_deviation_uses_scaled_reference() { + let standard_error = bias_standard_error( + &[0.0, 0.0, 0.0], + &[f64::MAX, -f64::MAX, -f64::MAX], + ) + .expect("scaled deviation path"); + assert!(standard_error.is_finite()); + assert!(standard_error > 0.0); + } + + #[test] + fn exact_subnormal_rational_scale_covers_projection_boundaries() { + assert_eq!(exact_subnormal_rational_scale(0.0, 1, 1), None); + assert_eq!(exact_subnormal_rational_scale(f64::INFINITY, 1, 1), None); + assert_eq!(exact_subnormal_rational_scale(1.0, 0, 1), None); + assert_eq!(exact_subnormal_rational_scale(1.0, 1, 0), None); + + assert_eq!( + exact_subnormal_rational_scale(f64::from_bits(22), 3, 44), + Some(Ok(f64::from_bits(2))) + ); + assert_eq!( + exact_subnormal_rational_scale(f64::from_bits(66), 3, 44), + Some(Ok(f64::from_bits(4))) + ); + assert_eq!( + exact_subnormal_rational_scale(f64::from_bits(1), 3, 44), + Some(Err(ValidationError::InvalidInput)) + ); + assert_eq!( + exact_subnormal_rational_scale(f64::from_bits(0x004d_5555_5555_5555), 3, 44), + Some(Ok(f64::MIN_POSITIVE)) + ); + assert_eq!(exact_subnormal_rational_scale(f64::MIN_POSITIVE * 2.0, 1, 1), None); + assert_eq!(exact_subnormal_rational_scale(f64::MAX, 1, 1), None); + } + + #[test] + fn exact_three_level_standard_error_is_bounded_by_error_free_identity() { + let unit = 2.0_f64.powi(-10); + assert_eq!( + exact_three_level_standard_error(-5.0 * unit, 16.0 * unit) + .expect("exact identity") + .expect("exact rational-square path") + .to_bits(), + 0x3f79_5555_5555_5555 + ); + let large_unit = 2.0_f64.powi(590); + assert_eq!( + exact_three_level_standard_error(-5.0 * large_unit, 16.0 * large_unit) + .expect("scale-invariant exact identity") + .expect("normalized rational-square path") + .to_bits(), + 0x64f9_5555_5555_5555 + ); + assert_eq!( + exact_three_level_standard_error(-f64::MIN_POSITIVE, f64::MIN_POSITIVE) + .expect("underflow-normalized fallback"), + None + ); + assert_eq!( + exact_three_level_standard_error(-1.0, 2.0).expect("fallback"), + None + ); + assert_eq!( + exact_three_level_standard_error(f64::MAX, -f64::MAX).expect("overflow fallback"), + None + ); + assert_eq!( + exact_three_level_standard_error(0.1, 0.2).expect("inexact-product fallback"), + None + ); + assert_eq!( + exact_three_level_standard_error(1.0, 2.0_f64.powi(-27)) + .expect("inexact-sum fallback"), + None ); } } diff --git a/crates/validation_core/src/bias_se.rs b/crates/validation_core/src/bias_se.rs new file mode 100644 index 000000000..87dc38331 --- /dev/null +++ b/crates/validation_core/src/bias_se.rs @@ -0,0 +1,801 @@ +//! Exact represented-input admission for mean-bias standard error. +//! +//! The general bias implementation remains the fallback authority. This module +//! admits a bounded small-sample exact pair-distance identity when represented +//! residuals admit a neutral-zero linear proof or error-free pairwise differences; +//! the exact rational square root is +//! then rounded against binary64 midpoints without first rounding the ratio under +//! the square root. + +use crate::ValidationError; +use core::cmp::Ordering; + +fn subtraction_roundoff(recovered: f64, truth: f64, residual: f64) -> f64 { + let negated_truth = -truth; + let truth_virtual = residual - recovered; + let recovered_virtual = residual - truth_virtual; + let recovered_roundoff = recovered - recovered_virtual; + let truth_roundoff = negated_truth - truth_virtual; + recovered_roundoff + truth_roundoff +} + +fn positive_dyadic(value: f64) -> Option<(u128, i32)> { + if !value.is_finite() || value <= 0.0 { + return None; + } + let bits = value.to_bits(); + let exponent_bits = i32::try_from((bits >> 52) & 0x7ff).ok()?; + let fraction = bits & 0x000f_ffff_ffff_ffff; + let (mut significand, mut exponent) = if exponent_bits == 0 { + (u128::from(fraction), -1074) + } else { + ( + u128::from((1_u64 << 52) | fraction), + exponent_bits - 1023 - 52, + ) + }; + if significand == 0 { + return None; + } + let trailing = significand.trailing_zeros(); + significand >>= trailing; + exponent += i32::try_from(trailing).ok()?; + Some((significand, exponent)) +} + +fn multiply_by_power_of_two(value: u128, shift: u32) -> Option { + let factor = 1_u128.checked_shl(shift)?; + value.checked_mul(factor) +} + +#[derive(Clone, Copy, Debug, Eq, Ord, PartialEq, PartialOrd)] +struct Wide256 { + high: u128, + low: u128, +} + +impl Wide256 { + const fn from_u128(value: u128) -> Self { + Self { + high: 0, + low: value, + } + } + + fn multiply_u128(left: u128, right: u128) -> Self { + let mask = u128::from(u64::MAX); + let left_limbs = [ + u64::try_from(left & mask).expect("masked low limb fits u64"), + u64::try_from(left >> 64).expect("high limb fits u64"), + ]; + let right_limbs = [ + u64::try_from(right & mask).expect("masked low limb fits u64"), + u64::try_from(right >> 64).expect("high limb fits u64"), + ]; + let mut limbs = [0_u64; 4]; + + for (left_index, left_limb) in left_limbs.iter().copied().enumerate() { + let mut carry = 0_u128; + for (right_index, right_limb) in right_limbs.iter().copied().enumerate() { + let limb_index = left_index + right_index; + let accumulator = u128::from(left_limb) + .checked_mul(u128::from(right_limb)) + .expect("64-bit limb product fits u128") + .checked_add(u128::from(limbs[limb_index])) + .expect("schoolbook partial sum fits u128") + .checked_add(carry) + .expect("schoolbook carry sum fits u128"); + limbs[limb_index] = + u64::try_from(accumulator & mask).expect("masked schoolbook limb fits u64"); + carry = accumulator >> 64; + } + limbs[left_index + 2] = + u64::try_from(carry).expect("schoolbook multiplication carry fits u64"); + } + + Self { + high: u128::from(limbs[2]) | (u128::from(limbs[3]) << 64), + low: u128::from(limbs[0]) | (u128::from(limbs[1]) << 64), + } + } + + fn checked_sub(self, right: Self) -> Option { + if self < right { + return None; + } + let borrow = if self.low < right.low { 1_u128 } else { 0_u128 }; + Some(Self { + high: self.high.checked_sub(right.high)?.checked_sub(borrow)?, + low: self.low.wrapping_sub(right.low), + }) + } + + const fn to_u128(self) -> Option { + if self.high == 0 { Some(self.low) } else { None } + } + + const fn is_zero(self) -> bool { + self.high == 0 && self.low == 0 + } + + fn bit_len(self) -> u32 { + if self.high != 0 { + 128 + (u128::BITS - self.high.leading_zeros()) + } else { + u128::BITS - self.low.leading_zeros() + } + } + + fn bit(self, index: u32) -> bool { + if index < u128::BITS { + ((self.low >> index) & 1) != 0 + } else { + let high_index = index - u128::BITS; + ((self.high >> high_index) & 1) != 0 + } + } +} + +fn compare_scaled_wide( + left: Wide256, + left_exponent: i32, + right: Wide256, + right_exponent: i32, +) -> Option { + match (left.is_zero(), right.is_zero()) { + (true, true) => return Some(Ordering::Equal), + (true, false) => return Some(Ordering::Less), + (false, true) => return Some(Ordering::Greater), + (false, false) => {} + } + + let left_bits = left.bit_len(); + let right_bits = right.bit_len(); + let left_top = left_exponent.checked_add(i32::try_from(left_bits.checked_sub(1)?).ok()?)?; + let right_top = right_exponent.checked_add(i32::try_from(right_bits.checked_sub(1)?).ok()?)?; + match left_top.cmp(&right_top) { + Ordering::Less => return Some(Ordering::Less), + Ordering::Greater => return Some(Ordering::Greater), + Ordering::Equal => {} + } + + let width = left_bits.max(right_bits); + for offset in 0..width { + let left_bit = offset < left_bits && left.bit(left_bits - 1 - offset); + let right_bit = offset < right_bits && right.bit(right_bits - 1 - offset); + match left_bit.cmp(&right_bit) { + Ordering::Less => return Some(Ordering::Less), + Ordering::Greater => return Some(Ordering::Greater), + Ordering::Equal => {} + } + } + Some(Ordering::Equal) +} + +fn compare_scaled_ratio_to_dyadic_square( + numerator: u128, + numerator_exponent: i32, + denominator: u128, + significand: u128, + exponent: i32, +) -> Option { + let square = significand.checked_mul(significand)?; + let square_exponent = exponent.checked_mul(2)?; + let right = Wide256::multiply_u128(denominator, square); + compare_scaled_wide( + Wide256::from_u128(numerator), + numerator_exponent, + right, + square_exponent, + ) +} + +fn midpoint_dyadic(left: f64, right: f64) -> Option<(u128, i32)> { + let (left_significand, left_exponent) = positive_dyadic(left)?; + let (right_significand, right_exponent) = positive_dyadic(right)?; + let common_exponent = left_exponent.min(right_exponent); + let left_shift = left_exponent.checked_sub(common_exponent)?.unsigned_abs(); + let right_shift = right_exponent.checked_sub(common_exponent)?.unsigned_abs(); + let left_units = multiply_by_power_of_two(left_significand, left_shift)?; + let right_units = multiply_by_power_of_two(right_significand, right_shift)?; + let mut midpoint_significand = left_units.checked_add(right_units)?; + let mut midpoint_exponent = common_exponent.checked_sub(1)?; + let trailing = midpoint_significand.trailing_zeros(); + midpoint_significand >>= trailing; + midpoint_exponent += i32::try_from(trailing).ok()?; + Some((midpoint_significand, midpoint_exponent)) +} + +fn exact_power_of_two(exponent: i32) -> Option { + if (-1022..=1023).contains(&exponent) { + let biased_exponent = u64::try_from(exponent + 1023).ok()?; + return Some(f64::from_bits(biased_exponent << 52)); + } + if (-1074..=-1023).contains(&exponent) { + let shift = u32::try_from(exponent + 1074).ok()?; + return Some(f64::from_bits(1_u64 << shift)); + } + None +} + +fn correctly_rounded_scaled_sqrt_ratio( + numerator: u128, + denominator: u128, + unit_exponent: i32, +) -> Option { + const MAX_EXACT_BINARY64_DENOMINATOR: u128 = 1_u128 << 53; + if numerator == 0 || denominator == 0 || denominator > MAX_EXACT_BINARY64_DENOMINATOR { + return None; + } + let unit = exact_power_of_two(unit_exponent)?; + let denominator_f64 = denominator as f64; + // The binary64 numerator conversion is only a seed. The returned value is + // admitted solely after the exact two-limb dyadic-square and midpoint + // comparisons below. This lets the bounded proof retain exact reduced + // numerators above 2^53 without pretending that their seed conversion is exact. + let mut candidate = ((numerator as f64) / denominator_f64).sqrt() * unit; + if !candidate.is_finite() || candidate <= 0.0 { + return None; + } + let target_exponent = unit_exponent.checked_mul(2)?; + + for _ in 0..4 { + let (candidate_significand, candidate_exponent) = positive_dyadic(candidate)?; + let candidate_comparison = compare_scaled_ratio_to_dyadic_square( + numerator, + target_exponent, + denominator, + candidate_significand, + candidate_exponent, + )?; + if candidate_comparison == Ordering::Equal { + return Some(candidate); + } + + let upward = candidate_comparison == Ordering::Greater; + let bits = candidate.to_bits(); + let neighbor = if upward { + f64::from_bits(bits.checked_add(1)?) + } else { + if bits == 1 { + return None; + } + f64::from_bits(bits - 1) + }; + if !neighbor.is_finite() || neighbor <= 0.0 { + return None; + } + let (midpoint_significand, midpoint_exponent) = midpoint_dyadic(candidate, neighbor)?; + let midpoint_comparison = compare_scaled_ratio_to_dyadic_square( + numerator, + target_exponent, + denominator, + midpoint_significand, + midpoint_exponent, + )?; + + let neighbor_is_closer = if upward { + midpoint_comparison == Ordering::Greater + } else { + midpoint_comparison == Ordering::Less + }; + if neighbor_is_closer { + candidate = neighbor; + continue; + } + if midpoint_comparison == Ordering::Equal && candidate.to_bits() & 1 == 1 { + return Some(neighbor); + } + return Some(candidate); + } + None +} + +fn exact_pairwise_pair_square_sum(residuals: &[f64]) -> Option<(u128, i32)> { + let pair_count = residuals + .len() + .checked_mul(residuals.len().checked_sub(1)?)? + / 2; + let mut pair_dyadics = Vec::with_capacity(pair_count); + let mut unit_exponent = i32::MAX; + for left in 0..residuals.len() { + for right in left + 1..residuals.len() { + let difference = residuals[left] - residuals[right]; + if !difference.is_finite() + || subtraction_roundoff(residuals[left], residuals[right], difference) != 0.0 + { + return None; + } + if difference == 0.0 { + pair_dyadics.push(None); + continue; + } + let dyadic = positive_dyadic(difference.abs())?; + unit_exponent = unit_exponent.min(dyadic.1); + pair_dyadics.push(Some(dyadic)); + } + } + if unit_exponent == i32::MAX { + return Some((0, 0)); + } + + let mut pair_square_sum = 0_u128; + for dyadic in pair_dyadics.into_iter().flatten() { + let shift = dyadic.1.checked_sub(unit_exponent)?.unsigned_abs(); + let coefficient = multiply_by_power_of_two(dyadic.0, shift)?; + let square = coefficient.checked_mul(coefficient)?; + pair_square_sum = pair_square_sum.checked_add(square)?; + } + Some((pair_square_sum, unit_exponent)) +} + +fn exact_neutral_zero_linear_pair_square_sum(residuals: &[f64]) -> Option<(u128, i32)> { + // Every finite represented residual is already an exact coordinate relative + // to neutral zero. First choose the common dyadic unit, then rescan to build + // the signed first moment and square sum. This keeps the proof O(n) with O(1) + // proof storage and avoids materializing pair distances. + let mut unit_exponent = i32::MAX; + for &coordinate in residuals { + if coordinate == 0.0 { + continue; + } + let (_, exponent) = positive_dyadic(coordinate.abs())?; + unit_exponent = unit_exponent.min(exponent); + } + if unit_exponent == i32::MAX { + return Some((0, 0)); + } + + let mut positive_sum = 0_u128; + let mut negative_sum = 0_u128; + let mut square_sum = 0_u128; + for &coordinate in residuals { + if coordinate == 0.0 { + continue; + } + let (significand, exponent) = positive_dyadic(coordinate.abs())?; + let shift = exponent.checked_sub(unit_exponent)?.unsigned_abs(); + let coefficient = multiply_by_power_of_two(significand, shift)?; + if coordinate.is_sign_negative() { + negative_sum = negative_sum.checked_add(coefficient)?; + } else { + positive_sum = positive_sum.checked_add(coefficient)?; + } + square_sum = square_sum.checked_add(coefficient.checked_mul(coefficient)?)?; + } + + let sample_count = u128::try_from(residuals.len()).ok()?; + let scaled_square_sum = Wide256::multiply_u128(sample_count, square_sum); + let signed_sum_magnitude = positive_sum.abs_diff(negative_sum); + let squared_sum = Wide256::multiply_u128(signed_sum_magnitude, signed_sum_magnitude); + let numerator = scaled_square_sum.checked_sub(squared_sum)?.to_u128()?; + Some((numerator, unit_exponent)) +} + +fn exact_pair_distance_standard_error( + truth: &[f64], + recovered: &[f64], +) -> Option> { + // Keep the exact proof deliberately bounded. n=2 and n=3 have cheaper exact + // identities in `bias.rs`; four through sixteen observations are the smallest + // remaining sample sizes with demonstrated one-ULP errors in translated + // floating moment/sqrt paths. + if truth.len() != recovered.len() || !(4..=16).contains(&truth.len()) { + return None; + } + let sample_count = truth.len(); + + let mut residuals = Vec::with_capacity(sample_count); + for index in 0..sample_count { + let truth_value = truth[index]; + let recovered_value = recovered[index]; + if !truth_value.is_finite() || !recovered_value.is_finite() { + return None; + } + let residual = recovered_value - truth_value; + if !residual.is_finite() + || subtraction_roundoff(recovered_value, truth_value, residual) != 0.0 + { + return None; + } + residuals.push(residual); + } + + // Attempt the neutral-zero two-pass proof first: finite represented residuals + // are exact coordinates around zero, so this is O(n) with O(1) proof storage. + // Keep pairwise O(n²) only as a fail-closed comparison reference while broader + // represented-input equivalence and release-mode budgets are characterized. + // The former conditioned observed-anchor scan was removed because no production + // fixture demonstrated unique admission after the neutral-zero proof refused. + let (pair_square_sum, unit_exponent) = exact_neutral_zero_linear_pair_square_sum(&residuals) + .or_else(|| exact_pairwise_pair_square_sum(&residuals))?; + if pair_square_sum == 0 { + return Some(Ok(0.0)); + } + + // For n observations, sum((ri-rj)^2, i Result { + if let Some(result) = exact_pair_distance_standard_error(truth, recovered) { + return result; + } + crate::bias::bias_standard_error(truth, recovered) +} + +#[cfg(test)] +mod tests { + use super::{ + Wide256, compare_scaled_wide, correctly_rounded_scaled_sqrt_ratio, + exact_neutral_zero_linear_pair_square_sum, exact_pair_distance_standard_error, + exact_pairwise_pair_square_sum, exact_power_of_two, + midpoint_dyadic, multiply_by_power_of_two, positive_dyadic, + }; + use core::cmp::Ordering; + + #[test] + fn exact_ratio_sqrt_corrects_both_adjacent_rounding_directions() { + assert_eq!( + correctly_rounded_scaled_sqrt_ratio(116, 48, 0) + .expect("bounded exact ratio") + .to_bits(), + 0x3ff8_df7d_a2e6_6e88 + ); + assert_eq!( + correctly_rounded_scaled_sqrt_ratio(136, 48, 0) + .expect("bounded exact ratio") + .to_bits(), + 0x3ffa_ee98_6a40_25f8 + ); + assert_eq!(correctly_rounded_scaled_sqrt_ratio(48, 48, 0), Some(1.0)); + assert_eq!( + correctly_rounded_scaled_sqrt_ratio(1_739_374_438_758_325_417, 16, 0) + .expect("large exact numerator remains bounded by exact midpoint proof") + .to_bits(), + 0x41b3_a706_d408_9e32 + ); + assert_eq!( + correctly_rounded_scaled_sqrt_ratio(155_324_619_328_335_851, 25, 0) + .expect("five-observation reduced ratio remains bounded") + .to_bits(), + 0x4192_caf1_6406_5ad0 + ); + assert_eq!( + correctly_rounded_scaled_sqrt_ratio(621_603_287_214_182_303, 45, 0) + .expect("six-observation reduced ratio remains bounded") + .to_bits(), + 0x419c_057d_42fc_5857 + ); + } + + #[test] + fn exact_ratio_sqrt_admits_wide_scaled_products_for_represented_n2050_boundary() { + assert_eq!( + correctly_rounded_scaled_sqrt_ratio( + 332_306_998_946_228_931_332_463_617_650_984_961, + 8_610_922_500, + 0, + ) + .expect("exact represented boundary survives comparison-product width") + .to_bits(), + 0x4296_998e_1aff_78de + ); + } + + #[test] + fn wide_scaled_comparison_covers_full_width_exponents_and_zero_ordering() { + let zero = Wide256::from_u128(0); + let one = Wide256::from_u128(1); + let two = Wide256::from_u128(2); + let three = Wide256::from_u128(3); + let product = Wide256::multiply_u128(u128::MAX, u128::MAX); + + assert_eq!(product.high, u128::MAX - 1); + assert_eq!(product.low, 1); + assert_eq!(product.bit_len(), 256); + assert!(product.bit(255)); + assert!(product.bit(0)); + assert_eq!(one.bit_len(), 1); + assert!(!one.bit(128)); + + assert_eq!(compare_scaled_wide(zero, 0, zero, 0), Some(Ordering::Equal)); + assert_eq!( + compare_scaled_wide(zero, -2_148, one, 2_047), + Some(Ordering::Less) + ); + assert_eq!( + compare_scaled_wide(one, 2_047, zero, -2_148), + Some(Ordering::Greater) + ); + assert_eq!( + compare_scaled_wide(one, -2_148, two, -2_149), + Some(Ordering::Equal) + ); + assert_eq!( + compare_scaled_wide(one, -2_148, three, -2_149), + Some(Ordering::Less) + ); + assert_eq!( + compare_scaled_wide(three, 2_046, one, 2_047), + Some(Ordering::Greater) + ); + assert_eq!(compare_scaled_wide(one, 1, one, 0), Some(Ordering::Greater)); + assert_eq!(compare_scaled_wide(one, 0, one, 1), Some(Ordering::Less)); + } + + #[test] + fn wide_subtraction_preserves_borrow_and_bounded_downcast() { + let left = Wide256 { high: 1, low: 0 }; + let right = Wide256::from_u128(1); + assert_eq!( + left.checked_sub(right), + Some(Wide256 { + high: 0, + low: u128::MAX, + }) + ); + assert_eq!(left.checked_sub(left), Some(Wide256::from_u128(0))); + assert_eq!(right.checked_sub(left), None); + assert_eq!(Wide256::from_u128(7).to_u128(), Some(7)); + assert_eq!(left.to_u128(), None); + } + + #[test] + fn exact_ratio_sqrt_refuses_outside_bounded_proof() { + let too_large_denominator = (1_u128 << 53) + 1; + assert_eq!(correctly_rounded_scaled_sqrt_ratio(0, 48, 0), None); + assert_eq!(correctly_rounded_scaled_sqrt_ratio(1, 0, 0), None); + assert_eq!( + correctly_rounded_scaled_sqrt_ratio(1, too_large_denominator, 0), + None + ); + assert_eq!(correctly_rounded_scaled_sqrt_ratio(1, 48, 1024), None); + assert_eq!(correctly_rounded_scaled_sqrt_ratio(1, 48, -1075), None); + } + + #[test] + fn dyadic_helpers_cover_normal_subnormal_and_refusal_boundaries() { + assert_eq!(positive_dyadic(1.0), Some((1, 0))); + assert_eq!(positive_dyadic(f64::from_bits(1)), Some((1, -1074))); + assert_eq!(positive_dyadic(0.0), None); + assert_eq!(positive_dyadic(-1.0), None); + assert_eq!(positive_dyadic(f64::INFINITY), None); + assert_eq!(exact_power_of_two(0), Some(1.0)); + assert_eq!(exact_power_of_two(-1074), Some(f64::from_bits(1))); + assert_eq!(exact_power_of_two(1024), None); + assert_eq!(exact_power_of_two(-1075), None); + assert_eq!(multiply_by_power_of_two(3, 2), Some(12)); + assert_eq!(multiply_by_power_of_two(1, 128), None); + assert_eq!(multiply_by_power_of_two(u128::MAX, 1), None); + assert!(midpoint_dyadic(1.0, f64::from_bits(1.0_f64.to_bits() + 1)).is_some()); + assert_eq!(midpoint_dyadic(0.0, f64::from_bits(1)), None); + } + + #[test] + fn neutral_zero_linear_route_matches_pairwise_on_common_domain() { + let residuals = [0.0, 1.0, 2.0, 7.0]; + assert_eq!( + exact_neutral_zero_linear_pair_square_sum(&residuals), + exact_pairwise_pair_square_sum(&residuals) + ); + } + + #[test] + fn neutral_zero_linear_route_recovers_pairwise_refusal_geometries() { + let tiny = 2.0_f64.powi(-54); + let small = [0.0, 1.0, tiny, 2.0]; + assert_eq!(exact_pairwise_pair_square_sum(&small), None); + assert!(exact_neutral_zero_linear_pair_square_sum(&small).is_some()); + + let diameter = 9_007_199_254_740_992.0_f64; + let wide = [0.0, 1.0, 2.0, -diameter]; + assert_eq!(exact_pairwise_pair_square_sum(&wide), None); + let (numerator, unit_exponent) = exact_neutral_zero_linear_pair_square_sum(&wide) + .expect("zero is an exact non-minimum translation anchor"); + assert_eq!(unit_exponent, 0); + assert_eq!(numerator, 243_388_915_243_820_099_130_562_543_878_155_u128); + + let no_observed_anchor = [1.0, tiny, 2.0, 3.0]; + assert_eq!(exact_pairwise_pair_square_sum(&no_observed_anchor), None); + let (numerator, unit_exponent) = + exact_neutral_zero_linear_pair_square_sum(&no_observed_anchor) + .expect("neutral zero is exact when no observed residual is a universal anchor"); + assert_eq!(unit_exponent, -54); + assert_eq!( + numerator, + 6_490_371_073_168_534_319_490_338_297_741_315_u128 + ); + } + + #[test] + fn four_observation_identity_is_power_of_two_scale_invariant() { + let truth = [0.0; 4]; + let recovered = [0.0, 1.0, 2.0, 7.0]; + assert_eq!( + exact_pair_distance_standard_error(&truth, &recovered) + .expect("admitted") + .expect("representable") + .to_bits(), + 0x3ff8_df7d_a2e6_6e88 + ); + + let unit = 2.0_f64.powi(400); + let scaled = recovered.map(|value| value * unit); + let expected = f64::from_bits(0x58f8_df7d_a2e6_6e88); + assert_eq!( + exact_pair_distance_standard_error(&truth, &scaled) + .expect("scaled admitted") + .expect("scaled representable"), + expected + ); + } + + #[test] + fn four_observation_identity_reduces_the_exact_ratio_before_bounded_admission() { + let truth = [0.0; 4]; + let recovered = [0.0, 14_099_687.0, 16_729_100.0, 94_045_527.0]; + assert_eq!( + exact_pair_distance_standard_error(&truth, &recovered) + .expect("reduced ratio admitted") + .expect("representable") + .to_bits(), + 0x4174_46e5_76f8_7445 + ); + } + + #[test] + fn four_observation_identity_keeps_large_reduced_numerator_in_bounded_proof() { + let truth = [0.0; 4]; + let recovered = [19_274_968.0, 693_729_138.0, 711_353_557.0, 1_625_519_116.0]; + assert_eq!( + exact_pair_distance_standard_error(&truth, &recovered) + .expect("large reduced ratio admitted") + .expect("representable") + .to_bits(), + 0x41b3_a706_d408_9e32 + ); + } + + #[test] + fn five_observation_identity_keeps_exact_pair_distance_ratio_authoritative() { + let truth = [0.0; 5]; + let recovered = [ + 1_342_748_146.0, + 1_434_848_064.0, + 1_525_257_611.0, + 1_685_877_224.0, + 1_771_341_094.0, + ]; + assert_eq!( + exact_pair_distance_standard_error(&truth, &recovered) + .expect("five-observation ratio admitted") + .expect("representable") + .to_bits(), + 0x4192_caf1_6406_5ad0 + ); + } + + #[test] + fn six_observation_identity_keeps_exact_pair_distance_ratio_authoritative() { + let truth = [0.0; 6]; + let recovered = [ + 1_120_315_269.0, + 1_513_609_015.0, + 1_569_037_659.0, + 1_789_057_504.0, + 1_807_936_669.0, + 1_914_796_738.0, + ]; + assert_eq!( + exact_pair_distance_standard_error(&truth, &recovered) + .expect("six-observation ratio admitted") + .expect("representable") + .to_bits(), + 0x419c_057d_42fc_5857 + ); + } + + #[test] + fn bounded_pair_distance_identity_covers_exact_zero_and_fallbacks() { + let truth = [0.0; 4]; + assert_eq!( + exact_pair_distance_standard_error(&truth, &[3.0; 4]), + Some(Ok(0.0)) + ); + assert_eq!( + exact_pair_distance_standard_error(&truth[..3], &[0.0; 3]), + None + ); + assert_eq!( + exact_pair_distance_standard_error(&[0.0; 10], &[0.0; 10]), + Some(Ok(0.0)) + ); + assert_eq!( + exact_pair_distance_standard_error(&[0.0; 11], &[0.0; 11]), + Some(Ok(0.0)) + ); + assert_eq!( + exact_pair_distance_standard_error(&[0.0; 12], &[0.0; 12]), + Some(Ok(0.0)) + ); + assert_eq!( + exact_pair_distance_standard_error(&[0.0; 13], &[0.0; 13]), + Some(Ok(0.0)) + ); + assert_eq!( + exact_pair_distance_standard_error(&[0.0; 14], &[0.0; 14]), + Some(Ok(0.0)) + ); + assert_eq!( + exact_pair_distance_standard_error(&[0.0; 15], &[0.0; 15]), + Some(Ok(0.0)) + ); + assert_eq!( + exact_pair_distance_standard_error(&[0.0; 16], &[0.0; 16]), + Some(Ok(0.0)) + ); + assert_eq!( + exact_pair_distance_standard_error(&[0.0; 17], &[0.0; 17]), + None + ); + assert_eq!( + exact_pair_distance_standard_error(&truth, &[0.0, 1.0, f64::INFINITY, 2.0]), + None + ); + assert_eq!( + exact_pair_distance_standard_error(&truth, &[f64::MAX, -f64::MAX, 0.0, 0.0]), + None + ); + + let tiny = 2.0_f64.powi(-54); + assert_eq!( + exact_pair_distance_standard_error(&truth, &[0.0, 1.0, tiny, 2.0]) + .expect("neutral-zero linear route admits rounded non-anchor pair") + .expect("represented result") + .to_bits(), + 0x3fde_a33e_2c83_c140 + ); + assert_eq!( + exact_pair_distance_standard_error(&truth, &[1.0, tiny, 2.0, 3.0]) + .expect("neutral-zero linear route admits the represented geometry") + .expect("represented result") + .to_bits(), + 0x3fe4_a7e9_cb8a_3491 + ); + assert_eq!( + exact_pair_distance_standard_error(&[1.0, 0.0, 0.0, 0.0], &[tiny, 0.0, 0.0, 0.0]), + None + ); + assert_eq!( + exact_pair_distance_standard_error(&truth, &[0.0, 1.0, 2.0, 67_108_864.0]), + None + ); + } +} diff --git a/crates/validation_core/src/coverage.rs b/crates/validation_core/src/coverage.rs index 67b42e381..191f9d08d 100644 --- a/crates/validation_core/src/coverage.rs +++ b/crates/validation_core/src/coverage.rs @@ -2,17 +2,11 @@ use crate::ValidationError; -/// Empirical coverage of closed intervals `[lower, upper]` for truth values. -/// -/// # Errors -/// -/// Returns [`ValidationError::InvalidInput`] when vectors are empty, lengths -/// differ, bounds are non-finite, or any interval is inverted (`lower > upper`). -pub fn interval_coverage( +pub(crate) fn interval_covered_count( truth: &[f64], lower: &[f64], upper: &[f64], -) -> Result { +) -> Result { if truth.is_empty() || truth.len() != lower.len() || truth.len() != upper.len() { return Err(ValidationError::InvalidInput); } @@ -27,19 +21,379 @@ pub fn interval_coverage( if lo > hi { return Err(ValidationError::InvalidInput); } - let low_ok = t >= lo; - let high_ok = t <= hi; - if low_ok && high_ok { + if t >= lo && t <= hi { covered += 1; } } - Ok(covered as f64 / truth.len() as f64) + Ok(covered) +} + +fn correctly_rounded_unit_ratio(numerator: u64, denominator: u64) -> f64 { + debug_assert!(denominator > 0); + debug_assert!(numerator <= denominator); + if numerator == 0 { + return 0.0; + } + + // u64/u64 lies in [2^-64, 1], so its binary64 result is always normal. + // Determine floor(log2(numerator/denominator)) without first rounding either + // integer to f64, then round the exact scaled significand ties-to-even. + let numerator_bits = 64_i32 - numerator.leading_zeros() as i32; + let denominator_bits = 64_i32 - denominator.leading_zeros() as i32; + let mut exponent = numerator_bits - denominator_bits; + if exponent == 0 { + if numerator < denominator { + exponent = -1; + } + } else { + let exponent_shift = (-exponent) as u32; + if (numerator as u128) << exponent_shift < denominator as u128 { + exponent -= 1; + } + } + + let significand_shift = (52 - exponent) as u32; + let scaled_numerator = (numerator as u128) << significand_shift; + let denominator_u128 = denominator as u128; + let quotient = scaled_numerator / denominator_u128; + let remainder = scaled_numerator % denominator_u128; + let twice_remainder = remainder << 1; + let round_up = twice_remainder > denominator_u128 + || (twice_remainder == denominator_u128 && quotient & 1 == 1); + let mut significand = quotient + u128::from(round_up); + + if significand == 1_u128 << 53 { + significand >>= 1; + exponent += 1; + } + + debug_assert!((1_u128 << 52..1_u128 << 53).contains(&significand)); + let biased_exponent = (exponent + 1023) as u64; + let fraction = significand as u64 - (1_u64 << 52); + f64::from_bits((biased_exponent << 52) | fraction) +} + +fn u64_is_exact_binary64_integer(value: u64) -> bool { + if value == 0 { + return true; + } + let significant_bits = 64 - value.leading_zeros(); + if significant_bits <= 53 { + return true; + } + let discarded_bits = significant_bits - 53; + let discarded_mask = (1_u64 << discarded_bits) - 1; + value & discarded_mask == 0 +} + +fn positive_f64_over_inexact_u64(value: f64, denominator: u64) -> f64 { + debug_assert!(value.is_finite() && value >= 0.0); + debug_assert!(denominator > (1_u64 << 53)); + debug_assert!(!u64_is_exact_binary64_integer(denominator)); + if value == 0.0 { + return 0.0; + } + + // Decode the exact binary64 value as `significand * 2^exponent`. The + // significand is at most 53 bits, while this path is restricted to an + // inexact u64 denominator above 2^53, so significand/denominator is a unit + // ratio that the integer routine can round without first rounding the + // denominator. Multiplication by an exact power of two then restores the + // original scale. This avoids the extra `round(1/n) * value` step that can + // move the final quotient by one ULP. + let bits = value.to_bits(); + let exponent_bits = ((bits >> 52) & 0x7ff) as i32; + let fraction = bits & ((1_u64 << 52) - 1); + let (significand, exponent) = if exponent_bits == 0 { + (fraction, -1074) + } else { + ((1_u64 << 52) | fraction, exponent_bits - 1023 - 52) + }; + let ratio = correctly_rounded_unit_ratio(significand, denominator); + let power_of_two = if exponent >= -1022 { + f64::from_bits(((exponent + 1023) as u64) << 52) + } else { + f64::from_bits(1_u64 << (exponent + 1074)) + }; + ratio * power_of_two +} + +pub(crate) fn represented_coverage_from_counts( + covered_count: u64, + sample_count: u64, +) -> Result { + if sample_count == 0 || covered_count > sample_count { + return Err(ValidationError::InvalidInput); + } + Ok(correctly_rounded_unit_ratio(covered_count, sample_count)) +} + +/// Empirical coverage of closed intervals `[lower, upper]` for truth values. +/// +/// # Errors +/// +/// Returns [`ValidationError::InvalidInput`] when vectors are empty, lengths +/// differ, bounds are non-finite, or any interval is inverted (`lower > upper`). +pub fn interval_coverage( + truth: &[f64], + lower: &[f64], + upper: &[f64], +) -> Result { + let covered = interval_covered_count(truth, lower, upper)?; + represented_coverage_from_counts(covered as u64, truth.len() as u64) +} + +fn rationalized_wilson_positive_lower(n: f64, p: f64, z: f64, z2: f64) -> f64 { + if z2 >= 1.0 { + // Rationalize the lower root and divide through by z²: + // 2 n p² / (z² + 2 n p + z sqrt(z² + 4 n p (1-p))). + // This form avoids subtracting nearly equal O(z²) terms and avoids + // materializing the O(z²) denominator sum directly. + let normalized_numerator = 2.0 * n * p * p / z2; + let normalized_denominator = 1.0 + + 2.0 * n * p / z2 + + (1.0 + 4.0 * n * p * (1.0 - p) / z2).sqrt(); + return (normalized_numerator / normalized_denominator).clamp(0.0, 1.0); + } + + // For z² < 1, dividing through by z² can overflow even though the Wilson + // endpoint is ordinary and representable. Evaluate the same rationalized + // root on its natural scale instead. + let numerator = 2.0 * n * p * p; + let denominator = + z2 + 2.0 * n * p + z * (z2 + 4.0 * n * p * (1.0 - p)).sqrt(); + (numerator / denominator).clamp(0.0, 1.0) +} + +fn rationalized_wilson_positive_lower_from_inverse_sample_count( + inverse_n: f64, + p: f64, + z: f64, + z2: f64, +) -> f64 { + // This path is used only when the exact u64 denominator cannot be represented + // as binary64. Such counts exceed 2^53, so inverse_n is small enough that + // the natural reciprocal-scale form remains finite even for the largest z + // whose square is finite. It avoids materializing a rounded sample count. + let numerator = 2.0 * p * p; + let inverse_n_squared = inverse_n * inverse_n; + let denominator = z2 * inverse_n + + 2.0 * p + + z * (z2 * inverse_n_squared + 4.0 * p * (1.0 - p) * inverse_n).sqrt(); + (numerator / denominator).clamp(0.0, 1.0) +} + +fn all_covered_wilson_lower_from_inexact_sample_count(sample_count: u64, z2: f64) -> f64 { + let z2_over_n = positive_f64_over_inexact_u64(z2, sample_count); + if z2_over_n <= 1.0 { + // For small z²/n, 1 / (1 + z²/n) can round to exact one before the + // finite-sample miss mass is represented. Subtract the miss mass instead. + let uncovered_mass = z2_over_n / (1.0 + z2_over_n); + (1.0 - uncovered_mass).clamp(0.0, 1.0) + } else { + // For large z²/n, the complementary miss mass rounds to exact one and + // subtraction would erase an ordinary positive lower endpoint. + (1.0 / (1.0 + z2_over_n)).clamp(0.0, 1.0) + } +} + +fn all_covered_wilson_lower_from_exact_sample_count(n: f64, z2: f64) -> f64 { + let denominator = n + z2; + let direct_lower = n / denominator; + if direct_lower == 1.0 && z2 > 0.0 { + // A tiny positive z² can be absorbed when the denominator is formed even + // though the Wilson miss mass is still representable immediately below + // one. Preserve that boundary through the complementary miss fraction. + let uncovered_mass = z2 / denominator; + return (1.0 - uncovered_mass).clamp(0.0, 1.0); + } + + // Recover the exact rounding residual of n + z² with TwoSum. The direct + // quotient is correctly rounded for the rounded denominator, so use the + // residual only to decide whether the exact represented-input quotient lies + // beyond the adjacent binary64 midpoint. Adding a rounded correction can + // itself force a one-ULP move when the true correction is below that midpoint. + let z2_virtual = denominator - n; + let denominator_residual = + (n - (denominator - z2_virtual)) + (z2 - z2_virtual); + if denominator_residual != 0.0 { + let division_residual = (-direct_lower).mul_add(denominator, n); + let exact_residual = + (-direct_lower).mul_add(denominator_residual, division_residual); + if exact_residual != 0.0 { + let neighbor = if exact_residual.is_sign_negative() { + f64::from_bits(direct_lower.to_bits() - 1) + } else { + f64::from_bits(direct_lower.to_bits() + 1) + }; + let ulp_toward_exact = (neighbor - direct_lower).abs(); + let midpoint_residual = 0.5 + * ulp_toward_exact.mul_add( + denominator, + ulp_toward_exact * denominator_residual, + ); + let residual_magnitude = exact_residual.abs(); + if residual_magnitude > midpoint_residual + || (residual_magnitude == midpoint_residual && direct_lower.to_bits() & 1 == 1) + { + return neighbor.clamp(0.0, 1.0); + } + } + } + direct_lower +} + +fn wilson_bounds_from_represented_proportion( + n: f64, + p: f64, + z: f64, + z2: f64, +) -> (f64, f64) { + let low = if p > 0.0 { + rationalized_wilson_positive_lower(n, p, z, z2) + } else { + 0.0 + }; + + let denominator = 1.0 + z2 / n; + let center = p + z2 / (2.0 * n); + let radical = (p * (1.0 - p) / n) + z2 / (4.0 * n * n); + let margin = z * radical.sqrt(); + let direct_high = ((center + margin) / denominator).clamp(0.0, 1.0); + let high = if direct_high == 1.0 && p < 1.0 && z2 > 0.0 { + let uncovered_lower = rationalized_wilson_positive_lower(n, 1.0 - p, z, z2); + (1.0 - uncovered_lower).clamp(0.0, 1.0) + } else { + direct_high + }; + (low, high) +} + +fn wilson_bounds_from_represented_proportion_and_inverse_sample_count( + inverse_n: f64, + p: f64, + z: f64, + z2: f64, +) -> (f64, f64) { + let low = if p > 0.0 { + rationalized_wilson_positive_lower_from_inverse_sample_count(inverse_n, p, z, z2) + } else { + 0.0 + }; + + let z2_over_n = z2 * inverse_n; + let denominator = 1.0 + z2_over_n; + let center = p + z2_over_n / 2.0; + let radical = p * (1.0 - p) * inverse_n + z2 * inverse_n * inverse_n / 4.0; + let margin = z * radical.sqrt(); + let direct_high = ((center + margin) / denominator).clamp(0.0, 1.0); + let high = if direct_high == 1.0 && p < 1.0 && z2 > 0.0 { + let uncovered_lower = rationalized_wilson_positive_lower_from_inverse_sample_count( + inverse_n, + 1.0 - p, + z, + z2, + ); + (1.0 - uncovered_lower).clamp(0.0, 1.0) + } else { + direct_high + }; + (low, high) +} + +fn wilson_bounds_for_sample_count( + n: f64, + inverse_n: Option, + p: f64, + z: f64, + z2: f64, +) -> (f64, f64) { + if let Some(inverse_n) = inverse_n { + wilson_bounds_from_represented_proportion_and_inverse_sample_count(inverse_n, p, z, z2) + } else { + wilson_bounds_from_represented_proportion(n, p, z, z2) + } +} + +pub(crate) fn wilson_coverage_interval_from_counts( + covered_count: u64, + sample_count: u64, + z: f64, +) -> Result<(f64, f64), ValidationError> { + if sample_count == 0 || covered_count > sample_count { + return Err(ValidationError::InvalidInput); + } + if !z.is_finite() || z <= 0.0 { + return Err(ValidationError::InvalidConfiguration); + } + + let n = sample_count as f64; + let z2 = z * z; + if !z2.is_finite() { + return Err(ValidationError::InvalidConfiguration); + } + let inverse_n = if u64_is_exact_binary64_integer(sample_count) { + None + } else { + Some(correctly_rounded_unit_ratio(1, sample_count)) + }; + + if covered_count == sample_count { + if inverse_n.is_some() { + return Ok(( + all_covered_wilson_lower_from_inexact_sample_count(sample_count, z2), + 1.0, + )); + } + return Ok((all_covered_wilson_lower_from_exact_sample_count(n, z2), 1.0)); + } + + let uncovered_count = sample_count - covered_count; + if covered_count > uncovered_count { + // Near all-covered samples can lose observed misses if both integer + // counts are independently rounded before division. Wilson intervals + // are complement-symmetric, so evaluate the correctly rounded smaller + // uncovered proportion and reflect its endpoints instead. + let uncovered = correctly_rounded_unit_ratio(uncovered_count, sample_count); + let (uncovered_low, uncovered_high) = + wilson_bounds_for_sample_count(n, inverse_n, uncovered, z, z2); + return Ok(( + (1.0 - uncovered_high).clamp(0.0, 1.0), + (1.0 - uncovered_low).clamp(0.0, 1.0), + )); + } + + let p = correctly_rounded_unit_ratio(covered_count, sample_count); + Ok(wilson_bounds_for_sample_count(n, inverse_n, p, z, z2)) } /// Wilson score lower/upper bounds for a binomial coverage proportion. /// /// Returns `(lower, upper)` for the empirical coverage rate at the stated -/// normal critical value `z` (for example `1.96` for nominal 95%). +/// normal critical value `z` (for example `1.96` for nominal 95%). For an +/// all-covered sample, the exact Wilson lower endpoint is algebraically +/// `n / (n + z²)`. When an exactly representable sample count and positive +/// finite `z²` make the denominator sum inexact, the implementation recovers +/// the TwoSum residual and uses an FMA quotient residual to compare the exact +/// represented-input quotient with the adjacent binary64 midpoint before +/// changing the direct result. A boundary-specific complementary miss fraction +/// preserves representable uncertainty when the direct quotient has already +/// collapsed spuriously to exact one, while genuine sub-ULP uncertainty remains +/// exact one. For nonzero strict-interior coverage, the lower endpoint is +/// evaluated through the algebraically rationalized positive root rather than +/// `center - margin`; the implementation switches scale at `z² = 1` so the +/// stable form neither suffers large-z cancellation nor small-z division +/// overflow. Count proportions are rounded to binary64 from their exact integer +/// ratio before Wilson evaluation. When the exact sample count itself is not +/// binary64-representable, strict-interior Wilson scale terms use the correctly +/// rounded reciprocal `1 / n` rather than a pre-rounded `n as f64`. The +/// inexact-count all-covered path additionally decodes finite `z²` into its +/// binary significand and power-of-two scale, divides that significand by the +/// exact retained `u64` denominator, and then switches between complementary- +/// miss and direct reciprocal forms at `z² / n = 1`. This avoids both +/// reciprocal-product double rounding in the exposed extreme-`z` contract and +/// false exact 0/1 boundary failures. Near the all-covered boundary, the smaller +/// uncovered count is evaluated and reflected by Wilson complement symmetry. /// /// # Errors /// @@ -54,28 +408,72 @@ pub fn wilson_coverage_interval( if !z.is_finite() || z <= 0.0 { return Err(ValidationError::InvalidConfiguration); } - let p = interval_coverage(truth, lower, upper)?; - let n = truth.len() as f64; - let z2 = z * z; - if !z2.is_finite() { - return Err(ValidationError::InvalidConfiguration); - } - let denominator = 1.0 + z2 / n; - let center = p + z2 / (2.0 * n); - let radical = (p * (1.0 - p) / n) + z2 / (4.0 * n * n); - // With finite z² and coverage p in [0,1], Wilson terms remain finite. - let margin = z * radical.sqrt(); - // radical and z are finite and non-negative; margin/bounds stay finite in [0,1]. - let low = ((center - margin) / denominator).clamp(0.0, 1.0); - let high = ((center + margin) / denominator).clamp(0.0, 1.0); - Ok((low, high)) + let covered_count = interval_covered_count(truth, lower, upper)?; + wilson_coverage_interval_from_counts(covered_count as u64, truth.len() as u64, z) } #[cfg(test)] mod tests { - use super::{interval_coverage, wilson_coverage_interval}; + use super::{ + correctly_rounded_unit_ratio, interval_coverage, positive_f64_over_inexact_u64, + u64_is_exact_binary64_integer, wilson_coverage_interval, + }; use crate::ValidationError; + #[test] + fn exact_integer_ratio_rounding_covers_binary64_boundaries() { + assert_eq!(correctly_rounded_unit_ratio(0, 3), 0.0); + assert_eq!(correctly_rounded_unit_ratio(1, 1), 1.0); + assert_eq!(correctly_rounded_unit_ratio(1, 2), 0.5); + assert_eq!(correctly_rounded_unit_ratio(1, 3), 1.0 / 3.0); + + // Halfway between 0.5 and its successor: lower significand is even. + assert_eq!( + correctly_rounded_unit_ratio((1_u64 << 53) + 1, 1_u64 << 54), + 0.5 + ); + // Just above that midpoint rounds upward. + assert_eq!( + correctly_rounded_unit_ratio((1_u64 << 54) + 3, 1_u64 << 55).to_bits(), + 0.5_f64.to_bits() + 1 + ); + // Just below the midpoint rounds downward. + assert_eq!( + correctly_rounded_unit_ratio((1_u64 << 54) + 1, 1_u64 << 55), + 0.5 + ); + // Halfway between predecessor(1.0) and 1.0: 1.0 has the even + // significand, so ties-to-even rounds upward and renormalizes. + assert_eq!( + correctly_rounded_unit_ratio((1_u64 << 54) - 1, 1_u64 << 54), + 1.0 + ); + } + + #[test] + fn sample_count_exactness_detection_matches_binary64_integer_spacing() { + assert!(u64_is_exact_binary64_integer(0)); + assert!(u64_is_exact_binary64_integer(1_u64 << 53)); + assert!(!u64_is_exact_binary64_integer((1_u64 << 53) + 1)); + assert!(u64_is_exact_binary64_integer((1_u64 << 53) + 2)); + assert!(!u64_is_exact_binary64_integer((1_u64 << 55) + 3)); + assert!(u64_is_exact_binary64_integer((1_u64 << 55) + 8)); + } + + #[test] + fn inexact_u64_scaled_ratio_avoids_reciprocal_product_double_rounding() { + let sample_count = (1_u64 << 53) + 1; + assert_eq!(positive_f64_over_inexact_u64(0.0, sample_count), 0.0); + assert_eq!( + positive_f64_over_inexact_u64(f64::MIN_POSITIVE, sample_count), + 0.0 + ); + assert_eq!( + positive_f64_over_inexact_u64(1e40, sample_count).to_bits(), + 0x44ed_6329_f1c3_5ca4 + ); + } + #[test] fn coverage_and_wilson_bounds_are_oracle_correct() { let truth = [0.0, 1.0, 2.0, 3.0]; diff --git a/crates/validation_core/src/coverage_evidence.rs b/crates/validation_core/src/coverage_evidence.rs new file mode 100644 index 000000000..c4c1c785f --- /dev/null +++ b/crates/validation_core/src/coverage_evidence.rs @@ -0,0 +1,192 @@ +//! Versioned provenance carrier for empirical interval-coverage evidence. + +use crate::ValidationError; +use crate::coverage::{ + interval_covered_count, represented_coverage_from_counts, wilson_coverage_interval_from_counts, +}; +use serde::{Deserialize, Serialize}; + +const SCHEMA: &str = "tepp.wilson_coverage_evidence.v1"; +const CRITICAL_VALUE_KIND: &str = "standard_normal_z"; +const INTERVAL_SIDEDNESS: &str = "two_sided"; + +/// Durable Wilson interval-coverage evidence with denominator and critical-value provenance. +/// +/// The carrier stores fixed-width retained-sample and covered counts rather than only the projected +/// empirical proportion, plus the caller-supplied standard-normal critical value used by TEPP's +/// canonical two-sided Wilson producer. Serialization fixes the schema identifier, critical-value +/// scale, and interval sidedness so a numeric `z` cannot later be reinterpreted as a Student-t value +/// or a one-sided confidence claim. Validation recomputes represented empirical coverage and both +/// Wilson endpoints from the stored counts and `z`; tampered or internally inconsistent artifacts +/// fail closed. Near all-covered samples are evaluated from the smaller uncovered count so a real +/// miss is not erased when integer counts exceed binary64's exact-integer range. +#[derive(Clone, Copy, Debug, PartialEq)] +pub struct WilsonCoverageEvidenceV1 { + /// Number of interval/truth triples admitted to the empirical coverage calculation. + pub sample_count: u64, + /// Number of admitted triples whose closed interval contains the corresponding truth value. + pub covered_count: u64, + /// Caller-supplied standard-normal critical value used by the Wilson score producer. + pub normal_critical_value: f64, + /// Empirical coverage projected from `covered_count / sample_count`. + pub empirical_coverage: f64, + /// Canonical Wilson score lower endpoint. + pub wilson_lower: f64, + /// Canonical Wilson score upper endpoint. + pub wilson_upper: f64, +} + +impl WilsonCoverageEvidenceV1 { + /// Build versioned coverage evidence from the same interval triples used by the Wilson producer. + /// + /// # Errors + /// + /// Returns [`ValidationError::InvalidInput`] for empty, mismatched, non-finite, or inverted + /// interval triples, and [`ValidationError::InvalidConfiguration`] for a non-positive, + /// non-finite, or square-overflowing normal critical value. + pub fn from_intervals( + truth: &[f64], + lower: &[f64], + upper: &[f64], + normal_critical_value: f64, + ) -> Result { + if !normal_critical_value.is_finite() || normal_critical_value <= 0.0 { + return Err(ValidationError::InvalidConfiguration); + } + let covered_count = interval_covered_count(truth, lower, upper)? as u64; + let sample_count = truth.len() as u64; + let (wilson_lower, wilson_upper) = wilson_coverage_interval_from_counts( + covered_count, + sample_count, + normal_critical_value, + )?; + let evidence = Self { + sample_count, + covered_count, + normal_critical_value, + empirical_coverage: represented_coverage_from_counts(covered_count, sample_count)?, + wilson_lower, + wilson_upper, + }; + evidence.validate()?; + Ok(evidence) + } + + /// Validate denominator, standard-normal critical-value, and exact recomputation coherence. + /// + /// Numeric equality intentionally treats IEEE `-0.0` and `+0.0` as one zero-valued + /// scientific state. The JSON representation produced by this crate round-trips binary64 + /// values exactly, so canonical evidence must reproduce the stored coverage and endpoints + /// rather than merely fall within a loose interval-pair tolerance. + /// + /// # Errors + /// + /// Returns [`ValidationError::InvalidInput`] when counts, numeric domains, or recomputed + /// empirical/Wilson values disagree with the stored artifact. + pub fn validate(&self) -> Result<(), ValidationError> { + if self.sample_count == 0 || self.covered_count > self.sample_count { + return Err(ValidationError::InvalidInput); + } + if !self.normal_critical_value.is_finite() || self.normal_critical_value <= 0.0 { + return Err(ValidationError::InvalidInput); + } + for value in [ + self.empirical_coverage, + self.wilson_lower, + self.wilson_upper, + ] { + if !value.is_finite() || !(0.0..=1.0).contains(&value) { + return Err(ValidationError::InvalidInput); + } + } + + let expected_coverage = + represented_coverage_from_counts(self.covered_count, self.sample_count)?; + let (expected_lower, expected_upper) = wilson_coverage_interval_from_counts( + self.covered_count, + self.sample_count, + self.normal_critical_value, + ) + .map_err(|_| ValidationError::InvalidInput)?; + + if self.empirical_coverage != expected_coverage + || self.wilson_lower != expected_lower + || self.wilson_upper != expected_upper + { + return Err(ValidationError::InvalidInput); + } + Ok(()) + } + + /// Serialize the validated carrier to canonical JSON. + /// + /// # Errors + /// + /// Returns [`ValidationError::InvalidInput`] when the artifact is inconsistent or JSON + /// serialization fails. + pub fn to_json(&self) -> Result { + self.validate()?; + serde_json::to_string(self).map_err(|_| ValidationError::InvalidInput) + } +} + +impl Serialize for WilsonCoverageEvidenceV1 { + fn serialize(&self, serializer: S) -> Result + where + S: serde::Serializer, + { + use serde::ser::SerializeStruct; + + self.validate().map_err(serde::ser::Error::custom)?; + let mut state = serializer.serialize_struct("WilsonCoverageEvidenceV1", 9)?; + state.serialize_field("schema", SCHEMA)?; + state.serialize_field("sample_count", &self.sample_count)?; + state.serialize_field("covered_count", &self.covered_count)?; + state.serialize_field("critical_value_kind", CRITICAL_VALUE_KIND)?; + state.serialize_field("interval_sidedness", INTERVAL_SIDEDNESS)?; + state.serialize_field("normal_critical_value", &self.normal_critical_value)?; + state.serialize_field("empirical_coverage", &self.empirical_coverage)?; + state.serialize_field("wilson_lower", &self.wilson_lower)?; + state.serialize_field("wilson_upper", &self.wilson_upper)?; + state.end() + } +} + +impl<'de> Deserialize<'de> for WilsonCoverageEvidenceV1 { + fn deserialize(deserializer: D) -> Result + where + D: serde::Deserializer<'de>, + { + #[derive(Deserialize)] + #[serde(deny_unknown_fields)] + struct Raw { + schema: String, + sample_count: u64, + covered_count: u64, + critical_value_kind: String, + interval_sidedness: String, + normal_critical_value: f64, + empirical_coverage: f64, + wilson_lower: f64, + wilson_upper: f64, + } + + let raw = Raw::deserialize(deserializer)?; + if raw.schema != SCHEMA + || raw.critical_value_kind != CRITICAL_VALUE_KIND + || raw.interval_sidedness != INTERVAL_SIDEDNESS + { + return Err(serde::de::Error::custom("unsupported Wilson coverage evidence schema")); + } + let evidence = Self { + sample_count: raw.sample_count, + covered_count: raw.covered_count, + normal_critical_value: raw.normal_critical_value, + empirical_coverage: raw.empirical_coverage, + wilson_lower: raw.wilson_lower, + wilson_upper: raw.wilson_upper, + }; + evidence.validate().map_err(serde::de::Error::custom)?; + Ok(evidence) + } +} diff --git a/crates/validation_core/src/lib.rs b/crates/validation_core/src/lib.rs index 8c4637fb7..5574d38f3 100644 --- a/crates/validation_core/src/lib.rs +++ b/crates/validation_core/src/lib.rs @@ -13,19 +13,23 @@ //! Metrics are pure `f64` CPU reference implementations. mod bias; +mod bias_se; mod claim; mod coverage; +mod coverage_evidence; mod error; mod graph_metrics; mod input; mod matching; mod monte_carlo; +mod numeric; mod report; mod rmse; mod temporal_order; +mod validation_evidence; /// Standard error of mean signed bias. -pub use bias::bias_standard_error; +pub use bias_se::bias_standard_error; /// Mean signed bias. pub use bias::mean_bias; /// Four ADR 0014 claim authorities. @@ -48,6 +52,8 @@ pub use claim::promote_scientific_recovery; pub use coverage::interval_coverage; /// Wilson bounds for coverage proportions. pub use coverage::wilson_coverage_interval; +/// Versioned Wilson coverage evidence with denominator and critical-value provenance. +pub use coverage_evidence::WilsonCoverageEvidenceV1; /// Fail-closed validation errors. pub use error::ValidationError; /// Undirected edge identity. @@ -74,3 +80,5 @@ pub use rmse::rmse_standard_error; pub use rmse::root_mean_square_error; /// Pairwise temporal-order accuracy. pub use temporal_order::temporal_order_accuracy; +/// Versioned durable validation envelope binding projections to scientific provenance. +pub use validation_evidence::ValidationEvidenceV1; diff --git a/crates/validation_core/src/matching.rs b/crates/validation_core/src/matching.rs index f189b1a6c..9b3b666ba 100644 --- a/crates/validation_core/src/matching.rs +++ b/crates/validation_core/src/matching.rs @@ -8,7 +8,8 @@ use crate::input::require_paired_finite; /// # Errors /// /// Returns [`ValidationError::InvalidInput`] when lengths differ, inputs are -/// empty, or any value is non-finite. +/// empty, any value is non-finite, or a finite input pair has an absolute +/// residual outside binary64 range. pub fn absolute_residuals(truth: &[f64], recovered: &[f64]) -> Result, ValidationError> { require_paired_finite(truth, recovered)?; let mut residuals = Vec::with_capacity(truth.len()); @@ -24,6 +25,13 @@ pub fn absolute_residuals(truth: &[f64], recovered: &[f64]) -> Result, /// Count exact matches within absolute tolerance `epsilon`. /// +/// The decision metric does not require every absolute residual to be +/// representable. If subtraction of two finite endpoints overflows, the true +/// absolute residual is larger than `f64::MAX` and therefore larger than every +/// admitted finite tolerance, so that pair is deterministically a mismatch. +/// `absolute_residuals` remains fail-closed when callers request the residual +/// magnitude itself. +/// /// # Errors /// /// Returns [`ValidationError::InvalidInput`] for bad vectors or non-finite @@ -39,10 +47,14 @@ pub fn match_count( if epsilon < 0.0 { return Err(ValidationError::InvalidConfiguration); } - let residuals = absolute_residuals(truth, recovered)?; - Ok(residuals + require_paired_finite(truth, recovered)?; + Ok(truth .iter() - .filter(|residual| **residual <= epsilon) + .zip(recovered) + .filter(|(truth_value, recovered_value)| { + let residual = (**truth_value - **recovered_value).abs(); + residual.is_finite() && residual <= epsilon + }) .count()) } @@ -80,10 +92,12 @@ mod tests { match_count(&truth, &recovered, f64::NAN), Err(ValidationError::InvalidInput) ); - // Opposite-sign extremes overflow the residual to infinity. + // Opposite-sign extremes have no representable binary64 residual. assert_eq!( absolute_residuals(&[f64::MAX], &[-f64::MAX]), Err(ValidationError::InvalidInput) ); + // The threshold decision is still exact for every finite epsilon. + assert_eq!(match_count(&[f64::MAX], &[-f64::MAX], f64::MAX), Ok(0)); } } diff --git a/crates/validation_core/src/monte_carlo.rs b/crates/validation_core/src/monte_carlo.rs index 1a4f2f345..259eff30f 100644 --- a/crates/validation_core/src/monte_carlo.rs +++ b/crates/validation_core/src/monte_carlo.rs @@ -2,6 +2,10 @@ use crate::ValidationError; use crate::input::require_finite; +use crate::numeric::{deterministic_compensated_sum, deterministic_representable_mean}; + +const STANDARD_ERROR_RELATIVE_TOLERANCE: f64 = 64.0 * f64::EPSILON; +const EMPIRICAL_SUPPORT_RELATIVE_TOLERANCE: f64 = 64.0 * f64::EPSILON; /// Summary of Monte Carlo replications for a scalar metric. #[derive(Clone, Copy, Debug, PartialEq)] @@ -21,12 +25,34 @@ pub struct MonteCarloSummary { } impl MonteCarloSummary { - /// Validate structural invariants for a Monte Carlo summary payload. + /// Validate structural and uncertainty-domain invariants for a Monte Carlo summary payload. + /// + /// `standard_error` is the standard error of the retained-replication mean, + /// so a positive sample SD must agree numerically with `SD / sqrt(n)`. + /// Admission allows a small relative binary64 tolerance rather than requiring + /// cross-language bit-for-bit equality, but rejects materially understated or + /// overstated uncertainty. Zero spread requires zero SE and degenerate + /// empirical percentile support at the represented mean; the same support + /// rule applies to the canonical singleton summary. For positive spread, + /// nearest-rank percentile endpoints are retained observations and therefore + /// must fit the support implied directly by the recorded sample spread: + /// `|x - mean| <= SD * sqrt(n - 1)`. Distinct lower and upper endpoints are + /// distinct retained values, so their squared deviations must also fit the + /// same total `(n - 1) * SD^2` deviation budget jointly. With exactly two + /// replications, two distinct nearest-rank endpoint values exhaust the sample; + /// the recorded mean and sample SD must therefore agree with the summary of + /// those two endpoint values themselves. These rules remain valid when the + /// represented binary64 mean is a rounded projection of the mathematical + /// sample mean. Comparisons are scale-normalized so opposite-sign full-range + /// finite values do not create overflowing validation-only arithmetic. + /// Numeric equality keeps IEEE `-0.0` and `+0.0` as one zero-valued scientific state. /// /// # Errors /// /// Returns [`ValidationError::InvalidInput`] when counts or numeric fields - /// violate the summary contract. + /// violate the summary contract, empirical percentile support is impossible + /// for the represented mean/sample spread/count, or the standard-error field + /// is incoherent with the represented sample spread/count. pub fn validate(self) -> Result { if self.replication_count == 0 { return Err(ValidationError::InvalidInput); @@ -48,20 +74,175 @@ impl MonteCarloSummary { if self.percentile_lower > self.percentile_upper { return Err(ValidationError::InvalidInput); } + if self.standard_deviation == 0.0 { + if self.standard_error != 0.0 + || self.percentile_lower != self.mean + || self.percentile_upper != self.mean + { + return Err(ValidationError::InvalidInput); + } + } else { + if self.replication_count == 1 || self.standard_error == 0.0 { + return Err(ValidationError::InvalidInput); + } + let expected_standard_error = + self.standard_deviation / (self.replication_count as f64).sqrt(); + if expected_standard_error == 0.0 { + return Err(ValidationError::InvalidInput); + } + let relative_error = + (self.standard_error / expected_standard_error - 1.0).abs(); + if !relative_error.is_finite() || relative_error > STANDARD_ERROR_RELATIVE_TOLERANCE { + return Err(ValidationError::InvalidInput); + } + + let moment_factor = ((self.replication_count - 1) as f64).sqrt(); + for endpoint in [self.percentile_lower, self.percentile_upper] { + let scale = self + .mean + .abs() + .max(endpoint.abs()) + .max(self.standard_deviation) + .max(1.0); + let scaled_deviation = ((endpoint / scale) - (self.mean / scale)).abs(); + let scaled_support = (self.standard_deviation / scale) * moment_factor; + if !scaled_deviation.is_finite() + || !scaled_support.is_finite() + || scaled_deviation + > scaled_support * (1.0 + EMPIRICAL_SUPPORT_RELATIVE_TOLERANCE) + { + return Err(ValidationError::InvalidInput); + } + } + + if self.percentile_lower != self.percentile_upper { + let scale = self + .mean + .abs() + .max(self.percentile_lower.abs()) + .max(self.percentile_upper.abs()) + .max(self.standard_deviation); + let scaled_mean = self.mean / scale; + let scaled_lower_deviation = (self.percentile_lower / scale) - scaled_mean; + let scaled_upper_deviation = (self.percentile_upper / scale) - scaled_mean; + let combined_scaled_deviation = + scaled_lower_deviation.hypot(scaled_upper_deviation); + let scaled_support = (self.standard_deviation / scale) * moment_factor; + if !combined_scaled_deviation.is_finite() + || !scaled_support.is_finite() + || combined_scaled_deviation + > scaled_support * (1.0 + EMPIRICAL_SUPPORT_RELATIVE_TOLERANCE) + { + return Err(ValidationError::InvalidInput); + } + + if self.replication_count == 2 { + let endpoint_samples = [self.percentile_lower, self.percentile_upper]; + let expected_mean = deterministic_representable_mean(&endpoint_samples)?; + let expected_standard_deviation = + scaled_sample_standard_deviation(&endpoint_samples, expected_mean)?; + for (recorded, expected) in [ + (self.mean, expected_mean), + (self.standard_deviation, expected_standard_deviation), + ] { + let coherence_scale = recorded.abs().max(expected.abs()); + if coherence_scale == 0.0 { + continue; + } + let relative_distance = + ((recorded / coherence_scale) - (expected / coherence_scale)).abs(); + if relative_distance > EMPIRICAL_SUPPORT_RELATIVE_TOLERANCE { + return Err(ValidationError::InvalidInput); + } + } + } + } + } Ok(self) } } +fn standard_deviation_from_deviations(deviations: &[f64]) -> Result { + let scale = deviations + .iter() + .map(|deviation| deviation.abs()) + .fold(0.0, f64::max); + if scale == 0.0 { + return Ok(0.0); + } + + let normalized_squares = deviations + .iter() + .map(|deviation| { + let normalized = *deviation / scale; + normalized * normalized + }) + .collect(); + let square_sum = deterministic_compensated_sum(normalized_squares); + let normalized_standard_deviation = + (square_sum / (deviations.len() as f64 - 1.0)).sqrt(); + let standard_deviation = scale * normalized_standard_deviation; + if !standard_deviation.is_finite() + || (standard_deviation == 0.0 && normalized_standard_deviation != 0.0) + { + Err(ValidationError::InvalidInput) + } else if standard_deviation == 0.0 { + Ok(0.0) + } else { + Ok(standard_deviation) + } +} + +fn scaled_sample_standard_deviation(samples: &[f64], mean: f64) -> Result { + let direct_deviations: Option> = samples + .iter() + .map(|value| { + let deviation = *value - mean; + deviation.is_finite().then_some(deviation) + }) + .collect(); + if let Some(deviations) = direct_deviations { + return standard_deviation_from_deviations(&deviations); + } + + let outer_scale = samples.iter().map(|value| value.abs()).fold(0.0, f64::max); + if outer_scale == 0.0 { + return Ok(0.0); + } + let normalized_mean = mean / outer_scale; + let normalized_deviations: Vec<_> = samples + .iter() + .map(|value| (*value / outer_scale) - normalized_mean) + .collect(); + let normalized_standard_deviation = + standard_deviation_from_deviations(&normalized_deviations)?; + let standard_deviation = outer_scale * normalized_standard_deviation; + if !standard_deviation.is_finite() + || (standard_deviation == 0.0 && normalized_standard_deviation != 0.0) + { + Err(ValidationError::InvalidInput) + } else if standard_deviation == 0.0 { + Ok(0.0) + } else { + Ok(standard_deviation) + } +} + /// Aggregate Monte Carlo metric replications with percentile bounds. /// /// Percentiles use the inclusive nearest-rank method on sorted finite samples. -/// Mean and variance use Welford accumulation so large finite samples do not -/// overflow intermediate sums. +/// Mean and sampling uncertainty use deterministic cancellation-safe/scaled +/// binary64 references so an avoidable raw sum, Welford delta product, or raw +/// square cannot reject a representable summary. A mathematically nonzero +/// standard deviation or standard error that becomes exact zero only at the +/// binary64 projection boundary fails closed rather than reporting no Monte +/// Carlo uncertainty. /// /// # Errors /// -/// Returns input errors for empty/non-finite samples or non-finite summaries, -/// and configuration errors for invalid percentile bounds. +/// Returns input errors for empty/non-finite samples, an unrepresentable mean, +/// standard deviation, standard error, or summary; and configuration errors for +/// invalid percentile bounds. /// /// # Panics /// @@ -81,18 +262,21 @@ pub fn summarize_replications( return Err(ValidationError::InvalidConfiguration); } let mut sorted = samples.to_vec(); - sorted.sort_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal)); - let (mean, m2, count) = welford_moments(&sorted)?; - let n = count as f64; - let standard_deviation = if count == 1 { + sorted.sort_by(f64::total_cmp); + let mean = deterministic_representable_mean(&sorted)?; + let n = sorted.len() as f64; + let standard_deviation = if sorted.len() == 1 { 0.0 } else { - require_finite((m2 / (n - 1.0)).sqrt())? + scaled_sample_standard_deviation(&sorted, mean)? }; let standard_error = require_finite(standard_deviation / n.sqrt())?; + if standard_error == 0.0 && standard_deviation != 0.0 { + return Err(ValidationError::InvalidInput); + } let summary = MonteCarloSummary { replication_count: sorted.len(), - mean: require_finite(mean)?, + mean, standard_deviation, standard_error, percentile_lower: nearest_rank(&sorted, lower_percentile), @@ -101,10 +285,181 @@ pub fn summarize_replications( summary.validate() } +/// Decode a nonnegative finite binary64 magnitude into an exact integer significand and power-of-two exponent. +fn binary64_magnitude_components(value: f64) -> (u64, i32) { + let bits = value.to_bits() & 0x7fff_ffff_ffff_ffff; + let exponent_bits = ((bits >> 52) & 0x7ff) as i32; + let fraction = bits & 0x000f_ffff_ffff_ffff; + if exponent_bits == 0 { + (fraction, -1074) + } else { + ((1_u64 << 52) | fraction, exponent_bits - 1023 - 52) + } +} + +/// Compare two exact nonzero `significand * 2^exponent` values without floating-point rounding. +fn scaled_u128_le( + lhs_significand: u128, + lhs_exponent: i32, + rhs_significand: u128, + rhs_exponent: i32, +) -> bool { + let lhs_bits = 128_i32 - lhs_significand.leading_zeros() as i32; + let rhs_bits = 128_i32 - rhs_significand.leading_zeros() as i32; + let lhs_top_exponent = lhs_exponent + lhs_bits - 1; + let rhs_top_exponent = rhs_exponent + rhs_bits - 1; + if lhs_top_exponent != rhs_top_exponent { + return lhs_top_exponent < rhs_top_exponent; + } + + let common_exponent = lhs_exponent.min(rhs_exponent); + let lhs_shift = (lhs_exponent - common_exponent) as u32; + let rhs_shift = (rhs_exponent - common_exponent) as u32; + debug_assert!(lhs_shift < 128); + debug_assert!(rhs_shift < 128); + (lhs_significand << lhs_shift) <= (rhs_significand << rhs_shift) +} + +/// Compare one represented nonnegative binary64 magnitude with an exact product of two represented factors. +fn represented_magnitude_le_exact_product(value: f64, factor_a: f64, factor_b: f64) -> bool { + let (value_significand, value_exponent) = binary64_magnitude_components(value); + let (a_significand, a_exponent) = binary64_magnitude_components(factor_a); + let (b_significand, b_exponent) = binary64_magnitude_components(factor_b); + scaled_u128_le( + value_significand as u128, + value_exponent, + (a_significand as u128) * (b_significand as u128), + a_exponent + b_exponent, + ) +} + +/// Compare an equal nonzero projected correction with the exact product roundoff. +fn represented_correction_le_exact_product_roundoff( + correction: f64, + factor_a: f64, + factor_b: f64, + rounded_product: f64, +) -> bool { + debug_assert!(correction.is_finite() && correction != 0.0); + debug_assert!(rounded_product.is_finite() && rounded_product > 0.0); + debug_assert_eq!( + factor_a.mul_add(factor_b, -rounded_product), + correction, + "caller must provide the equal nonzero projected product correction" + ); + + let (a_significand, a_exponent) = binary64_magnitude_components(factor_a); + let (b_significand, b_exponent) = binary64_magnitude_components(factor_b); + let product_significand = (a_significand as u128) * (b_significand as u128); + let product_exponent = a_exponent + b_exponent; + let (rounded_significand, rounded_exponent) = + binary64_magnitude_components(rounded_product); + let common_exponent = product_exponent.min(rounded_exponent); + let product_shift = (product_exponent - common_exponent) as u32; + let rounded_shift = (rounded_exponent - common_exponent) as u32; + debug_assert!(product_significand.leading_zeros() >= product_shift); + debug_assert!((rounded_significand as u128).leading_zeros() >= rounded_shift); + let exact_product = product_significand << product_shift; + let rounded_product_significand = (rounded_significand as u128) << rounded_shift; + let (product_roundoff_negative, product_roundoff_significand) = + if exact_product < rounded_product_significand { + (true, rounded_product_significand - exact_product) + } else { + (false, exact_product - rounded_product_significand) + }; + + debug_assert_ne!(product_roundoff_significand, 0); + let correction_negative = correction.is_sign_negative(); + debug_assert_eq!(correction_negative, product_roundoff_negative); + let (correction_significand, correction_exponent) = + binary64_magnitude_components(correction.abs()); + if correction_negative { + scaled_u128_le( + product_roundoff_significand, + common_exponent, + correction_significand as u128, + correction_exponent, + ) + } else { + scaled_u128_le( + correction_significand as u128, + correction_exponent, + product_roundoff_significand, + common_exponent, + ) + } +} + +/// Compare the exact represented residual magnitude with `k * SE` after both direct operations overflow. +fn both_overflow_acceptance( + estimate: f64, + target: f64, + standard_error: f64, + k: f64, +) -> bool { + let (estimate_significand, estimate_exponent) = + binary64_magnitude_components(estimate.abs()); + let (target_significand, target_exponent) = binary64_magnitude_components(target.abs()); + let common_residual_exponent = estimate_exponent.min(target_exponent); + let estimate_shift = (estimate_exponent - common_residual_exponent) as u32; + let target_shift = (target_exponent - common_residual_exponent) as u32; + + // Finite subtraction can overflow only for opposite signs whose magnitudes + // are close enough to the top of binary64 that exact alignment needs at most + // one 53-bit significand-width shift. + debug_assert!(estimate_shift <= 53); + debug_assert!(target_shift <= 53); + let residual_significand = ((estimate_significand as u128) << estimate_shift) + + ((target_significand as u128) << target_shift); + + let (k_significand, k_exponent) = binary64_magnitude_components(k); + let (se_significand, se_exponent) = binary64_magnitude_components(standard_error); + let bound_significand = (k_significand as u128) * (se_significand as u128); + let bound_exponent = k_exponent + se_exponent; + + scaled_u128_le( + residual_significand, + common_residual_exponent, + bound_significand, + bound_exponent, + ) +} + +/// Return the error-free low term of a finite binary64 subtraction. +fn subtraction_roundoff(minuend: f64, subtrahend: f64, difference: f64) -> f64 { + let virtual_subtrahend = minuend - difference; + let virtual_minuend = difference + virtual_subtrahend; + let subtrahend_roundoff = virtual_subtrahend - subtrahend; + let minuend_roundoff = minuend - virtual_minuend; + minuend_roundoff + subtrahend_roundoff +} + /// SE-aware acceptance: accept when `|estimate − target| ≤ k · se`. /// -/// Comparison scales all terms by a shared finite magnitude so opposite-sign -/// extremes do not overflow both sides of the inequality to infinity. +/// Finite represented residuals and finite represented `k · se` bounds are +/// compared before any normalization. If both rounded finite quantities are equal +/// and nonzero, TEPP compares the error-free subtraction correction (with its sign +/// adjusted for the absolute residual) with the fused multiply-add product +/// correction. Different correction projections preserve the represented-input +/// ordering even when either direct operation rounded to the same binary64 value. +/// If both projected corrections are zero, the subtraction is exact at the rounded +/// residual while the product correction may still have underflowed below binary64 +/// resolution; TEPP therefore compares the represented residual with the exact +/// dyadic product of represented `k` and `se`. If the two nonzero correction +/// projections are equal, TEPP compares the represented subtraction correction with +/// the exact dyadic product roundoff before deciding the tie; only the two signed +/// orderings reachable from that equal-projection branch are implemented. This +/// preserves the represented-input inequality without adding dead generic branches +/// or claiming a broader exact comparator for unequal rounded residuals and bounds. +/// This also preserves a positive acceptance bound when dividing `se` by a much +/// larger estimate/target scale would underflow to zero. If only the positive bound +/// overflows, every finite residual is covered; if only the residual overflows, a +/// finite bound cannot cover it. When both direct operations overflow, TEPP compares +/// the exact binary64 input rationals by decoding their integer significands and +/// powers of two, avoiding a false accept/reject caused by independently rounded +/// normalization. A zero standard error or zero multiplier remains an exact-recovery +/// gate and is compared before either path. Exact recovery uses numeric equality, +/// for which IEEE `-0.0` and `+0.0` denote the same zero-valued scientific result. /// /// # Errors /// @@ -125,37 +480,58 @@ pub fn accept_within_standard_errors( if k < 0.0 || standard_error < 0.0 { return Err(ValidationError::InvalidConfiguration); } - if standard_error == 0.0 { - // Exact recovery only: zero SE admits no estimation residual. - return Ok(estimate.total_cmp(&target).is_eq()); + if standard_error == 0.0 || k == 0.0 { + // Exact recovery is numerical equality; signed zero is one zero value. + return Ok(estimate == target); } - let scale = estimate - .abs() - .max(target.abs()) - .max(standard_error) - .max(1.0); - let scaled_error = (estimate / scale) - (target / scale); - let scaled_bound = k * (standard_error / scale); - // scale is at least 1.0 and all inputs are finite, so scaled terms are finite. - Ok(scaled_error.abs() <= scaled_bound) -} -/// Welford one-pass mean and sum of squared deviations. -fn welford_moments(samples: &[f64]) -> Result<(f64, f64, usize), ValidationError> { - let mut mean = 0.0_f64; - let mut m2 = 0.0_f64; - let mut count = 0_usize; - for value in samples { - count += 1; - let delta = value - mean; - mean += delta / count as f64; - if !mean.is_finite() { - return Err(ValidationError::InvalidInput); + let direct_error = estimate - target; + let direct_bound = k * standard_error; + if direct_error.is_finite() { + if direct_bound.is_finite() { + let residual = direct_error.abs(); + if residual == direct_bound && residual != 0.0 { + let difference_roundoff = subtraction_roundoff(estimate, target, direct_error); + let residual_roundoff = if direct_error.is_sign_negative() { + -difference_roundoff + } else { + difference_roundoff + }; + let product_roundoff = k.mul_add(standard_error, -direct_bound); + if residual_roundoff != product_roundoff { + return Ok(residual_roundoff < product_roundoff); + } + if residual_roundoff == 0.0 { + return Ok(represented_magnitude_le_exact_product( + residual, + k, + standard_error, + )); + } + return Ok(represented_correction_le_exact_product_roundoff( + residual_roundoff, + k, + standard_error, + direct_bound, + )); + } + return Ok(residual <= direct_bound); } - let delta2 = value - mean; - m2 += delta * delta2; + // A finite residual is necessarily inside a positive bound whose + // represented multiplication overflowed beyond binary64's finite range. + return Ok(true); + } + if direct_bound.is_finite() { + // The represented residual overflowed while the positive bound did not. + return Ok(false); } - Ok((mean, m2, count)) + + Ok(both_overflow_acceptance( + estimate, + target, + standard_error, + k, + )) } #[allow(clippy::cast_possible_truncation)] diff --git a/crates/validation_core/src/numeric.rs b/crates/validation_core/src/numeric.rs new file mode 100644 index 000000000..3b6a611eb --- /dev/null +++ b/crates/validation_core/src/numeric.rs @@ -0,0 +1,465 @@ +//! Deterministic binary64 accumulation shared by validation metrics. + +use crate::ValidationError; + +fn deterministic_compensated_parts(mut values: Vec) -> (f64, f64) { + values.sort_by(f64::total_cmp); + let mut sum = 0.0_f64; + let mut correction = 0.0_f64; + for value in values { + let next = sum + value; + if sum.abs() >= value.abs() { + correction += (sum - next) + value; + } else { + correction += (value - next) + sum; + } + sum = next; + } + (sum, correction) +} + +fn deterministic_compensated_parts_with_tail(mut values: Vec) -> (f64, f64, f64) { + values.sort_by(f64::total_cmp); + let mut sum = 0.0_f64; + let mut correction = 0.0_f64; + let mut correction_tail = 0.0_f64; + for value in values { + let next = sum + value; + let increment = if sum.abs() >= value.abs() { + (sum - next) + value + } else { + (value - next) + sum + }; + let (next_correction, correction_roundoff) = error_free_sum(correction, increment); + let (next_tail, tail_roundoff) = error_free_sum(correction_tail, correction_roundoff); + correction = next_correction; + correction_tail = next_tail + tail_roundoff; + sum = next; + } + (sum, correction, correction_tail) +} + +/// Sum finite values in a canonical order with Neumaier compensation. +/// +/// Callers own domain validation and any scale normalization needed to keep the +/// final sum representable. Canonical ordering keeps equivalent metric inputs +/// from changing only because transport order changed. +pub(crate) fn deterministic_compensated_sum(values: Vec) -> f64 { + let (sum, correction) = deterministic_compensated_parts(values); + sum + correction +} + +pub(crate) fn exact_power_of_two_scale(max_magnitude: f64) -> f64 { + let bits = max_magnitude.to_bits(); + let exponent = (bits >> 52) & 0x7ff; + if exponent == 0 { + let significand = bits & 0x000f_ffff_ffff_ffff; + let highest_bit = 63 - significand.leading_zeros(); + f64::from_bits(1_u64 << highest_bit) + } else { + f64::from_bits(exponent << 52) + } +} + +fn same_sign_mean_over_total(values: &[f64], total_count: usize) -> Result { + let max_magnitude = values + .iter() + .map(|value| value.abs()) + .fold(0.0, f64::max); + + if max_magnitude < f64::MIN_POSITIVE && total_count >= values.len() { + // Subnormal values are exact integer multiples of 2^-1074. With a divisor + // at least as large as the term count, the exact mean remains subnormal; + // summing 52-bit units over any supported `usize` length fits in `u128`. + // Round those represented units once instead of normalizing and then + // rounding again while restoring a subnormal power-of-two scale. + let negative = values.iter().any(|value| *value < 0.0); + let total_units: u128 = values + .iter() + .map(|value| (value.to_bits() & 0x000f_ffff_ffff_ffff) as u128) + .sum(); + let denominator = total_count as u128; + let mut rounded_units = total_units / denominator; + let remainder = total_units % denominator; + let twice_remainder = remainder * 2; + if twice_remainder > denominator + || (twice_remainder == denominator && rounded_units & 1 == 1) + { + rounded_units += 1; + } + if rounded_units == 0 { + return Err(ValidationError::InvalidInput); + } + + let sign = if negative { 1_u64 << 63 } else { 0 }; + return Ok(f64::from_bits(sign | rounded_units as u64)); + } + + let scale = exact_power_of_two_scale(max_magnitude); + let normalized = values.iter().map(|value| *value / scale).collect(); + let (normalized_sum, normalized_correction) = deterministic_compensated_parts(normalized); + let denominator = total_count as f64; + let leading_mean = normalized_sum / denominator; + let division_residual = (-leading_mean).mul_add(denominator, normalized_sum); + let normalized_mean = leading_mean + (division_residual + normalized_correction) / denominator; + let mean = normalized_mean * scale; + if !mean.is_finite() || mean == 0.0 { + Err(ValidationError::InvalidInput) + } else { + Ok(mean) + } +} + +fn error_free_sum(left: f64, right: f64) -> (f64, f64) { + let sum = left + right; + let right_virtual = sum - left; + let left_virtual = sum - right_virtual; + let left_roundoff = left - left_virtual; + let right_roundoff = right - right_virtual; + (sum, left_roundoff + right_roundoff) +} + +fn adjacent_float(value: f64, upward: bool) -> f64 { + if value == 0.0 { + return if upward { + f64::from_bits(1) + } else { + f64::from_bits((1_u64 << 63) | 1) + }; + } + + let bits = value.to_bits(); + if upward { + if value.is_sign_positive() { + f64::from_bits(bits + 1) + } else { + f64::from_bits(bits - 1) + } + } else if value.is_sign_positive() { + f64::from_bits(bits - 1) + } else { + f64::from_bits(bits + 1) + } +} + +fn round_candidate_with_tail(candidate: f64, tail_head: f64, tail_tail: f64) -> f64 { + let (tail, tail_roundoff) = error_free_sum(tail_head, tail_tail); + if tail == 0.0 && tail_roundoff == 0.0 { + return candidate; + } + + let upward = if tail != 0.0 { + tail.is_sign_positive() + } else { + tail_roundoff.is_sign_positive() + }; + let neighbor = adjacent_float(candidate, upward); + if !neighbor.is_finite() { + return neighbor; + } + + let half_gap = ((neighbor - candidate).abs()) * 0.5; + if half_gap == 0.0 { + return neighbor; + } + + let tail_magnitude = tail.abs(); + let beyond_midpoint = if tail_magnitude > half_gap { + true + } else if tail_magnitude < half_gap { + false + } else if tail_roundoff == 0.0 { + candidate.to_bits() & 1 == 1 + } else if upward { + tail_roundoff > 0.0 + } else { + tail_roundoff < 0.0 + }; + + if beyond_midpoint { neighbor } else { candidate } +} + +fn mixed_remainder_mean_over_total( + values: &[f64], + total_count: usize, +) -> Result { + let max_magnitude = values + .iter() + .map(|value| value.abs()) + .fold(0.0, f64::max); + if max_magnitude == 0.0 { + return Ok(0.0); + } + + let scale = exact_power_of_two_scale(max_magnitude); + let normalized = values.iter().map(|value| *value / scale).collect(); + let (normalized_sum, normalized_correction, normalized_correction_tail) = + deterministic_compensated_parts_with_tail(normalized); + if normalized_sum == 0.0 + && normalized_correction == 0.0 + && normalized_correction_tail == 0.0 + { + return Ok(0.0); + } + + let denominator = total_count as f64; + let leading_mean = normalized_sum / denominator; + let division_residual = (-leading_mean).mul_add(denominator, normalized_sum); + let (correction_numerator, correction_numerator_roundoff) = + error_free_sum(division_residual, normalized_correction); + let correction_mean = correction_numerator / denominator; + let correction_division_residual = + (-correction_mean).mul_add(denominator, correction_numerator) / denominator; + let (candidate, addition_roundoff) = error_free_sum(leading_mean, correction_mean); + let (tail_head, tail_tail) = deterministic_compensated_parts(vec![ + addition_roundoff, + correction_division_residual, + correction_numerator_roundoff / denominator, + normalized_correction_tail / denominator, + ]); + let normalized_mean = round_candidate_with_tail(candidate, tail_head, tail_tail); + let mean = normalized_mean * scale; + if !mean.is_finite() || mean == 0.0 { + Err(ValidationError::InvalidInput) + } else { + Ok(mean) + } +} + +/// Deterministically divide the represented sum of finite binary64 values by an explicit count. +/// +/// Opposite signs cancel before exact power-of-two scale reduction. Each +/// opposite-sign addition also retains its error-free low term so repeated +/// sub-ULP contributions cannot disappear one at a time before they collectively +/// become representable. When those retained terms remain material after +/// normalization, their compensation is carried through the scientific divisor +/// instead of being rounded into a single numerator first. The divisor is +/// independent of `values.len()`, which lets callers preserve an original +/// scientific denominator when an algebraically equivalent expanded term set is +/// needed to avoid overflowing intermediate differences. Exact cancellation +/// returns canonical zero; a nonzero quotient outside or below binary64 range +/// fails closed. +/// +/// # Errors +/// +/// Returns [`ValidationError::InvalidInput`] for an empty value set, a zero +/// divisor, non-finite input, or an unrepresentable nonzero quotient. +pub(crate) fn deterministic_representable_sum_over_count( + values: &[f64], + total_count: usize, +) -> Result { + if values.is_empty() || total_count == 0 || values.iter().any(|value| !value.is_finite()) { + return Err(ValidationError::InvalidInput); + } + + let mut positives = Vec::new(); + let mut negatives = Vec::new(); + for &value in values { + if value > 0.0 { + positives.push(value); + } else if value < 0.0 { + negatives.push(value); + } + } + + if positives.is_empty() && negatives.is_empty() { + return Ok(0.0); + } + if positives.is_empty() || negatives.is_empty() { + return same_sign_mean_over_total(values, total_count); + } + + positives.sort_by(|left, right| right.total_cmp(left)); + negatives.sort_by(|left, right| left.total_cmp(right)); + + let mut positive_index = 0_usize; + let mut negative_index = 0_usize; + let mut positive = positives[0]; + let mut negative = negatives[0]; + let mut residuals = Vec::with_capacity(values.len()); + let mut roundoff_terms = Vec::new(); + + loop { + let (residual, roundoff) = error_free_sum(positive, negative); + if roundoff != 0.0 { + roundoff_terms.push(roundoff); + } + + if residual > 0.0 { + positive = residual; + negative_index += 1; + if negative_index == negatives.len() { + residuals.push(positive); + residuals.extend_from_slice(&positives[positive_index + 1..]); + break; + } + negative = negatives[negative_index]; + } else if residual < 0.0 { + negative = residual; + positive_index += 1; + if positive_index == positives.len() { + residuals.push(negative); + residuals.extend_from_slice(&negatives[negative_index + 1..]); + break; + } + positive = positives[positive_index]; + } else { + positive_index += 1; + negative_index += 1; + if positive_index == positives.len() || negative_index == negatives.len() { + residuals.extend_from_slice(&positives[positive_index..]); + residuals.extend_from_slice(&negatives[negative_index..]); + break; + } + positive = positives[positive_index]; + negative = negatives[negative_index]; + } + } + + if roundoff_terms.is_empty() { + if residuals.is_empty() { + return Ok(0.0); + } + return same_sign_mean_over_total(&residuals, total_count); + } + + residuals.extend(roundoff_terms); + mixed_remainder_mean_over_total(&residuals, total_count) +} + +/// Deterministic mean of finite binary64 values with cancellation before scale reduction. +/// +/// Opposite signs cancel at represented magnitude before the remaining mass is +/// normalized by an exact power of two. Error-free low terms from cancellation +/// are retained so several individually sub-ULP contributions can still affect +/// the represented mean when their combined mass is large enough, and retained +/// compensation is carried through the original sample-count division before the +/// final scale is restored. Exact all-zero input and exact mixed-sign cancellation +/// return canonical zero; a mathematically nonzero mean that falls below +/// binary64 range fails closed. +/// +/// # Errors +/// +/// Returns [`ValidationError::InvalidInput`] for empty or non-finite input, or +/// when a nonzero represented mass has no nonzero binary64 mean. +pub(crate) fn deterministic_representable_mean(values: &[f64]) -> Result { + deterministic_representable_sum_over_count(values, values.len()) +} + +#[cfg(test)] +mod tests { + use super::{ + deterministic_compensated_sum, deterministic_representable_mean, + deterministic_representable_sum_over_count, + }; + use crate::ValidationError; + + #[test] + fn canonical_compensated_sum_is_order_stable() { + let left = deterministic_compensated_sum(vec![1.0, 1e-100, -1.0]); + let right = deterministic_compensated_sum(vec![-1.0, 1.0, 1e-100]); + assert_eq!(left.to_bits(), right.to_bits()); + } + + #[test] + fn representable_mean_preserves_full_range_cancellation() { + let minimum_subnormal = f64::from_bits(1); + let twice_minimum_subnormal = f64::from_bits(2); + let positive = [ + f64::MAX, + twice_minimum_subnormal, + twice_minimum_subnormal, + -f64::MAX, + ]; + let negative = [ + -f64::MAX, + -twice_minimum_subnormal, + -twice_minimum_subnormal, + f64::MAX, + ]; + assert_eq!( + deterministic_representable_mean(&positive) + .expect("positive") + .to_bits(), + minimum_subnormal.to_bits() + ); + assert_eq!( + deterministic_representable_mean(&negative) + .expect("negative") + .to_bits(), + (-minimum_subnormal).to_bits() + ); + assert_eq!( + deterministic_representable_mean(&[f64::MAX, -f64::MAX]), + Ok(0.0) + ); + } + + #[test] + fn representable_mean_retains_accumulated_opposite_sign_roundoff() { + let quarter_ulp_at_one = 2.0_f64.powi(-54); + let mean = deterministic_representable_mean(&[ + 1.0, + -quarter_ulp_at_one, + -quarter_ulp_at_one, + -quarter_ulp_at_one, + -quarter_ulp_at_one, + ]) + .expect("representable mixed-sign mean"); + assert_eq!(mean.to_bits(), 0x3fc9_9999_9999_9998); + } + + #[test] + fn explicit_denominator_preserves_representable_expanded_sum() { + let minimum_subnormal = f64::from_bits(1); + assert_eq!( + deterministic_representable_sum_over_count( + &[f64::MAX, -f64::MAX, f64::from_bits(3)], + 3, + ) + .expect("explicit denominator") + .to_bits(), + minimum_subnormal.to_bits() + ); + assert_eq!( + deterministic_representable_sum_over_count(&[f64::MAX, f64::MAX], 1), + Err(ValidationError::InvalidInput) + ); + assert_eq!( + deterministic_representable_sum_over_count(&[0.0], 0), + Err(ValidationError::InvalidInput) + ); + } + + #[test] + fn representable_mean_covers_admission_and_residual_paths() { + let minimum_subnormal = f64::from_bits(1); + assert_eq!( + deterministic_representable_mean(&[]), + Err(ValidationError::InvalidInput) + ); + assert_eq!( + deterministic_representable_mean(&[f64::NAN]), + Err(ValidationError::InvalidInput) + ); + assert_eq!(deterministic_representable_mean(&[0.0, -0.0]), Ok(0.0)); + assert_eq!(deterministic_representable_mean(&[1.0, 1.0]), Ok(1.0)); + assert_eq!(deterministic_representable_mean(&[-1.0, -1.0]), Ok(-1.0)); + assert_eq!( + deterministic_representable_mean(&[minimum_subnormal, 0.0]), + Err(ValidationError::InvalidInput) + ); + assert_eq!( + deterministic_representable_mean(&[3.0, -1.0, -1.0]), + Ok(1.0 / 3.0) + ); + assert_eq!( + deterministic_representable_mean(&[-3.0, 1.0, 1.0]), + Ok(-1.0 / 3.0) + ); + assert_eq!( + deterministic_representable_mean(&[3.0, -1.0, -1.0, -1.0]), + Ok(0.0) + ); + } +} diff --git a/crates/validation_core/src/report.rs b/crates/validation_core/src/report.rs index e458f142f..174f23230 100644 --- a/crates/validation_core/src/report.rs +++ b/crates/validation_core/src/report.rs @@ -4,8 +4,78 @@ use crate::MonteCarloSummary; use crate::ValidationError; use serde::{Deserialize, Serialize}; +const RMSE_STANDARD_ERROR_RELATIVE_TOLERANCE: f64 = 64.0 * f64::EPSILON; +const MONTE_CARLO_RMSE_SUPPORT_RELATIVE_TOLERANCE: f64 = 64.0 * f64::EPSILON; +const WILSON_PAIR_ABSOLUTE_TOLERANCE: f64 = 64.0 * f64::EPSILON; + +fn minority_zero_lower_is_rounding_feasible(p: f64, upper: f64) -> bool { + debug_assert!(p > 0.0 && p < 0.5); + debug_assert!(upper >= p && upper <= 1.0); + + // Solving the Wilson root identity for the lower endpoint avoids the + // cancellation that makes the complement-form residual insensitive near + // p = 0. The multiplication order keeps p² from underflowing before the + // final represented lower root itself would round to zero. + let p_squared = p * p; + let denominator = p_squared + (1.0 - 2.0 * p) * upper; + debug_assert!(denominator.is_finite() && denominator > 0.0); + let implied_lower = p * ((p / denominator) * (1.0 - upper)); + implied_lower == 0.0 +} + +/// Check whether a stored Wilson endpoint pair can arise from one Wilson score interval. +/// +/// For empirical coverage `p` and `a = z² / n`, the Wilson roots satisfy +/// `L * U = p² / (1 + a)` and `L + U = 1 + (2p - 1) / (1 + a)`. Eliminating +/// the unrecorded `a` gives a necessary endpoint-pair identity. For `p < 0.5`, +/// the equivalent identity on the uncovered proportion avoids squaring a tiny +/// `p`. All terms remain probability-scaled, so a small absolute binary64 +/// tolerance is sufficient for ordinary interior pairs without overflow-prone +/// reconstruction of `n` or `z`. A boundary endpoint needs one additional +/// peer-root check: the eliminated identity can lose all sensitivity to a +/// representable minority lower root when the stored lower endpoint is exact +/// zero. The complement-symmetric case applies to an exact-one upper endpoint +/// above `p = 0.5`. At exact all-covered `p = 1`, the eliminated identity is +/// degenerate, while the canonical producer still requires the lower endpoint +/// `n / (n + z²)` to be strictly positive for every non-empty sample and finite +/// represented `z²`. +fn wilson_pair_is_algebraically_coherent(p: f64, lower: f64, upper: f64) -> bool { + if p == 0.0 { + return true; + } + if p == 1.0 { + return lower > 0.0; + } + + if p < 0.5 && lower == 0.0 && !minority_zero_lower_is_rounding_feasible(p, upper) { + return false; + } + if p > 0.5 + && upper == 1.0 + && !minority_zero_lower_is_rounding_feasible(1.0 - p, 1.0 - lower) + { + return false; + } + + let endpoint_sum = lower + upper; + let (left, right) = if p >= 0.5 { + ( + p * p * (endpoint_sum - 1.0), + (2.0 * p - 1.0) * lower * upper, + ) + } else { + let uncovered = 1.0 - p; + ( + uncovered * uncovered * (1.0 - endpoint_sum), + (1.0 - 2.0 * p) * (1.0 - lower) * (1.0 - upper), + ) + }; + + (left - right).abs() <= WILSON_PAIR_ABSOLUTE_TOLERANCE +} + /// Machine-readable recovery report for a single study. -#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)] +#[derive(Clone, Debug, PartialEq)] pub struct ValidationReport { /// Study label (not free-form PII). pub study_label: String, @@ -30,12 +100,41 @@ pub struct ValidationReport { } impl ValidationReport { - /// Reject non-finite numeric fields before serialization or export. + /// Validate numeric and scientific invariants before serialization or export. + /// + /// RMSE and standard errors are nonnegative. Under the crate's squared-residual + /// delta-method producer, `SE(RMSE) <= RMSE / 2`: for `x_i = r_i^2 >= 0`, the + /// sample standard deviation satisfies `sd(x) <= sqrt(n) * mean(x)`. Admission + /// allows a small relative binary64 tolerance at that support boundary. Exact + /// zero RMSE is perfect recovery and therefore still requires exact-zero RMSE + /// standard error. Empirical coverage, Wilson endpoints, and temporal-order + /// accuracy are probabilities in `[0, 1]`; the Wilson interval is ordered, + /// contains the empirical coverage recorded in the same report, and its two + /// endpoints must satisfy the same Wilson-score root identity for that + /// coverage. A stored zero/one endpoint is additionally refused when the peer + /// endpoint implies a representable non-boundary Wilson root. This prevents two + /// individually plausible bounds from being combined into an interval that no + /// finite positive Wilson `z² / n` can produce. Mean signed bias remains + /// unrestricted in sign. A generic [`MonteCarloSummary`] may summarize a signed + /// metric, but when it occupies `monte_carlo_rmse` every retained replication is + /// nonnegative. Its mean and percentile endpoints are therefore nonnegative. + /// Nonnegative sample support additionally implies `SD <= sqrt(n) * mean`, + /// `SE(mean) <= mean`, and every retained value—and thus every inclusive + /// nearest-rank percentile endpoint—is at most `n * mean`. Admission evaluates + /// the percentile support as `endpoint / mean <= n` with a small relative + /// binary64 tolerance so the check does not overflow a finite sample sum. A + /// zero Monte Carlo RMSE mean is exact perfect recovery across every retained + /// replication, so spread, standard error, and empirical percentile endpoints + /// must all be zero as well. These checks prevent a finite but scientifically + /// impossible payload from becoming durable Validation Evidence. /// /// # Errors /// - /// Returns [`ValidationError::InvalidInput`] when any `f64` field or the - /// optional Monte Carlo summary violates finiteness / summary invariants. + /// Returns [`ValidationError::InvalidInput`] when any `f64` field is + /// non-finite, violates its metric domain, point RMSE and its standard error + /// exceed squared-residual support, Wilson evidence is incoherent, or the + /// optional Monte Carlo RMSE summary violates either generic summary invariants + /// or the nonnegative RMSE support. pub fn validate(&self) -> Result<(), ValidationError> { for value in [ self.rmse, @@ -51,8 +150,76 @@ impl ValidationReport { return Err(ValidationError::InvalidInput); } } + + if self.rmse < 0.0 || self.rmse_standard_error < 0.0 || self.bias_standard_error < 0.0 { + return Err(ValidationError::InvalidInput); + } + if self.rmse == 0.0 { + if self.rmse_standard_error != 0.0 { + return Err(ValidationError::InvalidInput); + } + } else { + let relative_standard_error = self.rmse_standard_error / self.rmse; + if !relative_standard_error.is_finite() + || relative_standard_error > 0.5 + RMSE_STANDARD_ERROR_RELATIVE_TOLERANCE + { + return Err(ValidationError::InvalidInput); + } + } + if !(0.0..=1.0).contains(&self.interval_coverage) + || !(0.0..=1.0).contains(&self.coverage_wilson_lower) + || !(0.0..=1.0).contains(&self.coverage_wilson_upper) + || !(0.0..=1.0).contains(&self.temporal_order_accuracy) + { + return Err(ValidationError::InvalidInput); + } + if self.coverage_wilson_lower > self.coverage_wilson_upper + || self.interval_coverage < self.coverage_wilson_lower + || self.interval_coverage > self.coverage_wilson_upper + || !wilson_pair_is_algebraically_coherent( + self.interval_coverage, + self.coverage_wilson_lower, + self.coverage_wilson_upper, + ) + { + return Err(ValidationError::InvalidInput); + } + if let Some(summary) = self.monte_carlo_rmse { - summary.validate()?; + let summary = summary.validate()?; + if summary.mean < 0.0 + || summary.percentile_lower < 0.0 + || summary.percentile_upper < 0.0 + { + return Err(ValidationError::InvalidInput); + } + if summary.mean == 0.0 { + if summary.standard_deviation != 0.0 + || summary.standard_error != 0.0 + || summary.percentile_lower != 0.0 + || summary.percentile_upper != 0.0 + { + return Err(ValidationError::InvalidInput); + } + } else { + let relative_standard_error = summary.standard_error / summary.mean; + if !relative_standard_error.is_finite() + || relative_standard_error + > 1.0 + RMSE_STANDARD_ERROR_RELATIVE_TOLERANCE + { + return Err(ValidationError::InvalidInput); + } + + let relative_upper_percentile = summary.percentile_upper / summary.mean; + let replication_support = summary.replication_count as f64; + if !relative_upper_percentile.is_finite() + || relative_upper_percentile + > replication_support + * (1.0 + MONTE_CARLO_RMSE_SUPPORT_RELATIVE_TOLERANCE) + { + return Err(ValidationError::InvalidInput); + } + } } Ok(()) } @@ -61,17 +228,23 @@ impl ValidationReport { /// /// # Errors /// - /// Returns [`ValidationError::InvalidInput`] when fields are non-finite or - /// serialization fails. + /// Returns [`ValidationError::InvalidInput`] when fields violate report + /// invariants or serialization fails. pub fn to_json(&self) -> Result { self.validate()?; serde_json::to_string(self).map_err(|_| ValidationError::InvalidInput) } - /// Render a short human-readable summary line. - #[must_use] - pub fn to_human_summary(&self) -> String { - format!( + /// Render a short human-readable summary line after validating the report. + /// + /// # Errors + /// + /// Returns [`ValidationError::InvalidInput`] when the report violates its + /// numeric or scientific invariants. Human-readable projection is therefore + /// subject to the same fail-closed boundary as JSON ingress and egress. + pub fn to_human_summary(&self) -> Result { + self.validate()?; + Ok(format!( "study={} rmse={:.6} (se={:.6}) bias={:.6} (se={:.6}) coverage={:.3} temporal_order={:.3}", self.study_label, self.rmse, @@ -80,7 +253,67 @@ impl ValidationReport { self.bias_standard_error, self.interval_coverage, self.temporal_order_accuracy - ) + )) + } +} + +impl Serialize for ValidationReport { + fn serialize(&self, serializer: S) -> Result + where + S: serde::Serializer, + { + use serde::ser::SerializeStruct; + + self.validate().map_err(serde::ser::Error::custom)?; + let mut state = serializer.serialize_struct("ValidationReport", 10)?; + state.serialize_field("study_label", &self.study_label)?; + state.serialize_field("rmse", &self.rmse)?; + state.serialize_field("rmse_standard_error", &self.rmse_standard_error)?; + state.serialize_field("mean_bias", &self.mean_bias)?; + state.serialize_field("bias_standard_error", &self.bias_standard_error)?; + state.serialize_field("interval_coverage", &self.interval_coverage)?; + state.serialize_field("coverage_wilson_lower", &self.coverage_wilson_lower)?; + state.serialize_field("coverage_wilson_upper", &self.coverage_wilson_upper)?; + state.serialize_field("temporal_order_accuracy", &self.temporal_order_accuracy)?; + state.serialize_field("monte_carlo_rmse", &self.monte_carlo_rmse)?; + state.end() + } +} + +impl<'de> Deserialize<'de> for ValidationReport { + fn deserialize(deserializer: D) -> Result + where + D: serde::Deserializer<'de>, + { + #[derive(Deserialize)] + struct Raw { + study_label: String, + rmse: f64, + rmse_standard_error: f64, + mean_bias: f64, + bias_standard_error: f64, + interval_coverage: f64, + coverage_wilson_lower: f64, + coverage_wilson_upper: f64, + temporal_order_accuracy: f64, + monte_carlo_rmse: Option, + } + + let raw = Raw::deserialize(deserializer)?; + let report = Self { + study_label: raw.study_label, + rmse: raw.rmse, + rmse_standard_error: raw.rmse_standard_error, + mean_bias: raw.mean_bias, + bias_standard_error: raw.bias_standard_error, + interval_coverage: raw.interval_coverage, + coverage_wilson_lower: raw.coverage_wilson_lower, + coverage_wilson_upper: raw.coverage_wilson_upper, + temporal_order_accuracy: raw.temporal_order_accuracy, + monte_carlo_rmse: raw.monte_carlo_rmse, + }; + report.validate().map_err(serde::de::Error::custom)?; + Ok(report) } } @@ -91,6 +324,8 @@ impl Serialize for MonteCarloSummary { S: serde::Serializer, { use serde::ser::SerializeStruct; + + (*self).validate().map_err(serde::ser::Error::custom)?; let mut state = serializer.serialize_struct("MonteCarloSummary", 6)?; state.serialize_field("replication_count", &self.replication_count)?; state.serialize_field("mean", &self.mean)?; @@ -143,15 +378,15 @@ mod tests { rmse_standard_error: 0.01, mean_bias: 0.0, bias_standard_error: 0.02, - interval_coverage: 0.95, - coverage_wilson_lower: 0.9, - coverage_wilson_upper: 0.98, + interval_coverage: 0.5, + coverage_wilson_lower: 0.2, + coverage_wilson_upper: 0.8, temporal_order_accuracy: 1.0, monte_carlo_rmse: Some(MonteCarloSummary { replication_count: 10, mean: 0.11, standard_deviation: 0.01, - standard_error: 0.003, + standard_error: 0.01 / 10.0_f64.sqrt(), percentile_lower: 0.09, percentile_upper: 0.13, }), @@ -159,7 +394,12 @@ mod tests { let json = report.to_json().expect("json"); let decoded: ValidationReport = serde_json::from_str(&json).expect("decode"); assert_eq!(decoded.study_label, "foundation-recovery"); - assert!(report.to_human_summary().contains("rmse=0.100000")); + assert!( + report + .to_human_summary() + .expect("human summary") + .contains("rmse=0.100000") + ); let none_report = ValidationReport { monte_carlo_rmse: None, ..report.clone() diff --git a/crates/validation_core/src/rmse.rs b/crates/validation_core/src/rmse.rs index 52b2a8323..c1a30c769 100644 --- a/crates/validation_core/src/rmse.rs +++ b/crates/validation_core/src/rmse.rs @@ -1,37 +1,91 @@ //! Root-mean-square error recovery metric. use crate::ValidationError; -use crate::input::require_finite; use crate::matching::absolute_residuals; +use crate::numeric::deterministic_compensated_sum; + +struct ScaledRmse { + scale: f64, + rmse: f64, + normalized_rmse: f64, + normalized_mean_square: f64, + normalized_squares: Vec, +} + +fn scaled_rmse(residuals: &[f64]) -> Result { + if residuals.is_empty() || residuals.iter().any(|residual| !residual.is_finite()) { + return Err(ValidationError::InvalidInput); + } + + let scale = residuals + .iter() + .map(|residual| residual.abs()) + .fold(0.0, f64::max); + if scale == 0.0 { + return Ok(ScaledRmse { + scale: 0.0, + rmse: 0.0, + normalized_rmse: 0.0, + normalized_mean_square: 0.0, + normalized_squares: vec![0.0; residuals.len()], + }); + } + + let normalized_squares: Vec<_> = residuals + .iter() + .map(|residual| { + let normalized = *residual / scale; + normalized * normalized + }) + .collect(); + let normalized_mean_square = + deterministic_compensated_sum(normalized_squares.clone()) / residuals.len() as f64; + let normalized_rmse = normalized_mean_square.sqrt(); + let rmse = scale * normalized_rmse; + if !rmse.is_finite() || (rmse == 0.0 && normalized_rmse != 0.0) { + Err(ValidationError::InvalidInput) + } else { + Ok(ScaledRmse { + scale, + rmse: if rmse == 0.0 { 0.0 } else { rmse }, + normalized_rmse, + normalized_mean_square, + normalized_squares, + }) + } +} /// Compute RMSE between truth and recovered parameter vectors. /// +/// Residuals are normalized by their largest magnitude before squaring. This +/// preserves a representable RMSE when raw residual squares would overflow or +/// underflow, while a mathematically non-zero RMSE that is itself below the +/// binary64 range fails closed rather than becoming false perfect recovery. +/// /// # Errors /// /// Returns [`ValidationError::InvalidInput`] for empty, unequal-length, -/// non-finite inputs, or squared-residual overflow. +/// non-finite inputs, an unrepresentable residual, or an unrepresentable final +/// RMSE. pub fn root_mean_square_error(truth: &[f64], recovered: &[f64]) -> Result { let residuals = absolute_residuals(truth, recovered)?; - let mut square_sum = 0.0_f64; - for residual in &residuals { - let square = residual * residual; - if !square.is_finite() { - return Err(ValidationError::InvalidInput); - } - square_sum += square; - } - require_finite((square_sum / residuals.len() as f64).sqrt()) + Ok(scaled_rmse(&residuals)?.rmse) } /// Approximate standard error of the RMSE under independent squared residuals. /// /// Uses the delta-method form `se ≈ sd(r²) / (2 · RMSE · √n)` with sample SD of -/// squared residuals. Returns `0.0` when RMSE is zero. +/// squared residuals. Squared residuals stay normalized by the largest residual +/// magnitude, so representable RMSE standard errors do not fail because an +/// avoidable raw square or squared-deviation intermediate overflows. Returns +/// `0.0` when every residual is exactly zero or the squared residuals are +/// exactly constant. /// /// # Errors /// -/// Returns [`ValidationError::InvalidInput`] for invalid pairs or when fewer -/// than two observations are present for a non-zero RMSE. +/// Returns [`ValidationError::InvalidInput`] for invalid pairs, when fewer than +/// two observations are present for a non-zero RMSE, or when a non-zero RMSE or +/// standard error is outside the representable binary64 range. pub fn rmse_standard_error(truth: &[f64], recovered: &[f64]) -> Result { let residuals = absolute_residuals(truth, recovered)?; rmse_standard_error_from_residuals(&residuals) @@ -39,36 +93,37 @@ pub fn rmse_standard_error(truth: &[f64], recovered: &[f64]) -> Result Result { - let n = residuals.len() as f64; - let mut square_sum = 0.0_f64; - let mut squares = Vec::with_capacity(residuals.len()); - for residual in residuals { - let square = residual * residual; - if !square.is_finite() { - return Err(ValidationError::InvalidInput); - } - squares.push(square); - square_sum += square; - } - let rmse = require_finite((square_sum / n).sqrt())?; - if rmse <= 0.0 { + let scaled = scaled_rmse(residuals)?; + if scaled.rmse == 0.0 { return Ok(0.0); } if residuals.len() < 2 { return Err(ValidationError::InvalidInput); } - let mean = require_finite(squares.iter().sum::() / n)?; - let mut variance_sum = 0.0_f64; - for value in &squares { - let delta = value - mean; - let square = delta * delta; - if !square.is_finite() { - return Err(ValidationError::InvalidInput); - } - variance_sum += square; + + let n = residuals.len() as f64; + let normalized_deviation_squares: Vec<_> = scaled + .normalized_squares + .iter() + .map(|value| { + let deviation = *value - scaled.normalized_mean_square; + deviation * deviation + }) + .collect(); + let normalized_sample_variance = + deterministic_compensated_sum(normalized_deviation_squares) / (n - 1.0); + let denominator = 2.0 * scaled.normalized_rmse * n.sqrt(); + let normalized_standard_error = normalized_sample_variance.sqrt() / denominator; + let standard_error = scaled.scale * normalized_standard_error; + if !standard_error.is_finite() + || (standard_error == 0.0 && normalized_standard_error != 0.0) + { + Err(ValidationError::InvalidInput) + } else if standard_error == 0.0 { + Ok(0.0) + } else { + Ok(standard_error) } - let variance = variance_sum / (n - 1.0); - require_finite(require_finite(variance.sqrt())? / (2.0 * rmse * n.sqrt())) } #[cfg(test)] @@ -82,8 +137,8 @@ mod tests { let recovered = [3.0, 4.0, 0.0]; let rmse = root_mean_square_error(&truth, &recovered).expect("ok"); assert!((rmse - (25.0_f64 / 3.0).sqrt()).abs() < 1e-12); - assert!((root_mean_square_error(&[1.0], &[1.0]).expect("zero") - 0.0).abs() < 1e-12); - assert!((rmse_standard_error(&[1.0], &[1.0]).expect("se0") - 0.0).abs() < 1e-12); + assert_eq!(root_mean_square_error(&[1.0], &[1.0]), Ok(0.0)); + assert_eq!(rmse_standard_error(&[1.0], &[1.0]), Ok(0.0)); let se = rmse_standard_error(&truth, &recovered).expect("se"); assert!(se.is_finite()); assert!(se > 0.0); @@ -100,35 +155,49 @@ mod tests { Err(ValidationError::InvalidInput) ); assert_eq!(rmse_standard_error_from_residuals(&[0.0]), Ok(0.0)); - // Squared residual overflow yields non-finite RMSE. + } + + #[test] + fn representable_extremes_avoid_raw_square_overflow() { + assert_eq!( + root_mean_square_error(&[0.0, 0.0], &[f64::MAX, f64::MAX]), + Ok(f64::MAX) + ); assert_eq!( rmse_standard_error_from_residuals(&[f64::MAX, f64::MAX]), - Err(ValidationError::InvalidInput) + Ok(0.0) ); + + let huge = 1e200; assert_eq!( - root_mean_square_error(&[0.0], &[f64::MAX]), - Err(ValidationError::InvalidInput) + rmse_standard_error_from_residuals(&[huge, huge, huge]), + Ok(0.0) ); + + let finite_se = rmse_standard_error_from_residuals(&[1e154, 0.0]) + .expect("normalized squared-residual deviations remain representable"); + assert!(finite_se.is_finite()); + assert!(finite_se > 0.0); } #[test] - fn overflow_and_nonfinite_intermediates_fail_closed() { + fn subnormal_rmse_distinguishes_representable_error_from_false_zero() { + let ulp = f64::from_bits(1); + assert_eq!(root_mean_square_error(&[0.0, 0.0], &[ulp, 0.0]), Ok(ulp)); assert_eq!( - root_mean_square_error(&[0.0, 0.0], &[f64::MAX, f64::MAX]), - Err(ValidationError::InvalidInput) - ); - assert_eq!( - rmse_standard_error_from_residuals(&[f64::MAX, f64::MAX]), + root_mean_square_error(&[0.0, 0.0, 0.0, 0.0], &[ulp, 0.0, 0.0, 0.0]), Err(ValidationError::InvalidInput) ); - let huge = 1e200; + } + + #[test] + fn unrepresentable_residual_still_fails_closed() { assert_eq!( - rmse_standard_error_from_residuals(&[huge, -huge, huge]), + root_mean_square_error(&[f64::MAX], &[-f64::MAX]), Err(ValidationError::InvalidInput) ); - // Finite residual squares whose variance deviations overflow. assert_eq!( - rmse_standard_error_from_residuals(&[1e154, 0.0]), + rmse_standard_error(&[f64::MAX, 0.0], &[-f64::MAX, 0.0]), Err(ValidationError::InvalidInput) ); } diff --git a/crates/validation_core/src/validation_evidence.rs b/crates/validation_core/src/validation_evidence.rs new file mode 100644 index 000000000..ff465f15f --- /dev/null +++ b/crates/validation_core/src/validation_evidence.rs @@ -0,0 +1,104 @@ +//! Versioned durable envelope for validation projections and their scientific provenance. + +use crate::{ValidationError, ValidationReport, WilsonCoverageEvidenceV1}; +use serde::{Deserialize, Serialize}; + +const SCHEMA: &str = "tepp.validation_evidence.v1"; + +/// Durable validation evidence envelope with a report projection and recomputable coverage proof. +/// +/// [`ValidationReport`] remains the compact projection used by existing callers. This versioned +/// envelope binds that projection to [`WilsonCoverageEvidenceV1`], which retains the empirical +/// denominator, covered count, standard-normal critical value, and two-sided interval semantics. +/// Admission requires the report's coverage proportion and Wilson endpoints to equal the values +/// recomputed by the nested provenance carrier, preventing a durable artifact from pairing a +/// valid report with evidence produced from a different finite sample or critical value. +#[derive(Clone, Debug, PartialEq)] +pub struct ValidationEvidenceV1 { + /// Existing compact validation projection. + pub report: ValidationReport, + /// Versioned, recomputable Wilson coverage provenance for the report's coverage fields. + pub coverage: WilsonCoverageEvidenceV1, +} + +impl ValidationEvidenceV1 { + /// Construct a validated durable evidence envelope. + /// + /// # Errors + /// + /// Returns [`ValidationError::InvalidInput`] when either nested artifact is invalid or the + /// report's empirical coverage/Wilson projection does not exactly match its provenance. + pub fn new( + report: ValidationReport, + coverage: WilsonCoverageEvidenceV1, + ) -> Result { + let evidence = Self { report, coverage }; + evidence.validate()?; + Ok(evidence) + } + + /// Validate nested artifacts and their cross-artifact coverage projection identity. + /// + /// # Errors + /// + /// Returns [`ValidationError::InvalidInput`] when nested validation fails or the report's + /// coverage proportion/lower/upper endpoints differ from the recomputed versioned evidence. + pub fn validate(&self) -> Result<(), ValidationError> { + self.report.validate()?; + self.coverage.validate()?; + if self.report.interval_coverage != self.coverage.empirical_coverage + || self.report.coverage_wilson_lower != self.coverage.wilson_lower + || self.report.coverage_wilson_upper != self.coverage.wilson_upper + { + return Err(ValidationError::InvalidInput); + } + Ok(()) + } + + /// Serialize the validated envelope to canonical JSON. + /// + /// # Errors + /// + /// Returns [`ValidationError::InvalidInput`] when validation or serialization fails. + pub fn to_json(&self) -> Result { + self.validate()?; + serde_json::to_string(self).map_err(|_| ValidationError::InvalidInput) + } +} + +impl Serialize for ValidationEvidenceV1 { + fn serialize(&self, serializer: S) -> Result + where + S: serde::Serializer, + { + use serde::ser::SerializeStruct; + + self.validate().map_err(serde::ser::Error::custom)?; + let mut state = serializer.serialize_struct("ValidationEvidenceV1", 3)?; + state.serialize_field("schema", SCHEMA)?; + state.serialize_field("report", &self.report)?; + state.serialize_field("coverage", &self.coverage)?; + state.end() + } +} + +impl<'de> Deserialize<'de> for ValidationEvidenceV1 { + fn deserialize(deserializer: D) -> Result + where + D: serde::Deserializer<'de>, + { + #[derive(Deserialize)] + #[serde(deny_unknown_fields)] + struct Raw { + schema: String, + report: ValidationReport, + coverage: WilsonCoverageEvidenceV1, + } + + let raw = Raw::deserialize(deserializer)?; + if raw.schema != SCHEMA { + return Err(serde::de::Error::custom("unsupported validation evidence schema")); + } + Self::new(raw.report, raw.coverage).map_err(serde::de::Error::custom) + } +} diff --git a/crates/validation_core/tests/bias_cancellation_roundoff_contract.rs b/crates/validation_core/tests/bias_cancellation_roundoff_contract.rs new file mode 100644 index 000000000..3e34f8311 --- /dev/null +++ b/crates/validation_core/tests/bias_cancellation_roundoff_contract.rs @@ -0,0 +1,41 @@ +use validation_core::mean_bias; + +#[test] +fn mixed_sign_bias_preserves_small_opposing_mass_before_division() { + let quarter_ulp_at_one = 2.0_f64.powi(-54); + let truth = [ + 0.0, + quarter_ulp_at_one, + quarter_ulp_at_one, + quarter_ulp_at_one, + quarter_ulp_at_one, + ]; + let recovered = [1.0, 0.0, 0.0, 0.0, 0.0]; + + let bias = mean_bias(&truth, &recovered) + .expect("the represented-input mean bias is finite and representable"); + + assert_eq!( + bias.to_bits(), + 0x3fc9_9999_9999_9998, + "four quarter-ulp opposing residuals sum to one full ulp before division and must not be rounded away one at a time" + ); +} + +#[test] +fn mixed_sign_bias_roundoff_contract_is_sign_symmetric() { + let quarter_ulp_at_one = 2.0_f64.powi(-54); + let truth = [ + 0.0, + -quarter_ulp_at_one, + -quarter_ulp_at_one, + -quarter_ulp_at_one, + -quarter_ulp_at_one, + ]; + let recovered = [-1.0, 0.0, 0.0, 0.0, 0.0]; + + let bias = mean_bias(&truth, &recovered) + .expect("the represented-input mean bias is finite and representable"); + + assert_eq!(bias.to_bits(), (-f64::from_bits(0x3fc9_9999_9999_9998)).to_bits()); +} diff --git a/crates/validation_core/tests/bias_cancellation_subnormal_contract.rs b/crates/validation_core/tests/bias_cancellation_subnormal_contract.rs new file mode 100644 index 000000000..4559e023b --- /dev/null +++ b/crates/validation_core/tests/bias_cancellation_subnormal_contract.rs @@ -0,0 +1,60 @@ +use validation_core::mean_bias; + +#[test] +fn extreme_cancellation_preserves_representable_subnormal_bias() { + let minimum_subnormal = f64::from_bits(1); + let twice_minimum_subnormal = f64::from_bits(2); + let truth = [0.0; 4]; + + let positive_bias = [ + f64::MAX, + twice_minimum_subnormal, + twice_minimum_subnormal, + -f64::MAX, + ]; + let recovered_positive = mean_bias(&truth, &positive_bias).expect("representable positive bias"); + assert_eq!(recovered_positive.to_bits(), minimum_subnormal.to_bits()); + + let negative_bias = [ + -f64::MAX, + -twice_minimum_subnormal, + -twice_minimum_subnormal, + f64::MAX, + ]; + let recovered_negative = mean_bias(&truth, &negative_bias).expect("representable negative bias"); + assert_eq!( + recovered_negative.to_bits(), + (-minimum_subnormal).to_bits() + ); +} + +#[test] +fn mixed_sign_bias_is_canonical_under_transport_permutation() { + let truth = [0.0; 3]; + let first = [3.0, -1.0, -1.0]; + let permuted = [-1.0, 3.0, -1.0]; + let expected = 1.0 / 3.0; + + let first_bias = mean_bias(&truth, &first).expect("first bias"); + let permuted_bias = mean_bias(&truth, &permuted).expect("permuted bias"); + assert_eq!(first_bias.to_bits(), expected.to_bits()); + assert_eq!(permuted_bias.to_bits(), first_bias.to_bits()); + + let negative = [-3.0, 1.0, 1.0]; + let negative_bias = mean_bias(&truth, &negative).expect("negative bias"); + assert_eq!(negative_bias.to_bits(), (-expected).to_bits()); +} + +#[test] +fn full_range_exact_cancellation_remains_exact_zero() { + let minimum_subnormal = f64::from_bits(1); + let truth = [0.0; 4]; + let recovered = [ + f64::MAX, + minimum_subnormal, + -f64::MAX, + -minimum_subnormal, + ]; + + assert_eq!(mean_bias(&truth, &recovered), Ok(0.0)); +} diff --git a/crates/validation_core/tests/bias_compensated_division_contract.rs b/crates/validation_core/tests/bias_compensated_division_contract.rs new file mode 100644 index 000000000..cf2eda523 --- /dev/null +++ b/crates/validation_core/tests/bias_compensated_division_contract.rs @@ -0,0 +1,20 @@ +use validation_core::mean_bias; + +#[test] +fn mean_bias_divides_compensated_numerator_without_double_rounding() { + let two_to_minus_53 = 2.0_f64.powi(-53); + let two_to_minus_52 = 2.0_f64.powi(-52); + let truth = [0.0, 0.0, 0.0]; + let recovered = [ + two_to_minus_53, + -two_to_minus_52, + -(1.0 + two_to_minus_52), + ]; + + let bias = mean_bias(&truth, &recovered).expect("represented mean bias"); + assert_eq!(bias.to_bits(), 0xbfd5_5555_5555_5557); + + let mirrored: Vec<_> = recovered.iter().map(|value| -*value).collect(); + let mirrored_bias = mean_bias(&truth, &mirrored).expect("mirrored represented mean bias"); + assert_eq!(mirrored_bias.to_bits(), 0x3fd5_5555_5555_5557); +} diff --git a/crates/validation_core/tests/bias_overflow_safe_mean_contract.rs b/crates/validation_core/tests/bias_overflow_safe_mean_contract.rs new file mode 100644 index 000000000..322644a74 --- /dev/null +++ b/crates/validation_core/tests/bias_overflow_safe_mean_contract.rs @@ -0,0 +1,20 @@ +use validation_core::{bias_standard_error, mean_bias}; + +#[test] +fn representable_extreme_constant_bias_survives_raw_sum_overflow() { + let truth = [0.0, 0.0]; + let recovered = [f64::MAX, f64::MAX]; + + assert_eq!(mean_bias(&truth, &recovered), Ok(f64::MAX)); + assert_eq!(bias_standard_error(&truth, &recovered), Ok(0.0)); +} + +#[test] +fn representable_bias_standard_error_survives_raw_square_sum_overflow() { + let truth = [0.0, 0.0, 0.0]; + let recovered = [1.0e154, -1.0e154, 0.0]; + + let expected = 1.0e154 / 3.0_f64.sqrt(); + let actual = bias_standard_error(&truth, &recovered).expect("representable standard error"); + assert!(((actual - expected) / expected).abs() <= f64::EPSILON); +} diff --git a/crates/validation_core/tests/bias_overflowing_residual_cancellation_contract.rs b/crates/validation_core/tests/bias_overflowing_residual_cancellation_contract.rs new file mode 100644 index 000000000..cda52afa4 --- /dev/null +++ b/crates/validation_core/tests/bias_overflowing_residual_cancellation_contract.rs @@ -0,0 +1,33 @@ +use validation_core::{ValidationError, mean_bias}; + +#[test] +fn opposite_overflowing_residuals_cancel_to_representable_zero() { + let truth = [-f64::MAX, f64::MAX]; + let recovered = [f64::MAX, -f64::MAX]; + + assert_eq!( + mean_bias(&truth, &recovered), + Ok(0.0), + "representable mean bias must not fail only because individual signed residuals overflow" + ); +} + +#[test] +fn overflowing_residual_cancellation_preserves_minimum_subnormal_mean() { + let minimum_subnormal = f64::from_bits(1); + let three_minimum_subnormals = f64::from_bits(3); + let truth = [-f64::MAX, f64::MAX, 0.0]; + let recovered = [f64::MAX, -f64::MAX, three_minimum_subnormals]; + + let bias = mean_bias(&truth, &recovered) + .expect("the exact represented-input mean bias is one minimum subnormal"); + assert_eq!(bias.to_bits(), minimum_subnormal.to_bits()); +} + +#[test] +fn one_sided_unrepresentable_mean_bias_still_fails_closed() { + assert_eq!( + mean_bias(&[-f64::MAX], &[f64::MAX]), + Err(ValidationError::InvalidInput) + ); +} diff --git a/crates/validation_core/tests/bias_pairwise_subtraction_roundoff_contract.rs b/crates/validation_core/tests/bias_pairwise_subtraction_roundoff_contract.rs new file mode 100644 index 000000000..6e0f680e1 --- /dev/null +++ b/crates/validation_core/tests/bias_pairwise_subtraction_roundoff_contract.rs @@ -0,0 +1,27 @@ +use validation_core::mean_bias; + +fn assert_bias_bits(truth: [f64; 2], recovered: [f64; 2], expected: u64) { + let bias = mean_bias(&truth, &recovered).expect("represented mean bias"); + assert_eq!(bias.to_bits(), expected); + + let mirrored_truth = truth.map(|value| -value); + let mirrored_recovered = recovered.map(|value| -value); + let mirrored_bias = mean_bias(&mirrored_truth, &mirrored_recovered) + .expect("mirrored represented mean bias"); + assert_eq!(mirrored_bias.to_bits(), expected | (1_u64 << 63)); +} + +#[test] +fn mean_bias_preserves_pairwise_subtraction_roundoff_before_averaging() { + assert_bias_bits( + [2.0_f64.powi(-108), 2.0_f64.powi(-53)], + [2.0_f64.powi(-54), 1.0], + 0x3fdf_ffff_ffff_ffff, + ); + + assert_bias_bits( + [2.0_f64.powi(-105), 2.0_f64.powi(-53)], + [2.0_f64.powi(-51), 1.0], + 0x3fe0_0000_0000_0001, + ); +} diff --git a/crates/validation_core/tests/bias_same_sign_remainder_division_contract.rs b/crates/validation_core/tests/bias_same_sign_remainder_division_contract.rs new file mode 100644 index 000000000..48dd4a68c --- /dev/null +++ b/crates/validation_core/tests/bias_same_sign_remainder_division_contract.rs @@ -0,0 +1,18 @@ +use validation_core::mean_bias; + +#[test] +fn mean_bias_keeps_same_sign_remainder_compensation_through_division() { + let truth = [0.0, 0.0, 0.0]; + let recovered = [ + f64::from_bits(0x3fc0_0000_0000_0004), + f64::from_bits(0x3fbf_ffff_ffff_fffc), + f64::from_bits(0xbfbf_ffff_ffff_fffd), + ]; + + let bias = mean_bias(&truth, &recovered).expect("represented mean bias"); + assert_eq!(bias.to_bits(), 0x3fa5_5555_5555_555a); + + let mirrored: Vec<_> = recovered.iter().map(|value| -*value).collect(); + let mirrored_bias = mean_bias(&truth, &mirrored).expect("mirrored represented mean bias"); + assert_eq!(mirrored_bias.to_bits(), 0xbfa5_5555_5555_555a); +} diff --git a/crates/validation_core/tests/bias_same_sign_subnormal_double_rounding_contract.rs b/crates/validation_core/tests/bias_same_sign_subnormal_double_rounding_contract.rs new file mode 100644 index 000000000..57aaf0d12 --- /dev/null +++ b/crates/validation_core/tests/bias_same_sign_subnormal_double_rounding_contract.rs @@ -0,0 +1,75 @@ +//! Contracts exact binary64 mean-bias rounding for same-sign subnormal inputs. +//! +//! These cases guard the final represented-value decision against double rounding: +//! accumulation may cross the normal/subnormal boundary or land exactly halfway +//! between subnormal units, so the public metric must round once at the final scale +//! with IEEE 754 ties-to-even semantics. + +use validation_core::mean_bias; + +#[test] +fn mean_bias_does_not_double_round_same_sign_subnormal_mean() { + let truth = [0.0, 0.0, 0.0]; + let minimum_normal_units = 1_u64 << 52; + let recovered = [ + f64::from_bits(minimum_normal_units - 32), + f64::from_bits(minimum_normal_units - 12), + f64::from_bits(minimum_normal_units - 20), + ]; + + // The exact represented-input mean is (3 * 2^52 - 64) / 3 subnormal units, + // which rounds to 2^52 - 21. A normalized intermediate can round first to + // a midpoint and then scale back to 2^52 - 22, so the public contract pins + // the single-rounding result at the final binary64 scale. + let bias = mean_bias(&truth, &recovered).expect("represented subnormal mean bias"); + assert_eq!(bias.to_bits(), minimum_normal_units - 21); + + let mirrored: Vec<_> = recovered.iter().map(|value| -*value).collect(); + let mirrored_bias = mean_bias(&truth, &mirrored).expect("mirrored subnormal mean bias"); + assert_eq!( + mirrored_bias.to_bits(), + (1_u64 << 63) | (minimum_normal_units - 21) + ); +} + +#[test] +fn mean_bias_rounds_subnormal_halfway_cases_to_even_units() { + let truth = [0.0, 0.0]; + let minimum_subnormal = f64::from_bits(1); + + let odd_floor = [minimum_subnormal, f64::from_bits(2)]; + let odd_floor_bias = mean_bias(&truth, &odd_floor).expect("odd-floor halfway mean"); + assert_eq!(odd_floor_bias.to_bits(), 2); + + let even_floor = [f64::from_bits(2), f64::from_bits(3)]; + let even_floor_bias = mean_bias(&truth, &even_floor).expect("even-floor halfway mean"); + assert_eq!(even_floor_bias.to_bits(), 2); +} + +#[test] +fn mean_bias_uses_exact_subnormal_units_when_float_compensation_hits_a_halfway_case() { + let truth = [0.0; 16]; + let recovered = [ + f64::from_bits(0x0cb6_6819_cb62_2e), + f64::from_bits(0x0947_199e_fa89_8b), + f64::from_bits(0x06f1_7b1c_5d14_35), + f64::from_bits(0x0017_01d7_09e8_e5), + f64::from_bits(0x0c4f_af0b_45b7_f2), + f64::from_bits(0x0618_7545_3090_72), + f64::from_bits(0x0a85_ae8e_ad81_bb), + f64::from_bits(0x08c0_d3d1_61a3_65), + f64::from_bits(0x0dd3_b523_39f6_96), + f64::from_bits(0x0d7e_53f0_d4c2_46), + f64::from_bits(0x0175_247a_3171_15), + f64::from_bits(0x0c74_caf9_0802_51), + f64::from_bits(0x0a8a_2b1f_baba_e0), + f64::from_bits(0x04dd_bbf2_5f17_35), + f64::from_bits(0x0efe_fdfe_b832_bd), + f64::from_bits(0x0887_8ef3_1114_1d), + ]; + + // The exact unit sum leaves remainder 8 on division by 16, exactly halfway + // between adjacent subnormals. The lower candidate is even and must win. + let bias = mean_bias(&truth, &recovered).expect("halfway represented mean bias"); + assert_eq!(bias.to_bits(), 0x092d_f11e_7dd9_b8); +} diff --git a/crates/validation_core/tests/bias_standard_error_anchor_conditioning_contract.rs b/crates/validation_core/tests/bias_standard_error_anchor_conditioning_contract.rs new file mode 100644 index 000000000..ee9a0e58c --- /dev/null +++ b/crates/validation_core/tests/bias_standard_error_anchor_conditioning_contract.rs @@ -0,0 +1,38 @@ +//! Regression contract for correctly rounded three-observation bias standard error. +//! +//! The historical filename predates removal of the conditioned-anchor production +//! route. The durable contract is order and sign invariance for represented +//! three-observation inputs handled by the established small-sample authority. + +use validation_core::bias_standard_error; + +const EXPECTED_STANDARD_ERROR_BITS: u64 = 0x3fd1_7a99_c875_b980; + +fn represented_sample() -> [f64; 3] { + [ + f64::from_bits(0x3ff7_c8a6_308f_7624), + f64::from_bits(0x3ff0_4284_fcf1_21a0), + f64::from_bits(0x3fff_659d_6d25_7410), + ] +} + +#[test] +fn represented_three_observation_rounding_is_order_and_sign_invariant() { + let [middle, low, high] = represented_sample(); + let truth = [0.0; 3]; + let permutations = [ + [low, middle, high], + [middle, low, high], + [high, low, middle], + ]; + + for recovered in permutations { + let standard_error = bias_standard_error(&truth, &recovered).expect("finite standard error"); + assert_eq!(standard_error.to_bits(), EXPECTED_STANDARD_ERROR_BITS); + + let mirrored = recovered.map(|value| -value); + let mirrored_standard_error = + bias_standard_error(&truth, &mirrored).expect("finite mirrored standard error"); + assert_eq!(mirrored_standard_error.to_bits(), EXPECTED_STANDARD_ERROR_BITS); + } +} diff --git a/crates/validation_core/tests/bias_standard_error_anchor_linear_admission_characterization.rs b/crates/validation_core/tests/bias_standard_error_anchor_linear_admission_characterization.rs new file mode 100644 index 000000000..cd437794c --- /dev/null +++ b/crates/validation_core/tests/bias_standard_error_anchor_linear_admission_characterization.rs @@ -0,0 +1,101 @@ +//! Characterizes an anchor-linear exact proof that is stricter about coordinates than pair subtraction. +//! +//! This is test-only evidence for issue #491. Production admission remains +//! `n=4..=16` and the current pairwise proof remains the fail-closed authority. +//! The fixture isolates a represented-input geometry where every subtraction from +//! the minimum anchor is exact, while one non-anchor pair subtraction is rounded. +//! Exact integer coordinates still recover the pair-distance numerator through +//! `n * sum(c_i^2) - (sum(c_i))^2`, so pairwise-f64 subtraction exactness is not a +//! scientific prerequisite for this bounded represented geometry. + +use validation_core::bias_standard_error; + +fn subtraction_roundoff(recovered: f64, truth: f64, residual: f64) -> f64 { + let negated_truth = -truth; + let truth_virtual = residual - recovered; + let recovered_virtual = residual - truth_virtual; + let recovered_roundoff = recovered - recovered_virtual; + let truth_roundoff = negated_truth - truth_virtual; + recovered_roundoff + truth_roundoff +} + +fn exact_pair_numerator(coefficients: &[u128]) -> u128 { + let mut total = 0_u128; + for left in 0..coefficients.len() { + for right in left + 1..coefficients.len() { + let difference = coefficients[left].abs_diff(coefficients[right]); + total = total + .checked_add( + difference + .checked_mul(difference) + .expect("bounded coefficient square fits u128"), + ) + .expect("bounded pair numerator fits u128"); + } + } + total +} + +#[test] +fn anchor_linear_identity_survives_a_rounded_non_anchor_pair_difference() { + let tiny = 2.0_f64.powi(-54); + let residuals = [0.0, 1.0, tiny, 2.0]; + + for residual in residuals { + let anchored = residual - 0.0; + assert_eq!( + subtraction_roundoff(residual, 0.0, anchored), + 0.0, + "minimum-anchor coordinates must be represented exactly" + ); + } + + let rounded_non_anchor_pair = residuals[1] - residuals[2]; + assert_ne!( + subtraction_roundoff(residuals[1], residuals[2], rounded_non_anchor_pair), + 0.0, + "the fixture must remain outside the current pairwise-f64 subtraction proof" + ); + + // Common exact unit is 2^-54, giving integer coordinates + // [0, 2^54, 1, 2^55]. + let coefficients = [0_u128, 1_u128 << 54, 1, 1_u128 << 55]; + let coefficient_sum = coefficients + .iter() + .copied() + .try_fold(0_u128, |sum, value| sum.checked_add(value)) + .expect("bounded coefficient sum fits u128"); + let square_sum = coefficients + .iter() + .copied() + .try_fold(0_u128, |sum, value| { + sum.checked_add(value.checked_mul(value)?) + }) + .expect("bounded squared-coordinate sum fits u128"); + let sample_count = u128::try_from(coefficients.len()).expect("sample count fits u128"); + let linear_numerator = sample_count + .checked_mul(square_sum) + .and_then(|scaled_square_sum| { + coefficient_sum + .checked_mul(coefficient_sum) + .and_then(|squared_sum| scaled_square_sum.checked_sub(squared_sum)) + }) + .expect("bounded anchor-linear numerator fits u128"); + let pair_numerator = exact_pair_numerator(&coefficients); + + assert_eq!(linear_numerator, pair_numerator); + assert_eq!( + linear_numerator, + 3_569_704_090_242_693_886_528_325_169_446_915_u128 + ); + + // The public result already remains numerically correct through the generic + // fallback. This characterization concerns exact-proof admission and the + // avoidable O(n^2) pairwise-f64 prerequisite, not a changed public value. + assert_eq!( + bias_standard_error(&[0.0; 4], &residuals) + .expect("represented fixture remains scientifically computable") + .to_bits(), + 0x3fde_a33e_2c83_c140 + ); +} diff --git a/crates/validation_core/tests/bias_standard_error_anchor_permutation_contract.rs b/crates/validation_core/tests/bias_standard_error_anchor_permutation_contract.rs new file mode 100644 index 000000000..5f9a54412 --- /dev/null +++ b/crates/validation_core/tests/bias_standard_error_anchor_permutation_contract.rs @@ -0,0 +1,30 @@ +//! Regression contract for permutation and sign invariance of the three-sample metric. +//! +//! The historical filename references an earlier anchor implementation. The +//! scientific invariant is that reordering or mirroring the represented sample +//! cannot change the correctly rounded standard error. + +use validation_core::bias_standard_error; + +#[test] +fn three_observation_standard_error_is_permutation_and_sign_invariant() { + let low = f64::from_bits(0x4194_f788_9184_b980); + let middle = f64::from_bits(0x420c_409f_fce3_8390); + let high = f64::from_bits(0x4222_70c4_634c_c6b6); + let expected_bits = 0x4205_7185_8078_f946; + + for recovered in [ + [low, middle, high], + [middle, low, high], + [high, middle, low], + ] { + let standard_error = bias_standard_error(&[0.0; 3], &recovered) + .expect("represented-input bias standard error"); + assert_eq!(standard_error.to_bits(), expected_bits); + + let mirrored = recovered.map(|value| -value); + let mirrored_standard_error = bias_standard_error(&[0.0; 3], &mirrored) + .expect("mirrored represented-input bias standard error"); + assert_eq!(mirrored_standard_error.to_bits(), expected_bits); + } +} diff --git a/crates/validation_core/tests/bias_standard_error_common_high_mean_roundoff_contract.rs b/crates/validation_core/tests/bias_standard_error_common_high_mean_roundoff_contract.rs new file mode 100644 index 000000000..b886d5165 --- /dev/null +++ b/crates/validation_core/tests/bias_standard_error_common_high_mean_roundoff_contract.rs @@ -0,0 +1,28 @@ +//! Preserves low-order represented dispersion when rounded residual highs coincide. +//! +//! The contract fixes a three-observation geometry where all binary64 residual +//! highs round to one while an exact subtraction low term changes the correctly +//! rounded standard error. Sign mirroring must leave that uncertainty unchanged. + +use validation_core::bias_standard_error; + +#[test] +fn common_rounded_residual_highs_do_not_recenter_low_terms_on_a_rounded_mean() { + let quarter_ulp_at_one = 2.0_f64.powi(-54); + let truth = [quarter_ulp_at_one, 0.0, 0.0]; + let recovered = [1.0; 3]; + + let standard_error = + bias_standard_error(&truth, &recovered).expect("represented standard error"); + // Every binary64 subtraction rounds to 1.0, but the represented-input + // residuals are [1 - 2^-54, 1, 1]. Their exact mean is 1 - 2^-54 / 3, + // so SE is exactly 2^-54 / 3 before the final binary64 rounding. Rounding + // the low-term mean first and then centering moves the result one ULP up. + assert_eq!(standard_error.to_bits(), 0x3c75_5555_5555_5555); + + let mirrored_truth = truth.map(|value| -value); + let mirrored_recovered = recovered.map(|value| -value); + let mirrored_standard_error = bias_standard_error(&mirrored_truth, &mirrored_recovered) + .expect("mirrored represented standard error"); + assert_eq!(mirrored_standard_error.to_bits(), 0x3c75_5555_5555_5555); +} diff --git a/crates/validation_core/tests/bias_standard_error_distinct_high_roundoff_contract.rs b/crates/validation_core/tests/bias_standard_error_distinct_high_roundoff_contract.rs new file mode 100644 index 000000000..8ad719a0b --- /dev/null +++ b/crates/validation_core/tests/bias_standard_error_distinct_high_roundoff_contract.rs @@ -0,0 +1,34 @@ +//! Preserves subtraction low terms when represented residual high parts differ. +//! +//! The contract distinguishes exact represented-input dispersion from the larger +//! result obtained after prematurely rounding pairwise residuals, while retaining +//! sign symmetry and the established exact-residual control geometry. + +use validation_core::bias_standard_error; + +#[test] +fn bias_standard_error_preserves_roundoff_when_residual_high_parts_differ() { + let quarter_ulp_at_one = 2.0_f64.powi(-54); + let truth = [quarter_ulp_at_one, 3.0 * quarter_ulp_at_one, 0.0]; + let recovered = [1.0; 3]; + + let standard_error = + bias_standard_error(&truth, &recovered).expect("represented standard error"); + // Exact represented residuals are 1-2^-54, 1-3*2^-54, and 1. Their + // standard error rounds to this value. Rounding the pairwise residuals first + // instead yields [1, 1-2^-52, 1] and a materially larger result. + assert_eq!(standard_error.to_bits(), 0x3c8c_38aa_37c3_f68d); + + let mirrored_truth = truth.map(|value| -value); + let mirrored_recovered = recovered.map(|value| -value); + let mirrored_standard_error = bias_standard_error(&mirrored_truth, &mirrored_recovered) + .expect("mirrored represented standard error"); + assert_eq!(mirrored_standard_error.to_bits(), 0x3c8c_38aa_37c3_f68d); + + // A symmetric exact-residual case remains on the established direct path. + let ulp_at_one = 2.0_f64.powi(-52); + let exact_residuals = [1.0 - ulp_at_one, 1.0, 1.0 + ulp_at_one]; + let control = bias_standard_error(&[0.0; 3], &exact_residuals) + .expect("symmetric exact-residual control standard error"); + assert_eq!(control.to_bits(), 0x3ca2_79a7_4590_331d); +} diff --git a/crates/validation_core/tests/bias_standard_error_eight_observation_pair_distance_contract.rs b/crates/validation_core/tests/bias_standard_error_eight_observation_pair_distance_contract.rs new file mode 100644 index 000000000..479aeed21 --- /dev/null +++ b/crates/validation_core/tests/bias_standard_error_eight_observation_pair_distance_contract.rs @@ -0,0 +1,57 @@ +//! Eight-observation represented pair-distance rounding contract. +//! +//! Three orderings and their sign mirrors must preserve the audited exact +//! pair-distance ratio and its correctly rounded binary64 standard error, rather +//! than falling back to a translated floating-moment result one ULP higher. + +use validation_core::bias_standard_error; + +fn assert_eight_observation_pair_distance_contract(recovered: [f64; 8]) { + let truth = [0.0; 8]; + let standard_error = bias_standard_error(&truth, &recovered).expect("representable SE"); + assert_eq!( + standard_error.to_bits(), + 0x41ac_8601_97ac_cd4c, + "the exact eight-observation pair-distance ratio must determine the represented-input SE; the translated floating moment/sqrt fallback rounds one ULP high" + ); +} + +#[test] +fn exact_eight_observation_dispersion_uses_pair_distance_midpoint_proof() { + let samples = [ + [ + 121_838_114.0, + 741_324_193.0, + 994_684_355.0, + 1_673_429_661.0, + 1_824_103_795.0, + 1_861_311_798.0, + 1_872_117_478.0, + 1_936_395_613.0, + ], + [ + 1_936_395_613.0, + 1_872_117_478.0, + 1_861_311_798.0, + 1_824_103_795.0, + 1_673_429_661.0, + 994_684_355.0, + 741_324_193.0, + 121_838_114.0, + ], + [ + 1_824_103_795.0, + 121_838_114.0, + 1_936_395_613.0, + 994_684_355.0, + 1_872_117_478.0, + 741_324_193.0, + 1_861_311_798.0, + 1_673_429_661.0, + ], + ]; + for recovered in samples { + assert_eight_observation_pair_distance_contract(recovered); + assert_eight_observation_pair_distance_contract(recovered.map(|value| -value)); + } +} diff --git a/crates/validation_core/tests/bias_standard_error_eleven_observation_pair_distance_contract.rs b/crates/validation_core/tests/bias_standard_error_eleven_observation_pair_distance_contract.rs new file mode 100644 index 000000000..5b39d22fb --- /dev/null +++ b/crates/validation_core/tests/bias_standard_error_eleven_observation_pair_distance_contract.rs @@ -0,0 +1,66 @@ +//! Eleven-observation represented pair-distance rounding contract. +//! +//! Audited forward, reversed, and permuted samples and their sign mirrors must +//! preserve the exact represented pair-distance ratio and the same correctly +//! rounded standard error instead of the one-ULP-low floating-moment fallback. + +use validation_core::bias_standard_error; + +fn assert_eleven_observation_pair_distance_contract(recovered: [f64; 11]) { + let truth = [0.0; 11]; + let standard_error = bias_standard_error(&truth, &recovered).expect("representable SE"); + assert_eq!( + standard_error.to_bits(), + 0x41a4_47fc_a451_7b3f, + "the exact eleven-observation pair-distance ratio must determine the represented-input SE; the translated floating moment/sqrt fallback rounds one ULP low" + ); +} + +#[test] +fn exact_eleven_observation_dispersion_uses_pair_distance_midpoint_proof() { + let samples = [ + [ + 50_511_426.0, + 167_164_486.0, + 318_141_475.0, + 357_712_576.0, + 407_960_427.0, + 441_767_841.0, + 691_573_103.0, + 733_495_428.0, + 1_082_192_974.0, + 1_543_970_183.0, + 1_797_594_737.0, + ], + [ + 1_797_594_737.0, + 1_543_970_183.0, + 1_082_192_974.0, + 733_495_428.0, + 691_573_103.0, + 441_767_841.0, + 407_960_427.0, + 357_712_576.0, + 318_141_475.0, + 167_164_486.0, + 50_511_426.0, + ], + [ + 691_573_103.0, + 50_511_426.0, + 1_797_594_737.0, + 318_141_475.0, + 1_543_970_183.0, + 167_164_486.0, + 1_082_192_974.0, + 407_960_427.0, + 733_495_428.0, + 357_712_576.0, + 441_767_841.0, + ], + ]; + for recovered in samples { + assert_eleven_observation_pair_distance_contract(recovered); + assert_eleven_observation_pair_distance_contract(recovered.map(|value| -value)); + } +} diff --git a/crates/validation_core/tests/bias_standard_error_exact_midpoint_tie_even_contract.rs b/crates/validation_core/tests/bias_standard_error_exact_midpoint_tie_even_contract.rs new file mode 100644 index 000000000..c87bfb46f --- /dev/null +++ b/crates/validation_core/tests/bias_standard_error_exact_midpoint_tie_even_contract.rs @@ -0,0 +1,35 @@ +//! Exact-midpoint tie-to-even regression contracts for four-observation bias SE. + +use validation_core::bias_standard_error; + +#[test] +fn exact_midpoint_selects_lower_even_binary64_neighbor() { + let truth = [0.0; 4]; + let recovered = [ + 0.0, + 0.0, + 8_106_479_329_266_891.0, + 9_007_199_254_740_990.0, + ]; + + let standard_error = + bias_standard_error(&truth, &recovered).expect("finite exact-midpoint standard error"); + + assert_eq!(standard_error.to_bits(), 0x4321_9999_9999_9998); +} + +#[test] +fn exact_midpoint_selects_upper_even_binary64_neighbor() { + let truth = [0.0; 4]; + let recovered = [ + 0.0, + 0.0, + 8_106_479_329_266_873.0, + 9_007_199_254_740_970.0, + ]; + + let standard_error = + bias_standard_error(&truth, &recovered).expect("finite exact-midpoint standard error"); + + assert_eq!(standard_error.to_bits(), 0x4321_9999_9999_998e); +} diff --git a/crates/validation_core/tests/bias_standard_error_exact_proof_budget_characterization.rs b/crates/validation_core/tests/bias_standard_error_exact_proof_budget_characterization.rs new file mode 100644 index 000000000..f79a22330 --- /dev/null +++ b/crates/validation_core/tests/bias_standard_error_exact_proof_budget_characterization.rs @@ -0,0 +1,379 @@ +use validation_core::bias_standard_error; + +const SEVENTEEN_OBSERVATION_FIXTURE: [u128; 17] = [ + 38_557_579, + 48_779_805, + 63_558_649, + 106_352_599, + 139_863_777, + 142_786_819, + 267_163_239, + 275_103_292, + 375_678_558, + 454_709_869, + 484_300_224, + 623_646_610, + 989_643_121, + 1_027_595_814, + 1_520_220_488, + 1_569_903_156, + 1_805_452_085, +]; + +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +struct Wide256 { + high: u128, + low: u128, +} + +impl Wide256 { + fn multiply_u128(left: u128, right: u128) -> Self { + let mask = u128::from(u64::MAX); + let left_limbs = [ + u64::try_from(left & mask).expect("masked low limb fits u64"), + u64::try_from(left >> 64).expect("high limb fits u64"), + ]; + let right_limbs = [ + u64::try_from(right & mask).expect("masked low limb fits u64"), + u64::try_from(right >> 64).expect("high limb fits u64"), + ]; + let mut limbs = [0_u64; 4]; + + for (left_index, left_limb) in left_limbs.iter().copied().enumerate() { + let mut carry = 0_u128; + for (right_index, right_limb) in right_limbs.iter().copied().enumerate() { + let limb_index = left_index + right_index; + let accumulator = u128::from(left_limb) + .checked_mul(u128::from(right_limb)) + .expect("64-bit limb product fits u128") + .checked_add(u128::from(limbs[limb_index])) + .expect("schoolbook partial sum fits u128") + .checked_add(carry) + .expect("schoolbook carry sum fits u128"); + limbs[limb_index] = u64::try_from(accumulator & mask) + .expect("masked schoolbook limb fits u64"); + carry = accumulator >> 64; + } + limbs[left_index + 2] = + u64::try_from(carry).expect("schoolbook multiplication carry fits u64"); + } + + Self { + high: u128::from(limbs[2]) | (u128::from(limbs[3]) << 64), + low: u128::from(limbs[0]) | (u128::from(limbs[1]) << 64), + } + } + + fn checked_sub(self, right: Self) -> Option { + let (low, borrow) = self.low.overflowing_sub(right.low); + let high = self + .high + .checked_sub(right.high)? + .checked_sub(u128::from(u8::from(borrow)))?; + Some(Self { high, low }) + } + + fn checked_shl(self, shift: u32) -> Option { + if shift == 0 { + return Some(self); + } + if shift >= 256 { + return None; + } + if shift >= 128 { + if self.high != 0 { + return None; + } + let high_shift = shift - 128; + if high_shift == 0 { + return Some(Self { + high: self.low, + low: 0, + }); + } + if self.low >> (128 - high_shift) != 0 { + return None; + } + return Some(Self { + high: self.low << high_shift, + low: 0, + }); + } + if self.high >> (128 - shift) != 0 { + return None; + } + Some(Self { + high: (self.high << shift) | (self.low >> (128 - shift)), + low: self.low << shift, + }) + } + + fn as_u128(self) -> Option { + (self.high == 0).then_some(self.low) + } +} + +fn deterministic_compact_fixture(sample_count: usize) -> Vec { + (0..sample_count) + .map(|index| { + let value = u128::try_from(index).expect("fixture index fits u128"); + (value * 1_000_003 + value * value * 97 + 17) % 4_000_000_001 + }) + .collect() +} + +fn pair_square_sum_quadratic(values: &[u128]) -> Option { + let mut sum = 0_u128; + for left in 0..values.len() { + for right in left + 1..values.len() { + let difference = values[left].abs_diff(values[right]); + sum = sum.checked_add(difference.checked_mul(difference)?)?; + } + } + Some(sum) +} + +fn pair_square_sum_linear(values: &[u128]) -> Option { + let minimum = *values.iter().min()?; + let sample_count = u128::try_from(values.len()).ok()?; + let common_shift = values + .iter() + .filter_map(|value| { + let coefficient = value.checked_sub(minimum)?; + (coefficient != 0).then_some(coefficient.trailing_zeros()) + }) + .min() + .unwrap_or(0); + + let mut coefficient_sum = 0_u128; + let mut square_sum = 0_u128; + for value in values { + let coefficient = value.checked_sub(minimum)? >> common_shift; + coefficient_sum = coefficient_sum.checked_add(coefficient)?; + square_sum = square_sum.checked_add(coefficient.checked_mul(coefficient)?)?; + } + let normalized_sum = sample_count + .checked_mul(square_sum)? + .checked_sub(coefficient_sum.checked_mul(coefficient_sum)?)?; + let squared_unit = 1_u128.checked_shl(common_shift.checked_mul(2)?)?; + normalized_sum.checked_mul(squared_unit) +} + +fn pair_square_sum_linear_wide_product(values: &[u128]) -> Option { + let minimum = *values.iter().min()?; + let sample_count = u128::try_from(values.len()).ok()?; + let common_shift = values + .iter() + .filter_map(|value| { + let coefficient = value.checked_sub(minimum)?; + (coefficient != 0).then_some(coefficient.trailing_zeros()) + }) + .min() + .unwrap_or(0); + + let mut coefficient_sum = 0_u128; + let mut square_sum = 0_u128; + for value in values { + let coefficient = value.checked_sub(minimum)? >> common_shift; + coefficient_sum = coefficient_sum.checked_add(coefficient)?; + square_sum = square_sum.checked_add(coefficient.checked_mul(coefficient)?)?; + } + + let normalized_sum = Wide256::multiply_u128(sample_count, square_sum) + .checked_sub(Wide256::multiply_u128(coefficient_sum, coefficient_sum))?; + normalized_sum.checked_shl(common_shift.checked_mul(2)?) +} + +fn greatest_common_divisor(mut left: u128, mut right: u128) -> u128 { + while right != 0 { + let remainder = left % right; + left = right; + right = remainder; + } + left +} + +fn pair_record_count(sample_count: u128) -> Option { + sample_count + .checked_mul(sample_count.checked_sub(1)?)? + .checked_div(2) +} + +fn worst_case_linear_intermediate(sample_count: u128, diameter: u128) -> Option { + sample_count + .checked_mul(sample_count)? + .checked_mul(diameter.checked_mul(diameter)?) +} + +fn worst_case_pair_square_numerator(sample_count: u128, diameter: u128) -> Option { + let split_product = (sample_count / 2) + .checked_mul(sample_count.checked_sub(sample_count / 2)?)?; + split_product.checked_mul(diameter.checked_mul(diameter)?) +} + +fn scientific_denominator(sample_count: u128) -> Option { + sample_count + .checked_mul(sample_count)? + .checked_mul(sample_count.checked_sub(1)?) +} + +#[test] +fn seventeen_observation_fixture_proves_linear_identity_matches_pair_reference() { + const EXACT_PAIR_SQUARE_SUM: u128 = 92_549_865_125_191_410_206; + const SCIENTIFIC_DENOMINATOR: u128 = 4_624; + const REDUCED_NUMERATOR: u128 = 46_274_932_562_595_705_103; + const REDUCED_DENOMINATOR: u128 = 2_312; + + let pairwise = pair_square_sum_quadratic(&SEVENTEEN_OBSERVATION_FIXTURE) + .expect("quadratic exact pair sum stays within u128"); + let linear = pair_square_sum_linear(&SEVENTEEN_OBSERVATION_FIXTURE) + .expect("linear exact identity stays within u128"); + assert_eq!(pairwise, EXACT_PAIR_SQUARE_SUM); + assert_eq!(linear, pairwise); + + let divisor = greatest_common_divisor(pairwise, SCIENTIFIC_DENOMINATOR); + assert_eq!(divisor, 2); + assert_eq!(pairwise / divisor, REDUCED_NUMERATOR); + assert_eq!(SCIENTIFIC_DENOMINATOR / divisor, REDUCED_DENOMINATOR); + + let truth = [0.0; 17]; + let recovered = SEVENTEEN_OBSERVATION_FIXTURE.map(|value| { + f64::from(u32::try_from(value).expect("fixture value fits u32 exactly")) + }); + assert_eq!( + bias_standard_error(&truth, &recovered) + .expect("current bounded fallback remains representable") + .to_bits(), + 0x41a0_dd77_9ac3_8e98 + ); +} + +#[test] +fn linear_checked_integer_kernel_matches_pair_reference_when_it_admits() { + for sample_count in [4_usize, 16, 17, 32, 64, 128, 256] { + let values = deterministic_compact_fixture(sample_count); + let pairwise = pair_square_sum_quadratic(&values) + .expect("compact-grid pair reference stays within u128"); + let linear = pair_square_sum_linear(&values) + .expect("compact-grid linear kernel stays within u128"); + assert_eq!( + linear, pairwise, + "linear sufficient proof must preserve the exact pair numerator at n={sample_count}" + ); + } +} + +#[test] +fn linear_checked_integer_kernel_normalizes_a_common_power_of_two_unit() { + let diameter = 1_u128 << 58; + let mut values = Vec::with_capacity(65); + values.push(0); + values.extend((0..64).map(|_| diameter)); + + let pairwise = pair_square_sum_quadratic(&values) + .expect("common-power pair numerator stays within u128"); + assert_eq!(pairwise, 1_u128 << 122); + assert_eq!( + pair_square_sum_linear(&values), + Some(pairwise), + "a shared 2^58 dyadic unit must be removed before checked O(n) intermediates are judged" + ); +} + +#[test] +fn linear_checked_integer_kernel_is_not_admission_equivalent_to_pair_reference() { + let diameter = (1_u128 << 58) + 1; + + let mut fits_both = Vec::with_capacity(64); + fits_both.push(0); + fits_both.extend((0..63).map(|_| diameter)); + let pairwise_64 = pair_square_sum_quadratic(&fits_both) + .expect("64-sample pair numerator stays within u128"); + assert_eq!( + pair_square_sum_linear(&fits_both), + Some(pairwise_64), + "n=64 remains inside the normalized linear intermediate budget" + ); + + let mut pair_only = Vec::with_capacity(65); + pair_only.push(0); + pair_only.extend((0..64).map(|_| diameter)); + let pairwise_65 = pair_square_sum_quadratic(&pair_only) + .expect("65-sample pair numerator still stays within u128"); + let expected_pairwise_65 = 64_u128 + .checked_mul(diameter.checked_mul(diameter).expect("diameter square fits")) + .expect("pair numerator fits"); + assert_eq!(pairwise_65, expected_pairwise_65); + assert_eq!( + pair_square_sum_linear(&pair_only), + None, + "odd diameter prevents dyadic rescaling, so n*sum(c_i^2) overflows before cancellation while the exact pair numerator still fits" + ); +} + +#[test] +fn wide_product_reference_recovers_the_pair_only_odd_boundary() { + let diameter = (1_u128 << 58) + 1; + let mut pair_only = Vec::with_capacity(65); + pair_only.push(0); + pair_only.extend((0..64).map(|_| diameter)); + + let pairwise = pair_square_sum_quadratic(&pair_only) + .expect("65-sample pair numerator stays within u128"); + assert_eq!(pair_square_sum_linear(&pair_only), None); + assert_eq!( + pair_square_sum_linear_wide_product(&pair_only).and_then(Wide256::as_u128), + Some(pairwise), + "two-limb intermediate products must distinguish a narrow-u128 refusal from an exact pair refusal" + ); +} + +#[test] +fn wide_product_reference_preserves_full_width_u128_multiplication() { + let maximum = u128::MAX; + assert_eq!( + Wide256::multiply_u128(maximum, maximum), + Wide256 { + high: maximum - 1, + low: 1, + } + ); +} + +#[test] +fn pair_record_counts_are_exact_resource_evidence() { + assert_eq!(pair_record_count(16), Some(120)); + assert_eq!(pair_record_count(17), Some(136)); + assert_eq!(pair_record_count(2_048), Some(2_096_128)); + assert_eq!(pair_record_count(3_162), Some(4_997_541)); +} + +#[test] +fn compact_dyadic_linear_intermediate_ceiling_is_2047_samples() { + let exact_integer_diameter = 1_u128 << 53; + assert!(worst_case_linear_intermediate(2_047, exact_integer_diameter).is_some()); + assert!(worst_case_linear_intermediate(2_048, exact_integer_diameter).is_none()); + + let maximum_safe_denominator = scientific_denominator(2_047).expect("bounded denominator"); + assert!(maximum_safe_denominator < (1_u128 << 53)); +} + +#[test] +fn exact_pair_square_numerator_has_a_wider_u128_envelope_than_linear_intermediates() { + let exact_integer_diameter = 1_u128 << 53; + assert!(worst_case_pair_square_numerator(4_095, exact_integer_diameter).is_some()); + assert!(worst_case_pair_square_numerator(4_096, exact_integer_diameter).is_none()); +} + +#[test] +fn unreduced_scientific_denominator_crosses_binary64_integer_bound_after_208064() { + let maximum_exact_binary64_integer = 1_u128 << 53; + assert!( + scientific_denominator(208_064).expect("denominator fits u128") + <= maximum_exact_binary64_integer + ); + assert!( + scientific_denominator(208_065).expect("denominator fits u128") + > maximum_exact_binary64_integer + ); +} diff --git a/crates/validation_core/tests/bias_standard_error_exact_residual_mean_roundoff_contract.rs b/crates/validation_core/tests/bias_standard_error_exact_residual_mean_roundoff_contract.rs new file mode 100644 index 000000000..62b14e587 --- /dev/null +++ b/crates/validation_core/tests/bias_standard_error_exact_residual_mean_roundoff_contract.rs @@ -0,0 +1,26 @@ +//! Correctly rounded bias standard error from exact represented residual geometry. +//! +//! The contract fixes a three-observation case where rounding the residual mean +//! before dispersion changes the binary64 result. It also checks sign symmetry, +//! so uncertainty must be derived from the represented residual geometry rather +//! than from a prematurely rounded residual mean. + +use validation_core::bias_standard_error; + +#[test] +fn bias_standard_error_does_not_round_the_exact_residual_mean_before_dispersion() { + let ulp_at_one = 2.0_f64.powi(-52); + let recovered = [1.0, 1.0 - ulp_at_one, 1.0]; + + let standard_error = bias_standard_error(&[0.0; 3], &recovered) + .expect("exact-residual represented standard error"); + // The represented residuals are exact and have mean 1 - 2^-52/3. Their + // exact standard error is 2^-52/3. Rounding that mean first instead creates + // deviations [2^-53, -2^-53, 2^-53] and overstates the uncertainty. + assert_eq!(standard_error.to_bits(), 0x3c95_5555_5555_5555); + + let mirrored = recovered.map(|value| -value); + let mirrored_standard_error = bias_standard_error(&[0.0; 3], &mirrored) + .expect("mirrored exact-residual represented standard error"); + assert_eq!(mirrored_standard_error.to_bits(), 0x3c95_5555_5555_5555); +} diff --git a/crates/validation_core/tests/bias_standard_error_fifteen_observation_pair_distance_contract.rs b/crates/validation_core/tests/bias_standard_error_fifteen_observation_pair_distance_contract.rs new file mode 100644 index 000000000..e46652681 --- /dev/null +++ b/crates/validation_core/tests/bias_standard_error_fifteen_observation_pair_distance_contract.rs @@ -0,0 +1,72 @@ +use validation_core::bias_standard_error; + +fn assert_fifteen_observation_pair_distance_contract(recovered: [f64; 15]) { + let truth = [0.0; 15]; + let standard_error = bias_standard_error(&truth, &recovered).expect("representable SE"); + assert_eq!( + standard_error.to_bits(), + 0x41a1_254f_de99_720d, + "the exact fifteen-observation pair-distance ratio must determine the represented-input SE; the translated floating moment/sqrt fallback rounds one ULP low" + ); +} + +#[test] +fn exact_fifteen_observation_dispersion_uses_pair_distance_midpoint_proof() { + let samples = [ + [ + 224_611_356.0, + 291_740_781.0, + 326_373_353.0, + 461_196_594.0, + 686_313_913.0, + 812_468_689.0, + 889_538_573.0, + 1_089_098_727.0, + 1_093_012_981.0, + 1_094_199_400.0, + 1_387_143_595.0, + 1_412_604_591.0, + 1_556_072_759.0, + 1_847_457_618.0, + 1_990_087_657.0, + ], + [ + 1_990_087_657.0, + 1_847_457_618.0, + 1_556_072_759.0, + 1_412_604_591.0, + 1_387_143_595.0, + 1_094_199_400.0, + 1_093_012_981.0, + 1_089_098_727.0, + 889_538_573.0, + 812_468_689.0, + 686_313_913.0, + 461_196_594.0, + 326_373_353.0, + 291_740_781.0, + 224_611_356.0, + ], + [ + 1_089_098_727.0, + 224_611_356.0, + 1_990_087_657.0, + 461_196_594.0, + 1_412_604_591.0, + 291_740_781.0, + 1_094_199_400.0, + 686_313_913.0, + 1_847_457_618.0, + 326_373_353.0, + 1_387_143_595.0, + 812_468_689.0, + 1_556_072_759.0, + 889_538_573.0, + 1_093_012_981.0, + ], + ]; + for recovered in samples { + assert_fifteen_observation_pair_distance_contract(recovered); + assert_fifteen_observation_pair_distance_contract(recovered.map(|value| -value)); + } +} diff --git a/crates/validation_core/tests/bias_standard_error_five_observation_pair_distance_contract.rs b/crates/validation_core/tests/bias_standard_error_five_observation_pair_distance_contract.rs new file mode 100644 index 000000000..3d47b8ac4 --- /dev/null +++ b/crates/validation_core/tests/bias_standard_error_five_observation_pair_distance_contract.rs @@ -0,0 +1,42 @@ +use validation_core::bias_standard_error; + +fn assert_five_observation_pair_distance_contract(recovered: [f64; 5]) { + let truth = [0.0; 5]; + let standard_error = bias_standard_error(&truth, &recovered).expect("representable SE"); + assert_eq!( + standard_error.to_bits(), + 0x4192_caf1_6406_5ad0, + "the exact five-observation pair-distance ratio must determine the represented-input SE; the translated floating moment/sqrt fallback rounds one ULP high" + ); +} + +#[test] +fn exact_five_observation_dispersion_uses_pair_distance_midpoint_proof() { + let samples = [ + [ + 1_342_748_146.0, + 1_434_848_064.0, + 1_525_257_611.0, + 1_685_877_224.0, + 1_771_341_094.0, + ], + [ + 1_771_341_094.0, + 1_525_257_611.0, + 1_342_748_146.0, + 1_685_877_224.0, + 1_434_848_064.0, + ], + [ + 1_525_257_611.0, + 1_342_748_146.0, + 1_771_341_094.0, + 1_434_848_064.0, + 1_685_877_224.0, + ], + ]; + for recovered in samples { + assert_five_observation_pair_distance_contract(recovered); + assert_five_observation_pair_distance_contract(recovered.map(|value| -value)); + } +} diff --git a/crates/validation_core/tests/bias_standard_error_four_observation_large_reduced_ratio_contract.rs b/crates/validation_core/tests/bias_standard_error_four_observation_large_reduced_ratio_contract.rs new file mode 100644 index 000000000..729c5d1a9 --- /dev/null +++ b/crates/validation_core/tests/bias_standard_error_four_observation_large_reduced_ratio_contract.rs @@ -0,0 +1,25 @@ +use validation_core::bias_standard_error; + +fn assert_large_reduced_ratio_contract(recovered: [f64; 4]) { + let truth = [0.0; 4]; + let standard_error = bias_standard_error(&truth, &recovered).expect("representable SE"); + assert_eq!( + standard_error.to_bits(), + 0x41b3_a706_d408_9e32, + "the exact reduced pair-distance ratio remains authoritative even when its numerator exceeds 2^53; the floating ratio/sqrt fallback rounds one ULP low" + ); +} + +#[test] +fn exact_four_observation_dispersion_keeps_large_reduced_ratio_in_midpoint_proof() { + let samples = [ + [19_274_968.0, 693_729_138.0, 711_353_557.0, 1_625_519_116.0], + [1_625_519_116.0, 711_353_557.0, 693_729_138.0, 19_274_968.0], + [693_729_138.0, 1_625_519_116.0, 19_274_968.0, 711_353_557.0], + [711_353_557.0, 19_274_968.0, 1_625_519_116.0, 693_729_138.0], + ]; + for recovered in samples { + assert_large_reduced_ratio_contract(recovered); + assert_large_reduced_ratio_contract(recovered.map(|value| -value)); + } +} diff --git a/crates/validation_core/tests/bias_standard_error_four_observation_ratio_sqrt_rounding_contract.rs b/crates/validation_core/tests/bias_standard_error_four_observation_ratio_sqrt_rounding_contract.rs new file mode 100644 index 000000000..d35a69db9 --- /dev/null +++ b/crates/validation_core/tests/bias_standard_error_four_observation_ratio_sqrt_rounding_contract.rs @@ -0,0 +1,25 @@ +use validation_core::bias_standard_error; + +fn assert_four_observation_contract(recovered: [f64; 4]) { + let truth = [0.0; 4]; + let standard_error = bias_standard_error(&truth, &recovered).expect("representable SE"); + assert_eq!( + standard_error.to_bits(), + 0x3ff8_df7d_a2e6_6e88, + "exact represented-input SE is sqrt(29/12); ratio-first sqrt must not round one ULP low" + ); +} + +#[test] +fn exact_four_observation_dispersion_avoids_ratio_sqrt_double_rounding() { + let samples = [ + [0.0, 1.0, 2.0, 7.0], + [7.0, 2.0, 1.0, 0.0], + [1.0, 7.0, 0.0, 2.0], + [2.0, 0.0, 7.0, 1.0], + ]; + for recovered in samples { + assert_four_observation_contract(recovered); + assert_four_observation_contract(recovered.map(|value| -value)); + } +} diff --git a/crates/validation_core/tests/bias_standard_error_four_observation_reduced_ratio_contract.rs b/crates/validation_core/tests/bias_standard_error_four_observation_reduced_ratio_contract.rs new file mode 100644 index 000000000..628f47227 --- /dev/null +++ b/crates/validation_core/tests/bias_standard_error_four_observation_reduced_ratio_contract.rs @@ -0,0 +1,33 @@ +//! Exact reduced-ratio contract for the four-observation bias standard error. +//! +//! This geometry makes the exact pair-distance numerator and denominator share a +//! factor of four. Reducing that ratio before the bounded square-root proof is +//! required to reach the correctly rounded binary64 result; the unreduced +//! fallback lands one ULP low. Permutation and sign changes must preserve the +//! same public metric bits. + +use validation_core::bias_standard_error; + +fn assert_reduced_ratio_contract(recovered: [f64; 4]) { + let truth = [0.0; 4]; + let standard_error = bias_standard_error(&truth, &recovered).expect("representable SE"); + assert_eq!( + standard_error.to_bits(), + 0x4174_46e5_76f8_7445, + "exact pair-distance ratio reduces by gcd 4 before the bounded sqrt proof; fallback rounds one ULP low" + ); +} + +#[test] +fn exact_four_observation_dispersion_reduces_ratio_before_bounded_sqrt_proof() { + let samples = [ + [0.0, 14_099_687.0, 16_729_100.0, 94_045_527.0], + [94_045_527.0, 16_729_100.0, 14_099_687.0, 0.0], + [14_099_687.0, 94_045_527.0, 0.0, 16_729_100.0], + [16_729_100.0, 0.0, 94_045_527.0, 14_099_687.0], + ]; + for recovered in samples { + assert_reduced_ratio_contract(recovered); + assert_reduced_ratio_contract(recovered.map(|value| -value)); + } +} diff --git a/crates/validation_core/tests/bias_standard_error_fourteen_observation_pair_distance_contract.rs b/crates/validation_core/tests/bias_standard_error_fourteen_observation_pair_distance_contract.rs new file mode 100644 index 000000000..513f94779 --- /dev/null +++ b/crates/validation_core/tests/bias_standard_error_fourteen_observation_pair_distance_contract.rs @@ -0,0 +1,69 @@ +use validation_core::bias_standard_error; + +fn assert_fourteen_observation_pair_distance_contract(recovered: [f64; 14]) { + let truth = [0.0; 14]; + let standard_error = bias_standard_error(&truth, &recovered).expect("representable SE"); + assert_eq!( + standard_error.to_bits(), + 0x41a2_3df9_5954_fb0b, + "the exact fourteen-observation pair-distance ratio must determine the represented-input SE; the translated floating moment/sqrt fallback rounds one ULP low" + ); +} + +#[test] +fn exact_fourteen_observation_dispersion_uses_pair_distance_midpoint_proof() { + let samples = [ + [ + 169_198_177.0, + 170_212_614.0, + 363_421_213.0, + 482_119_205.0, + 503_813_918.0, + 556_586_639.0, + 757_346_256.0, + 811_004_051.0, + 882_684_595.0, + 948_393_523.0, + 1_052_267_532.0, + 1_523_536_361.0, + 1_895_880_649.0, + 1_922_535_250.0, + ], + [ + 1_922_535_250.0, + 1_895_880_649.0, + 1_523_536_361.0, + 1_052_267_532.0, + 948_393_523.0, + 882_684_595.0, + 811_004_051.0, + 757_346_256.0, + 556_586_639.0, + 503_813_918.0, + 482_119_205.0, + 363_421_213.0, + 170_212_614.0, + 169_198_177.0, + ], + [ + 757_346_256.0, + 169_198_177.0, + 1_922_535_250.0, + 482_119_205.0, + 1_523_536_361.0, + 170_212_614.0, + 948_393_523.0, + 503_813_918.0, + 1_895_880_649.0, + 363_421_213.0, + 1_052_267_532.0, + 556_586_639.0, + 882_684_595.0, + 811_004_051.0, + ], + ]; + for recovered in samples { + assert_fourteen_observation_pair_distance_contract(recovered); + assert_fourteen_observation_pair_distance_contract(recovered.map(|value| -value)); + } +} diff --git a/crates/validation_core/tests/bias_standard_error_multi_observation_subtraction_roundoff_contract.rs b/crates/validation_core/tests/bias_standard_error_multi_observation_subtraction_roundoff_contract.rs new file mode 100644 index 000000000..4abb6dfeb --- /dev/null +++ b/crates/validation_core/tests/bias_standard_error_multi_observation_subtraction_roundoff_contract.rs @@ -0,0 +1,25 @@ +use validation_core::bias_standard_error; + +#[test] +fn bias_standard_error_preserves_multi_observation_subtraction_roundoff_spread() { + let truth = [2.0_f64.powi(-54), 2.0_f64.powi(-55), 0.0]; + let recovered = [1.0; 3]; + + let standard_error = + bias_standard_error(&truth, &recovered).expect("represented standard error"); + // The represented-input deviations are exactly [-2^-55, 0, 2^-55], so + // SE = 2^-55 / sqrt(3). Round the final mathematical value once rather + // than inheriting the 1-ULP double rounding from 1 / rounded_sqrt(3). + assert_eq!(standard_error.to_bits(), 0x3c72_79a7_4590_331c); + + let mirrored_truth = truth.map(|value| -value); + let mirrored_recovered = recovered.map(|value| -value); + let mirrored_standard_error = bias_standard_error(&mirrored_truth, &mirrored_recovered) + .expect("mirrored represented standard error"); + assert_eq!(mirrored_standard_error.to_bits(), 0x3c72_79a7_4590_331c); + + assert_eq!( + bias_standard_error(&[2.0_f64.powi(-54); 3], &[1.0; 3]), + Ok(0.0) + ); +} diff --git a/crates/validation_core/tests/bias_standard_error_neutral_zero_route_order_contract.rs b/crates/validation_core/tests/bias_standard_error_neutral_zero_route_order_contract.rs new file mode 100644 index 000000000..df1e6ece7 --- /dev/null +++ b/crates/validation_core/tests/bias_standard_error_neutral_zero_route_order_contract.rs @@ -0,0 +1,33 @@ +#![forbid(unsafe_code)] +//! Guards the exact bias-SE proof route against quadratic work without demonstrated +//! represented-input admission value. + +const SOURCE: &str = include_str!("../src/bias_se.rs"); + +#[test] +fn neutral_zero_linear_proof_precedes_only_the_pairwise_reference() { + let start = SOURCE + .find("fn exact_pair_distance_standard_error(") + .expect("production exact-proof entrypoint must exist"); + let end = SOURCE[start..] + .find("pub fn bias_standard_error(") + .map(|offset| start + offset) + .expect("public bias-SE entrypoint must follow the exact-proof helper"); + let route = &SOURCE[start..end]; + + let neutral_zero = route + .find("exact_neutral_zero_linear_pair_square_sum(&residuals)") + .expect("neutral-zero O(n) proof must be a production route"); + let pairwise = route + .find("exact_pairwise_pair_square_sum(&residuals)") + .expect("pairwise fail-closed reference must remain available"); + + assert!( + !route.contains("exact_anchor_linear_pair_square_sum(&residuals)"), + "do not retain an O(n²) conditioned-anchor scan without a represented fixture that uniquely recovers a neutral-zero refusal" + ); + assert!( + neutral_zero < pairwise, + "production proof order must be neutral-zero linear -> pairwise reference" + ); +} diff --git a/crates/validation_core/tests/bias_standard_error_nine_observation_pair_distance_contract.rs b/crates/validation_core/tests/bias_standard_error_nine_observation_pair_distance_contract.rs new file mode 100644 index 000000000..a1252470a --- /dev/null +++ b/crates/validation_core/tests/bias_standard_error_nine_observation_pair_distance_contract.rs @@ -0,0 +1,54 @@ +use validation_core::bias_standard_error; + +fn assert_nine_observation_pair_distance_contract(recovered: [f64; 9]) { + let truth = [0.0; 9]; + let standard_error = bias_standard_error(&truth, &recovered).expect("representable SE"); + assert_eq!( + standard_error.to_bits(), + 0x41a7_5f1f_6489_5d36, + "the exact nine-observation pair-distance ratio must determine the represented-input SE; the translated floating moment/sqrt fallback rounds one ULP low" + ); +} + +#[test] +fn exact_nine_observation_dispersion_uses_pair_distance_midpoint_proof() { + let samples = [ + [ + 354_161_013.0, + 513_228_884.0, + 592_175_752.0, + 1_188_877_551.0, + 1_313_358_136.0, + 1_582_012_649.0, + 1_600_441_891.0, + 1_764_736_057.0, + 1_957_134_661.0, + ], + [ + 1_957_134_661.0, + 1_764_736_057.0, + 1_600_441_891.0, + 1_582_012_649.0, + 1_313_358_136.0, + 1_188_877_551.0, + 592_175_752.0, + 513_228_884.0, + 354_161_013.0, + ], + [ + 1_313_358_136.0, + 354_161_013.0, + 1_957_134_661.0, + 592_175_752.0, + 1_764_736_057.0, + 513_228_884.0, + 1_600_441_891.0, + 1_188_877_551.0, + 1_582_012_649.0, + ], + ]; + for recovered in samples { + assert_nine_observation_pair_distance_contract(recovered); + assert_nine_observation_pair_distance_contract(recovered.map(|value| -value)); + } +} diff --git a/crates/validation_core/tests/bias_standard_error_nonminimum_anchor_exact_rounding_contract.rs b/crates/validation_core/tests/bias_standard_error_nonminimum_anchor_exact_rounding_contract.rs new file mode 100644 index 000000000..8725238f0 --- /dev/null +++ b/crates/validation_core/tests/bias_standard_error_nonminimum_anchor_exact_rounding_contract.rs @@ -0,0 +1,43 @@ +//! Regression contracts for exact bias-SE recovery beyond pairwise-f64 admission. +//! +//! Issue #491 previously characterized a minimum-anchor linear proof. The first +//! fixture shows why production admission must search represented anchors rather +//! than force the minimum. The second shows that represented anchors alone are +//! still incomplete: no observed residual is an exact universal anchor, while +//! the neutral dyadic anchor `0` preserves every residual exactly. Both fixtures +//! have exact pair numerators that fit the bounded proof and both differ by one +//! ULP from the predecessor translated floating-moment fallback. + +use validation_core::bias_standard_error; + +#[test] +fn exact_nonminimum_anchor_recovers_correctly_rounded_four_observation_bias_se() { + let diameter = 9_007_199_254_740_992.0_f64; // 2^53 + let truth = [0.0; 4]; + let recovered = [0.0, 1.0, 2.0, -diameter]; + + let forward = bias_standard_error(&truth, &recovered) + .expect("the exact represented residual geometry is scientifically computable"); + assert_eq!(forward.to_bits(), 0x4320_0000_0000_0001); + + let reversed = [recovered[3], recovered[2], recovered[1], recovered[0]]; + let reverse = bias_standard_error(&truth, &reversed) + .expect("permutation must preserve the exact represented geometry"); + assert_eq!(reverse.to_bits(), forward.to_bits()); +} + +#[test] +fn exact_zero_anchor_recovers_when_no_observed_residual_is_a_universal_anchor() { + let tiny = 2.0_f64.powi(-54); + let truth = [0.0; 4]; + let recovered = [1.0, tiny, 2.0, 3.0]; + + let forward = bias_standard_error(&truth, &recovered) + .expect("neutral-anchor exact geometry is scientifically computable"); + assert_eq!(forward.to_bits(), 0x3fe4_a7e9_cb8a_3491); + + let permuted = [recovered[2], recovered[0], recovered[3], recovered[1]]; + let permuted_result = bias_standard_error(&truth, &permuted) + .expect("neutral-anchor proof must remain permutation invariant"); + assert_eq!(permuted_result.to_bits(), forward.to_bits()); +} diff --git a/crates/validation_core/tests/bias_standard_error_pairwise_subtraction_roundoff_contract.rs b/crates/validation_core/tests/bias_standard_error_pairwise_subtraction_roundoff_contract.rs new file mode 100644 index 000000000..cf724701e --- /dev/null +++ b/crates/validation_core/tests/bias_standard_error_pairwise_subtraction_roundoff_contract.rs @@ -0,0 +1,27 @@ +//! Locks represented-residual pairwise subtraction roundoff in bias standard-error recovery. + +use validation_core::bias_standard_error; + +#[test] +fn bias_standard_error_preserves_pairwise_subtraction_roundoff_spread() { + let truth = [2.0_f64.powi(-54), 2.0_f64.powi(-55)]; + let recovered = [1.0, 1.0]; + + let standard_error = + bias_standard_error(&truth, &recovered).expect("represented standard error"); + assert_eq!(standard_error.to_bits(), 2.0_f64.powi(-56).to_bits()); + + let mirrored_truth = truth.map(|value| -value); + let mirrored_recovered = recovered.map(|value| -value); + let mirrored_standard_error = bias_standard_error(&mirrored_truth, &mirrored_recovered) + .expect("mirrored represented standard error"); + assert_eq!( + mirrored_standard_error.to_bits(), + 2.0_f64.powi(-56).to_bits() + ); + + assert_eq!( + bias_standard_error(&[2.0_f64.powi(-54); 2], &[1.0; 2]), + Ok(0.0) + ); +} diff --git a/crates/validation_core/tests/bias_standard_error_power_scale_rounding_contract.rs b/crates/validation_core/tests/bias_standard_error_power_scale_rounding_contract.rs new file mode 100644 index 000000000..8b20f34f1 --- /dev/null +++ b/crates/validation_core/tests/bias_standard_error_power_scale_rounding_contract.rs @@ -0,0 +1,21 @@ +use validation_core::bias_standard_error; + +#[test] +fn bias_standard_error_does_not_double_round_through_a_non_power_scale() { + let ulp_at_one = 2.0_f64.powi(-52); + let low = 1.0 - 4.0 * ulp_at_one; + let high = 1.0 + ulp_at_one; + let recovered = [low, low, high]; + + let standard_error = bias_standard_error(&[0.0; 3], &recovered) + .expect("represented-input standard error"); + // The exact represented residual gap is 5 * 2^-52. For [a, a, a+d], + // SE(mean) is exactly d / 3 before final binary64 rounding. Scaling by d + // first instead turns that into rounded(1/3) * d and lands one ULP low. + assert_eq!(standard_error.to_bits(), 0x3cba_aaaa_aaaa_aaab); + + let mirrored = recovered.map(|value| -value); + let mirrored_standard_error = bias_standard_error(&[0.0; 3], &mirrored) + .expect("mirrored represented-input standard error"); + assert_eq!(mirrored_standard_error.to_bits(), 0x3cba_aaaa_aaaa_aaab); +} diff --git a/crates/validation_core/tests/bias_standard_error_rational_scale_subnormal_rounding_contract.rs b/crates/validation_core/tests/bias_standard_error_rational_scale_subnormal_rounding_contract.rs new file mode 100644 index 000000000..a959713dd --- /dev/null +++ b/crates/validation_core/tests/bias_standard_error_rational_scale_subnormal_rounding_contract.rs @@ -0,0 +1,73 @@ +use validation_core::{ValidationError, bias_standard_error}; + +fn two_level_sample(gap: f64) -> Vec { + let mut recovered = vec![0.0; 6]; + recovered.extend(std::iter::repeat_n(gap, 27)); + recovered +} + +#[test] +fn bias_standard_error_rational_scale_rounds_subnormal_result_once() { + let gap = f64::from_bits(0x004a_2c74_6ac3_028e); + let recovered = two_level_sample(gap); + + let standard_error = bias_standard_error(&[0.0; 33], &recovered) + .expect("represented-input subnormal standard error"); + // For counts 6 and 27 of n=33, + // m(n-m)/(n^2(n-1)) = 162/34848 = 9/1936 = (3/44)^2. + // The represented-input target is therefore exactly 3*|gap|/44. The + // predecessor rational-square path normalizes the numerator before division + // and then restores a power-of-two scale into the subnormal range, producing + // an avoidable second rounding one ULP below the correctly rounded result. + assert_eq!(standard_error.to_bits(), 0x000e_46cb_22f6_0165); + + let mut permuted = recovered.clone(); + permuted.rotate_left(11); + let permuted_standard_error = bias_standard_error(&[0.0; 33], &permuted) + .expect("permuted represented-input subnormal standard error"); + assert_eq!(permuted_standard_error.to_bits(), 0x000e_46cb_22f6_0165); + + let mirrored: Vec<_> = recovered.iter().map(|value| -*value).collect(); + let mirrored_standard_error = bias_standard_error(&[0.0; 33], &mirrored) + .expect("mirrored represented-input subnormal standard error"); + assert_eq!(mirrored_standard_error.to_bits(), 0x000e_46cb_22f6_0165); +} + +#[test] +fn bias_standard_error_rational_scale_uses_ties_to_even_at_subnormal_units() { + // With the same exact 3/44 count scale, 22 minimum-subnormal gap units map + // to 1.5 result units and therefore round to the even value 2. + let odd_lower = two_level_sample(f64::from_bits(22)); + assert_eq!( + bias_standard_error(&[0.0; 33], &odd_lower) + .expect("odd lower midpoint") + .to_bits(), + 2 + ); + + // 66 gap units map to 4.5 result units and remain at the even lower value 4. + let even_lower = two_level_sample(f64::from_bits(66)); + assert_eq!( + bias_standard_error(&[0.0; 33], &even_lower) + .expect("even lower midpoint") + .to_bits(), + 4 + ); +} + +#[test] +fn bias_standard_error_rational_scale_preserves_range_boundary_and_refuses_false_zero() { + let rounds_to_minimum_normal = two_level_sample(f64::from_bits(0x004d_5555_5555_5555)); + assert_eq!( + bias_standard_error(&[0.0; 33], &rounds_to_minimum_normal) + .expect("minimum-normal boundary") + .to_bits(), + f64::MIN_POSITIVE.to_bits() + ); + + let minimum_subnormal = f64::from_bits(1); + assert_eq!( + bias_standard_error(&[0.0; 33], &two_level_sample(minimum_subnormal)), + Err(ValidationError::InvalidInput) + ); +} diff --git a/crates/validation_core/tests/bias_standard_error_repeated_level_rounding_contract.rs b/crates/validation_core/tests/bias_standard_error_repeated_level_rounding_contract.rs new file mode 100644 index 000000000..f210f8505 --- /dev/null +++ b/crates/validation_core/tests/bias_standard_error_repeated_level_rounding_contract.rs @@ -0,0 +1,32 @@ +//! Contracts exact bias-standard-error rounding for a repeated represented residual level. +//! +//! The three-observation geometry fixes the exact pairwise second moment when two observations +//! share one residual level. Forward, permuted, and sign-mirrored inputs must therefore produce +//! the same correctly rounded public standard error rather than depend on accumulation order. + +use validation_core::bias_standard_error; + +#[test] +fn bias_standard_error_preserves_three_observation_repeated_level_identity() { + let repeated = f64::from_bits(0x3fef_ffff_ffff_ffff); + let recovered = [0.0, repeated, repeated]; + + let standard_error = + bias_standard_error(&[0.0; 3], &recovered).expect("represented-input standard error"); + // For exactly represented residuals [0, a, a], the three-observation + // standard error simplifies algebraically to |a| / 3. The predecessor + // squared the normalized a values, formed the second moment, and then took + // a square root; that extra projection lands one ULP below the single + // correctly rounded division for a = next_down(1.0). + assert_eq!(standard_error.to_bits(), 0x3fd5_5555_5555_5555); + + let permuted = [repeated, 0.0, repeated]; + let permuted_standard_error = bias_standard_error(&[0.0; 3], &permuted) + .expect("permuted represented-input standard error"); + assert_eq!(permuted_standard_error.to_bits(), 0x3fd5_5555_5555_5555); + + let mirrored = recovered.map(|value| -value); + let mirrored_standard_error = bias_standard_error(&[0.0; 3], &mirrored) + .expect("mirrored represented-input standard error"); + assert_eq!(mirrored_standard_error.to_bits(), 0x3fd5_5555_5555_5555); +} diff --git a/crates/validation_core/tests/bias_standard_error_represented_route_equivalence_characterization.rs b/crates/validation_core/tests/bias_standard_error_represented_route_equivalence_characterization.rs new file mode 100644 index 000000000..a7322993e --- /dev/null +++ b/crates/validation_core/tests/bias_standard_error_represented_route_equivalence_characterization.rs @@ -0,0 +1,181 @@ +//! Characterizes represented-input bias-SE behavior without copying production arithmetic. +//! +//! Issue #491 requires an independent oracle for the admitted production domain. +//! These fixtures use exactly represented integer residuals, compute the pair-distance +//! numerator directly in integer arithmetic, and then assert the public API's audited +//! binary64 result and permutation invariance. Production admission remains `n=4..=16`. + +use validation_core::bias_standard_error; + +const DIAMETER_INTEGER: i128 = 1_i128 << 53; +const DIAMETER: f64 = (1_u64 << 53) as f64; + +const ADMITTED_CASES: [(usize, u128, u64); 13] = [ + ( + 4, + 324_518_553_658_426_690_754_359_001_612_291, + 0x4322_79a7_4590_331c, + ), + ( + 5, + 486_777_830_487_640_036_131_538_502_418_436, + 0x431f_5a7c_ecdb_684a, + ), + ( + 6, + 649_037_107_316_853_381_508_718_003_224_581, + 0x431a_fc19_d860_6169, + ), + ( + 7, + 811_296_384_146_066_726_885_897_504_030_726, + 0x4317_9b54_5654_ce5c, + ), + ( + 8, + 973_555_660_975_280_072_263_077_004_836_871, + 0x4314_f2ec_413c_b52a, + ), + ( + 9, + 1_135_814_937_804_493_417_640_256_505_643_016, + 0x4312_d071_7a82_a45e, + ), + ( + 10, + 1_298_074_214_633_706_763_017_436_006_449_161, + 0x4311_1111_1111_1111, + ), + ( + 11, + 1_460_333_491_462_920_108_394_615_507_255_306, + 0x430f_3940_7aa2_d4ec, + ), + ( + 12, + 1_622_592_768_292_133_453_771_795_008_061_451, + 0x430c_c40f_740a_8d6c, + ), + ( + 13, + 1_784_852_045_121_346_799_148_974_508_867_596, + 0x430a_a9db_d5af_20e5, + ), + ( + 14, + 1_947_111_321_950_560_144_526_154_009_673_741, + 0x4308_d86b_b06d_a1c7, + ), + ( + 15, + 2_109_370_598_779_773_489_903_333_510_479_886, + 0x4307_4208_c3da_686f, + ), + ( + 16, + 2_271_629_875_608_986_835_280_513_011_286_031, + 0x4305_dc33_8d87_81a3, + ), +]; + +fn represented_integer_family(sample_count: usize) -> Vec { + assert!((4..=16).contains(&sample_count)); + let mut values = Vec::with_capacity(sample_count); + values.extend([0, 1]); + values.extend((2..sample_count).map(|_| DIAMETER_INTEGER)); + values +} + +fn represented_f64_family(sample_count: usize) -> Vec { + represented_integer_family(sample_count) + .into_iter() + .map(|value| value as f64) + .collect() +} + +fn exact_integer_pair_square_sum(values: &[i128]) -> u128 { + let mut pair_square_sum = 0_u128; + for left in 0..values.len() { + for right in left + 1..values.len() { + let distance = (values[left] - values[right]).unsigned_abs(); + pair_square_sum = pair_square_sum + .checked_add( + distance + .checked_mul(distance) + .expect("bounded fixture square fits u128"), + ) + .expect("bounded fixture pair numerator fits u128"); + } + } + pair_square_sum +} + +#[test] +fn every_admitted_sample_count_matches_an_independent_integer_pair_oracle() { + for (sample_count, expected_pair_numerator, expected_bits) in ADMITTED_CASES { + let integer_values = represented_integer_family(sample_count); + assert_eq!( + exact_integer_pair_square_sum(&integer_values), + expected_pair_numerator, + "independent exact pair-distance oracle changed for n={sample_count}" + ); + + let values = represented_f64_family(sample_count); + let truth = vec![0.0; sample_count]; + assert_eq!( + bias_standard_error(&truth, &values) + .expect("represented family has a finite standard error") + .to_bits(), + expected_bits, + "public exact route must preserve the audited correctly-rounded result for n={sample_count}" + ); + } +} + +#[test] +fn every_admitted_sample_count_is_permutation_and_reversal_invariant() { + for (sample_count, _, expected_bits) in ADMITTED_CASES { + let truth = vec![0.0; sample_count]; + let values = represented_f64_family(sample_count); + + let mut reversed = values.clone(); + reversed.reverse(); + let mut rotated = values.clone(); + rotated.rotate_left((sample_count / 3).max(1)); + + for candidate in [&values, &reversed, &rotated] { + assert_eq!( + bias_standard_error(&truth, candidate) + .expect("permuted represented family has a finite standard error") + .to_bits(), + expected_bits, + "permutation changed the public result for n={sample_count}" + ); + } + } +} + +#[test] +fn neutral_zero_route_recovers_a_represented_geometry_that_direct_pair_subtraction_loses() { + let represented = [-DIAMETER, 0.0, 1.0, DIAMETER]; + let exact_integers = [-DIAMETER_INTEGER, 0, 1, DIAMETER_INTEGER]; + + assert_eq!( + -DIAMETER - 1.0, + -DIAMETER, + "direct binary64 pair subtraction rounds away the unit difference at -2^53" + ); + assert_eq!( + exact_integer_pair_square_sum(&exact_integers), + (1_u128 << 109) + 3, + "independent integer oracle must retain the unit contribution" + ); + + let truth = [0.0; 4]; + assert_eq!( + bias_standard_error(&truth, &represented) + .expect("neutral-zero exact proof recovers the represented geometry") + .to_bits(), + 0x432a_20bd_700c_2c3e + ); +} diff --git a/crates/validation_core/tests/bias_standard_error_represented_wide_recovery_characterization.rs b/crates/validation_core/tests/bias_standard_error_represented_wide_recovery_characterization.rs new file mode 100644 index 000000000..c0d2f76f8 --- /dev/null +++ b/crates/validation_core/tests/bias_standard_error_represented_wide_recovery_characterization.rs @@ -0,0 +1,324 @@ +//! Characterizes represented-input geometries that need wider exact proof products. +//! +//! These fixtures keep residual construction and every distinct pairwise subtraction +//! exact in binary64 while canonical anchor-relative coefficients make narrow `u128` +//! products overflow before cancellation. The exact pair numerator still fits `u128`. +//! A second boundary shows that the same width pressure reaches the exact +//! candidate/midpoint comparison, so widening only the O(n) numerator identity would +//! not yet establish end-to-end production admission equivalence. + +#[derive(Clone, Copy, Debug, Eq, Ord, PartialEq, PartialOrd)] +struct Wide256 { + high: u128, + low: u128, +} + +impl Wide256 { + fn multiply_u128(left: u128, right: u128) -> Self { + let mask = u128::from(u64::MAX); + let left_limbs = [ + u64::try_from(left & mask).expect("masked low limb fits u64"), + u64::try_from(left >> 64).expect("high limb fits u64"), + ]; + let right_limbs = [ + u64::try_from(right & mask).expect("masked low limb fits u64"), + u64::try_from(right >> 64).expect("high limb fits u64"), + ]; + let mut limbs = [0_u64; 4]; + + for (left_index, left_limb) in left_limbs.iter().copied().enumerate() { + let mut carry = 0_u128; + for (right_index, right_limb) in right_limbs.iter().copied().enumerate() { + let limb_index = left_index + right_index; + let accumulator = u128::from(left_limb) + .checked_mul(u128::from(right_limb)) + .expect("64-bit limb product fits u128") + .checked_add(u128::from(limbs[limb_index])) + .expect("schoolbook partial sum fits u128") + .checked_add(carry) + .expect("schoolbook carry sum fits u128"); + limbs[limb_index] = + u64::try_from(accumulator & mask).expect("masked schoolbook limb fits u64"); + carry = accumulator >> 64; + } + limbs[left_index + 2] = + u64::try_from(carry).expect("schoolbook multiplication carry fits u64"); + } + + Self { + high: u128::from(limbs[2]) | (u128::from(limbs[3]) << 64), + low: u128::from(limbs[0]) | (u128::from(limbs[1]) << 64), + } + } + + fn checked_sub(self, right: Self) -> Option { + let (low, borrow) = self.low.overflowing_sub(right.low); + let high = self + .high + .checked_sub(right.high)? + .checked_sub(u128::from(u8::from(borrow)))?; + Some(Self { high, low }) + } + + fn as_u128(self) -> Option { + (self.high == 0).then_some(self.low) + } +} + +fn subtraction_roundoff(recovered: f64, truth: f64, residual: f64) -> f64 { + let negated_truth = -truth; + let truth_virtual = residual - recovered; + let recovered_virtual = residual - truth_virtual; + let recovered_roundoff = recovered - recovered_virtual; + let truth_roundoff = negated_truth - truth_virtual; + recovered_roundoff + truth_roundoff +} + +fn positive_dyadic(value: f64) -> Option<(u128, i32)> { + if !value.is_finite() || value <= 0.0 { + return None; + } + let bits = value.to_bits(); + let exponent_bits = i32::try_from((bits >> 52) & 0x7ff).ok()?; + let fraction = bits & 0x000f_ffff_ffff_ffff; + let (mut significand, mut exponent) = if exponent_bits == 0 { + (u128::from(fraction), -1074) + } else { + ( + u128::from((1_u64 << 52) | fraction), + exponent_bits - 1023 - 52, + ) + }; + if significand == 0 { + return None; + } + let trailing = significand.trailing_zeros(); + significand >>= trailing; + exponent += i32::try_from(trailing).ok()?; + Some((significand, exponent)) +} + +fn midpoint_dyadic(left: f64, right: f64) -> Option<(u128, i32)> { + let (left_significand, left_exponent) = positive_dyadic(left)?; + let (right_significand, right_exponent) = positive_dyadic(right)?; + let common_exponent = left_exponent.min(right_exponent); + let left_shift = left_exponent.checked_sub(common_exponent)?.unsigned_abs(); + let right_shift = right_exponent + .checked_sub(common_exponent)? + .unsigned_abs(); + let left_units = left_significand.checked_mul(1_u128.checked_shl(left_shift)?)?; + let right_units = right_significand.checked_mul(1_u128.checked_shl(right_shift)?)?; + let mut midpoint_significand = left_units.checked_add(right_units)?; + let mut midpoint_exponent = common_exponent.checked_sub(1)?; + let trailing = midpoint_significand.trailing_zeros(); + midpoint_significand >>= trailing; + midpoint_exponent += i32::try_from(trailing).ok()?; + Some((midpoint_significand, midpoint_exponent)) +} + +fn represented_values(sample_count: usize) -> Vec { + assert!(sample_count >= 3); + let diameter = (1_u64 << 53) as f64; + let mut values = Vec::with_capacity(sample_count); + values.extend([0.0, 1.0]); + values.extend((2..sample_count).map(|_| diameter)); + values +} + +fn canonical_coefficients(sample_count: usize) -> Vec { + assert!(sample_count >= 3); + let diameter = 1_u128 << 53; + let mut coefficients = Vec::with_capacity(sample_count); + coefficients.extend([0, 1]); + coefficients.extend((2..sample_count).map(|_| diameter)); + coefficients +} + +fn exact_pair_numerator_for_three_level_fixture(sample_count: usize) -> u128 { + let repeated = u128::try_from(sample_count - 2).expect("sample count fits u128"); + let diameter = 1_u128 << 53; + let near_diameter = diameter - 1; + 1_u128 + .checked_add( + repeated + .checked_mul( + diameter + .checked_mul(diameter) + .expect("diameter square fits u128") + .checked_add( + near_diameter + .checked_mul(near_diameter) + .expect("near-diameter square fits u128"), + ) + .expect("two distinct pair squares fit u128"), + ) + .expect("pair-count-weighted squares fit u128"), + ) + .expect("exact pair numerator fits u128") +} + +fn assert_represented_subtractions_are_exact(sample_count: usize) { + let represented = represented_values(sample_count); + let diameter = (1_u64 << 53) as f64; + + for value in &represented { + let residual = *value - 0.0; + assert_eq!( + subtraction_roundoff(*value, 0.0, residual), + 0.0, + "truth-zero residual construction must be exact" + ); + } + + for (left, right) in [(0.0, 1.0), (0.0, diameter), (1.0, diameter)] { + let difference = left - right; + assert_eq!( + subtraction_roundoff(left, right, difference), + 0.0, + "every distinct represented pair subtraction used by the fixture must be exact" + ); + } +} + +fn canonical_sums(sample_count: usize) -> (u128, u128) { + let coefficients = canonical_coefficients(sample_count); + assert_eq!( + coefficients + .iter() + .copied() + .filter(|coefficient| *coefficient != 0) + .map(u128::trailing_zeros) + .min(), + Some(0), + "coefficient 1 prevents a removable dyadic scale from hiding width pressure" + ); + + let coefficient_sum = coefficients + .iter() + .copied() + .try_fold(0_u128, |sum, value| sum.checked_add(value)) + .expect("represented coefficient sum fits u128"); + let square_sum = coefficients + .iter() + .copied() + .try_fold(0_u128, |sum, value| { + sum.checked_add(value.checked_mul(value)?) + }) + .expect("represented square sum fits u128"); + (coefficient_sum, square_sum) +} + +#[test] +fn represented_pair_admission_can_require_wide_linear_products() { + const SAMPLE_COUNT: usize = 4_096; + assert_represented_subtractions_are_exact(SAMPLE_COUNT); + let (coefficient_sum, square_sum) = canonical_sums(SAMPLE_COUNT); + let sample_count = u128::try_from(SAMPLE_COUNT).expect("sample count fits u128"); + + assert!( + sample_count.checked_mul(square_sum).is_none(), + "n*S2 must overflow narrow u128 before cancellation" + ); + assert!( + coefficient_sum.checked_mul(coefficient_sum).is_none(), + "S1^2 must overflow narrow u128 before cancellation" + ); + + let exact_pair_numerator = exact_pair_numerator_for_three_level_fixture(SAMPLE_COUNT); + assert_eq!( + exact_pair_numerator, + 664_289_479_338_799_435_974_172_876_300_357_631_u128 + ); + + let wide_difference = Wide256::multiply_u128(sample_count, square_sum) + .checked_sub(Wide256::multiply_u128(coefficient_sum, coefficient_sum)) + .expect("n*S2 must dominate S1^2 exactly") + .as_u128() + .expect("the cancellation result remains pair-admissible u128"); + assert_eq!(wide_difference, exact_pair_numerator); + + let denominator = sample_count + .checked_mul(sample_count) + .and_then(|value| value.checked_mul(sample_count - 1)) + .expect("unreduced scientific denominator fits u128"); + assert_eq!(denominator, 68_702_699_520); + assert!( + denominator <= (1_u128 << 53), + "this represented width case is not blocked by the exact-denominator gate" + ); +} + +#[test] +fn represented_wide_recovery_also_needs_wider_exact_midpoint_products() { + const SAMPLE_COUNT: usize = 2_050; + assert_represented_subtractions_are_exact(SAMPLE_COUNT); + let (coefficient_sum, square_sum) = canonical_sums(SAMPLE_COUNT); + let sample_count = u128::try_from(SAMPLE_COUNT).expect("sample count fits u128"); + let numerator = exact_pair_numerator_for_three_level_fixture(SAMPLE_COUNT); + let denominator = sample_count + .checked_mul(sample_count) + .and_then(|value| value.checked_mul(sample_count - 1)) + .expect("unreduced scientific denominator fits u128"); + + assert_eq!( + Wide256::multiply_u128(sample_count, square_sum) + .checked_sub(Wide256::multiply_u128(coefficient_sum, coefficient_sum)) + .expect("wide cancellation remains ordered") + .as_u128(), + Some(numerator) + ); + assert!(sample_count.checked_mul(square_sum).is_none()); + assert!(coefficient_sum.checked_mul(coefficient_sum).is_none()); + assert_eq!(denominator, 8_610_922_500); + assert!(denominator <= (1_u128 << 53)); + + let candidate = ((numerator as f64) / (denominator as f64)).sqrt(); + assert_eq!(candidate.to_bits(), 0x4296_998e_1aff_78de); + let (candidate_significand, candidate_exponent) = + positive_dyadic(candidate).expect("positive candidate is dyadic"); + let candidate_square = candidate_significand + .checked_mul(candidate_significand) + .expect("binary64 significand square fits u128"); + let candidate_shift = candidate_exponent + .checked_mul(-2) + .and_then(|value| u32::try_from(value).ok()) + .expect("candidate comparison shift is positive and bounded"); + let candidate_factor = 1_u128 + .checked_shl(candidate_shift) + .expect("candidate comparison factor fits u128"); + + assert!( + denominator.checked_mul(candidate_square).is_none(), + "the current u128 exact-square comparator cannot form the right operand" + ); + assert!( + numerator.checked_mul(candidate_factor).is_none(), + "the current u128 exact-square comparator cannot form the scaled numerator" + ); + let wide_candidate_left = Wide256::multiply_u128(numerator, candidate_factor); + let wide_candidate_right = Wide256::multiply_u128(denominator, candidate_square); + assert!( + wide_candidate_left > wide_candidate_right, + "the exact target lies above the floating candidate square" + ); + + let neighbor = f64::from_bits(candidate.to_bits() + 1); + let (midpoint_significand, midpoint_exponent) = + midpoint_dyadic(candidate, neighbor).expect("adjacent midpoint is exact dyadic"); + let midpoint_square = midpoint_significand + .checked_mul(midpoint_significand) + .expect("midpoint significand square fits u128"); + let midpoint_shift = midpoint_exponent + .checked_mul(-2) + .and_then(|value| u32::try_from(value).ok()) + .expect("midpoint comparison shift is positive and bounded"); + let midpoint_factor = 1_u128 + .checked_shl(midpoint_shift) + .expect("midpoint comparison factor fits u128"); + let wide_midpoint_left = Wide256::multiply_u128(numerator, midpoint_factor); + let wide_midpoint_right = Wide256::multiply_u128(denominator, midpoint_square); + assert!( + wide_midpoint_left < wide_midpoint_right, + "the exact target lies below the upward midpoint square, proving the candidate is nearest" + ); +} diff --git a/crates/validation_core/tests/bias_standard_error_seven_observation_pair_distance_contract.rs b/crates/validation_core/tests/bias_standard_error_seven_observation_pair_distance_contract.rs new file mode 100644 index 000000000..69a4779b4 --- /dev/null +++ b/crates/validation_core/tests/bias_standard_error_seven_observation_pair_distance_contract.rs @@ -0,0 +1,48 @@ +use validation_core::bias_standard_error; + +fn assert_seven_observation_pair_distance_contract(recovered: [f64; 7]) { + let truth = [0.0; 7]; + let standard_error = bias_standard_error(&truth, &recovered).expect("representable SE"); + assert_eq!( + standard_error.to_bits(), + 0x4195_d9b7_0ca9_e6ee, + "the exact seven-observation pair-distance ratio must determine the represented-input SE; the translated floating moment/sqrt fallback rounds one ULP high" + ); +} + +#[test] +fn exact_seven_observation_dispersion_uses_pair_distance_midpoint_proof() { + let samples = [ + [ + 912_628_433.0, + 991_631_865.0, + 1_109_185_293.0, + 1_253_685_899.0, + 1_354_619_842.0, + 1_368_206_500.0, + 1_611_374_925.0, + ], + [ + 1_611_374_925.0, + 1_368_206_500.0, + 1_354_619_842.0, + 1_253_685_899.0, + 1_109_185_293.0, + 991_631_865.0, + 912_628_433.0, + ], + [ + 1_253_685_899.0, + 912_628_433.0, + 1_611_374_925.0, + 1_109_185_293.0, + 1_368_206_500.0, + 991_631_865.0, + 1_354_619_842.0, + ], + ]; + for recovered in samples { + assert_seven_observation_pair_distance_contract(recovered); + assert_seven_observation_pair_distance_contract(recovered.map(|value| -value)); + } +} diff --git a/crates/validation_core/tests/bias_standard_error_singleton_level_rounding_contract.rs b/crates/validation_core/tests/bias_standard_error_singleton_level_rounding_contract.rs new file mode 100644 index 000000000..031ec27cf --- /dev/null +++ b/crates/validation_core/tests/bias_standard_error_singleton_level_rounding_contract.rs @@ -0,0 +1,35 @@ +use validation_core::{ValidationError, bias_standard_error}; + +#[test] +fn bias_standard_error_preserves_singleton_repeated_level_identity_beyond_three_rows() { + let repeated = f64::from_bits(0x3fef_ffff_ffff_ffff); + let recovered = [0.0, repeated, repeated, repeated]; + + let standard_error = bias_standard_error(&[0.0; 4], &recovered) + .expect("represented-input standard error"); + // For an exactly represented four-observation two-level sample [0, a, a, a], + // the sample standard error of the mean simplifies to |a| / 4. The generic + // translated second-moment path squares and square-roots the exact gap and + // lands one ULP below that single exact power-of-two division for + // a = next_down(1.0). + assert_eq!(standard_error.to_bits(), 0x3fcf_ffff_ffff_ffff); + + let permuted = [repeated, 0.0, repeated, repeated]; + let permuted_standard_error = bias_standard_error(&[0.0; 4], &permuted) + .expect("permuted represented-input standard error"); + assert_eq!(permuted_standard_error.to_bits(), 0x3fcf_ffff_ffff_ffff); + + let mirrored = recovered.map(|value| -value); + let mirrored_standard_error = bias_standard_error(&[0.0; 4], &mirrored) + .expect("mirrored represented-input standard error"); + assert_eq!(mirrored_standard_error.to_bits(), 0x3fcf_ffff_ffff_ffff); + + let minimum_subnormal = f64::from_bits(1); + assert_eq!( + bias_standard_error( + &[0.0; 4], + &[0.0, minimum_subnormal, minimum_subnormal, minimum_subnormal], + ), + Err(ValidationError::InvalidInput) + ); +} diff --git a/crates/validation_core/tests/bias_standard_error_six_observation_pair_distance_contract.rs b/crates/validation_core/tests/bias_standard_error_six_observation_pair_distance_contract.rs new file mode 100644 index 000000000..eea906a3c --- /dev/null +++ b/crates/validation_core/tests/bias_standard_error_six_observation_pair_distance_contract.rs @@ -0,0 +1,51 @@ +//! Regression contract for exact six-observation bias standard-error rounding. +//! +//! The represented-input fixture requires the bounded pair-distance midpoint proof +//! to remain permutation- and sign-invariant where the translated floating-moment +//! fallback rounds one binary64 ULP high. + +use validation_core::bias_standard_error; + +fn assert_six_observation_pair_distance_contract(recovered: [f64; 6]) { + let truth = [0.0; 6]; + let standard_error = bias_standard_error(&truth, &recovered).expect("representable SE"); + assert_eq!( + standard_error.to_bits(), + 0x419c_057d_42fc_5857, + "the exact six-observation pair-distance ratio must determine the represented-input SE; the translated floating moment/sqrt fallback rounds one ULP high" + ); +} + +#[test] +fn exact_six_observation_dispersion_uses_pair_distance_midpoint_proof() { + let samples = [ + [ + 1_120_315_269.0, + 1_513_609_015.0, + 1_569_037_659.0, + 1_789_057_504.0, + 1_807_936_669.0, + 1_914_796_738.0, + ], + [ + 1_914_796_738.0, + 1_569_037_659.0, + 1_120_315_269.0, + 1_807_936_669.0, + 1_513_609_015.0, + 1_789_057_504.0, + ], + [ + 1_569_037_659.0, + 1_120_315_269.0, + 1_914_796_738.0, + 1_513_609_015.0, + 1_789_057_504.0, + 1_807_936_669.0, + ], + ]; + for recovered in samples { + assert_six_observation_pair_distance_contract(recovered); + assert_six_observation_pair_distance_contract(recovered.map(|value| -value)); + } +} diff --git a/crates/validation_core/tests/bias_standard_error_sixteen_observation_pair_distance_contract.rs b/crates/validation_core/tests/bias_standard_error_sixteen_observation_pair_distance_contract.rs new file mode 100644 index 000000000..da7579651 --- /dev/null +++ b/crates/validation_core/tests/bias_standard_error_sixteen_observation_pair_distance_contract.rs @@ -0,0 +1,75 @@ +use validation_core::bias_standard_error; + +fn assert_sixteen_observation_pair_distance_contract(recovered: [f64; 16]) { + let truth = [0.0; 16]; + let standard_error = bias_standard_error(&truth, &recovered).expect("representable SE"); + assert_eq!( + standard_error.to_bits(), + 0x419c_fcbb_b78d_2ad5, + "the exact sixteen-observation pair-distance ratio must determine the represented-input SE; the translated floating moment/sqrt fallback rounds one ULP low" + ); +} + +#[test] +fn exact_sixteen_observation_dispersion_uses_pair_distance_midpoint_proof() { + let samples = [ + [ + 314_270_929.0, + 327_661_307.0, + 371_854_441.0, + 398_522_837.0, + 413_483_290.0, + 416_184_956.0, + 565_808_551.0, + 682_627_163.0, + 724_514_517.0, + 731_058_943.0, + 740_662_035.0, + 970_233_120.0, + 1_141_566_755.0, + 1_320_628_283.0, + 1_526_331_271.0, + 1_992_574_092.0, + ], + [ + 1_992_574_092.0, + 1_526_331_271.0, + 1_320_628_283.0, + 1_141_566_755.0, + 970_233_120.0, + 740_662_035.0, + 731_058_943.0, + 724_514_517.0, + 682_627_163.0, + 565_808_551.0, + 416_184_956.0, + 413_483_290.0, + 398_522_837.0, + 371_854_441.0, + 327_661_307.0, + 314_270_929.0, + ], + [ + 682_627_163.0, + 314_270_929.0, + 1_992_574_092.0, + 398_522_837.0, + 1_141_566_755.0, + 327_661_307.0, + 731_058_943.0, + 413_483_290.0, + 1_526_331_271.0, + 371_854_441.0, + 970_233_120.0, + 416_184_956.0, + 1_320_628_283.0, + 565_808_551.0, + 740_662_035.0, + 724_514_517.0, + ], + ]; + for recovered in samples { + assert_sixteen_observation_pair_distance_contract(recovered); + assert_sixteen_observation_pair_distance_contract(recovered.map(|value| -value)); + } +} diff --git a/crates/validation_core/tests/bias_standard_error_sqrt_double_rounding_contract.rs b/crates/validation_core/tests/bias_standard_error_sqrt_double_rounding_contract.rs new file mode 100644 index 000000000..9bd055358 --- /dev/null +++ b/crates/validation_core/tests/bias_standard_error_sqrt_double_rounding_contract.rs @@ -0,0 +1,18 @@ +use validation_core::bias_standard_error; + +#[test] +fn bias_standard_error_rounds_the_final_normalized_se_once() { + let unit = 2.0_f64.powi(-55); + let truth = [0.0; 3]; + let recovered = [-unit, 0.0, unit]; + + let standard_error = bias_standard_error(&truth, &recovered).expect("represented standard error"); + assert_eq!(standard_error.to_bits(), 0x3c72_79a7_4590_331c); + + let mirrored_recovered = recovered.map(|value| -value); + let mirrored_standard_error = + bias_standard_error(&truth, &mirrored_recovered).expect("mirrored standard error"); + assert_eq!(mirrored_standard_error.to_bits(), 0x3c72_79a7_4590_331c); + + assert_eq!(bias_standard_error(&[0.0, 0.0], &[1.0, -1.0]), Ok(1.0)); +} diff --git a/crates/validation_core/tests/bias_standard_error_ten_observation_pair_distance_contract.rs b/crates/validation_core/tests/bias_standard_error_ten_observation_pair_distance_contract.rs new file mode 100644 index 000000000..e3ea895c0 --- /dev/null +++ b/crates/validation_core/tests/bias_standard_error_ten_observation_pair_distance_contract.rs @@ -0,0 +1,57 @@ +use validation_core::bias_standard_error; + +fn assert_ten_observation_pair_distance_contract(recovered: [f64; 10]) { + let truth = [0.0; 10]; + let standard_error = bias_standard_error(&truth, &recovered).expect("representable SE"); + assert_eq!( + standard_error.to_bits(), + 0x41a5_e8a1_0795_bf6c, + "the exact ten-observation pair-distance ratio must determine the represented-input SE; the translated floating moment/sqrt fallback rounds one ULP low" + ); +} + +#[test] +fn exact_ten_observation_dispersion_uses_pair_distance_midpoint_proof() { + let samples = [ + [ + 418_906_532.0, + 431_535_003.0, + 554_793_949.0, + 905_115_878.0, + 1_079_195_036.0, + 1_340_223_962.0, + 1_591_821_052.0, + 1_592_008_872.0, + 1_807_262_929.0, + 1_996_099_302.0, + ], + [ + 1_996_099_302.0, + 1_807_262_929.0, + 1_592_008_872.0, + 1_591_821_052.0, + 1_340_223_962.0, + 1_079_195_036.0, + 905_115_878.0, + 554_793_949.0, + 431_535_003.0, + 418_906_532.0, + ], + [ + 1_079_195_036.0, + 418_906_532.0, + 1_996_099_302.0, + 554_793_949.0, + 1_807_262_929.0, + 431_535_003.0, + 1_592_008_872.0, + 905_115_878.0, + 1_591_821_052.0, + 1_340_223_962.0, + ], + ]; + for recovered in samples { + assert_ten_observation_pair_distance_contract(recovered); + assert_ten_observation_pair_distance_contract(recovered.map(|value| -value)); + } +} diff --git a/crates/validation_core/tests/bias_standard_error_thirteen_observation_pair_distance_contract.rs b/crates/validation_core/tests/bias_standard_error_thirteen_observation_pair_distance_contract.rs new file mode 100644 index 000000000..15da23b51 --- /dev/null +++ b/crates/validation_core/tests/bias_standard_error_thirteen_observation_pair_distance_contract.rs @@ -0,0 +1,66 @@ +use validation_core::bias_standard_error; + +fn assert_thirteen_observation_pair_distance_contract(recovered: [f64; 13]) { + let truth = [0.0; 13]; + let standard_error = bias_standard_error(&truth, &recovered).expect("representable SE"); + assert_eq!( + standard_error.to_bits(), + 0x41a2_9a8e_6db8_cb76, + "the exact thirteen-observation pair-distance ratio must determine the represented-input SE; the translated floating moment/sqrt fallback rounds one ULP high" + ); +} + +#[test] +fn exact_thirteen_observation_dispersion_uses_pair_distance_midpoint_proof() { + let samples = [ + [ + 13_412_968.0, + 42_445_497.0, + 117_340_356.0, + 152_587_301.0, + 309_740_336.0, + 359_871_277.0, + 717_207_453.0, + 811_347_466.0, + 1_016_388_094.0, + 1_092_140_579.0, + 1_412_658_032.0, + 1_429_960_424.0, + 1_525_741_984.0, + ], + [ + 1_525_741_984.0, + 1_429_960_424.0, + 1_412_658_032.0, + 1_092_140_579.0, + 1_016_388_094.0, + 811_347_466.0, + 717_207_453.0, + 359_871_277.0, + 309_740_336.0, + 152_587_301.0, + 117_340_356.0, + 42_445_497.0, + 13_412_968.0, + ], + [ + 717_207_453.0, + 13_412_968.0, + 1_525_741_984.0, + 152_587_301.0, + 1_412_658_032.0, + 42_445_497.0, + 1_092_140_579.0, + 309_740_336.0, + 1_429_960_424.0, + 117_340_356.0, + 1_016_388_094.0, + 359_871_277.0, + 811_347_466.0, + ], + ]; + for recovered in samples { + assert_thirteen_observation_pair_distance_contract(recovered); + assert_thirteen_observation_pair_distance_contract(recovered.map(|value| -value)); + } +} diff --git a/crates/validation_core/tests/bias_standard_error_three_level_rational_scale_rounding_contract.rs b/crates/validation_core/tests/bias_standard_error_three_level_rational_scale_rounding_contract.rs new file mode 100644 index 000000000..9eed3618c --- /dev/null +++ b/crates/validation_core/tests/bias_standard_error_three_level_rational_scale_rounding_contract.rs @@ -0,0 +1,46 @@ +//! Correct-rounding contract for a represented three-level rational scale. +//! +//! The sample has an exact dyadic spacing whose standard-error scale reduces to +//! a non-dyadic rational before the final square root. The public result must be +//! rounded once to the expected binary64 value and remain bit-identical under +//! every permutation and sign reflection of the represented observations. + +use validation_core::bias_standard_error; + +const EXPECTED_STANDARD_ERROR_BITS: u64 = 0x3f79_5555_5555_5555; + +fn represented_sample() -> [f64; 3] { + [ + 0.0, + f64::from_bits(0x3f74_0000_0000_0000), + f64::from_bits(0x3f95_0000_0000_0000), + ] +} + +#[test] +fn exact_three_level_rational_scale_preserves_correct_rounding() { + let [low, middle, high] = represented_sample(); + let truth = [0.0; 3]; + let permutations = [ + [low, middle, high], + [low, high, middle], + [middle, low, high], + [middle, high, low], + [high, low, middle], + [high, middle, low], + ]; + + for recovered in permutations { + let standard_error = + bias_standard_error(&truth, &recovered).expect("finite standard error"); + assert_eq!(standard_error.to_bits(), EXPECTED_STANDARD_ERROR_BITS); + + let mirrored = recovered.map(|value| -value); + let mirrored_standard_error = + bias_standard_error(&truth, &mirrored).expect("finite mirrored standard error"); + assert_eq!( + mirrored_standard_error.to_bits(), + EXPECTED_STANDARD_ERROR_BITS + ); + } +} diff --git a/crates/validation_core/tests/bias_standard_error_three_level_scale_invariance_contract.rs b/crates/validation_core/tests/bias_standard_error_three_level_scale_invariance_contract.rs new file mode 100644 index 000000000..94ea31135 --- /dev/null +++ b/crates/validation_core/tests/bias_standard_error_three_level_scale_invariance_contract.rs @@ -0,0 +1,41 @@ +//! Regression contract for bit-stable three-level bias SE under exact power-of-two scaling. + +use validation_core::bias_standard_error; + +const EXPECTED_STANDARD_ERROR_BITS: u64 = 0x64f9_5555_5555_5555; + +fn represented_sample() -> [f64; 3] { + [ + 0.0, + f64::from_bits(0x64f4_0000_0000_0000), + f64::from_bits(0x6515_0000_0000_0000), + ] +} + +#[test] +fn exact_three_level_rational_scale_is_invariant_under_exact_power_of_two_scaling() { + let [low, middle, high] = represented_sample(); + let truth = [0.0; 3]; + let permutations = [ + [low, middle, high], + [low, high, middle], + [middle, low, high], + [middle, high, low], + [high, low, middle], + [high, middle, low], + ]; + + for recovered in permutations { + let standard_error = + bias_standard_error(&truth, &recovered).expect("finite standard error"); + assert_eq!(standard_error.to_bits(), EXPECTED_STANDARD_ERROR_BITS); + + let mirrored = recovered.map(|value| -value); + let mirrored_standard_error = + bias_standard_error(&truth, &mirrored).expect("finite mirrored standard error"); + assert_eq!( + mirrored_standard_error.to_bits(), + EXPECTED_STANDARD_ERROR_BITS + ); + } +} diff --git a/crates/validation_core/tests/bias_standard_error_three_level_underflow_scale_contract.rs b/crates/validation_core/tests/bias_standard_error_three_level_underflow_scale_contract.rs new file mode 100644 index 000000000..15cf86ccd --- /dev/null +++ b/crates/validation_core/tests/bias_standard_error_three_level_underflow_scale_contract.rs @@ -0,0 +1,31 @@ +use validation_core::bias_standard_error; + +const EXPECTED_STANDARD_ERROR_BITS: u64 = 0x0009_3cd3_a2c8_198e; + +fn represented_sample() -> [f64; 3] { + [0.0, f64::MIN_POSITIVE, 2.0 * f64::MIN_POSITIVE] +} + +#[test] +fn nonzero_three_level_standard_error_survives_square_underflow() { + let [low, middle, high] = represented_sample(); + let truth = [0.0; 3]; + let permutations = [ + [low, middle, high], + [low, high, middle], + [middle, low, high], + [middle, high, low], + [high, low, middle], + [high, middle, low], + ]; + + for recovered in permutations { + let standard_error = bias_standard_error(&truth, &recovered).expect("finite standard error"); + assert_eq!(standard_error.to_bits(), EXPECTED_STANDARD_ERROR_BITS); + + let mirrored = recovered.map(|value| -value); + let mirrored_standard_error = + bias_standard_error(&truth, &mirrored).expect("finite mirrored standard error"); + assert_eq!(mirrored_standard_error.to_bits(), EXPECTED_STANDARD_ERROR_BITS); + } +} diff --git a/crates/validation_core/tests/bias_standard_error_twelve_observation_pair_distance_contract.rs b/crates/validation_core/tests/bias_standard_error_twelve_observation_pair_distance_contract.rs new file mode 100644 index 000000000..b139e222d --- /dev/null +++ b/crates/validation_core/tests/bias_standard_error_twelve_observation_pair_distance_contract.rs @@ -0,0 +1,63 @@ +use validation_core::bias_standard_error; + +fn assert_twelve_observation_pair_distance_contract(recovered: [f64; 12]) { + let truth = [0.0; 12]; + let standard_error = bias_standard_error(&truth, &recovered).expect("representable SE"); + assert_eq!( + standard_error.to_bits(), + 0x41a6_5ddb_5161_045f, + "the exact twelve-observation pair-distance ratio must determine the represented-input SE; the translated floating moment/sqrt fallback rounds one ULP high" + ); +} + +#[test] +fn exact_twelve_observation_dispersion_uses_pair_distance_midpoint_proof() { + let samples = [ + [ + 18_775_780.0, + 73_991_125.0, + 198_689_967.0, + 631_050_858.0, + 778_682_730.0, + 826_435_964.0, + 853_584_967.0, + 1_530_809_509.0, + 1_562_270_376.0, + 1_586_067_346.0, + 1_682_017_356.0, + 1_750_122_820.0, + ], + [ + 1_750_122_820.0, + 1_682_017_356.0, + 1_586_067_346.0, + 1_562_270_376.0, + 1_530_809_509.0, + 853_584_967.0, + 826_435_964.0, + 778_682_730.0, + 631_050_858.0, + 198_689_967.0, + 73_991_125.0, + 18_775_780.0, + ], + [ + 826_435_964.0, + 18_775_780.0, + 1_750_122_820.0, + 198_689_967.0, + 1_682_017_356.0, + 73_991_125.0, + 1_530_809_509.0, + 778_682_730.0, + 1_586_067_346.0, + 631_050_858.0, + 1_562_270_376.0, + 853_584_967.0, + ], + ]; + for recovered in samples { + assert_twelve_observation_pair_distance_contract(recovered); + assert_twelve_observation_pair_distance_contract(recovered.map(|value| -value)); + } +} diff --git a/crates/validation_core/tests/bias_standard_error_two_level_count_rounding_contract.rs b/crates/validation_core/tests/bias_standard_error_two_level_count_rounding_contract.rs new file mode 100644 index 000000000..c3ea606cd --- /dev/null +++ b/crates/validation_core/tests/bias_standard_error_two_level_count_rounding_contract.rs @@ -0,0 +1,57 @@ +use validation_core::{ValidationError, bias_standard_error}; + +#[test] +fn bias_standard_error_preserves_exact_dyadic_two_level_count_geometry() { + let repeated = f64::from_bits(0x3fef_ffff_ffff_ffff); + let recovered = [ + 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, repeated, repeated, repeated, repeated, repeated, + repeated, repeated, repeated, repeated, repeated, + ]; + + let standard_error = bias_standard_error(&[0.0; 16], &recovered) + .expect("represented-input standard error"); + // With six observations at one exact residual level and ten at the other, + // m(n-m)/(n-1) = 6*10/15 = 4. Therefore SE(mean) is exactly |gap|/8. + // Reconstructing that dyadic identity through translated sums, squares and + // sqrt rounds next_down(1.0) up to 0.125 instead of preserving gap/8. + assert_eq!(standard_error.to_bits(), 0x3fbf_ffff_ffff_ffff); + + let permuted = [ + repeated, 0.0, repeated, 0.0, repeated, 0.0, repeated, 0.0, repeated, 0.0, + repeated, 0.0, repeated, repeated, repeated, repeated, + ]; + let permuted_standard_error = bias_standard_error(&[0.0; 16], &permuted) + .expect("permuted represented-input standard error"); + assert_eq!(permuted_standard_error.to_bits(), 0x3fbf_ffff_ffff_ffff); + + let mirrored = recovered.map(|value| -value); + let mirrored_standard_error = bias_standard_error(&[0.0; 16], &mirrored) + .expect("mirrored represented-input standard error"); + assert_eq!(mirrored_standard_error.to_bits(), 0x3fbf_ffff_ffff_ffff); + + let minimum_subnormal = f64::from_bits(1); + assert_eq!( + bias_standard_error( + &[0.0; 16], + &[ + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + minimum_subnormal, + minimum_subnormal, + minimum_subnormal, + minimum_subnormal, + minimum_subnormal, + minimum_subnormal, + minimum_subnormal, + minimum_subnormal, + minimum_subnormal, + minimum_subnormal, + ], + ), + Err(ValidationError::InvalidInput) + ); +} diff --git a/crates/validation_core/tests/bias_standard_error_two_level_integer_divisor_rounding_contract.rs b/crates/validation_core/tests/bias_standard_error_two_level_integer_divisor_rounding_contract.rs new file mode 100644 index 000000000..b76c37ca3 --- /dev/null +++ b/crates/validation_core/tests/bias_standard_error_two_level_integer_divisor_rounding_contract.rs @@ -0,0 +1,50 @@ +//! Locks exact integer-divisor two-level bias standard-error rounding and its fail-closed edge. + +use validation_core::{ValidationError, bias_standard_error}; + +#[test] +fn bias_standard_error_preserves_exact_integer_divisor_two_level_geometry() { + let repeated = f64::from_bits(0x3fef_ffff_ffff_fffd); + let recovered = [ + 0.0, 0.0, 0.0, repeated, repeated, repeated, repeated, repeated, repeated, + ]; + + let standard_error = bias_standard_error(&[0.0; 9], &recovered) + .expect("represented-input standard error"); + // With three observations at one exact residual level and six at the other, + // m(n-m)/(n^2(n-1)) = 3*6/(9^2*8) = 1/36. Therefore SE(mean) is exactly + // |gap|/6. The predecessor translated sum/square/sqrt path returns the + // adjacent upper binary64 value for this represented gap. + assert_eq!(standard_error.to_bits(), 0x3fc5_5555_5555_5553); + + let permuted = [ + repeated, 0.0, repeated, 0.0, repeated, 0.0, repeated, repeated, repeated, + ]; + let permuted_standard_error = bias_standard_error(&[0.0; 9], &permuted) + .expect("permuted represented-input standard error"); + assert_eq!(permuted_standard_error.to_bits(), 0x3fc5_5555_5555_5553); + + let mirrored = recovered.map(|value| -value); + let mirrored_standard_error = bias_standard_error(&[0.0; 9], &mirrored) + .expect("mirrored represented-input standard error"); + assert_eq!(mirrored_standard_error.to_bits(), 0x3fc5_5555_5555_5553); + + let minimum_subnormal = f64::from_bits(1); + assert_eq!( + bias_standard_error( + &[0.0; 9], + &[ + 0.0, + 0.0, + 0.0, + minimum_subnormal, + minimum_subnormal, + minimum_subnormal, + minimum_subnormal, + minimum_subnormal, + minimum_subnormal, + ], + ), + Err(ValidationError::InvalidInput) + ); +} diff --git a/crates/validation_core/tests/bias_standard_error_two_level_rational_scale_rounding_contract.rs b/crates/validation_core/tests/bias_standard_error_two_level_rational_scale_rounding_contract.rs new file mode 100644 index 000000000..9150debec --- /dev/null +++ b/crates/validation_core/tests/bias_standard_error_two_level_rational_scale_rounding_contract.rs @@ -0,0 +1,56 @@ +//! Contracts exact rounding for two-level represented residuals with a rational count factor. +//! +//! The 2-versus-8 split at n=10 has SE(mean)^2 = 4*gap^2/225, so the public result +//! depends on preserving the exact rational square through the final sqrt/rounding decision. +//! The contract also fixes permutation and sign symmetry and keeps underflow fail-closed. + +use validation_core::{ValidationError, bias_standard_error}; + +#[test] +fn bias_standard_error_preserves_exact_rational_square_two_level_geometry() { + let repeated = f64::from_bits(0x3fef_ffff_ffff_fffe); + let recovered = [ + 0.0, 0.0, repeated, repeated, repeated, repeated, repeated, repeated, repeated, repeated, + ]; + + let standard_error = + bias_standard_error(&[0.0; 10], &recovered).expect("represented-input standard error"); + // With two observations at one exact residual level and eight at the other, + // m(n-m)/(n^2(n-1)) = 2*8/(10^2*9) = 4/225. Therefore SE(mean) is exactly + // 2*|gap|/15. GAP-103 admits only reciprocal-integer-square count factors, + // so its translated sum/square/sqrt fallback returns the adjacent upper + // binary64 value for this represented gap. + assert_eq!(standard_error.to_bits(), 0x3fc1_1111_1111_1110); + + let permuted = [ + repeated, 0.0, repeated, repeated, repeated, 0.0, repeated, repeated, repeated, repeated, + ]; + let permuted_standard_error = bias_standard_error(&[0.0; 10], &permuted) + .expect("permuted represented-input standard error"); + assert_eq!(permuted_standard_error.to_bits(), 0x3fc1_1111_1111_1110); + + let mirrored = recovered.map(|value| -value); + let mirrored_standard_error = bias_standard_error(&[0.0; 10], &mirrored) + .expect("mirrored represented-input standard error"); + assert_eq!(mirrored_standard_error.to_bits(), 0x3fc1_1111_1111_1110); + + let minimum_subnormal = f64::from_bits(1); + assert_eq!( + bias_standard_error( + &[0.0; 10], + &[ + 0.0, + 0.0, + minimum_subnormal, + minimum_subnormal, + minimum_subnormal, + minimum_subnormal, + minimum_subnormal, + minimum_subnormal, + minimum_subnormal, + minimum_subnormal, + ], + ), + Err(ValidationError::InvalidInput) + ); +} diff --git a/crates/validation_core/tests/bias_standard_error_two_observation_mean_roundoff_contract.rs b/crates/validation_core/tests/bias_standard_error_two_observation_mean_roundoff_contract.rs new file mode 100644 index 000000000..767831c60 --- /dev/null +++ b/crates/validation_core/tests/bias_standard_error_two_observation_mean_roundoff_contract.rs @@ -0,0 +1,23 @@ +use validation_core::bias_standard_error; + +#[test] +fn bias_standard_error_uses_exact_two_observation_identity_when_mean_rounds() { + let upper = 1.0_f64; + let lower = f64::from_bits(upper.to_bits() - 1); + let truth = [0.0, 0.0]; + let recovered = [upper, lower]; + + let standard_error = + bias_standard_error(&truth, &recovered).expect("represented standard error"); + assert_eq!(standard_error.to_bits(), 2.0_f64.powi(-54).to_bits()); + + let mirrored_recovered = recovered.map(|value| -value); + let mirrored_standard_error = bias_standard_error(&truth, &mirrored_recovered) + .expect("mirrored represented standard error"); + assert_eq!( + mirrored_standard_error.to_bits(), + 2.0_f64.powi(-54).to_bits() + ); + + assert_eq!(bias_standard_error(&truth, &[upper, upper]), Ok(0.0)); +} diff --git a/crates/validation_core/tests/bias_standard_error_wide_linear_admission_bound_characterization.rs b/crates/validation_core/tests/bias_standard_error_wide_linear_admission_bound_characterization.rs new file mode 100644 index 000000000..12ff965fb --- /dev/null +++ b/crates/validation_core/tests/bias_standard_error_wide_linear_admission_bound_characterization.rs @@ -0,0 +1,142 @@ +//! Characterizes exact-proof admission bounds for canonical bias-standard-error coefficients. + +fn deterministic_compact_fixture(sample_count: usize) -> Vec { + (0..sample_count) + .map(|index| { + let value = u128::try_from(index).expect("fixture index fits u128"); + (value * 1_000_003 + value * value * 97 + 17) % 4_000_000_001 + }) + .collect() +} + +fn boundary_fixture(sample_count: usize, diameter: u128) -> Vec { + let mut values = Vec::with_capacity(sample_count); + values.push(0); + values.extend((1..sample_count).map(|_| diameter)); + values +} + +fn pair_square_sum(values: &[u128]) -> Option { + let mut sum = 0_u128; + for left in 0..values.len() { + for right in left + 1..values.len() { + let difference = values[left].abs_diff(values[right]); + sum = sum.checked_add(difference.checked_mul(difference)?)?; + } + } + Some(sum) +} + +fn normalized_linear_terms(values: &[u128]) -> Option<(u128, u128, u32)> { + let minimum = *values.iter().min()?; + let common_shift = values + .iter() + .filter_map(|value| { + let coefficient = value.checked_sub(minimum)?; + (coefficient != 0).then_some(coefficient.trailing_zeros()) + }) + .min() + .unwrap_or(0); + + let mut coefficient_sum = 0_u128; + let mut square_sum = 0_u128; + for value in values { + let coefficient = value.checked_sub(minimum)? >> common_shift; + coefficient_sum = coefficient_sum.checked_add(coefficient)?; + square_sum = square_sum.checked_add(coefficient.checked_mul(coefficient)?)?; + } + Some((coefficient_sum, square_sum, common_shift)) +} + +fn product_bit_upper_bound(left: u128, right: u128) -> u32 { + let left_bits = u128::BITS - left.leading_zeros(); + let right_bits = u128::BITS - right.leading_zeros(); + left_bits.saturating_add(right_bits) +} + +fn assert_pair_admission_implies_wide_linear_product_capacity() { + assert!( + usize::BITS <= u128::BITS, + "the supported target must convert Vec length to u128 without truncation" + ); + + // Pair admission gives P <= u128::MAX. For canonical nonnegative + // anchor-relative coefficients with at least one zero, S1 <= S2 <= P. + // Therefore both linear-identity products are products of two u128 values: + // n*S2 and S1*S1. Their exact bit-width upper bound is 256, which is exactly + // the capacity of the two-limb Wide256 characterization reference. + assert_eq!( + product_bit_upper_bound(u128::MAX, u128::MAX), + 256, + "the extremal u128-by-u128 product requires but does not exceed 256 bits" + ); +} + +fn assert_pair_admission_bounds_linear_accumulators(values: &[u128]) { + let pair_sum = pair_square_sum(values).expect("fixture pair numerator fits u128"); + let (coefficient_sum, square_sum, common_shift) = + normalized_linear_terms(values).expect("pair-admitted normalized terms fit u128"); + let squared_shift = common_shift + .checked_mul(2) + .expect("dyadic square shift fits u32"); + let normalized_pair_sum = pair_sum >> squared_shift; + let restored_pair_sum = normalized_pair_sum + .checked_shl(squared_shift) + .expect("fixture normalized pair numerator restores exactly"); + + assert_eq!(restored_pair_sum, pair_sum); + assert!( + coefficient_sum <= square_sum, + "integer anchor coefficients satisfy c <= c^2 termwise" + ); + assert!( + square_sum <= normalized_pair_sum, + "because at least one anchor coefficient is zero, every c_i^2 occurs in the exact pair numerator" + ); + assert!( + product_bit_upper_bound( + u128::try_from(values.len()).expect("supported Vec length fits u128"), + square_sum, + ) <= 256, + "pair-admitted n*S2 cannot exceed Wide256 product capacity" + ); + assert!( + product_bit_upper_bound(coefficient_sum, coefficient_sum) <= 256, + "pair-admitted S1^2 cannot exceed Wide256 product capacity" + ); +} + +#[test] +fn pair_admitted_bound_survives_known_compact_and_boundary_geometries() { + for sample_count in [4_usize, 16, 17, 32, 64, 128, 256] { + assert_pair_admission_bounds_linear_accumulators(&deterministic_compact_fixture( + sample_count, + )); + } + + assert_pair_admission_bounds_linear_accumulators(&boundary_fixture(65, 1_u128 << 58)); + assert_pair_admission_bounds_linear_accumulators(&boundary_fixture( + 65, + (1_u128 << 58) + 1, + )); +} + +#[test] +fn pair_admitted_bound_holds_across_small_integer_composition_space() { + for sample_count in 2_usize..=7 { + let state_count = 4_usize.pow(u32::try_from(sample_count).expect("small exponent")); + for mut state in 0..state_count { + let mut values = Vec::with_capacity(sample_count); + for _ in 0..sample_count { + values.push(u128::try_from(state % 4).expect("base-four digit fits u128")); + state /= 4; + } + assert_pair_admission_bounds_linear_accumulators(&values); + } + } +} + +#[test] +fn pair_admission_implies_wide_linear_product_capacity() { + assert_pair_admission_implies_wide_linear_product_capacity(); +} diff --git a/crates/validation_core/tests/bias_standard_error_wide_scaled_comparison_characterization.rs b/crates/validation_core/tests/bias_standard_error_wide_scaled_comparison_characterization.rs new file mode 100644 index 000000000..aeac4f3f1 --- /dev/null +++ b/crates/validation_core/tests/bias_standard_error_wide_scaled_comparison_characterization.rs @@ -0,0 +1,280 @@ +//! Characterizes exact scaled comparisons needed by wider bias-SE proofs. +//! +//! The current bounded production proof forms `u128` candidate-square and midpoint +//! products directly. Represented inputs can keep the exact pair numerator and +//! denominator admissible while those comparison products require more than 128 +//! bits. This test-only reference compares two-limb mantissas with signed dyadic +//! exponents without materializing large powers of two, preserving exact ordering +//! across normal and extreme exponent gaps. + +use core::cmp::Ordering; + +#[derive(Clone, Copy, Debug, Eq, Ord, PartialEq, PartialOrd)] +struct Wide256 { + high: u128, + low: u128, +} + +impl Wide256 { + const fn from_u128(value: u128) -> Self { + Self { + high: 0, + low: value, + } + } + + fn multiply_u128(left: u128, right: u128) -> Self { + let mask = u128::from(u64::MAX); + let left_limbs = [ + u64::try_from(left & mask).expect("masked low limb fits u64"), + u64::try_from(left >> 64).expect("high limb fits u64"), + ]; + let right_limbs = [ + u64::try_from(right & mask).expect("masked low limb fits u64"), + u64::try_from(right >> 64).expect("high limb fits u64"), + ]; + let mut limbs = [0_u64; 4]; + + for (left_index, left_limb) in left_limbs.iter().copied().enumerate() { + let mut carry = 0_u128; + for (right_index, right_limb) in right_limbs.iter().copied().enumerate() { + let limb_index = left_index + right_index; + let accumulator = u128::from(left_limb) + .checked_mul(u128::from(right_limb)) + .expect("64-bit limb product fits u128") + .checked_add(u128::from(limbs[limb_index])) + .expect("schoolbook partial sum fits u128") + .checked_add(carry) + .expect("schoolbook carry sum fits u128"); + limbs[limb_index] = + u64::try_from(accumulator & mask).expect("masked schoolbook limb fits u64"); + carry = accumulator >> 64; + } + limbs[left_index + 2] = + u64::try_from(carry).expect("schoolbook multiplication carry fits u64"); + } + + Self { + high: u128::from(limbs[2]) | (u128::from(limbs[3]) << 64), + low: u128::from(limbs[0]) | (u128::from(limbs[1]) << 64), + } + } + + const fn is_zero(self) -> bool { + self.high == 0 && self.low == 0 + } + + fn bit_len(self) -> u32 { + if self.high != 0 { + 128 + (u128::BITS - self.high.leading_zeros()) + } else { + u128::BITS - self.low.leading_zeros() + } + } + + fn bit(self, index: u32) -> bool { + if index < u128::BITS { + ((self.low >> index) & 1) != 0 + } else { + let high_index = index - u128::BITS; + ((self.high >> high_index) & 1) != 0 + } + } +} + +fn compare_scaled_wide( + left: Wide256, + left_exponent: i32, + right: Wide256, + right_exponent: i32, +) -> Option { + match (left.is_zero(), right.is_zero()) { + (true, true) => return Some(Ordering::Equal), + (true, false) => return Some(Ordering::Less), + (false, true) => return Some(Ordering::Greater), + (false, false) => {} + } + + let left_bits = left.bit_len(); + let right_bits = right.bit_len(); + let left_top = left_exponent + .checked_add(i32::try_from(left_bits.checked_sub(1)?).ok()?)?; + let right_top = right_exponent + .checked_add(i32::try_from(right_bits.checked_sub(1)?).ok()?)?; + match left_top.cmp(&right_top) { + Ordering::Less => return Some(Ordering::Less), + Ordering::Greater => return Some(Ordering::Greater), + Ordering::Equal => {} + } + + let width = left_bits.max(right_bits); + for offset in 0..width { + let left_bit = offset < left_bits && left.bit(left_bits - 1 - offset); + let right_bit = offset < right_bits && right.bit(right_bits - 1 - offset); + match left_bit.cmp(&right_bit) { + Ordering::Less => return Some(Ordering::Less), + Ordering::Greater => return Some(Ordering::Greater), + Ordering::Equal => {} + } + } + Some(Ordering::Equal) +} + +fn positive_dyadic(value: f64) -> Option<(u128, i32)> { + if !value.is_finite() || value <= 0.0 { + return None; + } + let bits = value.to_bits(); + let exponent_bits = i32::try_from((bits >> 52) & 0x7ff).ok()?; + let fraction = bits & 0x000f_ffff_ffff_ffff; + let (mut significand, mut exponent) = if exponent_bits == 0 { + (u128::from(fraction), -1074) + } else { + ( + u128::from((1_u64 << 52) | fraction), + exponent_bits - 1023 - 52, + ) + }; + if significand == 0 { + return None; + } + let trailing = significand.trailing_zeros(); + significand >>= trailing; + exponent += i32::try_from(trailing).ok()?; + Some((significand, exponent)) +} + +fn midpoint_dyadic(left: f64, right: f64) -> Option<(u128, i32)> { + let (left_significand, left_exponent) = positive_dyadic(left)?; + let (right_significand, right_exponent) = positive_dyadic(right)?; + let common_exponent = left_exponent.min(right_exponent); + let left_shift = left_exponent.checked_sub(common_exponent)?.unsigned_abs(); + let right_shift = right_exponent.checked_sub(common_exponent)?.unsigned_abs(); + let left_units = left_significand.checked_shl(left_shift)?; + let right_units = right_significand.checked_shl(right_shift)?; + let mut midpoint_significand = left_units.checked_add(right_units)?; + let mut midpoint_exponent = common_exponent.checked_sub(1)?; + let trailing = midpoint_significand.trailing_zeros(); + midpoint_significand >>= trailing; + midpoint_exponent += i32::try_from(trailing).ok()?; + Some((midpoint_significand, midpoint_exponent)) +} + +fn exact_pair_numerator(sample_count: usize) -> u128 { + let repeated = u128::try_from(sample_count - 2).expect("sample count fits u128"); + let diameter = 1_u128 << 53; + let near_diameter = diameter - 1; + 1_u128 + .checked_add( + repeated + .checked_mul( + diameter + .checked_mul(diameter) + .expect("diameter square fits u128") + .checked_add( + near_diameter + .checked_mul(near_diameter) + .expect("near-diameter square fits u128"), + ) + .expect("two pair-square classes fit u128"), + ) + .expect("pair-count weighted squares fit u128"), + ) + .expect("pair numerator fits u128") +} + +#[test] +fn represented_n2050_candidate_and_midpoint_order_without_large_shift_materialization() { + const SAMPLE_COUNT: usize = 2_050; + let sample_count = u128::try_from(SAMPLE_COUNT).expect("sample count fits u128"); + let numerator = exact_pair_numerator(SAMPLE_COUNT); + let denominator = sample_count + .checked_mul(sample_count) + .and_then(|value| value.checked_mul(sample_count - 1)) + .expect("scientific denominator fits u128"); + assert_eq!(numerator, 332_306_998_946_228_931_332_463_617_650_984_961); + assert_eq!(denominator, 8_610_922_500); + + let candidate = ((numerator as f64) / (denominator as f64)).sqrt(); + assert_eq!(candidate.to_bits(), 0x4296_998e_1aff_78de); + let (candidate_significand, candidate_exponent) = + positive_dyadic(candidate).expect("candidate is positive dyadic"); + let candidate_square = candidate_significand + .checked_mul(candidate_significand) + .expect("binary64 candidate significand square fits u128"); + assert!(denominator.checked_mul(candidate_square).is_none()); + let candidate_right = Wide256::multiply_u128(denominator, candidate_square); + assert_ne!(candidate_right.high, 0, "comparison really needs more than u128"); + assert_eq!( + compare_scaled_wide( + Wide256::from_u128(numerator), + 0, + candidate_right, + candidate_exponent.checked_mul(2).expect("candidate exponent doubles"), + ), + Some(Ordering::Greater), + "exact target lies above the floating candidate square" + ); + + let neighbor = f64::from_bits(candidate.to_bits() + 1); + let (midpoint_significand, midpoint_exponent) = + midpoint_dyadic(candidate, neighbor).expect("adjacent midpoint is exact dyadic"); + let midpoint_square = midpoint_significand + .checked_mul(midpoint_significand) + .expect("midpoint significand square fits u128"); + assert!(denominator.checked_mul(midpoint_square).is_none()); + let midpoint_right = Wide256::multiply_u128(denominator, midpoint_square); + assert_ne!(midpoint_right.high, 0, "midpoint comparison exceeds u128"); + assert_eq!( + compare_scaled_wide( + Wide256::from_u128(numerator), + 0, + midpoint_right, + midpoint_exponent.checked_mul(2).expect("midpoint exponent doubles"), + ), + Some(Ordering::Less), + "exact target lies below the upward midpoint square" + ); +} + +#[test] +fn scaled_comparison_handles_extreme_exponents_without_allocating_a_power_of_two() { + let one = Wide256::from_u128(1); + let two = Wide256::from_u128(2); + let three = Wide256::from_u128(3); + + assert_eq!( + compare_scaled_wide(one, -2_148, two, -2_149), + Some(Ordering::Equal) + ); + assert_eq!( + compare_scaled_wide(one, -2_148, three, -2_149), + Some(Ordering::Less) + ); + assert_eq!( + compare_scaled_wide(three, 2_046, one, 2_047), + Some(Ordering::Greater) + ); + assert_eq!( + compare_scaled_wide(Wide256::from_u128(0), -2_148, one, 2_047), + Some(Ordering::Less) + ); + assert_eq!( + compare_scaled_wide(one, 2_047, Wide256::from_u128(0), -2_148), + Some(Ordering::Greater) + ); + assert_eq!( + compare_scaled_wide(Wide256::from_u128(0), 0, Wide256::from_u128(0), 0), + Some(Ordering::Equal) + ); +} + +#[test] +fn full_width_product_keeps_all_256_product_bits() { + let product = Wide256::multiply_u128(u128::MAX, u128::MAX); + assert_eq!(product.high, u128::MAX - 1); + assert_eq!(product.low, 1); + assert_eq!(product.bit_len(), 256); + assert!(product.bit(255)); + assert!(product.bit(0)); +} diff --git a/crates/validation_core/tests/match_count_extreme_decision_contract.rs b/crates/validation_core/tests/match_count_extreme_decision_contract.rs new file mode 100644 index 000000000..2d917701b --- /dev/null +++ b/crates/validation_core/tests/match_count_extreme_decision_contract.rs @@ -0,0 +1,21 @@ +//! Regression contract for tolerance decisions when the represented residual itself overflows. +//! +//! `match_count` must decide directly from finite represented endpoints and tolerance instead of +//! requiring `absolute_residuals` to materialize an unrepresentable magnitude. + +use validation_core::{ValidationError, absolute_residuals, match_count}; + +#[test] +fn tolerance_match_decision_does_not_require_an_unrepresentable_residual() { + let truth = [f64::MAX]; + let recovered = [-f64::MAX]; + + assert_eq!( + absolute_residuals(&truth, &recovered), + Err(ValidationError::InvalidInput) + ); + assert_eq!(match_count(&truth, &recovered, f64::MAX), Ok(0)); + assert_eq!(match_count(&truth, &recovered, 0.0), Ok(0)); + + assert_eq!(match_count(&truth, &truth, 0.0), Ok(1)); +} diff --git a/crates/validation_core/tests/monte_carlo_extreme_moments_contract.rs b/crates/validation_core/tests/monte_carlo_extreme_moments_contract.rs new file mode 100644 index 000000000..8c24caac5 --- /dev/null +++ b/crates/validation_core/tests/monte_carlo_extreme_moments_contract.rs @@ -0,0 +1,52 @@ +use validation_core::{ValidationError, summarize_replications}; + +#[test] +fn representable_extreme_symmetric_moments_survive_intermediate_overflow() { + let samples = [f64::MAX, 0.0, 0.0, -f64::MAX]; + let summary = summarize_replications(&samples, 0.0, 1.0) + .expect("representable extreme Monte Carlo moments"); + + let expected_standard_deviation = f64::MAX * (2.0_f64 / 3.0).sqrt(); + let expected_standard_error = expected_standard_deviation / 2.0; + assert_eq!(summary.mean.to_bits(), 0.0_f64.to_bits()); + assert!( + ((summary.standard_deviation - expected_standard_deviation) + / expected_standard_deviation) + .abs() + <= 2.0 * f64::EPSILON + ); + assert!( + ((summary.standard_error - expected_standard_error) / expected_standard_error).abs() + <= 2.0 * f64::EPSILON + ); + assert_eq!(summary.percentile_lower, -f64::MAX); + assert_eq!(summary.percentile_upper, f64::MAX); +} + +#[test] +fn nonzero_monte_carlo_uncertainty_cannot_collapse_to_exact_zero() { + let minimum_subnormal = f64::from_bits(1); + let samples = [ + minimum_subnormal, + -minimum_subnormal, + minimum_subnormal, + -minimum_subnormal, + minimum_subnormal, + -minimum_subnormal, + minimum_subnormal, + -minimum_subnormal, + minimum_subnormal, + -minimum_subnormal, + minimum_subnormal, + -minimum_subnormal, + minimum_subnormal, + -minimum_subnormal, + minimum_subnormal, + -minimum_subnormal, + ]; + + assert_eq!( + summarize_replications(&samples, 0.0, 1.0), + Err(ValidationError::InvalidInput) + ); +} diff --git a/crates/validation_core/tests/monte_carlo_percentile_joint_moment_support_contract.rs b/crates/validation_core/tests/monte_carlo_percentile_joint_moment_support_contract.rs new file mode 100644 index 000000000..49071dbda --- /dev/null +++ b/crates/validation_core/tests/monte_carlo_percentile_joint_moment_support_contract.rs @@ -0,0 +1,44 @@ +//! Contracts joint moment support for serialized Monte Carlo percentile evidence. +//! +//! Distinct empirical percentile endpoints consume the same finite squared-deviation budget +//! recorded by the sample standard deviation; validating each endpoint independently is not +//! sufficient. Equal endpoints may refer to one retained observation and must not be charged twice. + +use validation_core::{MonteCarloSummary, ValidationError}; + +fn summary(percentile_lower: f64, percentile_upper: f64) -> MonteCarloSummary { + MonteCarloSummary { + replication_count: 4, + mean: 1.0, + standard_deviation: 0.5, + standard_error: 0.25, + percentile_lower, + percentile_upper, + } +} + +#[test] +fn distinct_empirical_percentiles_must_share_the_recorded_deviation_budget() { + // [0.75, 0.75, 0.75, 1.75] has mean 1, sample SD 0.5, and + // nearest-rank endpoints 0.75 and 1.75, so the joint support is attainable. + let attainable = summary(0.75, 1.75); + assert!(attainable.validate().is_ok()); + assert!(serde_json::to_string(&attainable).is_ok()); + + // Each endpoint below is individually within SD * sqrt(n - 1): + // |0.25 - 1| = |1.75 - 1| = 0.75 < 0.5 * sqrt(3). + // They are nevertheless impossible together because distinct observed + // endpoints consume at least 0.75^2 + 0.75^2 = 1.125 of squared-deviation + // budget while the recorded sample SD permits only (n - 1) * SD^2 = 0.75. + let impossible = summary(0.25, 1.75); + assert_eq!(impossible.validate(), Err(ValidationError::InvalidInput)); + assert!(serde_json::to_string(&impossible).is_err()); + + let payload = r#"{"replication_count":4,"mean":1.0,"standard_deviation":0.5,"standard_error":0.25,"percentile_lower":0.25,"percentile_upper":1.75}"#; + assert!(serde_json::from_str::(payload).is_err()); + + // Equal percentile endpoints may designate the same retained observation, + // so their squared deviation must not be counted twice. + let same_rank = summary(1.75, 1.75); + assert!(same_rank.validate().is_ok()); +} diff --git a/crates/validation_core/tests/monte_carlo_percentile_moment_support_contract.rs b/crates/validation_core/tests/monte_carlo_percentile_moment_support_contract.rs new file mode 100644 index 000000000..fac121766 --- /dev/null +++ b/crates/validation_core/tests/monte_carlo_percentile_moment_support_contract.rs @@ -0,0 +1,50 @@ +use validation_core::{MonteCarloSummary, ValidationError, summarize_replications}; + +fn summary(percentile_lower: f64, percentile_upper: f64) -> MonteCarloSummary { + MonteCarloSummary { + replication_count: 4, + mean: 1.0, + standard_deviation: 0.5, + standard_error: 0.25, + percentile_lower, + percentile_upper, + } +} + +#[test] +fn empirical_percentiles_must_fit_recorded_mean_and_sample_spread() { + let attainable = summary(0.75, 1.75); + assert!(attainable.validate().is_ok()); + assert!(serde_json::to_string(&attainable).is_ok()); + + // Every nearest-rank percentile is an observed retained replication. Because + // sample SD is computed from represented-mean deviations with denominator + // n - 1, every endpoint must satisfy |x - mean| <= SD * sqrt(n - 1). + // Here the support radius is 0.5 * sqrt(3) < 1, so endpoint 2.0 is impossible. + let impossible = summary(0.75, 2.0); + assert_eq!(impossible.validate(), Err(ValidationError::InvalidInput)); + assert!(serde_json::to_string(&impossible).is_err()); + + let payload = r#"{"replication_count":4,"mean":1.0,"standard_deviation":0.5,"standard_error":0.25,"percentile_lower":0.75,"percentile_upper":2.0}"#; + assert!(serde_json::from_str::(payload).is_err()); + + // Mean projection can round between adjacent binary64 observations. The + // support check must therefore use the recorded squared-deviation identity, + // not a stronger zero-sum-deviation bound that assumes an exact real mean. + let adjacent = f64::from_bits(1.0_f64.to_bits() + 1); + let rounded_mean = summarize_replications(&[1.0, adjacent], 0.0, 1.0) + .expect("rounded mean sample remains admissible"); + assert_eq!(rounded_mean.percentile_upper.to_bits(), adjacent.to_bits()); + + // The moment-support law is generic: signed scalar summaries remain valid + // when their empirical endpoints fit the represented mean and sample spread. + let signed = MonteCarloSummary { + replication_count: 4, + mean: -1.0, + standard_deviation: 0.5, + standard_error: 0.25, + percentile_lower: -1.75, + percentile_upper: -0.75, + }; + assert!(signed.validate().is_ok()); +} diff --git a/crates/validation_core/tests/monte_carlo_standard_error_coherence_contract.rs b/crates/validation_core/tests/monte_carlo_standard_error_coherence_contract.rs new file mode 100644 index 000000000..62871fa6f --- /dev/null +++ b/crates/validation_core/tests/monte_carlo_standard_error_coherence_contract.rs @@ -0,0 +1,56 @@ +use validation_core::{MonteCarloSummary, ValidationError}; + +fn summary(replication_count: usize, standard_deviation: f64, standard_error: f64) -> MonteCarloSummary { + MonteCarloSummary { + replication_count, + mean: 0.5, + standard_deviation, + standard_error, + percentile_lower: -2.0, + percentile_upper: 3.0, + } +} + +#[test] +fn monte_carlo_summary_rejects_impossible_standard_error_evidence() { + let understated_for_n = summary(4, 0.5, 0.2); + assert_eq!( + understated_for_n.validate(), + Err(ValidationError::InvalidInput) + ); + + let canonical_standard_error = 0.5 / 4.0_f64.sqrt(); + let adjacent_standard_error = f64::from_bits(canonical_standard_error.to_bits() + 1); + assert!(summary(4, 0.5, adjacent_standard_error).validate().is_ok()); + + let equal_to_sd_with_multiple_replications = summary(4, 0.5, 0.5); + assert_eq!( + equal_to_sd_with_multiple_replications.validate(), + Err(ValidationError::InvalidInput) + ); + + let larger_than_sd = summary(4, 0.5, 1.0); + assert_eq!(larger_than_sd.validate(), Err(ValidationError::InvalidInput)); + assert!(serde_json::to_string(&larger_than_sd).is_err()); + + let false_zero_uncertainty = summary(4, 0.5, 0.0); + assert_eq!( + false_zero_uncertainty.validate(), + Err(ValidationError::InvalidInput) + ); + + let zero_spread_with_positive_uncertainty = summary(4, 0.0, 0.1); + assert_eq!( + zero_spread_with_positive_uncertainty.validate(), + Err(ValidationError::InvalidInput) + ); + + let impossible_singleton_spread = summary(1, 0.5, 0.5); + assert_eq!( + impossible_singleton_spread.validate(), + Err(ValidationError::InvalidInput) + ); + + let payload = r#"{"replication_count":4,"mean":0.5,"standard_deviation":0.5,"standard_error":0.2,"percentile_lower":-2.0,"percentile_upper":3.0}"#; + assert!(serde_json::from_str::(payload).is_err()); +} diff --git a/crates/validation_core/tests/monte_carlo_two_replication_endpoint_exhaustion_contract.rs b/crates/validation_core/tests/monte_carlo_two_replication_endpoint_exhaustion_contract.rs new file mode 100644 index 000000000..492cb2d01 --- /dev/null +++ b/crates/validation_core/tests/monte_carlo_two_replication_endpoint_exhaustion_contract.rs @@ -0,0 +1,53 @@ +use validation_core::{MonteCarloSummary, ValidationError, summarize_replications}; + +#[test] +fn distinct_percentile_endpoints_exhaust_a_two_replication_sample() { + let attainable = summarize_replications(&[-0.5, 0.5], 0.5, 1.0) + .expect("two-replication nearest-rank summary"); + assert_eq!(attainable.replication_count, 2); + assert_eq!(attainable.mean, 0.0); + assert_eq!(attainable.percentile_lower, -0.5); + assert_eq!(attainable.percentile_upper, 0.5); + assert!(attainable.validate().is_ok()); + + // With exactly two retained replications, two distinct nearest-rank endpoint + // values exhaust the sample: there are no unobserved replications left that + // could supply additional spread. These endpoint values imply sample + // SD = sqrt(0.5), so recording SD = 1.0 is scientifically impossible even + // though each endpoint separately and jointly fits the looser moment budget. + let impossible_spread = MonteCarloSummary { + replication_count: 2, + mean: 0.0, + standard_deviation: 1.0, + standard_error: 1.0 / 2.0_f64.sqrt(), + percentile_lower: -0.5, + percentile_upper: 0.5, + }; + assert_eq!( + impossible_spread.validate(), + Err(ValidationError::InvalidInput) + ); + assert!(serde_json::to_string(&impossible_spread).is_err()); + + // The same exhaustion also fixes the represented mean. This payload still + // passes the looser individual/joint endpoint-radius budget, but its mean + // cannot be the mean of the only two retained values. + let impossible_mean = MonteCarloSummary { + replication_count: 2, + mean: 0.25, + standard_deviation: 1.0, + standard_error: 1.0 / 2.0_f64.sqrt(), + percentile_lower: -0.5, + percentile_upper: 0.5, + }; + assert_eq!( + impossible_mean.validate(), + Err(ValidationError::InvalidInput) + ); + + let payload = format!( + "{{\"replication_count\":2,\"mean\":0.0,\"standard_deviation\":1.0,\"standard_error\":{},\"percentile_lower\":-0.5,\"percentile_upper\":0.5}}", + 1.0 / 2.0_f64.sqrt() + ); + assert!(serde_json::from_str::(&payload).is_err()); +} diff --git a/crates/validation_core/tests/monte_carlo_zero_spread_support_contract.rs b/crates/validation_core/tests/monte_carlo_zero_spread_support_contract.rs new file mode 100644 index 000000000..aed50cb9c --- /dev/null +++ b/crates/validation_core/tests/monte_carlo_zero_spread_support_contract.rs @@ -0,0 +1,40 @@ +use validation_core::{MonteCarloSummary, ValidationError}; + +fn summary( + replication_count: usize, + mean: f64, + percentile_lower: f64, + percentile_upper: f64, +) -> MonteCarloSummary { + MonteCarloSummary { + replication_count, + mean, + standard_deviation: 0.0, + standard_error: 0.0, + percentile_lower, + percentile_upper, + } +} + +#[test] +fn zero_sample_spread_requires_degenerate_empirical_support() { + let impossible = summary(4, 1.0, 0.5, 1.5); + assert_eq!(impossible.validate(), Err(ValidationError::InvalidInput)); + assert!(serde_json::to_string(&impossible).is_err()); + + let payload = r#"{"replication_count":4,"mean":1.0,"standard_deviation":0.0,"standard_error":0.0,"percentile_lower":0.5,"percentile_upper":1.5}"#; + assert!(serde_json::from_str::(payload).is_err()); + + let impossible_singleton = summary(1, 2.0, 1.0, 2.0); + assert_eq!( + impossible_singleton.validate(), + Err(ValidationError::InvalidInput) + ); + + let constant_signed = summary(4, -3.0, -3.0, -3.0); + assert!(constant_signed.validate().is_ok()); + assert!(serde_json::to_string(&constant_signed).is_ok()); + + let signed_zero = summary(4, -0.0, 0.0, -0.0); + assert!(signed_zero.validate().is_ok()); +} diff --git a/crates/validation_core/tests/rmse_overflow_safe_contract.rs b/crates/validation_core/tests/rmse_overflow_safe_contract.rs new file mode 100644 index 000000000..0b0d8a623 --- /dev/null +++ b/crates/validation_core/tests/rmse_overflow_safe_contract.rs @@ -0,0 +1,21 @@ +use validation_core::{rmse_standard_error, root_mean_square_error}; + +#[test] +fn representable_extreme_rmse_does_not_fail_on_squared_residual_overflow() { + let truth = [0.0, 0.0]; + let recovered = [f64::MAX, f64::MAX]; + + assert_eq!(root_mean_square_error(&truth, &recovered), Ok(f64::MAX)); + assert_eq!(rmse_standard_error(&truth, &recovered), Ok(0.0)); +} + +#[test] +fn subnormal_rmse_preserves_representable_error_and_refuses_false_zero() { + let ulp = f64::from_bits(1); + + assert_eq!(root_mean_square_error(&[0.0, 0.0], &[ulp, 0.0]), Ok(ulp)); + assert_eq!( + root_mean_square_error(&[0.0, 0.0, 0.0, 0.0], &[ulp, 0.0, 0.0, 0.0]), + Err(validation_core::ValidationError::InvalidInput) + ); +} diff --git a/crates/validation_core/tests/standard_error_acceptance_equal_nonzero_correction_projection_contract.rs b/crates/validation_core/tests/standard_error_acceptance_equal_nonzero_correction_projection_contract.rs new file mode 100644 index 000000000..33110205d --- /dev/null +++ b/crates/validation_core/tests/standard_error_acceptance_equal_nonzero_correction_projection_contract.rs @@ -0,0 +1,54 @@ +use validation_core::accept_within_standard_errors; + +#[test] +fn equal_nonzero_correction_projection_preserves_exact_rejection() { + // The direct subtraction and product both round to the same finite value. + // Their first-order correction projections also round to the same positive + // subnormal. The represented subtraction correction is exact, while the + // exact product correction is smaller than that projected subnormal, so the + // represented-input inequality is strictly false and must not be admitted as + // a rounded equality. + let estimate = f64::from_bits(0x0210_2814_5144_3c99); + let correction = f64::from_bits(0x0000_0000_6398_c737); + let target = -correction; + let k = f64::from_bits(0x20d9_5434_7757_68c7); + let standard_error = f64::from_bits(0x2124_696e_33e2_baaa); + + assert_eq!(estimate - target, k * standard_error); + assert!(!accept_within_standard_errors(estimate, target, standard_error, k) + .expect("finite represented inputs")); +} + +#[test] +fn equal_nonzero_correction_projection_preserves_exact_acceptance() { + // Companion boundary: the exact product correction lies just above the + // same projected correction carried by the represented residual. A repair + // must discriminate the exact ordering rather than reject every equal + // nonzero correction projection. + let estimate = f64::from_bits(0x01c3_f43e_c52b_4312); + let correction = f64::from_bits(0x0000_0000_003b_9da9); + let target = -correction; + let k = f64::from_bits(0x20c8_7ace_8d72_9746); + let standard_error = f64::from_bits(0x20ea_1585_cc49_24ca); + + assert_eq!(estimate - target, k * standard_error); + assert!(accept_within_standard_errors(estimate, target, standard_error, k) + .expect("finite represented inputs")); +} + +#[test] +fn equal_negative_correction_projection_preserves_exact_rejection() { + // The same projection collision also exists below the rounded value. Here + // both first-order corrections are the same negative subnormal, but the + // exact product lies slightly farther below the rounded bound than the exact + // residual does. The strict inequality therefore remains a rejection. + let estimate = f64::from_bits(0x018b_af20_e855_2bb6); + let correction = f64::from_bits(0x0000_0000_0005_d636); + let target = correction; + let k = f64::from_bits(0x2c6f_de1e_a0d4_de3d); + let standard_error = f64::from_bits(0x150b_cc8f_a576_9411); + + assert_eq!(estimate - target, k * standard_error); + assert!(!accept_within_standard_errors(estimate, target, standard_error, k) + .expect("finite represented inputs")); +} diff --git a/crates/validation_core/tests/standard_error_acceptance_finite_rounding_tie_contract.rs b/crates/validation_core/tests/standard_error_acceptance_finite_rounding_tie_contract.rs new file mode 100644 index 000000000..8f503315d --- /dev/null +++ b/crates/validation_core/tests/standard_error_acceptance_finite_rounding_tie_contract.rs @@ -0,0 +1,157 @@ +use validation_core::accept_within_standard_errors; + +#[test] +fn finite_rounded_tie_preserves_strict_rejection_from_represented_inputs() { + let estimate = 1.0_f64; + let target = 0.0_f64; + let standard_error = f64::from_bits(0x3fef_ffff_fc00_0000); + let multiplier = f64::from_bits(0x3ff0_0000_0200_0000); + + let represented_residual = estimate - target; + let rounded_bound = multiplier * standard_error; + assert_eq!(represented_residual, 1.0); + assert_eq!(rounded_bound, represented_residual); + + // The represented factors are (1 - 2^-27) and (1 + 2^-27), + // so their exact product is 1 - 2^-54: strictly below the residual. + assert_eq!( + accept_within_standard_errors(estimate, target, standard_error, multiplier), + Ok(false), + "a multiplication tie rounded to the residual must not erase the strict represented-input rejection" + ); +} + +#[test] +fn finite_rounded_tie_repair_keeps_the_adjacent_acceptance() { + let estimate = 1.0_f64; + let target = 0.0_f64; + let standard_error = f64::from_bits(0x3fef_ffff_fc00_0000); + let multiplier = f64::from_bits(0x3ff0_0000_0200_0001); + + assert_eq!( + accept_within_standard_errors(estimate, target, standard_error, multiplier), + Ok(true), + "one ULP larger multiplier is on the admissible side of the represented-input boundary" + ); +} + +#[test] +fn finite_rounded_tie_accepts_when_exact_product_is_above_the_residual() { + let estimate = 1.0_f64; + let target = 0.0_f64; + let standard_error = f64::from_bits(0x3fef_ffff_ffff_fc19); + let multiplier = f64::from_bits(0x3ff0_0000_0000_01f4); + + assert_eq!(multiplier * standard_error, 1.0); + assert_eq!( + accept_within_standard_errors(estimate, target, standard_error, multiplier), + Ok(true), + "the tie discriminator must preserve acceptance when the exact represented product lies above one" + ); +} + +#[test] +fn finite_exact_tie_remains_accepted_when_product_has_no_roundoff() { + assert_eq!( + accept_within_standard_errors(1.0, 0.0, 1.0, 1.0), + Ok(true) + ); +} + +#[test] +fn inexact_subtraction_tie_below_bound_remains_accepted() { + let target = f64::from_bits(1); + assert_eq!(1.0 - target, 1.0); + assert_eq!( + accept_within_standard_errors(1.0, target, 1.0, 1.0), + Ok(true), + "the exact represented residual is below the exact unit bound" + ); +} + +#[test] +fn inexact_subtraction_tie_above_bound_preserves_strict_rejection() { + let target = f64::from_bits(0xbc90_0000_0000_0000); // -2^-54 + let represented_residual = 1.0 - target; + assert_eq!(represented_residual, 1.0); + + // The exact represented residual is 1 + 2^-54, but the subtraction rounds + // to 1.0. The bound is exactly 1.0, so the scientific inequality is false. + assert_eq!( + accept_within_standard_errors(1.0, target, 1.0, 1.0), + Ok(false), + "subtraction rounding must not turn a strict represented-input rejection into equality" + ); +} + +#[test] +fn negative_rounded_difference_uses_absolute_residual_correction_sign() { + let target = f64::from_bits(0x3c90_0000_0000_0000); // +2^-54 + let represented_difference = -1.0 - target; + assert_eq!(represented_difference, -1.0); + + // The exact difference is -1 - 2^-54, so the absolute residual is + // 1 + 2^-54 and remains strictly above the exact unit bound. + assert_eq!( + accept_within_standard_errors(-1.0, target, 1.0, 1.0), + Ok(false), + "the subtraction low term must flip sign when the rounded difference is negative" + ); +} + +#[test] +fn subnormal_bound_rounding_must_not_hide_a_strict_rejection() { + let minimum_subnormal = f64::from_bits(1); + let multiplier = f64::from_bits(0x1e5_8000_0000_0000); // 1.5 * 2^-538 + let standard_error = f64::from_bits(0x1e6_0000_0000_0000); // 2^-537 + + assert_eq!(multiplier * standard_error, minimum_subnormal); + // The exact represented product is 3/4 of the minimum subnormal. Its FMA + // correction is only -1/4 ULP at zero and therefore rounds to signed zero. + assert_eq!(multiplier.mul_add(standard_error, -minimum_subnormal), -0.0); + + assert_eq!( + accept_within_standard_errors(minimum_subnormal, 0.0, standard_error, multiplier), + Ok(false), + "a product rounded up to the minimum subnormal must not cover the larger exact residual" + ); +} + +#[test] +fn exact_minimum_subnormal_bound_remains_accepted() { + let minimum_subnormal = f64::from_bits(1); + assert_eq!( + accept_within_standard_errors(minimum_subnormal, 0.0, minimum_subnormal, 1.0), + Ok(true), + "an exactly represented minimum-subnormal bound still covers an equal residual" + ); +} + +#[test] +fn minimum_normal_bound_rounding_must_not_hide_a_strict_rejection() { + let minimum_normal = f64::MIN_POSITIVE; + let standard_error = f64::from_bits(0x1fff_ffff_fc00_0000); // (1 - 2^-27) * 2^-511 + let multiplier = f64::from_bits(0x2000_0000_0200_0000); // (1 + 2^-27) * 2^-511 + + assert_eq!(multiplier * standard_error, minimum_normal); + // The exact represented product is (1 - 2^-54) * 2^-1022, + // one quarter of a minimum-subnormal ULP below the minimum normal. The + // multiplication rounds up, while its FMA correction itself rounds to zero. + assert_eq!(multiplier.mul_add(standard_error, -minimum_normal), -0.0); + + assert_eq!( + accept_within_standard_errors(minimum_normal, 0.0, standard_error, multiplier), + Ok(false), + "an underflowed product correction at the normal/subnormal boundary must not erase a strict rejection" + ); +} + +#[test] +fn exact_minimum_normal_bound_remains_accepted() { + let minimum_normal = f64::MIN_POSITIVE; + assert_eq!( + accept_within_standard_errors(minimum_normal, 0.0, minimum_normal, 1.0), + Ok(true), + "an exactly represented minimum-normal bound still covers an equal residual" + ); +} diff --git a/crates/validation_core/tests/standard_error_acceptance_scale_underflow_contract.rs b/crates/validation_core/tests/standard_error_acceptance_scale_underflow_contract.rs new file mode 100644 index 000000000..a9bd5368c --- /dev/null +++ b/crates/validation_core/tests/standard_error_acceptance_scale_underflow_contract.rs @@ -0,0 +1,81 @@ +use validation_core::accept_within_standard_errors; + +#[test] +fn positive_standard_error_bound_survives_scale_reduction() { + let estimate = 1.0e308_f64; + let target = f64::from_bits(estimate.to_bits() - 1); + let standard_error = 2.2e-16_f64; + let multiplier = 1.0e308_f64; + + let represented_residual = estimate - target; + let represented_bound = multiplier * standard_error; + assert!(represented_residual.is_finite()); + assert!(represented_bound.is_finite()); + assert!(represented_residual <= represented_bound); + assert_eq!(standard_error / estimate, 0.0); + + assert_eq!( + accept_within_standard_errors(estimate, target, standard_error, multiplier), + Ok(true), + "a finite positive k*SE bound must not disappear because SE/scale underflows first" + ); +} + +#[test] +fn scale_underflow_repair_does_not_accept_a_smaller_finite_bound() { + let estimate = 1.0e308_f64; + let target = f64::from_bits(estimate.to_bits() - 1); + let standard_error = 1.8e-16_f64; + let multiplier = 1.0e308_f64; + + let represented_residual = estimate - target; + let represented_bound = multiplier * standard_error; + assert!(represented_residual.is_finite()); + assert!(represented_bound.is_finite()); + assert!(represented_residual > represented_bound); + assert_eq!(standard_error / estimate, 0.0); + + assert_eq!( + accept_within_standard_errors(estimate, target, standard_error, multiplier), + Ok(false), + "restoring the finite bound must preserve a nearby rejection" + ); +} + +#[test] +fn both_overflow_fallback_does_not_round_an_exact_rejection_into_acceptance() { + let estimate = f64::from_bits(0x7fee_446c_f80d_ddbc); + let target = f64::from_bits(0xffe2_d7e3_9796_6af3); + let standard_error = f64::from_bits(0x7362_0ad2_2ddb_6f38); + let multiplier = f64::from_bits(0x4c85_c69a_c1c7_a9ed); + + assert!((estimate - target).is_infinite()); + assert!((multiplier * standard_error).is_infinite()); + + let scale = estimate.abs().max(target.abs()).max(standard_error).max(1.0); + let predecessor_scaled_error = (estimate / scale) - (target / scale); + let predecessor_scaled_bound = multiplier * (standard_error / scale); + assert_eq!(predecessor_scaled_error, predecessor_scaled_bound); + + assert_eq!( + accept_within_standard_errors(estimate, target, standard_error, multiplier), + Ok(false), + "the both-overflow fallback must preserve the exact represented-input inequality instead of accepting a normalization tie" + ); +} + +#[test] +fn both_overflow_fallback_accepts_the_adjacent_multiplier_that_crosses_the_boundary() { + let estimate = f64::from_bits(0x7fee_446c_f80d_ddbc); + let target = f64::from_bits(0xffe2_d7e3_9796_6af3); + let standard_error = f64::from_bits(0x7362_0ad2_2ddb_6f38); + let multiplier = f64::from_bits(0x4c85_c69a_c1c7_a9ee); + + assert!((estimate - target).is_infinite()); + assert!((multiplier * standard_error).is_infinite()); + assert_eq!( + accept_within_standard_errors(estimate, target, standard_error, multiplier), + Ok(true), + "one ULP larger multiplier is on the admissible side of the represented-input boundary" + ); +} diff --git a/crates/validation_core/tests/standard_error_acceptance_zero_multiplier_contract.rs b/crates/validation_core/tests/standard_error_acceptance_zero_multiplier_contract.rs new file mode 100644 index 000000000..682f548a8 --- /dev/null +++ b/crates/validation_core/tests/standard_error_acceptance_zero_multiplier_contract.rs @@ -0,0 +1,36 @@ +use validation_core::accept_within_standard_errors; + +#[test] +fn zero_multiplier_requires_exact_recovery_before_scale_reduction() { + let minimum_subnormal = f64::from_bits(1); + + assert_eq!( + accept_within_standard_errors(minimum_subnormal, 0.0, f64::MAX, 0.0), + Ok(false), + "k = 0 is an exact-recovery gate; scaling by a huge SE must not erase a nonzero residual" + ); + assert_eq!( + accept_within_standard_errors(-minimum_subnormal, 0.0, f64::MAX, 0.0), + Ok(false), + "the negative mirror must remain a nonzero residual" + ); + assert_eq!( + accept_within_standard_errors(0.0, 0.0, f64::MAX, 0.0), + Ok(true), + "exact equality remains accepted when k = 0" + ); +} + +#[test] +fn exact_recovery_treats_signed_zero_as_one_numeric_value() { + assert_eq!( + accept_within_standard_errors(-0.0, 0.0, f64::MAX, 0.0), + Ok(true), + "zero multiplier must use numeric equality, not signed-zero bit identity" + ); + assert_eq!( + accept_within_standard_errors(0.0, -0.0, 0.0, 1.0), + Ok(true), + "zero-SE exact recovery must not split +0.0 and -0.0 into different scientific values" + ); +} diff --git a/crates/validation_core/tests/validation_evidence_v1_coverage_provenance_contract.rs b/crates/validation_core/tests/validation_evidence_v1_coverage_provenance_contract.rs new file mode 100644 index 000000000..f1271217d --- /dev/null +++ b/crates/validation_core/tests/validation_evidence_v1_coverage_provenance_contract.rs @@ -0,0 +1,86 @@ +use validation_core::{ + ValidationError, ValidationEvidenceV1, ValidationReport, WilsonCoverageEvidenceV1, +}; + +fn canonical_coverage() -> WilsonCoverageEvidenceV1 { + let truth = [0.0, 1.0, 2.0, 3.0]; + let lower = [-0.5, 0.5, 1.0, 4.0]; + let upper = [0.5, 1.5, 2.5, 5.0]; + WilsonCoverageEvidenceV1::from_intervals(&truth, &lower, &upper, 1.96) + .expect("coverage evidence") +} + +fn canonical_report(coverage: WilsonCoverageEvidenceV1) -> ValidationReport { + ValidationReport { + study_label: "wilson-provenance".into(), + rmse: 0.1, + rmse_standard_error: 0.01, + mean_bias: 0.0, + bias_standard_error: 0.02, + interval_coverage: coverage.empirical_coverage, + coverage_wilson_lower: coverage.wilson_lower, + coverage_wilson_upper: coverage.wilson_upper, + temporal_order_accuracy: 1.0, + monte_carlo_rmse: None, + } +} + +#[test] +fn validation_evidence_v1_round_trips_report_and_wilson_provenance() { + let coverage = canonical_coverage(); + let report = canonical_report(coverage); + let evidence = ValidationEvidenceV1::new(report, coverage).expect("validation evidence"); + + let json = evidence.to_json().expect("json"); + assert!(json.contains("\"schema\":\"tepp.validation_evidence.v1\"")); + assert!(json.contains("\"sample_count\":4")); + assert!(json.contains("\"covered_count\":3")); + assert!(json.contains("\"interval_sidedness\":\"two_sided\"")); + + let decoded: ValidationEvidenceV1 = serde_json::from_str(&json).expect("decode"); + assert_eq!(decoded, evidence); +} + +#[test] +fn report_projection_must_match_the_versioned_coverage_evidence() { + let coverage = canonical_coverage(); + let mut report = canonical_report(coverage); + report.interval_coverage = 0.5; + assert_eq!( + ValidationEvidenceV1::new(report, coverage), + Err(ValidationError::InvalidInput) + ); + + let coverage = canonical_coverage(); + let mut report = canonical_report(coverage); + report.coverage_wilson_lower = 0.0; + assert_eq!( + ValidationEvidenceV1::new(report, coverage), + Err(ValidationError::InvalidInput) + ); + + let coverage = canonical_coverage(); + let mut report = canonical_report(coverage); + report.coverage_wilson_upper = 1.0; + assert_eq!( + ValidationEvidenceV1::new(report, coverage), + Err(ValidationError::InvalidInput) + ); +} + +#[test] +fn serde_rejects_schema_drift_and_nested_provenance_tampering() { + let coverage = canonical_coverage(); + let evidence = ValidationEvidenceV1::new(canonical_report(coverage), coverage) + .expect("validation evidence"); + let json = evidence.to_json().expect("json"); + + let wrong_schema = json.replace("tepp.validation_evidence.v1", "tepp.validation_evidence.v2"); + assert!(serde_json::from_str::(&wrong_schema).is_err()); + + let wrong_count = json.replacen("\"sample_count\":4", "\"sample_count\":5", 1); + assert!(serde_json::from_str::(&wrong_count).is_err()); + + let unknown_field = json.replacen('{', "{\"scientific_authority\":true,", 1); + assert!(serde_json::from_str::(&unknown_field).is_err()); +} diff --git a/crates/validation_core/tests/validation_report_rmse_standard_error_upper_bound_contract.rs b/crates/validation_core/tests/validation_report_rmse_standard_error_upper_bound_contract.rs new file mode 100644 index 000000000..f8f62ac4c --- /dev/null +++ b/crates/validation_core/tests/validation_report_rmse_standard_error_upper_bound_contract.rs @@ -0,0 +1,43 @@ +use validation_core::{ + ValidationError, ValidationReport, rmse_standard_error, root_mean_square_error, +}; + +fn report_with(rmse: f64, rmse_standard_error: f64) -> ValidationReport { + ValidationReport { + study_label: "rmse-se-support".into(), + rmse, + rmse_standard_error, + mean_bias: 0.0, + bias_standard_error: 0.0, + interval_coverage: 0.5, + coverage_wilson_lower: 0.2, + coverage_wilson_upper: 0.8, + temporal_order_accuracy: 1.0, + monte_carlo_rmse: None, + } +} + +#[test] +fn report_rejects_rmse_standard_error_above_squared_residual_support_bound() { + // For x_i = r_i^2 >= 0 with sample SD in the crate's delta-method producer, + // sd(x) <= sqrt(n) * mean(x), hence SE(RMSE) <= RMSE / 2. + // Two residuals [0, 1] attain the mathematical boundary. + let truth = [0.0, 0.0]; + let recovered = [0.0, 1.0]; + let rmse = root_mean_square_error(&truth, &recovered).expect("rmse"); + let rmse_se = rmse_standard_error(&truth, &recovered).expect("rmse se"); + let canonical = report_with(rmse, rmse_se); + assert!(canonical.validate().is_ok()); + assert!(rmse_se / rmse <= 0.5 + 64.0 * f64::EPSILON); + + let impossible = report_with(0.2, 0.11); + assert_eq!(impossible.validate(), Err(ValidationError::InvalidInput)); + assert_eq!(impossible.to_json(), Err(ValidationError::InvalidInput)); + assert_eq!( + impossible.to_human_summary(), + Err(ValidationError::InvalidInput) + ); + + let ingress = r#"{"study_label":"rmse-se-support","rmse":0.2,"rmse_standard_error":0.11,"mean_bias":0.0,"bias_standard_error":0.0,"interval_coverage":0.5,"coverage_wilson_lower":0.2,"coverage_wilson_upper":0.8,"temporal_order_accuracy":1.0,"monte_carlo_rmse":null}"#; + assert!(serde_json::from_str::(ingress).is_err()); +} diff --git a/crates/validation_core/tests/validation_report_rmse_summary_domain_contract.rs b/crates/validation_core/tests/validation_report_rmse_summary_domain_contract.rs new file mode 100644 index 000000000..b2731b584 --- /dev/null +++ b/crates/validation_core/tests/validation_report_rmse_summary_domain_contract.rs @@ -0,0 +1,68 @@ +use validation_core::{MonteCarloSummary, ValidationError, ValidationReport}; + +fn report_with_rmse_summary(summary: MonteCarloSummary) -> ValidationReport { + ValidationReport { + study_label: "rmse-domain-contract".into(), + rmse: 0.2, + rmse_standard_error: 0.01, + mean_bias: 0.0, + bias_standard_error: 0.01, + interval_coverage: 0.5, + coverage_wilson_lower: 0.2, + coverage_wilson_upper: 0.8, + temporal_order_accuracy: 0.9, + monte_carlo_rmse: Some(summary), + } +} + +#[test] +fn validation_report_rejects_negative_monte_carlo_rmse_evidence() { + let negative_mean = report_with_rmse_summary(MonteCarloSummary { + replication_count: 20, + mean: -0.1, + standard_deviation: 0.02, + standard_error: 0.02 / 20.0_f64.sqrt(), + percentile_lower: -0.14, + percentile_upper: -0.06, + }); + assert_eq!(negative_mean.validate(), Err(ValidationError::InvalidInput)); + assert_eq!(negative_mean.to_json(), Err(ValidationError::InvalidInput)); + assert_eq!( + negative_mean.to_human_summary(), + Err(ValidationError::InvalidInput) + ); + + let negative_percentile = report_with_rmse_summary(MonteCarloSummary { + replication_count: 20, + mean: 0.02, + standard_deviation: 0.03, + standard_error: 0.03 / 20.0_f64.sqrt(), + percentile_lower: -0.01, + percentile_upper: 0.07, + }); + assert_eq!( + negative_percentile.validate(), + Err(ValidationError::InvalidInput) + ); + + let serialized = r#"{ + "study_label":"rmse-domain-contract", + "rmse":0.2, + "rmse_standard_error":0.01, + "mean_bias":0.0, + "bias_standard_error":0.01, + "interval_coverage":0.5, + "coverage_wilson_lower":0.2, + "coverage_wilson_upper":0.8, + "temporal_order_accuracy":0.9, + "monte_carlo_rmse":{ + "replication_count":20, + "mean":-0.1, + "standard_deviation":0.02, + "standard_error":0.004472135954999579, + "percentile_lower":-0.14, + "percentile_upper":-0.06 + } + }"#; + assert!(serde_json::from_str::(serialized).is_err()); +} diff --git a/crates/validation_core/tests/validation_report_rmse_summary_nonnegative_support_contract.rs b/crates/validation_core/tests/validation_report_rmse_summary_nonnegative_support_contract.rs new file mode 100644 index 000000000..0594909d2 --- /dev/null +++ b/crates/validation_core/tests/validation_report_rmse_summary_nonnegative_support_contract.rs @@ -0,0 +1,63 @@ +use validation_core::{MonteCarloSummary, ValidationError, ValidationReport}; + +fn report_with(summary: MonteCarloSummary) -> ValidationReport { + ValidationReport { + study_label: "rmse-summary-nonnegative-support".into(), + rmse: 0.2, + rmse_standard_error: 0.05, + mean_bias: 0.0, + bias_standard_error: 0.0, + interval_coverage: 0.5, + coverage_wilson_lower: 0.2, + coverage_wilson_upper: 0.8, + temporal_order_accuracy: 1.0, + monte_carlo_rmse: Some(summary), + } +} + +#[test] +fn monte_carlo_rmse_rejects_spread_impossible_for_nonnegative_replications() { + // For nonnegative replication metrics x_i with sample mean m, + // sample SD is at most sqrt(n) * m. Equality occurs when one replication + // carries the entire finite sum and the remaining n-1 replications are zero. + // Therefore SE(mean) = SD / sqrt(n) cannot exceed m. + let impossible_rmse_summary = MonteCarloSummary { + replication_count: 4, + mean: 1.0, + standard_deviation: 3.0, + standard_error: 1.5, + percentile_lower: 0.0, + percentile_upper: 4.0, + }; + + // The generic carrier is intentionally sign-neutral because it also serves + // signed metrics such as bias. The stronger support belongs only to the + // typed monte_carlo_rmse slot. + assert!(impossible_rmse_summary.validate().is_ok()); + + let report = report_with(impossible_rmse_summary); + assert_eq!(report.validate(), Err(ValidationError::InvalidInput)); + assert_eq!(report.to_json(), Err(ValidationError::InvalidInput)); + assert_eq!( + report.to_human_summary(), + Err(ValidationError::InvalidInput) + ); + + let payload = r#"{\"study_label\":\"rmse-summary-nonnegative-support\",\"rmse\":0.2,\"rmse_standard_error\":0.05,\"mean_bias\":0.0,\"bias_standard_error\":0.0,\"interval_coverage\":0.5,\"coverage_wilson_lower\":0.2,\"coverage_wilson_upper\":0.8,\"temporal_order_accuracy\":1.0,\"monte_carlo_rmse\":{\"replication_count\":4,\"mean\":1.0,\"standard_deviation\":3.0,\"standard_error\":1.5,\"percentile_lower\":0.0,\"percentile_upper\":4.0}}"#; + assert!(serde_json::from_str::(payload).is_err()); +} + +#[test] +fn monte_carlo_rmse_accepts_attainable_nonnegative_support_boundary() { + let boundary = MonteCarloSummary { + replication_count: 4, + mean: 1.0, + standard_deviation: 2.0, + standard_error: 1.0, + percentile_lower: 0.0, + percentile_upper: 4.0, + }; + + assert!(boundary.validate().is_ok()); + assert!(report_with(boundary).validate().is_ok()); +} diff --git a/crates/validation_core/tests/validation_report_rmse_summary_percentile_support_contract.rs b/crates/validation_core/tests/validation_report_rmse_summary_percentile_support_contract.rs new file mode 100644 index 000000000..8576c28c7 --- /dev/null +++ b/crates/validation_core/tests/validation_report_rmse_summary_percentile_support_contract.rs @@ -0,0 +1,66 @@ +use validation_core::{MonteCarloSummary, ValidationError, ValidationReport}; + +fn report_with(summary: MonteCarloSummary) -> ValidationReport { + ValidationReport { + study_label: "rmse-summary-percentile-support".into(), + rmse: 0.2, + rmse_standard_error: 0.05, + mean_bias: 0.0, + bias_standard_error: 0.0, + interval_coverage: 0.5, + coverage_wilson_lower: 0.2, + coverage_wilson_upper: 0.8, + temporal_order_accuracy: 1.0, + monte_carlo_rmse: Some(summary), + } +} + +#[test] +fn monte_carlo_rmse_rejects_percentile_above_nonnegative_sample_sum_support() { + // For n nonnegative RMSE replications with represented mean m, every retained + // replication is bounded by the finite sample sum n*m. Any empirical nearest-rank + // percentile is one of those retained values and therefore cannot exceed n*m. + // The generic signed carrier's moment support still admits this fixture. + let impossible_rmse_summary = MonteCarloSummary { + replication_count: 4, + mean: 1.0, + standard_deviation: 2.0, + standard_error: 1.0, + percentile_lower: 0.0, + percentile_upper: 4.1, + }; + + // The generic carrier cannot impose the nonnegative sample-sum bound because + // it also summarizes signed metrics. The stronger support belongs to the typed + // RMSE slot. + assert!(impossible_rmse_summary.validate().is_ok()); + + let report = report_with(impossible_rmse_summary); + assert_eq!(report.validate(), Err(ValidationError::InvalidInput)); + assert_eq!(report.to_json(), Err(ValidationError::InvalidInput)); + assert_eq!( + report.to_human_summary(), + Err(ValidationError::InvalidInput) + ); + + let payload = r#"{"study_label":"rmse-summary-percentile-support","rmse":0.2,"rmse_standard_error":0.05,"mean_bias":0.0,"bias_standard_error":0.0,"interval_coverage":0.5,"coverage_wilson_lower":0.2,"coverage_wilson_upper":0.8,"temporal_order_accuracy":1.0,"monte_carlo_rmse":{"replication_count":4,"mean":1.0,"standard_deviation":2.0,"standard_error":1.0,"percentile_lower":0.0,"percentile_upper":4.1}}"#; + assert!(serde_json::from_str::(payload).is_err()); +} + +#[test] +fn monte_carlo_rmse_accepts_empirical_percentile_at_nonnegative_sum_boundary() { + // [0, 0, 0, 4] attains max(x_i) = n*mean and also the existing SD/SE support + // boundary, so the endpoint must remain admissible rather than being tightened + // by an arbitrary heuristic. + let attainable_boundary = MonteCarloSummary { + replication_count: 4, + mean: 1.0, + standard_deviation: 2.0, + standard_error: 1.0, + percentile_lower: 0.0, + percentile_upper: 4.0, + }; + + assert!(attainable_boundary.validate().is_ok()); + assert!(report_with(attainable_boundary).validate().is_ok()); +} diff --git a/crates/validation_core/tests/validation_report_scientific_invariants_contract.rs b/crates/validation_core/tests/validation_report_scientific_invariants_contract.rs new file mode 100644 index 000000000..eb3ecd552 --- /dev/null +++ b/crates/validation_core/tests/validation_report_scientific_invariants_contract.rs @@ -0,0 +1,107 @@ +use validation_core::{MonteCarloSummary, ValidationError, ValidationReport}; + +fn valid_report() -> ValidationReport { + ValidationReport { + study_label: "validation-report-contract".into(), + rmse: 0.1, + rmse_standard_error: 0.01, + mean_bias: -0.02, + bias_standard_error: 0.02, + interval_coverage: 0.5, + coverage_wilson_lower: 0.2, + coverage_wilson_upper: 0.8, + temporal_order_accuracy: 0.75, + monte_carlo_rmse: None, + } +} + +#[test] +fn validation_report_rejects_impossible_metric_domains() { + let mut report = valid_report(); + report.rmse = -0.1; + assert_eq!(report.validate(), Err(ValidationError::InvalidInput)); + + let mut report = valid_report(); + report.rmse_standard_error = -0.01; + assert_eq!(report.validate(), Err(ValidationError::InvalidInput)); + + let mut report = valid_report(); + report.bias_standard_error = -0.01; + assert_eq!(report.validate(), Err(ValidationError::InvalidInput)); + + for invalid_coverage in [-0.01, 1.01] { + let mut report = valid_report(); + report.interval_coverage = invalid_coverage; + assert_eq!(report.validate(), Err(ValidationError::InvalidInput)); + } + + for invalid_accuracy in [-0.01, 1.01] { + let mut report = valid_report(); + report.temporal_order_accuracy = invalid_accuracy; + assert_eq!(report.validate(), Err(ValidationError::InvalidInput)); + } +} + +#[test] +fn validation_report_rejects_incoherent_wilson_evidence() { + let mut report = valid_report(); + report.coverage_wilson_lower = -0.01; + assert_eq!(report.validate(), Err(ValidationError::InvalidInput)); + + let mut report = valid_report(); + report.coverage_wilson_upper = 1.01; + assert_eq!(report.validate(), Err(ValidationError::InvalidInput)); + + let mut report = valid_report(); + report.coverage_wilson_lower = 0.95; + report.coverage_wilson_upper = 0.90; + assert_eq!(report.validate(), Err(ValidationError::InvalidInput)); + + let mut report = valid_report(); + report.coverage_wilson_lower = 0.81; + assert_eq!(report.validate(), Err(ValidationError::InvalidInput)); + + let mut report = valid_report(); + report.coverage_wilson_upper = 0.49; + assert_eq!(report.validate(), Err(ValidationError::InvalidInput)); +} + +#[test] +fn every_report_projection_enforces_validation() { + let mut report = valid_report(); + report.interval_coverage = 1.5; + assert_eq!(report.to_json(), Err(ValidationError::InvalidInput)); + assert!(serde_json::to_string(&report).is_err()); + assert_eq!( + report.to_human_summary(), + Err(ValidationError::InvalidInput) + ); + + let impossible = r#"{ + "study_label":"invalid-wire-report", + "rmse":0.1, + "rmse_standard_error":0.01, + "mean_bias":0.0, + "bias_standard_error":0.01, + "interval_coverage":1.5, + "coverage_wilson_lower":0.2, + "coverage_wilson_upper":0.8, + "temporal_order_accuracy":0.75, + "monte_carlo_rmse":null + }"#; + assert!(serde_json::from_str::(impossible).is_err()); +} + +#[test] +fn monte_carlo_summary_direct_serialization_preserves_its_validation_contract() { + let impossible = MonteCarloSummary { + replication_count: 2, + mean: 0.0, + standard_deviation: -0.1, + standard_error: 0.0, + percentile_lower: 0.0, + percentile_upper: 1.0, + }; + assert_eq!(impossible.validate(), Err(ValidationError::InvalidInput)); + assert!(serde_json::to_string(&impossible).is_err()); +} diff --git a/crates/validation_core/tests/validation_report_wilson_all_covered_positive_lower_contract.rs b/crates/validation_core/tests/validation_report_wilson_all_covered_positive_lower_contract.rs new file mode 100644 index 000000000..f30f20b59 --- /dev/null +++ b/crates/validation_core/tests/validation_report_wilson_all_covered_positive_lower_contract.rs @@ -0,0 +1,60 @@ +use validation_core::{ValidationError, ValidationReport, wilson_coverage_interval}; + +fn all_covered_report(wilson_lower: f64) -> ValidationReport { + ValidationReport { + study_label: "wilson-all-covered-positive-lower".into(), + rmse: 0.2, + rmse_standard_error: 0.05, + mean_bias: 0.0, + bias_standard_error: 0.01, + interval_coverage: 1.0, + coverage_wilson_lower: wilson_lower, + coverage_wilson_upper: 1.0, + temporal_order_accuracy: 1.0, + monte_carlo_rmse: None, + } +} + +#[test] +fn canonical_all_covered_wilson_lower_remains_strictly_positive() { + let truth = [0.0]; + let lower = [-1.0]; + let upper = [1.0]; + let (wilson_lower, wilson_upper) = + wilson_coverage_interval(&truth, &lower, &upper, 1.0e154).expect("wilson"); + + assert!(wilson_lower > 0.0); + assert_eq!(wilson_upper, 1.0); + assert_eq!(all_covered_report(wilson_lower).validate(), Ok(())); +} + +#[test] +fn zero_lower_all_covered_wilson_artifact_fails_closed() { + // For p = 1 the canonical producer returns n / (n + z^2). With a non-empty + // sample, finite z^2, and n >= 1, that represented lower endpoint is always + // strictly positive. A stored [0, 1] pair therefore cannot come from the + // producer even though it is ordered and contains the empirical coverage. + for impossible_lower in [0.0, -0.0] { + let report = all_covered_report(impossible_lower); + assert_eq!(report.validate(), Err(ValidationError::InvalidInput)); + assert_eq!(report.to_json(), Err(ValidationError::InvalidInput)); + assert_eq!( + report.to_human_summary(), + Err(ValidationError::InvalidInput) + ); + } + + let raw = r#"{ + "study_label":"wilson-all-covered-positive-lower", + "rmse":0.2, + "rmse_standard_error":0.05, + "mean_bias":0.0, + "bias_standard_error":0.01, + "interval_coverage":1.0, + "coverage_wilson_lower":0.0, + "coverage_wilson_upper":1.0, + "temporal_order_accuracy":1.0, + "monte_carlo_rmse":null + }"#; + assert!(serde_json::from_str::(raw).is_err()); +} diff --git a/crates/validation_core/tests/validation_report_wilson_boundary_pair_coherence_contract.rs b/crates/validation_core/tests/validation_report_wilson_boundary_pair_coherence_contract.rs new file mode 100644 index 000000000..349797cd8 --- /dev/null +++ b/crates/validation_core/tests/validation_report_wilson_boundary_pair_coherence_contract.rs @@ -0,0 +1,73 @@ +//! Wilson-pair boundary regression for representable peer roots. + +use validation_core::{ValidationError, ValidationReport}; + +const COVERAGE_ONE_IN_ONE_HUNDRED_MILLION: f64 = f64::from_bits(0x3e45_798e_e230_8c3a); +const PRODUCER_LOWER: f64 = f64::from_bits(0x3c9c_d2b2_8e2c_a873); +const PRODUCER_UPPER: f64 = f64::from_bits(0x3fe0_0000_055e_63b8); +const COMPLEMENT_COVERAGE: f64 = f64::from_bits(0x3fef_ffff_faa1_9c47); +const COMPLEMENT_LOWER: f64 = f64::from_bits(0x3fdf_ffff_f543_3890); +const MINIMUM_U64_COVERAGE: f64 = f64::from_bits(0x3bf0_0000_0000_0000); + +fn report_with_wilson_pair(coverage: f64, lower: f64, upper: f64) -> ValidationReport { + ValidationReport { + study_label: "wilson-boundary-pair".to_owned(), + rmse: 1.0, + rmse_standard_error: 0.0, + mean_bias: 0.0, + bias_standard_error: 0.0, + interval_coverage: coverage, + coverage_wilson_lower: lower, + coverage_wilson_upper: upper, + temporal_order_accuracy: 1.0, + monte_carlo_rmse: None, + } +} + +/// Rejects a zero lower endpoint when the peer endpoint implies a representable positive root. +#[test] +fn zero_lower_cannot_hide_representable_wilson_peer_root() { + let artifact = report_with_wilson_pair( + COVERAGE_ONE_IN_ONE_HUNDRED_MILLION, + 0.0, + PRODUCER_UPPER, + ); + + assert_eq!(artifact.validate(), Err(ValidationError::InvalidInput)); + assert_eq!(artifact.to_json(), Err(ValidationError::InvalidInput)); + + let raw = format!( + r#"{{"study_label":"wilson-boundary-pair","rmse":1.0,"rmse_standard_error":0.0,"mean_bias":0.0,"bias_standard_error":0.0,"interval_coverage":{coverage},"coverage_wilson_lower":0.0,"coverage_wilson_upper":{upper},"temporal_order_accuracy":1.0,"monte_carlo_rmse":null}}"#, + coverage = COVERAGE_ONE_IN_ONE_HUNDRED_MILLION, + upper = PRODUCER_UPPER, + ); + assert!(serde_json::from_str::(&raw).is_err()); +} + +/// Applies the same peer-root admission rule to the complement-symmetric exact-one upper boundary. +#[test] +fn exact_one_upper_cannot_hide_representable_uncovered_peer_root() { + let artifact = report_with_wilson_pair(COMPLEMENT_COVERAGE, COMPLEMENT_LOWER, 1.0); + + assert_eq!(artifact.validate(), Err(ValidationError::InvalidInput)); +} + +/// Preserves the actual rounded Wilson pair for `n = 100_000_000`, one cover, and `z = 10_000`. +#[test] +fn producer_pair_remains_admissible_at_the_same_coverage() { + let artifact = report_with_wilson_pair( + COVERAGE_ONE_IN_ONE_HUNDRED_MILLION, + PRODUCER_LOWER, + PRODUCER_UPPER, + ); + + assert!(artifact.validate().is_ok()); +} + +/// Keeps an unresolved extreme boundary admissible when the peer-root reconstruction also rounds to zero. +#[test] +fn rounded_zero_and_one_pair_remains_admissible_when_peer_root_is_unrepresentable() { + let artifact = report_with_wilson_pair(MINIMUM_U64_COVERAGE, 0.0, 1.0); + + assert!(artifact.validate().is_ok()); +} diff --git a/crates/validation_core/tests/validation_report_wilson_pair_coherence_contract.rs b/crates/validation_core/tests/validation_report_wilson_pair_coherence_contract.rs new file mode 100644 index 000000000..d98313cd3 --- /dev/null +++ b/crates/validation_core/tests/validation_report_wilson_pair_coherence_contract.rs @@ -0,0 +1,88 @@ +use validation_core::{ + ValidationError, ValidationReport, interval_coverage, wilson_coverage_interval, +}; + +fn base_report() -> ValidationReport { + ValidationReport { + study_label: "wilson-pair-coherence".into(), + rmse: 0.2, + rmse_standard_error: 0.05, + mean_bias: 0.0, + bias_standard_error: 0.01, + interval_coverage: 0.5, + coverage_wilson_lower: 0.2, + coverage_wilson_upper: 0.9, + temporal_order_accuracy: 1.0, + monte_carlo_rmse: None, + } +} + +#[test] +fn canonical_wilson_pair_remains_admissible() { + let truth = [0.0, 1.0, 2.0, 3.0]; + let lower = [-1.0, 0.0, 3.0, 4.0]; + let upper = [1.0, 2.0, 4.0, 5.0]; + let coverage = interval_coverage(&truth, &lower, &upper).expect("coverage"); + assert_eq!(coverage, 0.5); + let (wilson_lower, wilson_upper) = + wilson_coverage_interval(&truth, &lower, &upper, 1.96).expect("wilson"); + + let report = ValidationReport { + interval_coverage: coverage, + coverage_wilson_lower: wilson_lower, + coverage_wilson_upper: wilson_upper, + ..base_report() + }; + + assert_eq!(report.validate(), Ok(())); + assert!(report.to_json().is_ok()); + assert!(report.to_human_summary().is_ok()); +} + +#[test] +fn complementary_identity_accepts_non_symmetric_canonical_pair() { + let truth = [0.0, 1.0, 2.0, 3.0]; + let lower = [-1.0, 2.0, 3.0, 4.0]; + let upper = [1.0, 3.0, 4.0, 5.0]; + let coverage = interval_coverage(&truth, &lower, &upper).expect("coverage"); + assert_eq!(coverage, 0.25); + let (wilson_lower, wilson_upper) = + wilson_coverage_interval(&truth, &lower, &upper, 1.96).expect("wilson"); + + let report = ValidationReport { + interval_coverage: coverage, + coverage_wilson_lower: wilson_lower, + coverage_wilson_upper: wilson_upper, + ..base_report() + }; + assert_eq!(report.validate(), Ok(())); +} + +#[test] +fn impossible_wilson_pair_fails_closed_across_report_boundaries() { + // At p = 0.5 every Wilson score interval is symmetric about 0.5 for every + // finite positive z and non-empty denominator. [0.2, 0.9] contains p but + // cannot be emitted by the canonical Wilson producer for that proportion. + let report = base_report(); + + assert_eq!(report.validate(), Err(ValidationError::InvalidInput)); + assert_eq!(report.to_json(), Err(ValidationError::InvalidInput)); + assert_eq!( + report.to_human_summary(), + Err(ValidationError::InvalidInput) + ); + + let raw = r#"{ + "study_label":"wilson-pair-coherence", + "rmse":0.2, + "rmse_standard_error":0.05, + "mean_bias":0.0, + "bias_standard_error":0.01, + "interval_coverage":0.5, + "coverage_wilson_lower":0.2, + "coverage_wilson_upper":0.9, + "temporal_order_accuracy":1.0, + "monte_carlo_rmse":null + }"#; + assert!(serde_json::from_str::(raw).is_err()); +} diff --git a/crates/validation_core/tests/validation_report_zero_mean_rmse_summary_contract.rs b/crates/validation_core/tests/validation_report_zero_mean_rmse_summary_contract.rs new file mode 100644 index 000000000..13c430e05 --- /dev/null +++ b/crates/validation_core/tests/validation_report_zero_mean_rmse_summary_contract.rs @@ -0,0 +1,70 @@ +use validation_core::{MonteCarloSummary, ValidationError, ValidationReport}; + +fn report_with(summary: MonteCarloSummary) -> ValidationReport { + ValidationReport { + study_label: "zero-mean-rmse-summary".into(), + rmse: 0.0, + rmse_standard_error: 0.0, + mean_bias: 0.0, + bias_standard_error: 0.0, + interval_coverage: 1.0, + coverage_wilson_lower: 0.5, + coverage_wilson_upper: 1.0, + temporal_order_accuracy: 1.0, + monte_carlo_rmse: Some(summary), + } +} + +#[test] +fn zero_mean_monte_carlo_rmse_requires_zero_spread_and_zero_percentiles() { + let positive_spread = MonteCarloSummary { + replication_count: 4, + mean: 0.0, + standard_deviation: 1.0, + standard_error: 0.5, + percentile_lower: 0.0, + percentile_upper: 1.0, + }; + assert!(positive_spread.validate().is_ok()); + let report = report_with(positive_spread); + assert_eq!(report.validate(), Err(ValidationError::InvalidInput)); + assert_eq!(report.to_json(), Err(ValidationError::InvalidInput)); + assert_eq!( + report.to_human_summary(), + Err(ValidationError::InvalidInput) + ); + + // Zero sample spread with non-degenerate percentile support is already + // impossible for the generic summary carrier; the typed RMSE boundary must + // not weaken that upstream invariant. + let positive_percentile = MonteCarloSummary { + replication_count: 4, + mean: 0.0, + standard_deviation: 0.0, + standard_error: 0.0, + percentile_lower: 0.0, + percentile_upper: 1.0, + }; + assert_eq!( + positive_percentile.validate(), + Err(ValidationError::InvalidInput) + ); + assert_eq!( + report_with(positive_percentile).validate(), + Err(ValidationError::InvalidInput) + ); + + let perfect_recovery = MonteCarloSummary { + replication_count: 4, + mean: -0.0, + standard_deviation: 0.0, + standard_error: -0.0, + percentile_lower: -0.0, + percentile_upper: 0.0, + }; + assert!(perfect_recovery.validate().is_ok()); + assert!(report_with(perfect_recovery).validate().is_ok()); + + let payload = r#"{\"study_label\":\"zero-mean-rmse-summary\",\"rmse\":0.0,\"rmse_standard_error\":0.0,\"mean_bias\":0.0,\"bias_standard_error\":0.0,\"interval_coverage\":1.0,\"coverage_wilson_lower\":0.5,\"coverage_wilson_upper\":1.0,\"temporal_order_accuracy\":1.0,\"monte_carlo_rmse\":{\"replication_count\":4,\"mean\":0.0,\"standard_deviation\":1.0,\"standard_error\":0.5,\"percentile_lower\":0.0,\"percentile_upper\":1.0}}"#; + assert!(serde_json::from_str::(payload).is_err()); +} diff --git a/crates/validation_core/tests/validation_report_zero_rmse_standard_error_contract.rs b/crates/validation_core/tests/validation_report_zero_rmse_standard_error_contract.rs new file mode 100644 index 000000000..e6ef00b3f --- /dev/null +++ b/crates/validation_core/tests/validation_report_zero_rmse_standard_error_contract.rs @@ -0,0 +1,40 @@ +use validation_core::{ + ValidationError, ValidationReport, rmse_standard_error, root_mean_square_error, +}; + +fn report_with(rmse: f64, rmse_standard_error: f64) -> ValidationReport { + ValidationReport { + study_label: "zero-rmse-standard-error".into(), + rmse, + rmse_standard_error, + mean_bias: 0.0, + bias_standard_error: 0.0, + interval_coverage: 1.0, + coverage_wilson_lower: 0.5, + coverage_wilson_upper: 1.0, + temporal_order_accuracy: 1.0, + monte_carlo_rmse: None, + } +} + +#[test] +fn exact_zero_rmse_requires_exact_zero_rmse_standard_error() { + let truth = [1.0, -2.0, 3.0]; + let recovered = truth; + assert_eq!(root_mean_square_error(&truth, &recovered), Ok(0.0)); + assert_eq!(rmse_standard_error(&truth, &recovered), Ok(0.0)); + + let impossible = report_with(0.0, 0.1); + assert_eq!(impossible.validate(), Err(ValidationError::InvalidInput)); + assert_eq!(impossible.to_json(), Err(ValidationError::InvalidInput)); + assert_eq!( + impossible.to_human_summary(), + Err(ValidationError::InvalidInput) + ); + + let signed_zero = report_with(-0.0, -0.0); + assert!(signed_zero.validate().is_ok()); + + let payload = r#"{"study_label":"zero-rmse-standard-error","rmse":0.0,"rmse_standard_error":0.1,"mean_bias":0.0,"bias_standard_error":0.0,"interval_coverage":1.0,"coverage_wilson_lower":0.5,"coverage_wilson_upper":1.0,"temporal_order_accuracy":1.0,"monte_carlo_rmse":null}"#; + assert!(serde_json::from_str::(payload).is_err()); +} diff --git a/crates/validation_core/tests/wilson_all_covered_exact_count_extreme_scale_rounding_contract.rs b/crates/validation_core/tests/wilson_all_covered_exact_count_extreme_scale_rounding_contract.rs new file mode 100644 index 000000000..468380a86 --- /dev/null +++ b/crates/validation_core/tests/wilson_all_covered_exact_count_extreme_scale_rounding_contract.rs @@ -0,0 +1,26 @@ +//! Regression contract for all-covered Wilson evidence at an extreme finite critical value. +//! +//! A finite-count correction below the final binary64 half-ULP must not force the lower endpoint +//! away from the correctly rounded represented-input quotient. + +use validation_core::wilson_coverage_interval; + +#[test] +fn exact_count_all_covered_does_not_overcorrect_below_final_ulp_resolution() { + // n=3 and this finite binary64 z produce z^2 = 0x1.0000000000001p+985. + // The exact represented-input endpoint 3 / (3 + z^2) rounds to the same + // binary64 value as 3 / z^2 because the finite-count correction is below + // the final quotient's half-ULP. Residual compensation must therefore keep + // the already-correct direct quotient instead of forcing a one-ULP step. + let z = f64::from_bits(0x5eb6_a09e_667f_3bcd); + assert_eq!((z * z).to_bits(), 0x7d80_0000_0000_0001); + + let truth = [0.0; 3]; + let lower_bounds = [-1.0; 3]; + let upper_bounds = [1.0; 3]; + let (lower, upper) = wilson_coverage_interval(&truth, &lower_bounds, &upper_bounds, z) + .expect("finite positive z with three covered intervals must produce Wilson evidence"); + + assert_eq!(lower.to_bits(), 0x0277_ffff_ffff_ffff); + assert_eq!(upper, 1.0); +} diff --git a/crates/validation_core/tests/wilson_all_covered_exact_count_large_z_rounding_contract.rs b/crates/validation_core/tests/wilson_all_covered_exact_count_large_z_rounding_contract.rs new file mode 100644 index 000000000..9fa382d37 --- /dev/null +++ b/crates/validation_core/tests/wilson_all_covered_exact_count_large_z_rounding_contract.rs @@ -0,0 +1,40 @@ +use validation_core::wilson_coverage_interval; + +#[test] +fn exact_count_all_covered_preserves_large_z_denominator_residual() { + // This z is the binary64 value 0x1.fffffffffffffp+29. Squaring it yields + // 0x1.ffffffffffffep+59. For n=3, adding the exact sample count to z^2 + // rounds back to z^2, but the exact represented-input Wilson endpoint + // 3 / (3 + z^2) rounds one ULP below the quotient formed from that rounded + // denominator. The finite sample-count contribution must not disappear. + let z = f64::from_bits(0x41cf_ffff_ffff_ffff); + assert_eq!((z * z).to_bits(), 0x43af_ffff_ffff_fffe); + + let truth = [0.0; 3]; + let lower_bounds = [-1.0; 3]; + let upper_bounds = [1.0; 3]; + let (lower, upper) = wilson_coverage_interval(&truth, &lower_bounds, &upper_bounds, z) + .expect("three covered intervals with finite positive z must produce Wilson evidence"); + + assert_eq!(lower.to_bits(), 0x3c48_0000_0000_0001); + assert_eq!(upper, 1.0); +} + +#[test] +fn exact_count_all_covered_keeps_correct_power_of_two_large_z_rounding() { + // At z=2^30 the sample count is likewise fully absorbed by z^2 when the + // denominator is formed, but the ordinary quotient already lands on the + // correctly rounded represented-input endpoint. Residual compensation must + // preserve that value rather than forcing a one-ULP adjustment. + let z = f64::from_bits(0x41d0_0000_0000_0000); + assert_eq!((z * z).to_bits(), 0x43b0_0000_0000_0000); + + let truth = [0.0; 3]; + let lower_bounds = [-1.0; 3]; + let upper_bounds = [1.0; 3]; + let (lower, upper) = wilson_coverage_interval(&truth, &lower_bounds, &upper_bounds, z) + .expect("large finite power-of-two z must remain valid Wilson evidence"); + + assert_eq!(lower.to_bits(), 0x3c48_0000_0000_0000); + assert_eq!(upper, 1.0); +} diff --git a/crates/validation_core/tests/wilson_all_covered_exact_count_partial_denominator_rounding_contract.rs b/crates/validation_core/tests/wilson_all_covered_exact_count_partial_denominator_rounding_contract.rs new file mode 100644 index 000000000..331298d0e --- /dev/null +++ b/crates/validation_core/tests/wilson_all_covered_exact_count_partial_denominator_rounding_contract.rs @@ -0,0 +1,42 @@ +use validation_core::wilson_coverage_interval; + +#[test] +fn exact_count_all_covered_preserves_partial_denominator_residual() { + // z = 3 * 2^-28 is exactly representable. Squaring yields 9 * 2^-56, + // while 1 + z^2 rounds upward to 1 + 2^-52. Dividing by that rounded + // denominator lands two ULPs below one, even though the correctly rounded + // represented-input Wilson endpoint 1 / (1 + 9 * 2^-56) is next_down(1). + // The exact sample-count contribution is not fully absorbed, so the earlier + // complete-absorption repair cannot recover this ordinary inexact sum. + let z = f64::from_bits(0x3e48_0000_0000_0000); + assert_eq!((z * z).to_bits(), 0x3ca2_0000_0000_0000); + assert_eq!((1.0 + z * z).to_bits(), 0x3ff0_0000_0000_0001); + + let truth = [0.0]; + let lower_bounds = [-1.0]; + let upper_bounds = [1.0]; + let (lower, upper) = wilson_coverage_interval(&truth, &lower_bounds, &upper_bounds, z) + .expect("one covered interval with finite positive z must produce Wilson evidence"); + + assert_eq!(lower.to_bits(), 0x3fef_ffff_ffff_ffff); + assert_eq!(upper, 1.0); +} + +#[test] +fn exact_count_partial_denominator_compensation_keeps_correct_direct_rounding() { + // z = 3 * 2^-27 also makes 1 + z^2 inexact, but the direct quotient is + // already the correctly rounded represented-input endpoint. Compensation + // must preserve that value rather than mechanically moving every inexact sum. + let z = f64::from_bits(0x3e58_0000_0000_0000); + assert_eq!((z * z).to_bits(), 0x3cc2_0000_0000_0000); + assert_eq!((1.0 + z * z).to_bits(), 0x3ff0_0000_0000_0002); + + let truth = [0.0]; + let lower_bounds = [-1.0]; + let upper_bounds = [1.0]; + let (lower, upper) = wilson_coverage_interval(&truth, &lower_bounds, &upper_bounds, z) + .expect("one covered interval with finite positive z must produce Wilson evidence"); + + assert_eq!(lower.to_bits(), 0x3fef_ffff_ffff_fffc); + assert_eq!(upper, 1.0); +} diff --git a/crates/validation_core/tests/wilson_all_covered_exact_count_small_z_rounding_contract.rs b/crates/validation_core/tests/wilson_all_covered_exact_count_small_z_rounding_contract.rs new file mode 100644 index 000000000..539b7e978 --- /dev/null +++ b/crates/validation_core/tests/wilson_all_covered_exact_count_small_z_rounding_contract.rs @@ -0,0 +1,36 @@ +//! Regression contracts for all-covered Wilson intervals at exact-count small-z rounding boundaries. + +use validation_core::wilson_coverage_interval; + +#[test] +fn exact_count_all_covered_preserves_representable_small_z_uncertainty() { + // This z is the binary64 value 0x1.0000000000001p-27. Squaring it yields + // 0x1.0000000000002p-54. For n=1, the exact represented-input Wilson lower + // endpoint 1 / (1 + z^2) rounds to next_down(1.0), not to exact 1.0. + // Forming 1 + z^2 first rounds that denominator to 1.0 and erases the + // representable finite-sample uncertainty. + let z = f64::from_bits(0x3e40_0000_0000_0001); + assert_eq!((z * z).to_bits(), 0x3c90_0000_0000_0002); + + let (lower, upper) = wilson_coverage_interval(&[0.0], &[-1.0], &[1.0], z) + .expect("one covered interval with finite positive z must produce Wilson evidence"); + + assert_eq!(lower.to_bits(), 0x3fef_ffff_ffff_ffff); + assert_eq!(upper, 1.0); +} + +#[test] +fn exact_count_all_covered_keeps_unrepresentable_tiny_uncertainty_at_one() { + // At z=2^-28, z^2=2^-56. The exact lower endpoint differs from one by less + // than half of the binary64 spacing immediately below one, so exact 1.0 is + // the correctly rounded represented endpoint. The boundary repair must not + // manufacture an uncertainty value that binary64 cannot represent. + let z = f64::from_bits(0x3e30_0000_0000_0000); + assert_eq!((z * z).to_bits(), 0x3c70_0000_0000_0000); + + let (lower, upper) = wilson_coverage_interval(&[0.0], &[-1.0], &[1.0], z) + .expect("finite positive z below the representable miss threshold remains valid"); + + assert_eq!(lower, 1.0); + assert_eq!(upper, 1.0); +} diff --git a/crates/validation_core/tests/wilson_all_covered_extreme_z_contract.rs b/crates/validation_core/tests/wilson_all_covered_extreme_z_contract.rs new file mode 100644 index 000000000..119c9816e --- /dev/null +++ b/crates/validation_core/tests/wilson_all_covered_extreme_z_contract.rs @@ -0,0 +1,23 @@ +//! Locks the all-covered Wilson lower endpoint under a finite extreme critical value. + +use validation_core::wilson_coverage_interval; + +#[test] +fn all_covered_wilson_lower_bound_survives_finite_extreme_z() { + let truth = [0.0]; + let lower = [-1.0]; + let upper = [1.0]; + let z = 1.0e154_f64; + + let z_squared = z * z; + assert!(z_squared.is_finite()); + let expected_lower = 1.0 / (1.0 + z_squared); + assert!(expected_lower.is_finite()); + assert!(expected_lower > 0.0); + + let (actual_lower, actual_upper) = + wilson_coverage_interval(&truth, &lower, &upper, z).expect("finite Wilson interval"); + + assert_eq!(actual_lower.to_bits(), expected_lower.to_bits()); + assert_eq!(actual_upper, 1.0); +} diff --git a/crates/validation_core/tests/wilson_all_covered_inexact_count_extreme_z_contract.rs b/crates/validation_core/tests/wilson_all_covered_inexact_count_extreme_z_contract.rs new file mode 100644 index 000000000..923186007 --- /dev/null +++ b/crates/validation_core/tests/wilson_all_covered_inexact_count_extreme_z_contract.rs @@ -0,0 +1,59 @@ +//! Contracts all-covered Wilson evidence when durable counts exceed exact binary64 integers. +//! +//! A u64 sample count above 2^53 must retain its exact provenance while endpoint arithmetic +//! remains correctly rounded at extreme critical-value scales. The contract prevents +//! complementary-mass cancellation from erasing a positive lower endpoint and prevents +//! subnormal squared-z scales from inventing non-representable uncertainty. + +use validation_core::WilsonCoverageEvidenceV1; + +#[test] +fn inexact_durable_count_preserves_positive_all_covered_lower_at_extreme_z() { + // `sample_count = 2^53 + 1` is exact durable u64 provenance but is not + // representable as binary64. With z = 1e20, the canonical all-covered + // Wilson lower endpoint is n / (n + z^2) = 0x1.16c262777579dp-80. + // Computing the complementary miss mass first rounds it to 1.0 and then + // `1.0 - miss_mass` erases this ordinary representable positive endpoint. + let json = r#"{ + "schema":"tepp.wilson_coverage_evidence.v1", + "sample_count":9007199254740993, + "covered_count":9007199254740993, + "critical_value_kind":"standard_normal_z", + "interval_sidedness":"two_sided", + "normal_critical_value":1e20, + "empirical_coverage":1.0, + "wilson_lower":9.007199254740993e-25, + "wilson_upper":1.0 + }"#; + + let evidence: WilsonCoverageEvidenceV1 = serde_json::from_str(json) + .expect("exact durable sample count must preserve representable Wilson uncertainty"); + + assert_eq!(evidence.wilson_lower.to_bits(), 0x3af1_6c26_2777_579d); + assert!(evidence.wilson_lower > 0.0); + assert_eq!(evidence.wilson_upper, 1.0); +} + +#[test] +fn inexact_durable_count_handles_subnormal_squared_critical_value_scale() { + // z=1e-160 squares to a positive binary64 subnormal. Dividing that scale + // by n > 2^53 is below binary64 range, so the all-covered endpoint rounds + // to one rather than inventing a non-representable miss mass. + let json = r#"{ + "schema":"tepp.wilson_coverage_evidence.v1", + "sample_count":9007199254740993, + "covered_count":9007199254740993, + "critical_value_kind":"standard_normal_z", + "interval_sidedness":"two_sided", + "normal_critical_value":1e-160, + "empirical_coverage":1.0, + "wilson_lower":1.0, + "wilson_upper":1.0 + }"#; + + let evidence: WilsonCoverageEvidenceV1 = serde_json::from_str(json) + .expect("subnormal z-squared scale must remain a valid deterministic boundary case"); + + assert_eq!(evidence.wilson_lower, 1.0); + assert_eq!(evidence.wilson_upper, 1.0); +} diff --git a/crates/validation_core/tests/wilson_all_uncovered_upper_endpoint_contract.rs b/crates/validation_core/tests/wilson_all_uncovered_upper_endpoint_contract.rs new file mode 100644 index 000000000..2f57be054 --- /dev/null +++ b/crates/validation_core/tests/wilson_all_uncovered_upper_endpoint_contract.rs @@ -0,0 +1,43 @@ +use validation_core::wilson_coverage_interval; + +#[test] +fn all_uncovered_wilson_upper_bound_does_not_round_to_false_one() { + let truth = [0.0, 1.0]; + let lower = [1.0, 2.0]; + let upper = [2.0, 3.0]; + let z = 134_217_728.0_f64; // 2^27, so z^2 / n = 2^53 exactly for n = 2. + + let z_squared = z * z; + assert_eq!(z_squared, 18_014_398_509_481_984.0_f64); // 2^54. + + // For p-hat = 0, the Wilson upper endpoint is z^2 / (n + z^2). + // At this represented input its correctly rounded binary64 value is the + // immediate predecessor of 1.0, not the exact endpoint 1.0. + let expected_upper = f64::from_bits(1.0_f64.to_bits() - 1); + + let (actual_lower, actual_upper) = + wilson_coverage_interval(&truth, &lower, &upper, z).expect("finite Wilson interval"); + + assert_eq!(actual_lower, 0.0); + assert_eq!(actual_upper.to_bits(), expected_upper.to_bits()); +} + +#[test] +fn strict_interior_wilson_upper_bound_does_not_round_to_false_one() { + let truth = [0.0, 1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0]; + let lower = [-1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0]; + let upper = [1.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0, 9.0]; + let z = 268_435_456.0_f64; // 2^28. + + // Exactly one of eight replications is covered. At this represented z, + // the exact Wilson upper endpoint still rounds to next_down(1.0), while + // the direct center-plus-margin evaluation rounds the numerator and + // denominator to the same binary64 value and produces false exact 1.0. + let expected_upper = f64::from_bits(1.0_f64.to_bits() - 1); + + let (actual_lower, actual_upper) = + wilson_coverage_interval(&truth, &lower, &upper, z).expect("finite Wilson interval"); + + assert!(actual_lower > 0.0); + assert_eq!(actual_upper.to_bits(), expected_upper.to_bits()); +} diff --git a/crates/validation_core/tests/wilson_coverage_count_precision_contract.rs b/crates/validation_core/tests/wilson_coverage_count_precision_contract.rs new file mode 100644 index 000000000..9e7bf37ba --- /dev/null +++ b/crates/validation_core/tests/wilson_coverage_count_precision_contract.rs @@ -0,0 +1,34 @@ +use validation_core::WilsonCoverageEvidenceV1; + +#[test] +fn durable_counts_do_not_collapse_one_uncovered_case_to_all_covered() { + // n = 2^53 + 1 and k = 2^53 are distinct integer counts, but converting each + // count independently to binary64 rounds both to 2^53. The durable count + // provenance must therefore form the represented proportion from the small + // uncovered complement rather than erase the one observed miss. + let json = r#"{ + "schema":"tepp.wilson_coverage_evidence.v1", + "sample_count":9007199254740993, + "covered_count":9007199254740992, + "critical_value_kind":"standard_normal_z", + "interval_sidedness":"two_sided", + "normal_critical_value":1.96, + "empirical_coverage":0.9999999999999999, + "wilson_lower":0.9999999999999993, + "wilson_upper":1.0 + }"#; + + let evidence: WilsonCoverageEvidenceV1 = + serde_json::from_str(json).expect("large-count Wilson evidence must remain reproducible"); + + assert_eq!(evidence.sample_count as u64, 9_007_199_254_740_993); + assert_eq!(evidence.covered_count as u64, 9_007_199_254_740_992); + assert_eq!(evidence.empirical_coverage.to_bits(), 0x3fef_ffff_ffff_ffff); + assert_eq!(evidence.wilson_lower.to_bits(), 0x3fef_ffff_ffff_fffa); + assert_eq!(evidence.wilson_upper, 1.0); + + let round_trip = evidence.to_json().expect("validated durable evidence"); + let decoded: WilsonCoverageEvidenceV1 = + serde_json::from_str(&round_trip).expect("round-trip durable evidence"); + assert_eq!(decoded, evidence); +} diff --git a/crates/validation_core/tests/wilson_coverage_evidence_v1_contract.rs b/crates/validation_core/tests/wilson_coverage_evidence_v1_contract.rs new file mode 100644 index 000000000..d216bd060 --- /dev/null +++ b/crates/validation_core/tests/wilson_coverage_evidence_v1_contract.rs @@ -0,0 +1,139 @@ +//! Contracts the versioned Wilson coverage-evidence envelope used for validation receipts. +//! +//! The v1 schema binds sample and covered counts, the configured standard-normal +//! critical value, empirical coverage, and both Wilson endpoints. Deserialization +//! and re-validation must fail closed when those fields disagree, when unsupported +//! semantics are introduced, or when count/probability domains are impossible. + +use validation_core::{ValidationError, WilsonCoverageEvidenceV1}; + +fn canonical_evidence() -> WilsonCoverageEvidenceV1 { + let truth = [0.0, 1.0, 2.0, 3.0]; + let lower = [-0.5, 0.5, 1.5, 4.0]; + let upper = [0.5, 1.5, 2.5, 5.0]; + WilsonCoverageEvidenceV1::from_intervals(&truth, &lower, &upper, 1.96) + .expect("canonical Wilson coverage evidence") +} + +#[test] +fn versioned_wilson_coverage_evidence_round_trips_denominator_and_critical_value() { + let evidence = canonical_evidence(); + assert_eq!(evidence.sample_count, 4); + assert_eq!(evidence.covered_count, 3); + assert_eq!(evidence.empirical_coverage, 0.75); + assert_eq!(evidence.normal_critical_value, 1.96); + assert!(evidence.wilson_lower <= evidence.empirical_coverage); + assert!(evidence.empirical_coverage <= evidence.wilson_upper); + + let json = evidence.to_json().expect("canonical json"); + assert!(json.contains("\"schema\":\"tepp.wilson_coverage_evidence.v1\"")); + assert!(json.contains("\"critical_value_kind\":\"standard_normal_z\"")); + assert!(json.contains("\"interval_sidedness\":\"two_sided\"")); + + let decoded: WilsonCoverageEvidenceV1 = serde_json::from_str(&json).expect("decode"); + assert_eq!(decoded, evidence); +} + +#[test] +fn tampered_denominator_critical_value_or_endpoint_fails_closed() { + let evidence = canonical_evidence(); + + let mut wrong_denominator = evidence; + wrong_denominator.sample_count = 5; + assert_eq!( + wrong_denominator.validate(), + Err(ValidationError::InvalidInput) + ); + assert!(wrong_denominator.to_json().is_err()); + assert!(serde_json::to_string(&wrong_denominator).is_err()); + + let mut wrong_critical_value = evidence; + wrong_critical_value.normal_critical_value = 2.576; + assert_eq!( + wrong_critical_value.validate(), + Err(ValidationError::InvalidInput) + ); + + let mut wrong_lower = evidence; + wrong_lower.wilson_lower /= 2.0; + assert_eq!(wrong_lower.validate(), Err(ValidationError::InvalidInput)); + + let mut wrong_upper = evidence; + wrong_upper.wilson_upper = (wrong_upper.wilson_upper + 1.0) / 2.0; + assert_eq!(wrong_upper.validate(), Err(ValidationError::InvalidInput)); + + let mut wrong_coverage = evidence; + wrong_coverage.empirical_coverage = 0.5; + assert_eq!( + wrong_coverage.validate(), + Err(ValidationError::InvalidInput) + ); +} + +#[test] +fn serde_requires_version_critical_value_scale_and_two_sided_semantics() { + let json = canonical_evidence().to_json().expect("canonical json"); + + let wrong_schema = json.replace( + "tepp.wilson_coverage_evidence.v1", + "tepp.wilson_coverage_evidence.v2", + ); + assert!(serde_json::from_str::(&wrong_schema).is_err()); + + let wrong_kind = json.replace("standard_normal_z", "student_t"); + assert!(serde_json::from_str::(&wrong_kind).is_err()); + + let wrong_sidedness = json.replace("two_sided", "one_sided"); + assert!(serde_json::from_str::(&wrong_sidedness).is_err()); + + let unknown_field = json.replacen('{', "{\"confidence_level\":0.95,", 1); + assert!(serde_json::from_str::(&unknown_field).is_err()); +} + +#[test] +fn impossible_counts_and_numeric_domains_fail_closed() { + let evidence = canonical_evidence(); + + let mut zero_count = evidence; + zero_count.sample_count = 0; + assert_eq!(zero_count.validate(), Err(ValidationError::InvalidInput)); + + let mut impossible_counts = evidence; + impossible_counts.covered_count = impossible_counts.sample_count + 1; + assert_eq!( + impossible_counts.validate(), + Err(ValidationError::InvalidInput) + ); + + for invalid_z in [0.0, -1.0, f64::NAN, f64::INFINITY, 1e200] { + let mut invalid = evidence; + invalid.normal_critical_value = invalid_z; + assert_eq!(invalid.validate(), Err(ValidationError::InvalidInput)); + } + + for invalid_probability in [-0.1, 1.1, f64::NAN, f64::INFINITY] { + let mut invalid = evidence; + invalid.empirical_coverage = invalid_probability; + assert_eq!(invalid.validate(), Err(ValidationError::InvalidInput)); + } +} + +#[test] +fn constructor_preserves_existing_input_and_configuration_error_contracts() { + assert_eq!( + WilsonCoverageEvidenceV1::from_intervals(&[], &[], &[], 1.96), + Err(ValidationError::InvalidInput) + ); + assert_eq!( + WilsonCoverageEvidenceV1::from_intervals(&[0.0], &[1.0], &[0.0], 1.96), + Err(ValidationError::InvalidInput) + ); + assert_eq!( + WilsonCoverageEvidenceV1::from_intervals(&[0.0], &[-1.0], &[1.0], 0.0), + Err(ValidationError::InvalidConfiguration) + ); + assert_eq!( + WilsonCoverageEvidenceV1::from_intervals(&[0.0], &[-1.0], &[1.0], 1e200), + Err(ValidationError::InvalidConfiguration) + ); +} diff --git a/crates/validation_core/tests/wilson_coverage_ratio_rounding_contract.rs b/crates/validation_core/tests/wilson_coverage_ratio_rounding_contract.rs new file mode 100644 index 000000000..6fbf42d5e --- /dev/null +++ b/crates/validation_core/tests/wilson_coverage_ratio_rounding_contract.rs @@ -0,0 +1,43 @@ +//! Regression contract for exact-count Wilson coverage ratio rounding. +//! +//! Durable integer provenance must determine both the empirical coverage ratio and +//! the Wilson projection before binary64 rounding, including counts beyond the exact +//! integer range of a standalone `f64` conversion. + +use validation_core::WilsonCoverageEvidenceV1; + +#[test] +fn durable_coverage_ratio_is_correctly_rounded_before_wilson_projection() { + // These integer counts are exact provenance. Their quotient lies one binary64 + // ULP below the result obtained by independently rounding both integers to + // f64 before division: + // k/n = 4_503_599_627_370_396 / 9_007_199_254_740_993 + // -> 0x1.fffffffffff37p-2 (round-to-nearest, ties-to-even) + // while `(k as f64) / (n as f64)` becomes 0x1.fffffffffff38p-2. + // The same exact denominator provenance also matters inside the Wilson + // projection: evaluating through exact 1/n moves the lower endpoint two ULPs + // below the predecessor fixture that first repaired only the coverage ratio. + let json = r#"{ + "schema":"tepp.wilson_coverage_evidence.v1", + "sample_count":9007199254740993, + "covered_count":4503599627370396, + "critical_value_kind":"standard_normal_z", + "interval_sidedness":"two_sided", + "normal_critical_value":1.96, + "empirical_coverage":0.49999999999998884, + "wilson_lower":0.49999998967401094, + "wilson_upper":0.5000000103259667 + }"#; + + let evidence: WilsonCoverageEvidenceV1 = serde_json::from_str(json) + .expect("exact count provenance must determine coverage and Wilson projection"); + + assert_eq!(evidence.empirical_coverage.to_bits(), 0x3fdf_ffff_ffff_ff37); + assert_eq!(evidence.wilson_lower.to_bits(), 0x3fdf_ffff_f4e9_9d1e); + assert_eq!(evidence.wilson_upper.to_bits(), 0x3fe0_0000_058b_30a8); + + let round_trip = evidence.to_json().expect("validated durable evidence"); + let decoded: WilsonCoverageEvidenceV1 = + serde_json::from_str(&round_trip).expect("round-trip durable evidence"); + assert_eq!(decoded, evidence); +} diff --git a/crates/validation_core/tests/wilson_interior_cancellation_residual_contract.rs b/crates/validation_core/tests/wilson_interior_cancellation_residual_contract.rs new file mode 100644 index 000000000..bb687a74a --- /dev/null +++ b/crates/validation_core/tests/wilson_interior_cancellation_residual_contract.rs @@ -0,0 +1,57 @@ +use validation_core::wilson_coverage_interval; + +#[test] +fn interior_wilson_lower_bound_does_not_accept_a_roundoff_residual_as_signal() { + let truth = [0.0, 1.0, 2.0]; + let lower = [-1.0, 2.0, 3.0]; + let upper = [1.0, 3.0, 4.0]; + let z = 1.0e11_f64; + + let n = 3.0_f64; + let p = 1.0 / 3.0; + let z_squared = z * z; + assert!(z_squared.is_finite()); + + // The rationalized Wilson root is the same estimand without subtracting + // nearly equal O(z²) terms. At this finite z the predecessor subtraction + // leaves a nonzero floating-point residue, so an exact-zero fallback alone + // cannot detect the cancellation error. + let expected_lower = (2.0 * n * p * p / z_squared) + / (1.0 + + 2.0 * n * p / z_squared + + (1.0 + 4.0 * n * p * (1.0 - p) / z_squared).sqrt()); + assert!(expected_lower.is_finite()); + assert!(expected_lower > 0.0); + + let (actual_lower, actual_upper) = + wilson_coverage_interval(&truth, &lower, &upper, z).expect("finite Wilson interval"); + + assert_eq!(actual_lower.to_bits(), expected_lower.to_bits()); + assert!(actual_upper >= p); + assert!(actual_upper <= 1.0); +} + +#[test] +fn rationalized_lower_root_preserves_small_z_without_dividing_by_tiny_z_squared() { + let truth = [0.0, 1.0, 2.0]; + let lower = [-1.0, 2.0, 3.0]; + let upper = [1.0, 3.0, 4.0]; + let z = 0.5_f64; + + let n = 3.0_f64; + let p = 1.0 / 3.0; + let z_squared = z * z; + assert!(z_squared < 1.0); + + let expected_lower = (2.0 * n * p * p) + / (z_squared + + 2.0 * n * p + + z * (z_squared + 4.0 * n * p * (1.0 - p)).sqrt()); + + let (actual_lower, actual_upper) = + wilson_coverage_interval(&truth, &lower, &upper, z).expect("finite Wilson interval"); + + assert_eq!(actual_lower.to_bits(), expected_lower.to_bits()); + assert!(actual_upper >= p); + assert!(actual_upper <= 1.0); +} diff --git a/crates/validation_core/tests/wilson_interior_extreme_z_contract.rs b/crates/validation_core/tests/wilson_interior_extreme_z_contract.rs new file mode 100644 index 000000000..d2240f77e --- /dev/null +++ b/crates/validation_core/tests/wilson_interior_extreme_z_contract.rs @@ -0,0 +1,30 @@ +use validation_core::wilson_coverage_interval; + +#[test] +fn interior_wilson_lower_bound_survives_finite_extreme_z() { + let truth = [0.0, 1.0]; + let lower = [-1.0, 2.0]; + let upper = [1.0, 3.0]; + let z = 1.0e154_f64; + + let n = 2.0_f64; + let p = 0.5_f64; + let z_squared = z * z; + assert!(z_squared.is_finite()); + + // Algebraically rationalized Wilson lower endpoint. This form avoids the + // nearly-equal center-minus-margin subtraction used by the predecessor. + let expected_lower = (2.0 * n * p * p / z_squared) + / (1.0 + + 2.0 * n * p / z_squared + + (1.0 + 4.0 * n * p * (1.0 - p) / z_squared).sqrt()); + assert!(expected_lower.is_finite()); + assert!(expected_lower > 0.0); + + let (actual_lower, actual_upper) = + wilson_coverage_interval(&truth, &lower, &upper, z).expect("finite Wilson interval"); + + assert_eq!(actual_lower.to_bits(), expected_lower.to_bits()); + assert!(actual_upper >= p); + assert!(actual_upper <= 1.0); +} diff --git a/crates/validation_core/tests/wilson_sample_count_rounding_contract.rs b/crates/validation_core/tests/wilson_sample_count_rounding_contract.rs new file mode 100644 index 000000000..96d9b7ca7 --- /dev/null +++ b/crates/validation_core/tests/wilson_sample_count_rounding_contract.rs @@ -0,0 +1,64 @@ +//! Durable Wilson coverage evidence retains exact integer sample-count provenance. +//! +//! Counts above 2^53 are not exactly representable in binary64. These contracts +//! require Wilson endpoint reconstruction to use the versioned `u64` counts rather +//! than a pre-rounded floating denominator, including all-covered cases whose +//! positive miss mass remains representable immediately below one. + +use validation_core::WilsonCoverageEvidenceV1; + +#[test] +fn durable_wilson_endpoint_does_not_pre_round_exact_sample_count() { + // The durable carrier owns exact u64 counts. This denominator is not exactly + // representable in binary64: `n as f64` rounds 9_007_199_254_740_1013 down + // to 9_007_199_254_740_1012. The exact integer ratio k/n still rounds to the + // ordinary finite coverage value below, but pre-rounding n inside the Wilson + // formula moves the lower endpoint one ULP upward. + // + // Evaluating the same Wilson score formula at the exact integer n and the + // correctly rounded represented k/n gives lower = 0x1.2492482c43beap-3. + let json = r#"{ + "schema":"tepp.wilson_coverage_evidence.v1", + "sample_count":9007199254741013, + "covered_count":1286742750677287, + "critical_value_kind":"standard_normal_z", + "interval_sidedness":"two_sided", + "normal_critical_value":1.96, + "empirical_coverage":0.1428571428571428, + "wilson_lower":0.14285713563046382, + "wilson_upper":0.14285715008382208 + }"#; + + let evidence: WilsonCoverageEvidenceV1 = serde_json::from_str(json) + .expect("exact sample-count provenance must determine the Wilson endpoint"); + + assert_eq!(evidence.empirical_coverage.to_bits(), 0x3fc2_4924_9249_2490); + assert_eq!(evidence.wilson_lower.to_bits(), 0x3fc2_4924_82c4_3bea); + assert_eq!(evidence.wilson_upper.to_bits(), 0x3fc2_4924_a1ce_0d41); +} + +#[test] +fn durable_all_covered_lower_does_not_round_exact_sample_size_away() { + // At n = 2^55 + 3 the exact all-covered Wilson lower endpoint for z = 1.96 + // rounds to next_down(1.0). Materializing n as f64 first, or adding z^2/n + // to 1.0 before inversion, rounds the uncertainty away and returns exact 1. + // The complement form 1 - (z^2/n)/(1 + z^2/n) preserves the representable + // nonzero miss mass for this exact count provenance. + let json = r#"{ + "schema":"tepp.wilson_coverage_evidence.v1", + "sample_count":36028797018963971, + "covered_count":36028797018963971, + "critical_value_kind":"standard_normal_z", + "interval_sidedness":"two_sided", + "normal_critical_value":1.96, + "empirical_coverage":1.0, + "wilson_lower":0.9999999999999999, + "wilson_upper":1.0 + }"#; + + let evidence: WilsonCoverageEvidenceV1 = serde_json::from_str(json) + .expect("all-covered exact count provenance must retain Wilson uncertainty"); + + assert_eq!(evidence.wilson_lower.to_bits(), 1.0_f64.to_bits() - 1); + assert_eq!(evidence.wilson_upper, 1.0); +} diff --git a/docs/OPERABILITY.md b/docs/OPERABILITY.md index 3a8291634..b257b7a06 100644 --- a/docs/OPERABILITY.md +++ b/docs/OPERABILITY.md @@ -1,7 +1,7 @@ # TEPP Operability, Recovery, and Release Guide **Status:** Accepted target operating baseline with current maturity explicit. -**Last reviewed:** 2026-08-13 +**Last reviewed:** 2026-09-07 TEPP is still an implementation-stage research/product platform. Protected main currently contains the Rust workspace/evidence foundation plus implemented-main temporal primitives (merged PRs #8 and #9). Superseded PRs #5 and #6 are historical lineage only. Database adapters are partial; model fitting, GPU, services, visual analytics, and production deployment are later targets. This guide defines the operating evidence those stages must satisfy rather than claiming they already exist. Unmerged or draft PRs are not implemented-main claims. @@ -20,23 +20,7 @@ The Rust domain packages are embedded/library boundaries. They require no produc ## Planned service SLIs -When corresponding services exist, track: - -- source ingest success/rejection and exact error class; -- evidence/span count and lineage completeness; -- future-evidence exclusion count at each knowledge cutoff; -- temporal contradiction/path-consistency and budget exhaustion counts; -- event/link/tracking confidence and calibration; -- semantic-unit unknown/abstention rate by language; -- model convergence/ELBO/objective and posterior diagnostics; -- true-recovery/validation drift against release benchmark; -- CPU/GPU parity and fallback count; -- VRAM/RSS/transfer/kernel time; -- model/LLM provider failures and evidence-verifier rejection; -- artifact/export provenance completeness; -- tenant authorization/audit anomalies. - -Do not expose raw PII/source text in ordinary metrics/logs merely to gain observability. +When corresponding services exist, track source ingest success/rejection and exact error class; evidence/span count and lineage completeness; future-evidence exclusion at each knowledge cutoff; temporal contradiction/path-consistency and budget exhaustion; event/link/tracking confidence and calibration; semantic-unit unknown/abstention by language; model convergence/objective/posterior diagnostics; true-recovery drift; CPU/GPU parity and fallback; VRAM/RSS/transfer/kernel time; model/LLM provider failures and evidence-verifier rejection; artifact/export provenance completeness; and tenant authorization/audit anomalies. Do not expose raw PII/source text in ordinary metrics/logs merely to gain observability. ## Data snapshot and replay @@ -48,7 +32,7 @@ Before admitting a GPU job, estimate budget and reserve margin. On OOM: classify ## LLM degradation -LLM-backed semantic/interpreter functions use strict bounded requests and cached/versioned results where appropriate. Provider failure may retry only under bounded policy, route through contextual-orchestrator when configured, or return deferred/unresolved evidence. It must not corrupt deterministic/statistical results or expose credentials/source beyond approved policy. +LLM-backed semantic/interpreter functions consume only immutable released compatible contextual-orchestrator contracts. Provider failure may retry or route only through that owner contract and its policy, or return deferred/unresolved evidence. It must not corrupt deterministic/statistical results, silently substitute a mutable owner head, or expose credentials/source beyond approved policy. Model-backed Actions use `orchestrator/free` through the approved gateway route; provider/model hard-coding and LLM numerical authority remain prohibited. ## Database target recovery @@ -56,6 +40,45 @@ Migration `0007` (active PR) contracts policy-driven retention, legal-hold block Before PostgreSQL becomes production state, prove migrations and rollback, tenant isolation/RLS, temporal/lineage constraints, idempotency/concurrency, backup/restore, retention/deletion, and reconstruction from immutable artifacts. Concurrent document first-insert and revise stress is implemented-main. `persistence_postgres::mark_restored_state_usable` and `assert_restore_integrity` are the current fail-closed restore gate (active PR): they revalidate tenant identity, canonical digests, same-tenant knowledge-cutoff eligibility, temporal window order, and enabled append-only triggers. They do not yet revalidate relation-aware splits or full lineage graphs; those remain separate post-restore scientific steps. The gate does not replace operator `pg_dump`/`pg_restore` runbooks. +## Numerical proof resource budgets + +A numerical proof boundary is an operational resource contract when it changes asymptotic work, allocation, or buyer-path latency. It is not determined by the next sample count that happens to expose a rounding defect. + +Issue #491 owns the current bias-standard-error exact-proof budget. Production exact admission remains `n=4..=16`; larger counts are characterization evidence only. The work separates represented-input exactness, arithmetic width, exact-rounding width, and measured resource cost rather than treating one integer cutoff as all four. + +The old nonnegative minimum-anchor characterization established `Σc_i <= Σc_i² <= P` and showed why raw-scale `D=2^58,n=65` refusal disappears after common dyadic-unit normalization. Odd `D=2^58+1,n=65` remains a narrow-width witness: pair numerator fits in 123 bits while cancellation products require 129 bits. `Wide256` characterization `081000289f5a52e94863026d55696ee2a4daf923` and product-width RED `f74d9ac11cb0acf3eb8fdd9ad79ac3d2e9180993` → repair `e9a7dee29afb97542bfe2965f850c8ab5a34368e` show the characterized cancellation products need no more than two `u128` limbs. + +Represented-input reachability `5a19b6334487b43fb630abba7e487d7cf4c49960` reaches the wider numerator route at `n=4096` on `{0,1,2^53}`. Exact-rounding characterization `a8423173188fa53a26a16d3afdafeb76e114cc1d` shows represented `n=2050` needs 136-bit candidate-square and 140-bit adjacent-midpoint comparison operands even though its exact pair numerator is 118 bits. `aab9fe9115cee97225f2aa81e54a55ceafb23336` shows those comparisons can remain bounded as `Wide256` mantissa plus signed dyadic exponent. RED `f7717361ad8c5f0592688c1514c104cc1b4adabe` → repair `e4a85f53a611922be7492fe906d62ce65787c18e` integrates that comparison into the production exact rounder, and `1240ace8eb41a01fa72a4bb99df842fd550a1288` fixes both tie-to-even parity directions. + +Historical characterization `7a2ab0a1ef7a72d8cc9b9253d7f92c493e578943` proves represented pair/Wide256 exact-ratio equivalence for one family where the represented minimum is an exact anchor. That is not a universal production policy. Characterization `2bc1d2284d75154e020640adb573c1cfadf005fb` demonstrates `[0,1,2^-54,2]`, where a non-anchor pair subtraction rounds but neutral zero remains exact. + +Source RED `fd9f9ff2c5c395e4cc13042232f4deef018adb48` establishes the first public anchor defect with `[0,1,2,-2^53]`: minimum `-2^53` cannot exactly translate `1`, neutral zero preserves every coordinate, exact `P=243388915243820099130562543878155`, denominator `48`, and exact result `0x4320000000000001` while the predecessor translated floating-moment path returns `0x4320000000000000`. Initial repair `81ba770cc4812c8fbeb4b3529f0a73b41abbed0f` searched represented residual anchors. + +Follow-up RED `9f403194a2ec1636531c2dfe9229cfb34b73d747` shows that observed anchors alone are operationally incomplete. Residuals `[1,2^-54,2,3]` have no observed universal exact anchor, but neutral dyadic origin `0` preserves every residual exactly. On unit `2^-54`, exact `P=6490371073168534319490338297741315`; exact result `0x3fe4a7e9cb8a3491` differs by one ULP from the predecessor fallback `0x3fe4a7e9cb8a3492`. + +Correctness repair `6cf30eeb549c0df0377bda1111cf46396e8282a3` established neutral zero as an admissible exact translation origin, but its production order still paid pairwise O(n²) first and then O(n²) anchor search. Source-level route-order RED `e0b324864e48a503e2aba0d2a487a0b95f5276ed` required a linear neutral-zero proof before quadratic work; repair `2b62bd46eb0c391327d2285c2244a76f5a1e0449` added that two-pass path. A later retention criterion required conditioned observed-anchor search to demonstrate unique bounded admission after neutral-zero refusal. The checked-in corpus supplied no such represented fixture, so RED `d40bfbf98b36562164d15d505f8f8825fd1c1349` → repair `14e7862f4ddccce54f3b93d4dac89adbf047ba77` removed the unsupported O(n²) anchor route. Production order is now `neutral_zero_linear -> pairwise_reference -> generic_fallback`. + +The neutral-zero kernel scans residuals once to determine the common dyadic exponent and once more to accumulate positive/negative coefficient mass and `Σc_i²`; it then computes `n*Σc_i²-(Σc_i)²` with exact `Wide256` products/subtraction. This is O(n) time with O(1) proof storage after the already-required residual vector and allocates no pair records. If the bounded neutral-zero proof refuses, pairwise O(n²) is the comparison/fail-closed authority before the established generic fallback. + +The superseded route-order and conditioned-anchor-retention REDs did not finish hosted failing runs, so they are source-level TDD evidence only. Commit `da703fc50e136ed79e12262909c9f400a3945621` aligns the equivalence characterization with production signed neutral-zero arithmetic: represented `{0,1,2^53}` families retain exact pair/linear numerator and reduced-ratio equality at `n=4,16,17,65,257,2050` plus order invariance. Mixed-sign `[-2^53,0,1,2^53]` proves a neutral-zero-only admission: pairwise subtraction refuses, while neutral-zero yields unit exponent `0`, signed-sum magnitude `1`, `Σc_i²=2^107+1`, `P=2^109+3`, and public bits `0x432a20bd700c2c3e`. + +Operator implications: + +- attempt the neutral-zero linear proof before quadratic proof work for the current bounded production route; +- do not diagnose pairwise-f64 or minimum/observed-anchor refusal as scientific invalidity when neutral zero admits the bounded dyadic proof; +- do not reintroduce conditioned observed-anchor scanning without a represented fixture that proves unique scientific admission after neutral-zero bounded refusal; O(n²) search is not retained by assumption; +- do not make row order part of proof semantics; forward/reversed/permuted fixtures must be bit-identical; +- treat signed-coordinate accumulation, Wide256 subtraction/downcast, denominator reduction, or exact-rounding failure as a fail-closed proof refusal and use later proof/fallback routes rather than weakening checks; +- keep pairwise proof as the comparison/fail-closed authority, not as unconditional first work; +- separate route observability from numerical equality. The existing resource harness records characterization-specific `used_wide_product` and `used_pairwise_fallback`, but production evidence still must distinguish `neutral_zero_linear`, `pairwise_reference`, and `generic_fallback`; +- describe storage precisely: the neutral-zero kernel uses O(1) proof storage after the residual vector, while the public exact path still materializes O(n) residual storage; +- treat `n=2_047`, `4_095`, and `208_064` only as arithmetic envelope markers from older aligned characterizations, not service limits; +- before widening beyond 16, retain raw Rust 1.98.0 `--release` timing CSV, CPU/OS/build flags, p95, actual scratch capacity/payload, allocator/RSS, and any applicable buyer-path `p95<=20 ms` evidence without sample shrinkage or omitted proof work. + +Exact pair-record counts remain 120 at `n=16`, 136 at `n=17`, 2,096,128 at `n=2048`, and 4,997,541 at `n=3162`. A two-pass O(n²) implementation may remove pair-record storage but still requires exact-head scientific/resource evidence. The neutral-zero Wide256 O(n) implementation removes pair enumeration for admitted geometries but still requires broad pair-equivalence, neutral-zero-only admission, exact-rounding, refusal, route, and resource evidence on one surviving production head. + +No release-mode resource numbers are authoritative yet. Source-level RED runs that were superseded or cancelled are not counted as hosted RED. Pinned Rustfmt Evidence `34037910108` for predecessor `206c1789ea13759591c308705fc1f57b75c96b28` produced artifact `9991468416`, digest `sha256:cd03e7c35be9d398d86494b01306df08af94dc117b38482578d652d9e2b2283b`, with 36 formatted Rust files; it is predecessor-only evidence, not current-head formatting GREEN. + ## Model release/cutover A model artifact is promoted only after convergence, posterior diagnostics, true-parameter/recovery benchmarks, invariance/fairness/language evidence, uncertainty/calibration, security/privacy, and reproducibility gates meet the versioned policy. Model-selection or LLM review disagreement can require human scientific review. @@ -66,18 +89,8 @@ Trace the first failing boundary: evidence, temporal typing/reasoning, event/rel ## Actions workflow fleet -GitHub Actions registry identities survive YAML deletion. After any -bootstrap, diagnosis, or repair workflow is removed from the tree, run -`scripts/actions_workflow_fleet.py audit` and retain the JSON inventory -(workflow ID, path, state, classification, default-branch SHA, timestamp, -pagination receipts). Disable only re-fetched active orphans with -`disable-orphans --apply`. Never disable the protected CI, documentation, -hourly NIM, or hourly PR-maintenance paths, and never recreate deleted -bootstrap/repair YAML. The auditor uses only `GITHUB_TOKEN`/`GH_TOKEN`. -Product-development automation continues to use `NVIDIA_NIM_API_KEY` and -must not receive `COPILOT_GITHUB_TOKEN`. Operator procedure: -`docs/operations/ACTIONS_WORKFLOW_FLEET.md`. +GitHub Actions registry identities survive YAML deletion. After any bootstrap, diagnosis, or repair workflow is removed from the tree, run `scripts/actions_workflow_fleet.py audit` and retain the JSON inventory (workflow ID, path, state, classification, default-branch SHA, timestamp, pagination receipts). Disable only re-fetched active orphans with `disable-orphans --apply`. Never disable the protected CI, documentation, hourly NIM, or hourly PR-maintenance paths, and never recreate deleted bootstrap/repair YAML. The auditor uses only `GITHUB_TOKEN`/`GH_TOKEN`. Product-development automation continues to use the owner-approved model route and must not receive unrelated provider credentials. Operator procedure: `docs/operations/ACTIONS_WORKFLOW_FLEET.md`. ## Release gate -A software release requires exact protected-head CI/security/review, 100% production coverage/docs, validated migrations/rollback where present, scientific benchmark artifacts, SBOM/provenance, reproducible packages/images, operator runbooks, accessibility for product UI, CHANGELOG/version/tag consistency, and post-publish verification. TEPP has not reached that integrated release state merely because individual foundation PRs merge. \ No newline at end of file +A software release requires exact protected-head CI/security/review, 100% production coverage/docs, validated migrations/rollback where present, scientific benchmark artifacts, SBOM/provenance, reproducible packages/images, operator runbooks, accessibility for product UI, CHANGELOG/version/tag consistency, and post-publish verification. TEPP has not reached that integrated release state merely because individual foundation PRs merge. Unexecuted timing harnesses and branch-only resource characterization are not release evidence. \ No newline at end of file diff --git a/docs/TEST_STRATEGY.md b/docs/TEST_STRATEGY.md index 3180012d1..768d7edd5 100644 --- a/docs/TEST_STRATEGY.md +++ b/docs/TEST_STRATEGY.md @@ -1,7 +1,7 @@ # TEPP Test and Scientific Validation Strategy **Status:** Accepted quality baseline aligned to PRD v0.4 -**Last reviewed:** 2026-08-12 +**Last reviewed:** 2026-09-07 ## Mandatory repository gates @@ -57,16 +57,47 @@ CPU `f64` is reference. Required accelerator lanes execute real kernels; skipped ## LLM tests -Deterministic schema/security tests are primary. Bounded live tests use `NVIDIA_NIM_API_KEY` only when model conformance is material. Treat documents as prompt-injection data, require evidence-span grounding, test unsupported-claim rejection, record provider/model/prompt/reasoning hashes, and compare model/human agreement where the LLM acts as a rater. +Deterministic schema/security tests are primary. Bounded live tests use released contextual-orchestrator contracts when model conformance is material. Treat documents as prompt-injection data, require evidence-span grounding, test unsupported-claim rejection, record provider/model/prompt/reasoning hashes, and compare model/human agreement where the LLM acts as a rater. Model-backed Actions must use the approved `orchestrator/free` route and must not make an LLM authoritative for numerical or scientific acceptance. ## Monte Carlo acceptance Simulation thresholds account for Monte Carlo standard error and interval uncertainty. Do not require an observed replication proportion to exceed the nominal target exactly when sampling variability makes that scientifically invalid. +## Exact-proof resource budgeting + +Validation Evidence numerical proofs that add asymptotic work or material allocation require a measured resource contract before a production admission boundary is widened. For the bias-standard-error exact pair-distance path tracked by issue #491: + +- retain realistic represented-input counterexamples and permutation/sign-mirror contracts for scientific correctness; +- keep the exact represented-residual gate, GCD reduction, exact candidate/midpoint authorization, and fail-closed fallback; +- treat exact pairwise-f64 subtraction as a comparison/reference authority, not a scientific prerequisite or an unconditional first production route. Characterization `2bc1d2284d75154e020640adb573c1cfadf005fb` proves `[0,1,2^-54,2]` has exact neutral-zero coordinates even though a non-anchor pair subtraction rounds; +- do not require the represented minimum to be the exact anchor. Source RED `fd9f9ff2c5c395e4cc13042232f4deef018adb48` uses `[0,1,2,-2^53]`: minimum `-2^53` cannot exactly translate `1`, while neutral zero preserves all coordinates. Exact `P=243388915243820099130562543878155`, denominator `48`, and public result `0x4320000000000001` differ by one ULP from the predecessor fallback; +- do not restrict exact translation origins to observed residuals. Follow-up RED `9f403194a2ec1636531c2dfe9229cfb34b73d747` uses `[1,2^-54,2,3]`, where no observed residual exactly translates every other residual, but neutral dyadic origin `0` preserves all. On unit `2^-54`, exact `P=6490371073168534319490338297741315`; exact result `0x3fe4a7e9cb8a3491` differs by one ULP from predecessor fallback `0x3fe4a7e9cb8a3492`; +- retain the correctness lineage through repair `6cf30eeb549c0df0377bda1111cf46396e8282a3`, but do not retain its quadratic route order as production policy. Source-level route-order RED `e0b324864e48a503e2aba0d2a487a0b95f5276ed` required a linear neutral-zero proof before quadratic work; repair `2b62bd46eb0c391327d2285c2244a76f5a1e0449` implemented it. A later retention criterion required conditioned observed-anchor search to prove unique bounded admission after neutral-zero refusal. The checked-in corpus supplied no such fixture, so RED `d40bfbf98b36562164d15d505f8f8825fd1c1349` → repair `14e7862f4ddccce54f3b93d4dac89adbf047ba77` removed that unsupported O(n²) route. Production order is now `neutral_zero_linear -> pairwise_reference -> generic_fallback`. Neither superseded RED finished a hosted failing run and neither is cited as hosted RED evidence; +- test the neutral-zero kernel as O(n) in loop structure and O(1) in *proof* storage after the already-required residual vector. Do not describe the whole public call as O(1) space while it still materializes residuals; +- preserve pair-versus-neutral-zero equality wherever both admit and explicitly test neutral-zero-only admissions. Commit `da703fc50e136ed79e12262909c9f400a3945621` mirrors the production signed neutral-zero arithmetic, keeps exact pair/linear numerator and reduced-ratio equality for represented `{0,1,2^53}` at `n=4,16,17,65,257,2050`, preserves order invariance, and adds mixed-sign `[-2^53,0,1,2^53]`, where pairwise refuses but neutral-zero admits `P=2^109+3` and public bits `0x432a20bd700c2c3e`. Broaden deterministic represented-input equality beyond these fixtures before promotion; +- do not reintroduce conditioned observed-anchor scanning without a represented fixture that proves unique scientific admission value after neutral-zero bounded refusal; O(n²) search is not retained by assumption; +- neutral zero is a translation origin, not synthetic evidence: the observed residual values are unchanged. Do not claim it is globally resource-optimal without a separate proof; +- compare buffered O(n²), allocation-free two-pass O(n²), normalized narrow O(n), dependency-free Wide256 O(n), predecessor narrow→pair hybrid, and narrow→Wide256→pair candidate resource shapes before changing the sample budget; +- normalize the common power-of-two dyadic unit before narrow checked O(n) intermediates are judged. Raw `D=2^58,n=65` refusal is invalid after normalization; odd `D=2^58+1,n=65` remains the narrow 129-bit product witness; +- retain the earlier nonnegative minimum-anchor accumulator theorem `b7e4da353ac58069afd73ee7c0e8427d49993fdb`, `Σc_i <= Σc_i² <= P`, only within that characterized representation. Production neutral-zero coordinates may be signed, so positive and negative coefficient mass and resulting signed sum must be exercised separately; +- retain RED `f74d9ac11cb0acf3eb8fdd9ad79ac3d2e9180993` → repair `e9a7dee29afb97542bfe2965f850c8ab5a34368e`: canonical `u128` cancellation operands require no more than 256-bit products; +- retain represented reachability `5a19b6334487b43fb630abba7e487d7cf4c49960`: at `n=4096`, `{0,1,2^53}` reaches narrow-product overflow while Wide256 recovers exact 119-bit `P=664_289_479_338_799_435_974_172_876_300_357_631`; +- retain exact-rounding-width characterization `a8423173188fa53a26a16d3afdafeb76e114cc1d`: at represented `n=2050`, `P=332_306_998_946_228_931_332_463_617_650_984_961`, denominator `8_610_922_500`, candidate-square comparison requires 136 bits and the upward midpoint 140 bits; +- retain exponent-safe comparison characterization `aab9fe9115cee97225f2aa81e54a55ceafb23336`, production comparison RED `f7717361ad8c5f0592688c1514c104cc1b4adabe` → repair `e4a85f53a611922be7492fe906d62ce65787c18e`, and tie-to-even edge contract `1240ace8eb41a01fa72a4bb99df842fd550a1288`; +- retain historical minimum-anchor pair/Wide256 exact-ratio equivalence `7a2ab0a1ef7a72d8cc9b9253d7f92c493e578943` only within its characterized input family; use `da703fc50e136ed79e12262909c9f400a3945621` as the production-aligned signed neutral-zero equivalence evidence; +- keep normalized O(n) intermediate, exact pair-numerator, and denominator envelopes separate. At aligned diameter `2^53`, `n<=2_047`, `n<=4_095`, and `n<=208_064` are arithmetic evidence points, not production budgets; +- record exact pair counts, target `size_of::>()`, scratch `Vec` capacity/payload, and allocator/RSS evidence separately; field-width estimates are not allocation evidence; +- use `crates/validation_core/examples/bias_se_exact_proof_budget.rs` to prove exact restored-numerator equality before timing, but do not mislabel that characterization harness as production route telemetry. Production evidence must distinguish at least `neutral_zero_linear`, `pairwise_reference`, and `generic_fallback`; +- run the characterization and surviving production-route harness in release mode on recorded CPU/OS/Rust 1.98.0 and retain raw CSV plus p95. Unexecuted harness code is not performance evidence; +- if a service/API buyer path is affected, measure the complete applicable path against `p95 <= 20 ms` without shrinking samples, omitting proof work, or using unrealistic warm-cache-only setup; +- arithmetic representability, one successful exact origin, or one common-domain equality fixture does not authorize a production sample-count budget. + +Until broad same-head correctness, exact-head gates, independent review, and resource measurements exist, production bias-SE exact admission remains `n=4..=16`. + ## Release acceptance -A release requires one integrated protected head with all relevant scientific, numerical, security, migration, packaging, SBOM/provenance, accessibility, operational, and independent-review evidence passing. Planning validation, superseded-branch results, and local-only results are supporting evidence, not release proof. +A release requires one integrated protected head with all relevant scientific, numerical, security, migration, packaging, SBOM/provenance, accessibility, operational, and independent-review evidence passing. Planning validation, superseded-branch results, local-only results, and unexecuted benchmark tooling are supporting evidence, not release proof. ## References -The full APA 7th register is [`docs/research/standards-and-literature.md`](research/standards-and-literature.md). Method names used above cite Allen (1983) for interval algebra and Asparouhov & Muthén (2009), Asparouhov et al. (2018), and Marsh et al. (2014) for ESEM/DSEM. +The full APA 7th register is [`docs/research/standards-and-literature.md`](research/standards-and-literature.md). Method names used above cite Allen (1983) for interval algebra and Asparouhov & Muthén (2009), Asparouhov et al. (2018), and Marsh et al. (2014) for ESEM/DSEM. \ No newline at end of file diff --git a/docs/research/bias-compensated-division-rounding.md b/docs/research/bias-compensated-division-rounding.md new file mode 100644 index 000000000..8598776eb --- /dev/null +++ b/docs/research/bias-compensated-division-rounding.md @@ -0,0 +1,79 @@ +# Mean bias: compensated numerator division rounding + +## Problem + +TEPP defines signed bias as `mean(recovered - truth)`. GAP-087 retained error-free low terms from opposite-sign cancellation, but the mixed-remainder path still collapsed the compensated normalized numerator to one binary64 value before dividing by the scientific observation count. That extra rounding can change the final represented mean even when the compensated numerator carries enough information to avoid it. + +A public three-observation boundary is: + +- truth: `[0, 0, 0]`; +- recovered/residuals: `[2^-53, -2^-52, -(1 + 2^-52)]`. + +The exact represented-input numerator is + +`2^-53 - 2^-52 - (1 + 2^-52) = -(1 + 3*2^-53)`. + +Its exact mean is + +`-9007199254740995 / 27021597764222976`, + +which rounds to binary64 `-0x1.5555555555557p-2` (bits `0xbfd5555555555557`). The predecessor Neumaier path retained a high part and correction, but `sum + correction` rounded the numerator first to `-0x1.0000000000002p+0`; dividing that rounded numerator by three produced `-0x1.5555555555558p-2`, one ULP away from the represented-input mean. The mirrored positive case has the same defect. + +This is a Validation Evidence arithmetic defect. It does not change the scientific definition of bias, create a new estimator, or move reusable static psychometric ownership out of `fast-mlsirm`. + +## Public RED + +Commit `8b4d19d161cb4322db3a143b2e34125d3bcc08f1` adds `crates/validation_core/tests/bias_compensated_division_contract.rs` and fixes both signs of the boundary. The contract requires `mean_bias` to return bits `0xbfd5555555555557` and `0x3fd5555555555557` from public pair inputs. + +The RED is preserved as source-level reproducer evidence. Hosted runs superseded or cancelled by later source pushes are not promoted as current-head GREEN or RED execution evidence. + +## Causal repair + +Commit `7a33212b0c0f079a9fb138b6a8564881bc22fc9e` keeps the existing magnitude-ordered cancellation and GAP-087 low-term retention. It factors the canonical Neumaier pass into a private `(sum, correction)` result only for internal reuse. The existing `deterministic_compensated_sum` public-to-crate behavior remains `sum + correction` for callers that request a sum. + +Only `mixed_remainder_mean_over_total` changes its mean formation. It now: + +1. preserves the normalized Neumaier high part and correction separately; +2. divides the high part by the original scientific denominator; +3. uses binary64 FMA to recover the division residual of that high part; +4. combines that residual with the retained compensation before the final denominator division; +5. restores the same exact power-of-two scale and keeps the existing fail-closed non-finite/zero-underflow boundary. + +For the RED, the retained correction is `+2^-53` relative to the rounded negative high part. Carrying that mass through division moves the result from predecessor bits `0xbfd5555555555558` to the represented-input result `0xbfd5555555555557`. + +The repair intentionally does not alter the same-sign path or `bias_standard_error`; neither is implicated by this counterexample. CHANGELOG commit: `bad698dd686ce69a96aee1ca748bd7dc8e63aa1b`. + +## Alternatives rejected + +Replacing all Validation arithmetic with arbitrary precision was rejected because this bounded defect is caused by one avoidable rounding boundary and does not justify a new production dependency or latency surface. + +Dividing `sum + correction` was rejected because that is the defective double-rounding sequence demonstrated by the RED. Dividing `sum` and `correction` independently and simply adding the quotients was also not adopted as the contract: the chosen FMA step additionally recovers the high-part division residual before the retained correction is consumed. + +Changing the same-sign mean path or `bias_standard_error` without a represented-input counterexample was rejected as scope expansion. A future change there requires its own RED and causal proof. + +## Scope and residual risk + +This repair claims the demonstrated property that retained mixed-sign compensation is not forced through an additional numerator rounding before the original scientific count division. It does not claim globally correctly rounded summation or division for every binary64 sequence, every possible `usize` count, or every Validation metric. + +A later numerical gap requires an independent public counterexample in which the current canonical producer differs from the represented-input estimand. Algebraic suspicion or an oracle-only mismatch outside a declared public contract is not sufficient by itself. + +## Traceability + +- Bounded context: Validation Evidence +- Public API: `crates/validation_core/src/bias.rs` / `mean_bias` +- Shared arithmetic: `crates/validation_core/src/numeric.rs` / `deterministic_representable_sum_over_count` +- Public RED: `8b4d19d161cb4322db3a143b2e34125d3bcc08f1` +- Causal repair: `7a33212b0c0f079a9fb138b6a8564881bc22fc9e` +- CHANGELOG: `bad698dd686ce69a96aee1ca748bd7dc8e63aa1b` +- Contract test: `crates/validation_core/tests/bias_compensated_division_contract.rs` +- Landing vehicle: PR #488; only its latest exact head after this documentation commit is authoritative for hosted checks and review. + +## Normative and methodological references + +Institute of Electrical and Electronics Engineers. (2019). *IEEE standard for floating-point arithmetic* (IEEE Std 754-2019). IEEE Standards Association. https://doi.org/10.1109/IEEESTD.2019.8766229 + +International Organization for Standardization & International Electrotechnical Commission. (2020). *Information technology—Microprocessor systems—Floating-point arithmetic* (ISO/IEC 60559:2020). + +Morris, T. P., White, I. R., & Crowther, M. J. (2019). Using simulation studies to evaluate statistical methods. *Statistics in Medicine, 38*(11), 2074–2102. https://doi.org/10.1002/sim.8086 + +American Educational Research Association, American Psychological Association, & National Council on Measurement in Education. (2014). *Standards for educational and psychological testing*. American Educational Research Association. diff --git a/docs/research/bias-mixed-sign-cancellation-roundoff.md b/docs/research/bias-mixed-sign-cancellation-roundoff.md new file mode 100644 index 000000000..68b085c8e --- /dev/null +++ b/docs/research/bias-mixed-sign-cancellation-roundoff.md @@ -0,0 +1,69 @@ +# Mean bias: mixed-sign cancellation roundoff + +## Problem + +TEPP defines signed bias as `mean(recovered - truth)`. After GAP-086, overflowing pairwise residuals can be algebraically expanded and opposing extreme terms cancel before scale reduction. A separate finite-input defect remained inside that shared cancellation helper: repeatedly adding an opposite-sign term smaller than half an ULP of the current residual could round back to the same high part and silently discard the low term each time. + +For represented inputs + +- residuals: `[1, -2^-54, -2^-54, -2^-54, -2^-54]` +- equivalent public pair input: `truth=[0, 2^-54, 2^-54, 2^-54, 2^-54]`, `recovered=[1, 0, 0, 0, 0]` + +an implementation that materializes each cancellation as ordinary binary64 addition can keep the running high part at exactly `1.0` four times. The four discarded low terms sum to `2^-52`, so the exact represented-input numerator is `1 - 2^-52`; dividing by five rounds to bits `0x3fc9999999999998`, not ordinary `0.2` (`0x3fc999999999999a`). The final scientific estimand is representable, so discarding the low terms is an arithmetic defect rather than a justified fail-closed boundary. + +Morris, White, and Crowther (2019) treat bias against known truth as a simulation performance measure. TEPP therefore preserves the represented-input estimand rather than making the rounding behavior of an avoidable pairwise cancellation step authoritative. + +This remains Validation Evidence arithmetic. It does not move reusable psychometric estimation ownership from `fast-mlsirm` and does not introduce a second public bias definition. + +## Public RED + +Commit `63913727a3dfc2dd65c2508fe7e4acfdd6498508` adds `crates/validation_core/tests/bias_cancellation_roundoff_contract.rs`. + +The contract fixes both signs of the same scientific boundary. Four quarter-ULP opposing residuals must collectively affect the represented mean bias even though each individual subtraction from the running high part is too small to change that high part by itself. + +The RED commit is preserved as source-level reproducer lineage. Any later hosted run cancelled or superseded by subsequent source pushes is not treated as current-head RED execution evidence. + +## Causal repair + +Commit `5697cca51df2ec49e44a04730a14fd77656b48a5` changes only the shared deterministic cancellation path in `validation_core::numeric`. + +Each opposite-sign addition now uses an error-free TwoSum decomposition: the rounded high part continues to drive the existing magnitude-ordered cancellation, while any nonzero low term is retained. If every cancellation is exact, the predecessor same-sign scale-reduction path is unchanged. Only when low terms exist are the final high remainders and those low terms scale-normalized together and accumulated with the existing deterministic compensated summation before division by the original scientific denominator. + +This is narrower than replacing all mean arithmetic with arbitrary precision. It addresses the demonstrated loss of collectively material roundoff while preserving full-range cancellation, exact zero, the explicit denominator required by GAP-086, and fail-closed behavior for nonzero results outside or below binary64 range. + +CHANGELOG commit: `53f5c912fbca415172f621c909b626ea1db66582`. + +## Alternatives rejected + +Always summing normalized original inputs was rejected because extreme cancellation such as `MAX + (-MAX) + MIN_SUBNORMAL` can lose the tiny surviving term during normalization before the large terms cancel. Pairwise Kahan/Neumaier accumulation over the original unscaled inputs was rejected because same-sign extreme intermediates may overflow even when the final mean is representable. Arbitrary-precision production arithmetic was rejected as unnecessary for this bounded counterexample and would add a new runtime dependency and performance surface. + +Ignoring the low terms because each is individually below one ULP was rejected because the public RED proves their combined represented mass changes the final binary64 bias by two ULPs. + +## Scope and residual risk + +The repair claims only that opposite-sign cancellation no longer drops representable TwoSum low terms one at a time before their aggregate can affect the final mean. It does not claim globally correctly rounded summation for every possible binary64 sequence, nor does it alter `bias_standard_error`, whose estimand still requires individually representable signed residual dispersion. + +A future stronger summation rule requires an independent represented-input counterexample plus a bounded causal repair. Algebraic suspicion alone is not sufficient to create another Validation gap. + +## Traceability + +- Bounded context: Validation Evidence +- Public API: `crates/validation_core/src/bias.rs` / `mean_bias` +- Shared arithmetic: `crates/validation_core/src/numeric.rs` / `deterministic_representable_sum_over_count` +- Public RED: `63913727a3dfc2dd65c2508fe7e4acfdd6498508` +- Causal repair: `5697cca51df2ec49e44a04730a14fd77656b48a5` +- CHANGELOG: `53f5c912fbca415172f621c909b626ea1db66582` +- Contract test: `crates/validation_core/tests/bias_cancellation_roundoff_contract.rs` +- Landing vehicle: PR #488; only its latest exact head after this documentation commit is authoritative for hosted checks and review. + +## Normative and methodological references + +Institute of Electrical and Electronics Engineers. (2019). *IEEE standard for floating-point arithmetic* (IEEE Std 754-2019). IEEE Standards Association. https://doi.org/10.1109/IEEESTD.2019.8766229 + +International Organization for Standardization & International Electrotechnical Commission. (2020). *Information technology—Microprocessor systems—Floating-point arithmetic* (ISO/IEC 60559:2020). + +Morris, T. P., White, I. R., & Crowther, M. J. (2019). Using simulation studies to evaluate statistical methods. *Statistics in Medicine, 38*(11), 2074–2102. https://doi.org/10.1002/sim.8086 + +American Educational Research Association, American Psychological Association, & National Council on Measurement in Education. (2014). *Standards for educational and psychological testing*. American Educational Research Association. + +As of 2026-09-05, IEEE 754-2019 remains an active published standard; IEEE P754 remains an active revision PAR rather than a published replacement. ISO/IEC 60559:2020 remains published at stage 60.60. AERA continues to publish the 2014 Testing Standards while the AERA/APA/NCME Joint Committee revises that edition; the unpublished revision is not treated as current normative authority. diff --git a/docs/research/bias-overflowing-residual-cancellation.md b/docs/research/bias-overflowing-residual-cancellation.md new file mode 100644 index 000000000..7c0edc787 --- /dev/null +++ b/docs/research/bias-overflowing-residual-cancellation.md @@ -0,0 +1,72 @@ +# Mean bias: overflowing residual cancellation + +## Problem + +TEPP defines signed bias as `mean(recovered - truth)`. The predecessor required every pairwise signed residual to be representable before computing the mean. That made an intermediate binary64 limitation authoritative even when the final scientific estimand was representable. + +For finite represented inputs + +- `truth = [-f64::MAX, f64::MAX, 0]` +- `recovered = [f64::MAX, -f64::MAX, 3 * f64::MIN_SUBNORMAL]` + +the first two mathematical residuals are `+2*MAX` and `-2*MAX`, so direct binary64 subtraction overflows in both directions. Those terms cancel exactly in the mean numerator. The third residual is three minimum subnormals, leaving an exact represented-input mean bias of one minimum subnormal. Rejecting this payload because the two intermediate residuals are non-finite discards a representable recovery metric. + +Morris, White, and Crowther (2019) treat bias as a performance measure relative to known simulation truth. The numerical implementation therefore has to preserve the declared estimand rather than silently replace it with the representability of one avoidable intermediate expression. + +This remains TEPP Validation Evidence arithmetic. It does not move reusable psychometric estimation ownership from `fast-mlsirm`. + +## Public RED + +Commit `04e6e74507a1adb89aac4b2a58d3682746da30ea` adds `crates/validation_core/tests/bias_overflowing_residual_cancellation_contract.rs`. + +The contract fixes three boundaries: + +1. two overflowing signed residuals with exact cancellation must yield bias `0.0`; +2. the same extreme cancellation plus three minimum subnormals over three observations must preserve one minimum-subnormal mean bias; and +3. a one-sided `2*MAX` mean bias remains unrepresentable and must fail closed. + +The RED-head GitHub workflows were superseded by subsequent source commits and cancelled, so they are not promoted as hosted RED execution evidence. + +## Causal repair + +Commit `28b1d186ce2bd08b63ca267c8b98b2eae45d2da7` factors the existing deterministic cancellation path into crate-private `deterministic_representable_sum_over_count(values, total_count)`. The divisor is explicit so an algebraically expanded numerator can retain the original scientific observation count. The helper keeps the existing sign-cancellation and exact-power-of-two scaling strategy, adds an explicit non-finite final-result rejection because the divisor can now differ from the number of terms, and preserves fail-closed behavior for nonzero results below binary64 range. + +Commit `d1cd54615ddc311491e26fa427e956b5b5379e1a` changes only `mean_bias` admission. When all pairwise residuals are finite, the existing direct residual path remains authoritative. If at least one finite-input pairwise subtraction overflows, the fallback evaluates the same numerator as recovered values plus negated truth values and divides by the original paired-observation count. Opposing extreme terms can therefore cancel before scale reduction without creating a second public bias definition. + +`bias_standard_error` is intentionally unchanged. Its requested dispersion depends on individual signed residual magnitudes; an unrepresentable residual is therefore not merely an avoidable intermediate for that API. + +CHANGELOG commit: `d68dde34c2c63e610a859d45e528c2134b3c0f91`. + +## Alternatives rejected + +Returning zero whenever positive and negative residual overflows coexist was rejected because an additional finite residual can leave a nonzero representable bias. Clamping overflowing residuals to `f64::MAX` was rejected because it changes the estimand and can reverse cancellation. Computing `mean(recovered) - mean(truth)` as the universal implementation was rejected because independently rounded means introduce a different rounding contract and can lose a representable small bias. Replacing all current mean arithmetic with arbitrary-precision production code was rejected as unnecessary scope expansion. + +## Scope and residual risk + +The fallback is entered only for finite paired inputs whose direct signed residual path contains an overflow. Ordinary finite residuals retain the predecessor implementation. The repair claims preservation of representable signed mean bias under that intermediate-overflow condition; it does not claim arbitrary-precision bias, a changed standard-error target, or a new psychometric estimator. + +The branch still requires current-head Rust tests, 100% owned line/branch coverage, documentation/security/SAST gates, independent review, and protected-main integration before delivery. + +## Traceability + +- Bounded context: Validation Evidence +- Module/API: `crates/validation_core/src/bias.rs` / `mean_bias` +- Shared crate-private arithmetic: `crates/validation_core/src/numeric.rs` / `deterministic_representable_sum_over_count` +- Public RED: `04e6e74507a1adb89aac4b2a58d3682746da30ea` +- Shared arithmetic repair: `28b1d186ce2bd08b63ca267c8b98b2eae45d2da7` +- Bias causal repair: `d1cd54615ddc311491e26fa427e956b5b5379e1a` +- CHANGELOG: `d68dde34c2c63e610a859d45e528c2134b3c0f91` +- Contract test: `crates/validation_core/tests/bias_overflowing_residual_cancellation_contract.rs` +- Landing vehicle: PR #488; only its latest exact head after this documentation commit is authoritative for hosted checks and review. + +## Normative and methodological references + +Institute of Electrical and Electronics Engineers. (2019). *IEEE standard for floating-point arithmetic* (IEEE Std 754-2019). IEEE Standards Association. https://doi.org/10.1109/IEEESTD.2019.8766229 + +International Organization for Standardization & International Electrotechnical Commission. (2020). *Information technology—Microprocessor systems—Floating-point arithmetic* (ISO/IEC 60559:2020). + +Morris, T. P., White, I. R., & Crowther, M. J. (2019). Using simulation studies to evaluate statistical methods. *Statistics in Medicine, 38*(11), 2074–2102. https://doi.org/10.1002/sim.8086 + +American Educational Research Association, American Psychological Association, & National Council on Measurement in Education. (2014). *Standards for educational and psychological testing*. American Educational Research Association. + +As of 2026-09-05, IEEE 754-2019 remains an active published standard; IEEE P754 remains an active revision PAR rather than a published replacement. ISO/IEC 60559:2020 remains published at stage 60.60. The AERA/APA/NCME Joint Committee is revising the 2014 testing Standards; the unpublished revision is not treated as current normative authority. diff --git a/docs/research/bias-pairwise-subtraction-roundoff.md b/docs/research/bias-pairwise-subtraction-roundoff.md new file mode 100644 index 000000000..b79c6145f --- /dev/null +++ b/docs/research/bias-pairwise-subtraction-roundoff.md @@ -0,0 +1,41 @@ +# Mean-bias pairwise subtraction roundoff + +## Problem + +`mean_bias` is the Validation Evidence performance measure `mean(recovered - truth)` over represented binary64 recovery values. The predecessor treated each finite pairwise subtraction as authoritative before aggregation. That is not algebraically neutral when a finite subtraction rounds away a low-order dyadic term and several subsequent rounding steps place the mean on a binary64 midpoint. + +Public RED `a9d9bda3df1533eb91ece8e3bc6ce597b9c1400f` uses + +- `truth = [2^-108, 2^-53]` +- `recovered = [2^-54, 1]`. + +The first exact represented-input residual is `2^-54 - 2^-108`, but binary64 subtraction rounds it to `2^-54`. The second residual is the exactly representable `1 - 2^-53`. The predecessor therefore averaged the rounded residuals to the ties-to-even result `0x3fe0000000000000` (`0.5`). The represented-input numerator still contains `-2^-108`; after division by two it lies strictly below that midpoint, so the correct represented mean is `0x3fdfffffffffffff`, the float immediately below `0.5`. The sign mirror has the symmetric expected bits `0xbfdfffffffffffff`. + +## Constraints + +The repair must preserve the existing exact-subtraction fast path, the overflow-safe recovered-plus-negated-truth fallback, canonical order stability, original recovery-unit denominator, fail-closed underflow/overflow semantics, and the earlier subnormal one-rounding contract. It must not create a second public estimator, copy reusable psychometric arithmetic from fast-mlsirm, or add an arbitrary-precision production dependency. + +## Repair lineage + +The first source repair `96bff8e55083fc791650b185f819cd3aa90dac1b` correctly detected nonzero error-free subtraction roundoff and redirected such finite cases to the existing expanded represented-input numerator. The RED then exposed a second rounding loss inside that path: mixed-remainder Neumaier compensation stored its correction in one binary64 value, so the `-2^-108` contribution could disappear when the leading correction already sat at the final midpoint. + +Corrected causal repair `676ccda02da0186361555879d5637c4f02178a71` retains a second-order correction tail while accumulating the mixed remainder. It carries the leading division residual, correction numerator residual, correction-division residual, and retained compensation tail through the original scientific denominator. The final candidate is moved to an adjacent float only when the exact two-term tail is beyond the binary64 midpoint, or is exactly at the midpoint and ties-to-even selects the adjacent value. This keeps ordinary exact-subtraction inputs on the predecessor path and does not claim globally correctly rounded summation/division for every binary64 sequence. + +## Alternatives rejected + +Always accepting pairwise-rounded residuals was rejected because the RED proves that a finite intermediate subtraction can change the represented scientific result. Always expanding every bias numerator was rejected because it would unnecessarily change the established exact-residual and all-subnormal paths, including the bounded exact-unit rule added for GAP-090. Adding an arbitrary-precision runtime dependency was rejected because the demonstrated defect is resolved inside the existing deterministic binary64 reference without creating a new numerical owner or deployment dependency. Treating the first expanded-numerator patch as complete was rejected because the original RED remained failing until second-order mixed-remainder compensation was retained through final rounding. + +## Scientific and standards trace + +IEEE 754-2019 remains the active published floating-point standard; IEEE P754 is an active revision PAR rather than a published replacement. ISO/IEC 60559:2020 remains the published international standard adopting the same floating-point arithmetic model. AERA, APA, and NCME continue to publish the 2014 *Standards for Educational and Psychological Testing* while the Joint Committee conducts its announced revision. Morris, White, and Crowther (2019) provide the methodological basis for treating bias as a known-truth simulation performance measure and for reporting simulation design and Monte Carlo uncertainty explicitly. + +References: + +- IEEE. (2019). *IEEE Standard for Floating-Point Arithmetic* (IEEE Std 754-2019). +- ISO/IEC. (2020). *Information technology—Microprocessor systems—Floating-point arithmetic* (ISO/IEC 60559:2020). +- American Educational Research Association, American Psychological Association, & National Council on Measurement in Education. (2014). *Standards for educational and psychological testing*. AERA. +- Morris, T. P., White, I. R., & Crowther, M. J. (2019). Using simulation studies to evaluate statistical methods. *Statistics in Medicine, 38*(11), 2074–2102. https://doi.org/10.1002/sim.8086 + +## Scope and remaining risk + +This repair establishes the public represented-input counterexample and its sign mirror. It does not claim that every possible binary64 mean is globally correctly rounded, nor does it alter `bias_standard_error`, RMSE, Monte Carlo, Wilson, Longitudinal Modeling, or fast-mlsirm ownership. A future GAP requires a separate public counterexample where the current exact-head return value disagrees with the represented-input estimand or admission decision; algebraic suspicion alone is insufficient. diff --git a/docs/research/bias-same-sign-remainder-division-rounding.md b/docs/research/bias-same-sign-remainder-division-rounding.md new file mode 100644 index 000000000..3fcf1a3b7 --- /dev/null +++ b/docs/research/bias-same-sign-remainder-division-rounding.md @@ -0,0 +1,60 @@ +# Same-sign remainder compensation at the mean-bias division boundary + +## Decision status + +Proposed evidence for the Validation bounded context. This note records the represented-input numerical contract implemented on PR #488; it does not claim globally correctly rounded binary64 summation or division. + +## Problem + +`deterministic_representable_sum_over_count` cancels opposite signs before scale reduction. GAP-088 repaired the path where cancellation itself produced retained low terms, but exact opposite-sign cancellation can leave a same-sign remainder and no cancellation roundoff term. That path still called `same_sign_mean_over_total`, which formed the compensated numerator as `sum + correction` before dividing by the scientific count. + +The public RED commit `84476aad7ef2918c174c1ef986cbec2851cac656` fixes the represented residual payload to: + +- `0x1.0000000000004p-3` +- `0x1.ffffffffffffcp-4` +- `-0x1.ffffffffffffdp-4` + +Their exact represented-input numerator is `9007199254740999 / 72057594037927936`; dividing by three gives `3002399751580333 / 72057594037927936`. The correctly rounded binary64 mean is bits `0x3fa555555555555a`. The predecessor returned `0x3fa555555555555b` because the Neumaier high part and correction were rounded back together before the count division. The sign-mirrored payload has the symmetric expected bits `0xbfa555555555555a`. + +## Constraints + +- Keep `mean_bias` as TEPP Validation Evidence semantics; do not move this decision rule into fast-mlsirm. +- Preserve the original paired-observation denominator even when cancellation rewrites the represented numerator. +- Do not add arbitrary-precision arithmetic to the production hot path for a boundary that binary64 FMA and retained compensation can resolve. +- Do not change `bias_standard_error`; this counterexample concerns only the represented mean-bias numerator/division path. +- Do not infer a general correctly-rounded summation guarantee from this repair. + +## Alternatives considered + +Forming `sum + correction` before division was rejected because it reproduces the observed one-ULP error. Always switching to arbitrary-precision rational arithmetic was rejected because the public metric is a deterministic binary64 reference and the failure is caused by one avoidable intermediate rounding boundary. Replacing the cancellation algorithm was also rejected because the counterexample shows that the cancellation result is already correct; the defect is downstream in the same-sign remainder division. + +## Decision + +Causal repair `5f0d40b838e9b9867ec40281f1e4ab6db96a12cb` changes only `same_sign_mean_over_total`. It keeps the canonical Neumaier high part and correction separate, divides the high part by the original scientific denominator, recovers the division residual with binary64 FMA, and then carries that residual and the retained correction through the same denominator before restoring the exact power-of-two scale. This mirrors the already-established GAP-088 division boundary without changing the cancellation owner or public estimand. + +## Expected effect and risk + +The RED and its sign mirror now select the represented-input result rather than the predecessor's one-ULP neighbor. Existing same-sign and exact-cancellation paths retain their scientific denominator and scale policy. Remaining risk is intentionally bounded: `division_residual + correction` and the final correction addition are still binary64 operations, so unrelated payloads may require separate evidence before any stronger rounding claim is made. + +## Traceability + +- PR: #488, `fix/validation-bias-overflow-safe-mean` +- Public RED: `84476aad7ef2918c174c1ef986cbec2851cac656` +- Production repair: `5f0d40b838e9b9867ec40281f1e4ab6db96a12cb` +- Changelog: `5763cde0292b05fda4ebc07436e31e1309e97572` +- Production module: `crates/validation_core/src/numeric.rs` +- Public contract: `crates/validation_core/tests/bias_same_sign_remainder_division_contract.rs` + +## Standards and methodological authority + +IEEE 754-2019 remains the active IEEE floating-point standard as verified on 2026-09-05; IEEE P754 is an active revision PAR and not a published replacement. ISO/IEC 60559:2020 remains a published International Standard. The AERA/APA/NCME published testing standards remain the 2014 edition while the Joint Committee revision proceeds. Morris, White, and Crowther (2019) remains the methodological authority used here for defining simulation performance measures against known truth; it does not prescribe this implementation detail. + +## References + +American Educational Research Association, American Psychological Association, & National Council on Measurement in Education. (2014). *Standards for educational and psychological testing*. American Educational Research Association. + +IEEE. (2019). *IEEE standard for floating-point arithmetic* (IEEE Std 754-2019). https://doi.org/10.1109/IEEESTD.2019.8766229 + +International Organization for Standardization & International Electrotechnical Commission. (2020). *Information technology—Microprocessor systems—Floating-point arithmetic* (ISO/IEC 60559:2020). + +Morris, T. P., White, I. R., & Crowther, M. J. (2019). Using simulation studies to evaluate statistical methods. *Statistics in Medicine, 38*(11), 2074–2102. https://doi.org/10.1002/sim.8086 diff --git a/docs/research/bias-same-sign-subnormal-double-rounding.md b/docs/research/bias-same-sign-subnormal-double-rounding.md new file mode 100644 index 000000000..9c6bf75d2 --- /dev/null +++ b/docs/research/bias-same-sign-subnormal-double-rounding.md @@ -0,0 +1,67 @@ +# Same-sign subnormal mean double rounding + +## Decision status + +Proposed evidence for the Validation bounded context on PR #488. This note defines the represented-input boundary repaired by GAP-090. It does not claim globally correctly rounded binary64 summation or division. + +## Problem + +`same_sign_mean_over_total` normalized same-sign residuals by an exact power of two, formed a compensated normalized mean, and then restored the scale. That is safe from overflow, but when every represented residual is subnormal the restore step is itself a rounding to the coarser subnormal grid. A correctly rounded normalized intermediate can therefore be rounded a second time to the wrong final binary64 neighbor. + +Public RED `91abdb496ef13229ed95bcf8854770a2cc71b4b8` fixes three positive represented residuals whose subnormal-unit counts are `2^52 - 32`, `2^52 - 12`, and `2^52 - 20`. Their exact represented-input mean is + +`(3 * 2^52 - 64) / 3 = 2^52 - 21 - 1/3` + +minimum-subnormal units. Round-to-nearest, ties-to-even therefore selects bits `0x000f_ffff_ffff_ffeb` (`2^52 - 21`). The predecessor normalize-then-rescale path returned adjacent bits `0x000f_ffff_ffff_ffea` (`2^52 - 22`). The public contract includes the sign mirror. + +## Constraints + +- Keep the public estimand `mean(recovered - truth)` and the original recovery-unit denominator unchanged. +- Keep Validation Evidence arithmetic in TEPP; do not move this boundary into fast-mlsirm. +- Do not add arbitrary-precision runtime arithmetic. Binary64 subnormals already have an exact integer-unit representation at `2^-1074`. +- Preserve overflow-safe normalization for normal-scale inputs and for explicit-denominator cases whose surviving term count can exceed the divisor. +- Continue to fail closed when a mathematically nonzero represented mean rounds to zero under the existing admission policy. + +## Alternatives considered + +Keeping normalize-then-rescale was rejected because the RED demonstrates a real one-ULP double-rounding error at the public metric boundary. A first repair attempt, commit `e89dc34616dfba86683c8fc05611a7ac31a2d2c3`, bypassed scaling for all-subnormal same-sign inputs but still used floating-point compensated accumulation before division. Exact-unit checking found a separate halfway payload where that direct-float path selected the odd adjacent unit rather than ties-to-even. It was therefore not retained as the causal repair. + +Always replacing the mean path with exact integer or rational arithmetic was also rejected. The defect is specific to a bounded subnormal domain where each represented input is already an integer multiple of `2^-1074`; normal-scale and mixed-remainder behavior retains the existing deterministic binary64 reference. + +## Decision + +Corrected repair `1c0df8a77c4b65583e9d1945864f0a72bc598a71` handles the bounded case in represented subnormal units when `max_magnitude < f64::MIN_POSITIVE` and the scientific divisor is at least the surviving term count. Each magnitude contributes its exact 52-bit subnormal unit count. The total fits in `u128` on supported `usize` widths, the exact integer quotient and remainder are computed against the original divisor, and one final round-to-nearest, ties-to-even decision selects the binary64 subnormal result. Other paths retain the predecessor normalization and compensation policy. + +Edge contract `f79a1b9a1b299b34f87babbbdd766e4be6bd60df` covers both odd-floor and even-floor halfway cases. Follow-up fixture `eb29a79a3c51034dbff8d29782d8e71fe65012b6` records the halfway payload that invalidated the discarded direct-float repair: its exact unit sum leaves remainder 8 on division by 16, so the lower even unit `0x0009_2df1_1e7d_d9b8` must be selected. + +## Expected effect and remaining risk + +The affected same-sign all-subnormal `mean_bias` path now has a single final rounding decision in the represented unit system instead of a normalized rounding followed by subnormal rescaling. Positive and negative sign mirrors share the same magnitude rule. Normal-scale inputs, mixed-sign cancellation with retained roundoff, RMSE, coverage, Monte Carlo summaries, and `bias_standard_error` are unchanged. + +The repair is intentionally bounded. Explicit-denominator paths with a divisor smaller than the surviving term count still use the previous normalization path, because their quotient can leave the subnormal grid. Any defect there requires its own represented-input counterexample before widening this implementation. + +## Traceability + +- PR: #488, `fix/validation-bias-overflow-safe-mean` +- Public RED: `91abdb496ef13229ed95bcf8854770a2cc71b4b8` +- Discarded first repair: `e89dc34616dfba86683c8fc05611a7ac31a2d2c3` +- Corrected production repair: `1c0df8a77c4b65583e9d1945864f0a72bc598a71` +- Halfway edge coverage: `f79a1b9a1b299b34f87babbbdd766e4be6bd60df` +- Direct-float regression fixture: `eb29a79a3c51034dbff8d29782d8e71fe65012b6` +- Changelog: `65ff502b8f46f4043ca44983722e0f9722105a08` +- Production module: `crates/validation_core/src/numeric.rs` +- Public contract: `crates/validation_core/tests/bias_same_sign_subnormal_double_rounding_contract.rs` + +## Standards and methodological authority + +IEEE 754-2019 remains the active IEEE floating-point standard as verified on 2026-09-05; IEEE P754 is an active revision PAR approved 2024-06-06 and is not a published replacement. ISO/IEC 60559:2020 remains a published International Standard at stage 60.60. AERA, APA, and NCME continue to revise the 2014 *Standards for Educational and Psychological Testing*; the Joint Committee announced in 2024 is charged with revising that edition, so no unpublished revision is treated as normative authority. Morris, White, and Crowther (2019) remains the methodological basis for defining simulation performance measures against known truth; it does not prescribe this floating-point implementation. + +## References + +American Educational Research Association, American Psychological Association, & National Council on Measurement in Education. (2014). *Standards for educational and psychological testing*. American Educational Research Association. + +IEEE. (2019). *IEEE standard for floating-point arithmetic* (IEEE Std 754-2019). https://doi.org/10.1109/IEEESTD.2019.8766229 + +International Organization for Standardization & International Electrotechnical Commission. (2020). *Information technology—Microprocessor systems—Floating-point arithmetic* (ISO/IEC 60559:2020). + +Morris, T. P., White, I. R., & Crowther, M. J. (2019). Using simulation studies to evaluate statistical methods. *Statistics in Medicine, 38*(11), 2074–2102. https://doi.org/10.1002/sim.8086 diff --git a/docs/research/bias-standard-error-anchor-conditioning.md b/docs/research/bias-standard-error-anchor-conditioning.md new file mode 100644 index 000000000..16a1ce46b --- /dev/null +++ b/docs/research/bias-standard-error-anchor-conditioning.md @@ -0,0 +1,79 @@ +# Bias standard error exact-anchor conditioning + +## Problem + +GAP-106 made exact translated-residual admission permutation-invariant by considering every represented observation as an anchor and taking the first exact candidate in canonical `(high, low)` order. That removed transport-order drift, but canonical order is not a numerical conditioning criterion. When several anchors satisfy the same exactness proof, different exact translations can expose different binary64 magnitudes to the later square, compensated-sum, fused multiply-add, and square-root path. + +The public counterexample uses three exactly represented residuals with `truth = [0, 0, 0]`: + +- `middle = f64::from_bits(0x3ff7_c8a6_308f_7624)`, +- `low = f64::from_bits(0x3ff0_4284_fcf1_21a0)`, +- `high = f64::from_bits(0x3fff_659d_6d25_7410)`. + +For this represented multiset, the exact squared standard error of the mean is + +`850963186800334380866421373237 / 11408855402054064613470328848384`, + +whose correctly rounded binary64 square root is `0x3fd1_7a99_c875_b980`. + +All three observations are valid exact translation anchors. The canonical-low anchor produces translated magnitudes up to `0x1.e4630e068a4e0p-1` and the predecessor returns adjacent upper bits `0x3fd1_7a99_c875_b981`. The represented middle anchor produces translated magnitudes no larger than `0x1.e73dcf257f7b0p-2` and returns the correctly rounded `0x3fd1_7a99_c875_b980`. The estimand, represented residual multiset, and exact-admission predicate are unchanged; only the exact anchor's conditioning differs. + +This is deterministic arithmetic error, not Monte Carlo uncertainty. GAP-106 solved which *set* of observations may admit exact translation, but it still chose among several admissible representations using an arbitrary lexical criterion before a numerically sensitive second-moment reconstruction. + +## Constraints + +- TEPP owns Validation Evidence performance-measure arithmetic; reusable static psychometric estimation remains in `fast-mlsirm`. +- The existing error-free high delta, low delta, and recombined delta proofs remain mandatory. The repair must not classify any previously inexact translation as exact. +- Anchor selection must remain a function of the represented multiset rather than incoming observation order. +- The repair must not sort or otherwise rewrite the scientific observation pairing outside this local translation choice. +- Production arithmetic remains deterministic Rust binary64. No arbitrary-precision runtime dependency is added. +- Existing two-level algebraic shortcuts, subnormal rational projection, and fail-closed fallback semantics remain unchanged. + +## Decision + +Every represented observation remains an anchor candidate. For each candidate that satisfies all existing exactness proofs, TEPP computes the maximum absolute translated residual. It selects the exact candidate with the smallest such maximum magnitude. Canonical `(high, low)` total order is retained only as a deterministic tie-breaker. + +This criterion is local and causal: it does not claim a globally correctly rounded standard error for every `n > 2` sample. It removes an avoidably wide exact translation before the same power-of-two normalization and second-moment implementation. In the public counterexample, the centered represented observation halves the translated working radius and removes the one-ULP upward drift without changing the exact residual geometry. + +The selection is permutation-invariant because every candidate is evaluated and the objective plus tie-breaker depends only on represented candidate values. It is also monotone with respect to the specific numerical risk being repaired: among exact translations of the same multiset, a smaller maximum magnitude cannot increase the exponent range subsequently exposed to squaring. + +## Alternatives rejected + +1. **Keep the first exact anchor in canonical `(high, low)` order.** Rejected because the public contract shows that lexical canonicalization can select a one-ULP-worse exact representation. +2. **Use the incoming first or first exact observation.** Rejected because it reintroduces the permutation defect closed by GAP-106. +3. **Always use the median high residual.** Rejected because a median high part is not necessarily an exact anchor once subtraction low terms are part of the represented residual decomposition. +4. **Choose whichever anchor happens to reproduce an external high-precision oracle.** Rejected because production path selection cannot depend on an unavailable oracle and would amount to test-payload fitting. +5. **Replace the general path with arbitrary-precision variance arithmetic.** Rejected as broader than this bounded conditioning defect and outside the current Rust binary64 reference boundary. + +## Evidence and traceability + +- Public RED: `49343ab6e7f1a4cadc0b9c71e0757edea0256add` +- Causal source repair: `0fc3ea97106f5156a6d68d4db8fd6f4a2ace0ac4` +- CHANGELOG fragment: `2793ff5927ed3b79886cd3d4daa2369357c1710b` +- Module/API: `crates/validation_core/src/bias.rs` → `bias_standard_error` → `exact_translated_residual_standard_error` → `canonical_exact_translated_residuals` +- Public contract: `crates/validation_core/tests/bias_standard_error_anchor_conditioning_contract.rs` +- Correct represented result: `0x3fd1_7a99_c875_b980` +- GAP-106 canonical-low predecessor result: `0x3fd1_7a99_c875_b981` +- The contract covers low/middle/high-first permutations and sign mirrors so the GAP-106 permutation guarantee is retained while the conditioned anchor changes the numerical result. + +Hosted exact-head CI remains authoritative for GREEN after the surviving branch head is known. This repair is a bounded represented-input arithmetic acceptance claim, not a global correctly-rounded guarantee for all standard errors. + +## Standards and methodological basis + +IEEE 754-2019 remains the binary floating-point arithmetic basis for the Rust `f64` reference path. The active P754 project is a revision project that supersedes 754-2019 when completed; it is not yet a published replacement. ISO/IEC 60559:2020 remains the published international adoption of the 754-2019 arithmetic model. + +Morris, White, and Crowther (2019) distinguish deterministic performance-measure computation from Monte Carlo uncertainty. A fixed represented sample must therefore not acquire extra numerical variation from an avoidable choice among algebraically equivalent exact translations. + +The currently published AERA/APA/NCME *Standards for Educational and Psychological Testing* remains the 2014 edition while its announced revision is in progress. The published edition remains the normative testing reference until a replacement is issued. + +## References + +American Educational Research Association, American Psychological Association, & National Council on Measurement in Education. (2014). *Standards for educational and psychological testing*. American Educational Research Association. https://www.testingstandards.net/open-access-files.html + +Institute of Electrical and Electronics Engineers. (2019). *IEEE standard for floating-point arithmetic* (IEEE Std 754-2019). IEEE Standards Association. https://standards.ieee.org/ieee/754/6210/ + +Institute of Electrical and Electronics Engineers. (2024). *P754: Standard for floating-point arithmetic* [Active PAR]. IEEE Standards Association. https://standards.ieee.org/ieee/754/11684/ + +International Organization for Standardization, & International Electrotechnical Commission. (2020). *Information technology—Microprocessor systems—Floating-point arithmetic* (ISO/IEC 60559:2020). https://www.iso.org/standard/80985.html + +Morris, T. P., White, I. R., & Crowther, M. J. (2019). Using simulation studies to evaluate statistical methods. *Statistics in Medicine, 38*(11), 2074–2102. https://doi.org/10.1002/sim.8086 diff --git a/docs/research/bias-standard-error-anchor-permutation-invariance.md b/docs/research/bias-standard-error-anchor-permutation-invariance.md new file mode 100644 index 000000000..447fb1b66 --- /dev/null +++ b/docs/research/bias-standard-error-anchor-permutation-invariance.md @@ -0,0 +1,81 @@ +# Bias standard error exact-anchor permutation invariance + +## Problem + +GAP-105 fixed exact rational-scale projection at the normal/subnormal boundary, but the larger exact-translated residual path still selected `diffs[0]` as its translation anchor. Translation does not change a sample standard error, so the numerical admission decision must not depend on transport order. The predecessor did: if the first represented residual could not be subtracted exactly from every other represented residual, it abandoned the exact translated path even when another represented observation was a valid exact anchor. + +The public counterexample uses three exactly represented residuals with `truth = [0, 0, 0]`: + +- `low = f64::from_bits(0x4194_f788_9184_b980) = 46106761431411 / 524288`, +- `middle = f64::from_bits(0x420c_409f_fce3_8390) = 497022202165305 / 32768`, +- `high = f64::from_bits(0x4222_70c4_634c_c6b6) = 2595269181334363 / 65536`. + +For these represented inputs, the exact squared standard error of the mean is + +`327877142843256291246417577647793 / 2473901162496`, + +whose correctly rounded binary64 square root is `0x4205_7185_8078_f946`. + +With `middle` as the first residual, both `low - middle` and `high - middle` are exact binary64 differences, so the predecessor admits the translated second moment and returns `0x4205_7185_8078_f946`. With `low` first, `high - low` has a nonzero error-free subtraction tail of `2^-19`; with `high` first, `low - high` has the mirrored `-2^-19` tail. Those orderings reject the translated path and fall back to rounded-mean dispersion, returning adjacent lower bits `0x4205_7185_8078_f945`. + +The estimand and represented multiset are identical in every ordering. A one-ULP result change caused only by which observation is first is therefore a deterministic arithmetic defect, not Monte Carlo uncertainty. + +## Constraints + +- TEPP owns Validation Evidence performance-measure arithmetic. Reusable static psychometric estimators remain in `fast-mlsirm`. +- The repair must preserve the existing exact `high + low` translated-residual admission. It must not declare an inexact delta exact or weaken fail-closed behavior. +- Observation order is not scientific evidence and cannot select a different numerical estimator path. +- Production arithmetic remains deterministic Rust binary64; no arbitrary-precision runtime dependency is introduced. +- Existing two-level algebraic shortcuts, normal/subnormal projection policy, and fallback semantics remain intact. + +## Decision + +Exact translated-residual admission now considers every represented observation as a possible anchor, but candidate anchors are examined in canonical `(high, low)` binary64 total order. For each candidate, the implementation requires all three existing proofs to remain true for every observation: + +1. `high - anchor_high` is finite and error-free, +2. `low - anchor_low` is finite and error-free, +3. recombining the exact high and low deltas is finite and error-free. + +The first candidate in canonical order satisfying all three conditions supplies the translated residual vector. If no candidate satisfies them, the predecessor rounded-residual fallback remains authoritative. + +Canonical candidate ordering matters. Merely scanning observations in incoming order would fix the specific low-first payload only when a viable anchor happens to occur early, and multiple viable anchors could still make path selection transport-order dependent. Sorting the represented `(high, low)` candidate keys before admission makes the anchor choice a function of the represented multiset rather than array order. + +## Alternatives rejected + +1. **Keep `diffs[0]` as the anchor.** Rejected because the public contract demonstrates a one-ULP permutation violation for the same represented sample. +2. **Always choose the median high residual.** Rejected because subtraction low terms are part of the exact residual representation; a high-part median is not guaranteed to be an exact anchor for both high and low deltas. +3. **Scan anchors in incoming order and stop at the first exact candidate.** Rejected because the chosen exact anchor would still depend on transport order when multiple candidates are viable. +4. **Sort the whole observation sample before all Validation Evidence arithmetic.** Rejected as broader than the defect and capable of changing unrelated pairing/provenance assumptions. +5. **Use arbitrary-precision arithmetic for every standard error.** Rejected as substantially broader than this bounded exact-admission defect. + +## Evidence and traceability + +- Public RED: `5cb45a4f204ab4fbcd5581c4d4504e82f0339a30` +- Causal source repair: `159659a9510a7ced437ad872d02e26619abc8236` +- CHANGELOG fragment: `7e57b930daebb01b3583b4a5108ce3c1a89a06a6` +- Module/API: `crates/validation_core/src/bias.rs` → `bias_standard_error` → `exact_translated_residual_standard_error` +- Public contract: `crates/validation_core/tests/bias_standard_error_anchor_permutation_contract.rs` +- Correct represented result: `0x4205_7185_8078_f946` +- Predecessor low/high-anchor result: `0x4205_7185_8078_f945` +- Middle-anchor predecessor result: `0x4205_7185_8078_f946` +- Sign-mirrored permutations must produce the same positive standard error. + +The contract is intentionally a represented-input arithmetic acceptance test. It does not claim that every `n > 2` standard error is globally correctly rounded, and it does not expand the set of deltas treated as exact. Hosted exact-head CI remains the authority for GREEN after the surviving branch head is known. + +## Standards and methodological basis + +IEEE 754-2019 defines the binary floating-point arithmetic model used by the Rust `f64` reference path. ISO/IEC 60559:2020 adopts that floating-point model internationally. The relevant engineering requirement here is not a new statistical estimator: it is deterministic use of the same represented input multiset without making an incidental array position numerically authoritative. + +Morris, White, and Crowther (2019) distinguish deterministic simulation performance measures from Monte Carlo uncertainty. Bias and empirical standard-error evidence should therefore not acquire extra variation from observation permutation inside the implementation. + +The currently published AERA/APA/NCME *Standards for Educational and Psychological Testing* remains the 2014 edition while the announced revision is in progress. The published edition is retained as the normative testing reference until a replacement is actually issued. + +## References + +American Educational Research Association, American Psychological Association, & National Council on Measurement in Education. (2014). *Standards for educational and psychological testing*. American Educational Research Association. https://www.testingstandards.net/open-access-files.html + +Institute of Electrical and Electronics Engineers. (2019). *IEEE standard for floating-point arithmetic* (IEEE Std 754-2019). IEEE Standards Association. https://standards.ieee.org/ieee/754/6210/ + +International Organization for Standardization, & International Electrotechnical Commission. (2020). *Information technology—Microprocessor systems—Floating-point arithmetic* (ISO/IEC 60559:2020). https://www.iso.org/standard/80985.html + +Morris, T. P., White, I. R., & Crowther, M. J. (2019). Using simulation studies to evaluate statistical methods. *Statistics in Medicine, 38*(11), 2074–2102. https://doi.org/10.1002/sim.8086 diff --git a/docs/research/bias-standard-error-common-high-mean-roundoff.md b/docs/research/bias-standard-error-common-high-mean-roundoff.md new file mode 100644 index 000000000..13ce9d155 --- /dev/null +++ b/docs/research/bias-standard-error-common-high-mean-roundoff.md @@ -0,0 +1,39 @@ +# Common-high subtraction low terms must not inherit a rounded-mean dispersion geometry + +## Problem + +`bias_standard_error` already retained error-free subtraction low terms when several represented-input residuals rounded to one common binary64 high part. The predecessor then computed a representable mean of those low terms and centered each low term on that rounded mean before evaluating dispersion. + +For `truth = [2^-54, 0, 0]` and `recovered = [1, 1, 1]`, all three binary64 subtractions return `1.0`, but the represented-input residuals are exactly `[1 - 2^-54, 1, 1]`. The common high part is translation-invariant and contributes no dispersion. The retained low terms are `[-2^-54, 0, 0]`, whose represented-input standard error is exactly `2^-54 / 3` before final binary64 rounding, bits `0x3c75_5555_5555_5555`. + +The predecessor first rounded the low-term mean `-2^-54 / 3`, then formed three deviations from that rounded mean. That changes the represented dispersion geometry and returns the adjacent higher binary64 value. The sign-mirrored input has the same standard error and exposes the same defect. + +## Constraints and rejected alternatives + +The repair stays inside TEPP Validation Evidence. It does not create a reusable psychometric estimator, copy fast-mlsirm arithmetic, or change Longitudinal Modeling composition. It also does not claim globally correctly rounded standard errors for arbitrary binary64 samples. + +Replacing the whole standard-error implementation with arbitrary-precision production arithmetic was rejected because the counterexample only requires preserving a translation-invariant represented-input identity already used by the current Validation path. Keeping the rounded low-term mean as authoritative was rejected because the public RED proves that its intermediate rounding changes the target quantity. Removing the common-high path altogether was also rejected: when low-term anchor differences cannot be proven exactly representable, the existing bounded fallback still preserves earlier nonzero-dispersion behavior better than collapsing to the rounded high parts. + +## Decision + +When all rounded residual highs are equal and subtraction roundoff is present, first run the existing exact translated-residual second-moment calculation over the retained low terms with zero secondary roundoff. This path is admitted only when every anchor-relative low-term delta is exactly representable under the same proof used by GAP-095/096. If that proof fails, retain the predecessor scaled low-term mean/deviation fallback. + +Public contract: `crates/validation_core/tests/bias_standard_error_common_high_mean_roundoff_contract.rs`. + +RED commit: `d48e2515d62dfbe0a807b5dba40fbb7034d4fa9d`. + +Causal repair: `a63b8d7a8e79146cbb17ceb17855bcca312535a1`. + +CHANGELOG: `6fc8e82839389f2f8c07e1cf7ba78a29f19d2510`. + +## Scientific trace + +Bias and its Monte Carlo uncertainty are performance measures against known truth; preserving the represented-input statistic is therefore part of Validation Evidence rather than an LLM or projection judgment. Binary64 rounding behavior follows the published IEEE floating-point arithmetic contract. + +IEEE Computer Society. (2019). *IEEE standard for floating-point arithmetic* (IEEE Std 754-2019). IEEE. https://doi.org/10.1109/IEEESTD.2019.8766229 + +Morris, T. P., White, I. R., & Crowther, M. J. (2019). Using simulation studies to evaluate statistical methods. *Statistics in Medicine, 38*(11), 2074–2102. https://doi.org/10.1002/sim.8086 + +## Remaining risk + +This repair proves the common-high counterexample and its sign mirror only under exact anchor-relative low-term translation. If those translated low-term deltas themselves require rounding, the bounded predecessor fallback remains. A wider change requires a separate represented-input counterexample and RED rather than extrapolation from this case. diff --git a/docs/research/bias-standard-error-distinct-high-roundoff.md b/docs/research/bias-standard-error-distinct-high-roundoff.md new file mode 100644 index 000000000..424bffd8a --- /dev/null +++ b/docs/research/bias-standard-error-distinct-high-roundoff.md @@ -0,0 +1,62 @@ +# Bias standard error with distinct rounded residual highs + +## Finding + +`bias_standard_error` previously preserved subtraction low terms only when every rounded residual high part was identical. That condition is sufficient for the false-zero case repaired by GAP-093, but it is not necessary for discarded pairwise-subtraction mass to change a nonzero uncertainty estimate. + +Let `q = 2^-54`, `truth = [q, 3q, 0]`, and `recovered = [1, 1, 1]`. The represented inputs define exact residuals + +- `r1 = 1 - q`, +- `r2 = 1 - 3q`, +- `r3 = 1`. + +Their exact mean is `1 - 4q/3`, so the centered deviations are `[q/3, -5q/3, 4q/3]`. Therefore + +`SE = sqrt(sum((ri-r̄)^2) / (3*2)) = q * sqrt(7) / 3`, + +which rounds to binary64 bits `0x3c8c_38aa_37c3_f68d`. + +The predecessor first rounded each pairwise subtraction. Ties-to-even yields `[1, 1-2^-52, 1]`, whose rounded-residual path reports `0x3c96_a09e_667f_3bcd`. The difference is not a cosmetic ULP boundary: the predecessor is about 60% larger because it turns three nearby exact represented residuals into a different dispersion geometry before computing the standard error. + +## Constraints + +This is TEPP Validation Evidence performance-measure semantics. It does not create a reusable psychometric estimator and does not move static psychometric arithmetic away from `fast-mlsirm`. The repair must remain deterministic CPU `f64`, O(n), overflow-aware, and must not introduce arbitrary-precision runtime arithmetic or a second standard-error definition. + +The branch already has specialized exact handling for n=2 and for n>2 when every rounded residual high part collapses to one value. Those contracts remain authoritative. A wider repair must not replace them or claim global correct rounding for every finite input. + +## Alternatives considered + +Computing the standard error from rounded residuals was rejected because the RED proves that the rounded residual vector is not the represented-input scientific target. Computing all pairwise exact residual differences was also rejected: the identity is valid, but an O(n²) implementation is unnecessary for a production validation path. + +Reconstructing an arbitrary-precision residual vector in production was rejected because the represented inputs are binary64 and the owner contract requires a deterministic Rust `f64` reference. It would add runtime complexity without evidence that every path needs arbitrary precision. + +The selected repair uses the existing error-free subtraction decomposition `ri = high_i + low_i`. It chooses one residual as a translation anchor and admits the refinement only when every anchor-relative `high` difference, `low` difference, and their final sum are each exactly representable in binary64. Translation leaves variance unchanged. With the anchor translated to exact zero, the second-moment identity + +`SE² = (n * sum(di²) - sum(di)²) / (n² * (n-1))` + +can be evaluated in O(n). The zero anchor also bounds cancellation in the numerator because `sum(di)² <= (n-1) * sum(di²)`. If exact translated deltas cannot be established, the predecessor rounded-residual path remains in force rather than silently widening the numerical claim. + +## Evidence and traceability + +| Evidence | Exact reference | Role | +| --- | --- | --- | +| Public RED | `ae906ad609aa4eb948b8311c1cebd975f55eb2f6` | Adds the distinct-high counterexample, sign mirror, and a no-subtraction-roundoff control. | +| Causal repair | `d59b9c30bf810a6dc6fdceaba6d20e048fad985a` | Retains error-free low terms through exact anchor-relative deltas and an O(n) translation-invariant second moment. | +| CHANGELOG | `89c018630d5baadef04a8167034951b7e378a4b2` | Records the buyer-visible uncertainty correction and bounded scope. | +| Module/API | `crates/validation_core/src/bias.rs::bias_standard_error` | Canonical TEPP Validation Evidence producer. | +| Public contract | `crates/validation_core/tests/bias_standard_error_distinct_high_roundoff_contract.rs` | Exact positive case, sign symmetry, and unchanged exact-residual control. | +| Landing vehicle | PR `#488` | Exact surviving head and hosted evidence remain PR-authoritative. | + +The fix does **not** claim globally correctly rounded n>2 bias standard errors. In particular, if an anchor-relative exact residual delta itself needs rounding, this repair intentionally declines the exact-delta path until an independent represented-input counterexample justifies a further widening. + +## Methodological authority + +Bias and Monte Carlo uncertainty are performance measures that should be evaluated against known truth rather than implementation artifacts (Morris et al., 2019). The binary64 behavior described here is interpreted under IEEE 754-2019 / ISO/IEC 60559:2020 round-to-nearest, ties-to-even semantics. The broader validity claim remains evidence-oriented rather than numerical-method substitution: the *Standards for Educational and Psychological Testing* require validation evidence appropriate to the proposed interpretation and use of scores (AERA, APA, & NCME, 2014). + +## References + +American Educational Research Association, American Psychological Association, & National Council on Measurement in Education. (2014). *Standards for educational and psychological testing*. American Educational Research Association. + +IEEE. (2019). *IEEE standard for floating-point arithmetic* (IEEE Std 754-2019). https://doi.org/10.1109/IEEESTD.2019.8766229 + +Morris, T. P., White, I. R., & Crowther, M. J. (2019). Using simulation studies to evaluate statistical methods. *Statistics in Medicine, 38*(11), 2074–2102. https://doi.org/10.1002/sim.8086 diff --git a/docs/research/bias-standard-error-dyadic-two-level-count-rounding.md b/docs/research/bias-standard-error-dyadic-two-level-count-rounding.md new file mode 100644 index 000000000..b4c498ead --- /dev/null +++ b/docs/research/bias-standard-error-dyadic-two-level-count-rounding.md @@ -0,0 +1,63 @@ +# Bias standard error: dyadic two-level count geometry + +## Problem + +`validation_core::bias_standard_error` already preserves exact anchor-relative residual translations before evaluating larger-sample dispersion. The remaining two-level path still reconstructed dispersion from translated sums and squared values unless one residual level occurred exactly once. That reconstruction can round an otherwise exact count identity before the final square root. + +For an exactly translated sample with `m` observations at residual level `0`, `n-m` observations at residual level `g`, and `n > 1`, the represented-input sample standard error of the mean is + +`SE(mean) = |g| * sqrt(m(n-m) / (n^2(n-1)))`. + +The identity follows directly from the two-level sample mean and centered squared-deviation sum; no estimated latent quantity or LLM judgment is involved. + +## RED + +Public RED: `3bc43da21784d3bf2f506c2ffdaa66c90a76d85d` with `crates/validation_core/tests/bias_standard_error_two_level_count_rounding_contract.rs`. + +Let `g = next_down(1.0) = 0x1.fffffffffffffp-1`, `n = 16`, and use six residuals at `0` plus ten residuals at `g`. Then + +`m(n-m)/(n-1) = 6*10/15 = 4`, so + +`SE(mean) = |g| * sqrt(4 / 16^2) = |g| / 8`. + +Because division by eight is an exact binary exponent shift for this normal represented gap, the required binary64 result is `0x1.fffffffffffffp-4`, bits `0x3fbf_ffff_ffff_ffff`. The predecessor translated sum/square/square-root path returned `0x1.0000000000000p-3`, bits `0x3fc0_0000_0000_0000`, moving the standard error upward by one ULP. The contract also fixes permutation invariance, sign-mirrored dispersion, and fail-closed behavior when the same nonzero dyadic result would underflow from a minimum-subnormal gap. + +The hosted workflows created for the RED commit were cancelled after the branch advanced; they are not represented as completed RED execution evidence. The mathematical oracle and executable public contract remain the reproducer. + +## Causal repair + +Repair: `77ba10026d23252314f04d95a67ce1cfeb5e54a0`. + +After the existing exact translated-residual admission proves two represented residual levels, TEPP now checks the level counts with exact integer arithmetic. A direct path is used only when the count-only factor satisfies + +`m(n-m) * d^2 = n^2(n-1)` + +for a power-of-two divisor `d`. In that bounded case the requested standard error is exactly `|g| / d`, so the code applies the dyadic scaling before any rounded moment reconstruction. The existing singleton-level identity remains unchanged. Non-singleton two-level samples whose count geometry does not prove a reciprocal power-of-two factor continue through the prior exact-translated second-moment path. + +The repair uses checked `u128` count arithmetic. Failure to prove the dyadic relation is a fallback condition, not permission to approximate the count identity. A nonzero exact dyadic result that becomes binary64 zero remains `ValidationError::InvalidInput` under the existing no-false-perfect-recovery policy. + +## Alternatives rejected + +A general two-level closed form using a rounded floating-point count ratio was rejected for this change. It removes translated-moment cancellation in some cases but introduces a different rounding projection in others and therefore does not establish a stronger represented-input contract without a separate correctly-rounded square-root/product proof. + +A payload-specific `n = 16, m = 6` branch was rejected because sample-size constants are not a scientific abstraction. The accepted predicate is the algebraic dyadic count relation itself. + +Arbitrary-precision runtime arithmetic was rejected because this bounded binary64 identity is exactly decidable from integer counts plus power-of-two scaling. Adding a second numerical runtime for this case would increase production complexity without changing the estimand. + +## Scope and risk + +This repair does not claim globally correctly rounded `bias_standard_error` for every `n > 2` sample, nor does it redefine reusable static psychometric estimation owned by `fast-mlsirm`. It changes only TEPP Validation Evidence arithmetic after exact residual translation has already been established. Non-dyadic two-level count factors and general multi-level residual samples retain their prior bounded path and therefore remain candidates for separately demonstrated RED findings rather than implicit expansion of GAP-102. + +## Traceability + +- Public contract: `crates/validation_core/tests/bias_standard_error_two_level_count_rounding_contract.rs`. +- Production module/API: `crates/validation_core/src/bias.rs` → `bias_standard_error` and `exact_translated_residual_standard_error`. +- RED: `3bc43da21784d3bf2f506c2ffdaa66c90a76d85d`. +- Repair: `77ba10026d23252314f04d95a67ce1cfeb5e54a0`. +- CHANGELOG: `CHANGELOG.d/validation-bias-standard-error-dyadic-two-level-count.md` at `f165914eaceb17780370ca4f2e446c22efcf15ea`. + +## References + +IEEE Computer Society. (2019). *IEEE standard for floating-point arithmetic* (IEEE Std 754-2019). https://doi.org/10.1109/IEEESTD.2019.8766229 + +Morris, T. P., White, I. R., & Crowther, M. J. (2019). Using simulation studies to evaluate statistical methods. *Statistics in Medicine, 38*(11), 2074–2102. https://doi.org/10.1002/sim.8086 diff --git a/docs/research/bias-standard-error-eight-observation-pair-distance-rounding.md b/docs/research/bias-standard-error-eight-observation-pair-distance-rounding.md new file mode 100644 index 000000000..3b94d1db1 --- /dev/null +++ b/docs/research/bias-standard-error-eight-observation-pair-distance-rounding.md @@ -0,0 +1,69 @@ +# Eight-observation mean-bias standard error: exact pair-distance rounding + +## Problem + +After GAP-116, the bounded pair-distance/midpoint proof covered four through seven observations. An eight-observation sample with exact represented residuals still fell through to the translated floating second-moment path: + +```text +truth = [0, 0, 0, 0, 0, 0, 0, 0] +recovered = [121838114, 741324193, 994684355, 1673429661, + 1824103795, 1861311798, 1872117478, 1936395613] +``` + +All values are exactly represented binary64 integers and all pairwise differences are exact. The twenty-eight squared pair distances sum to + +```text +N = 25648518292283252135. +``` + +For `n=8`, + +```text +SE(mean)^2 = sum_{i2` standard errors. Extending the bound again requires a demonstrated represented-input counterexample plus numerical and performance evidence rather than a speculative increase. + +CHANGELOG `2641b2613a7c37e4153d9b8baa47487a1b80718e` records the contract. GAP-116 and all preceding Validation Evidence lineages remain inherited. + +## Alternatives rejected + +A payload-specific eight-value branch would encode a fixture rather than the estimator invariant. Replacing the translated fallback with an unconditional pair-distance formula would impose O(n^2) work on ordinary larger samples without evidence that the commercial path needs it. Raising the bound speculatively beyond the demonstrated case would likewise add quadratic work without a scientific defect to justify it. Arbitrary-precision production arithmetic would broaden ownership and dependency surface far beyond the demonstrated defect. The bounded admission instead reuses the existing checked dyadic proof and changes only its evidenced upper sample count. + +## Method and standards trace + +Morris, White, and Crowther (2019) distinguish deterministic performance-measure calculations from Monte Carlo uncertainty; a one-ULP deterministic bias-SE error is therefore repaired in the estimator rather than treated as simulation noise. Published floating-point authority remains IEEE 754-2019 / ISO/IEC 60559:2020. IEEE P754 is an active PAR approved June 6, 2024 to supersede 754-2019; it is revision work, not a published replacement. AERA, APA, and NCME continue to publish the 2014 *Standards for Educational and Psychological Testing* while their Joint Committee carries out the next revision. + +## Traceability + +- Bounded context: Validation Evidence. +- Production module/API: `crates/validation_core/src/bias_se.rs` → `bias_standard_error`. +- Public executable contract: `crates/validation_core/tests/bias_standard_error_eight_observation_pair_distance_contract.rs`. +- RED: `40413dc9e3279f06c3d02ed2180ac9c170a51001`. +- Causal source repair: `26ca2d68bf057b93b9fef1e14953bedbdcf754c0`. +- CHANGELOG: `2641b2613a7c37e4153d9b8baa47487a1b80718e`. +- Scientific invariant: for exact represented residuals admitted by the bounded proof, `SE(mean)^2 = sum_{i2` standard errors. `n=12` remains on the established path until a separate represented-input counterexample and performance evidence justify another extension. + +CHANGELOG `2e9c6a54c417ccbe4e567b8f70e50175f1c47553` records the contract. GAP-119 and all preceding Validation Evidence lineages remain inherited. + +## Alternatives rejected + +A payload-specific eleven-value branch would encode a fixture rather than the estimator invariant. Replacing the translated fallback with an unconditional pair-distance formula would impose O(n^2) work on ordinary larger samples without evidence that the commercial path needs it. Raising the bound speculatively beyond the demonstrated case would likewise add quadratic work without a scientific defect to justify it. Arbitrary-precision production arithmetic would broaden ownership and dependency surface beyond the demonstrated defect. The bounded admission reuses the existing checked dyadic proof and changes only its evidenced upper sample count. + +## Method and standards trace + +Morris, White, and Crowther (2019) distinguish deterministic performance-measure calculations from Monte Carlo uncertainty; a one-ULP deterministic bias-SE error is therefore repaired in the estimator rather than treated as simulation noise. Published floating-point authority remains IEEE 754-2019 / ISO/IEC 60559:2020. IEEE P754 remains an active PAR approved June 6, 2024 to supersede 754-2019; it is revision work, not a published replacement. ISO continues to list ISO/IEC 60559:2020 as a published International Standard at stage 60.60. AERA, APA, and NCME continue revising the 2014 *Standards for Educational and Psychological Testing*; the unpublished revision is not treated as normative authority. + +## Traceability + +- Bounded context: Validation Evidence. +- Production module/API: `crates/validation_core/src/bias_se.rs` → `bias_standard_error`. +- Public executable contract: `crates/validation_core/tests/bias_standard_error_eleven_observation_pair_distance_contract.rs`. +- RED: `967e1e093603cbefc01889adce0087d744bfcd90`. +- Causal source repair: `59c5a4ef3bb9693329bfb359cecf3bbd93eecb3b`. +- CHANGELOG: `2e9c6a54c417ccbe4e567b8f70e50175f1c47553`. +- Scientific invariant: for exact represented residuals admitted by the bounded proof, `SE(mean)^2 = sum_{i 2`, exact residual vectors can therefore use the same bounded translation-invariant path before any rounded residual mean is made authoritative. + +For the RED, anchor translation yields `[0, -2^-52, 0]`. After scale reduction the second-moment numerator is evaluated from `[0,-1,0]`, giving the exact normalized ratio `1/9` and the final `2^-52/3` result. A symmetric exact-residual control remains on the same translated identity and preserves its established result. + +This does not claim globally correctly rounded n>2 bias standard errors. The admitted path proves only that the translated residual deltas themselves are representable; later binary64 multiplication, compensated summation, division, square root, and scale restoration remain bounded operations whose independent counterexamples must be demonstrated before further widening. + +## Evidence and traceability + +| Evidence | Exact reference | Role | +| --- | --- | --- | +| Public RED | `9265b34a2163f45bd232d628c20f725d2844f50a` | Adds the exact-residual rounded-mean counterexample and its sign mirror; replaces GAP-095's invalid predecessor-preservation control with a symmetric scientific control. | +| Causal repair | `2fa266b21069460370f30243cfb498e2022888bf` | Admits the exact translated-residual second-moment path for all larger samples when its exact-delta preconditions hold, not only when subtraction roundoff is nonzero. | +| CHANGELOG | `ab361b25ec4296165f89e4eff26e6d3521c00571` | Records the buyer-visible uncertainty correction and bounded scope. | +| Module/API | `crates/validation_core/src/bias.rs::bias_standard_error` | Canonical TEPP Validation Evidence producer. | +| Public contract | `crates/validation_core/tests/bias_standard_error_exact_residual_mean_roundoff_contract.rs` | Exact positive/sign-mirror contract with expected bits `0x3c95_5555_5555_5555`. | +| GAP-095 regression | `crates/validation_core/tests/bias_standard_error_distinct_high_roundoff_contract.rs` | Keeps the distinct-high subtraction-roundoff repair and replaces the scientifically invalid control. | +| Landing vehicle | PR `#488` | Surviving Validation Evidence head; hosted/current-head evidence remains PR-authoritative. | + +## Methodological authority + +Morris, White, and Crowther (2019) treat bias and uncertainty as performance measures evaluated against known simulation truth and explicitly recommend reporting Monte Carlo uncertainty. The binary64 behavior here is interpreted under IEEE 754-2019 / ISO/IEC 60559:2020 arithmetic. The AERA/APA/NCME *Standards for Educational and Psychological Testing* (2014) remains the published edition while its sponsoring organizations revise that edition; validation claims therefore remain tied to published evidence rather than an unpublished revision. + +## References + +American Educational Research Association, American Psychological Association, & National Council on Measurement in Education. (2014). *Standards for educational and psychological testing*. American Educational Research Association. + +IEEE. (2019). *IEEE standard for floating-point arithmetic* (IEEE Std 754-2019). https://doi.org/10.1109/IEEESTD.2019.8766229 + +Morris, T. P., White, I. R., & Crowther, M. J. (2019). Using simulation studies to evaluate statistical methods. *Statistics in Medicine, 38*(11), 2074–2102. https://doi.org/10.1002/sim.8086 diff --git a/docs/research/bias-standard-error-five-observation-pair-distance-rounding.md b/docs/research/bias-standard-error-five-observation-pair-distance-rounding.md new file mode 100644 index 000000000..231271769 --- /dev/null +++ b/docs/research/bias-standard-error-five-observation-pair-distance-rounding.md @@ -0,0 +1,74 @@ +# Five-observation mean-bias standard error: exact pair-distance rounding + +## Problem + +The Validation Evidence `bias_standard_error` contract evaluates the standard error of represented signed recovery bias. After GAP-113, the bounded exact pair-distance/midpoint proof applied only to four observations. A five-observation sample with exact represented residuals could therefore fall through to the translated floating second-moment path even when every residual and every pairwise difference was exact. + +For + +```text +truth = [0, 0, 0, 0, 0] +recovered = [1342748146, 1434848064, 1525257611, 1685877224, 1771341094] +``` + +all inputs are exactly represented binary64 integers. The ten exact squared pair distances sum to + +```text +N = 621298477313343404 +``` + +For `n = 5`, the pair-distance identity is + +```text +SE(mean)^2 = sum_{i 2`. Larger samples and four-observation samples that cannot satisfy the bounded exact proof remain on the existing deterministic fallback and are candidates for later findings only when a realistic represented-input counterexample is established. + +## Standards and methodological trace + +IEEE P754 is currently an Active PAR, approved 2024-06-06, to revise/supersede IEEE 754-2019; it is not yet a published replacement. ISO/IEC 60559:2020 remains a published International Standard (stage 60.60) specifying floating-point formats, arithmetic, exceptions, and uniquely determined results for specified operations and destination formats. These standards support treating the sequence of rounding operations as part of the numerical contract rather than assuming algebraically equivalent source expressions are representation-equivalent. + +The AERA/APA/NCME public testing authority remains the 2014 *Standards for Educational and Psychological Testing* while a Joint Committee is revising that edition. No unpublished revision is treated as normative authority here. + +Morris, White, and Crowther (2019) frame simulation evaluation around known truth, estimands, methods, and explicit performance measures, and separately require Monte Carlo standard errors for uncertainty caused by finite simulation repetitions. That distinction is material here: `bias_standard_error` is itself a deterministic performance-measure calculation on represented inputs, so a reproducible one-ULP arithmetic error is not Monte Carlo error. + +### References + +American Educational Research Association, American Psychological Association, & National Council on Measurement in Education. (2014). *Standards for educational and psychological testing*. American Educational Research Association. + +IEEE. (2019). *IEEE standard for floating-point arithmetic* (IEEE Std 754-2019). + +International Organization for Standardization. (2020). *Information technology—Microprocessor systems—Floating-point arithmetic* (ISO/IEC 60559:2020). + +Morris, T. P., White, I. R., & Crowther, M. J. (2019). Using simulation studies to evaluate statistical methods. *Statistics in Medicine, 38*(11), 2074–2102. https://doi.org/10.1002/sim.8086 + +## Traceability + +- bounded context: Validation Evidence; +- public API: `validation_core::bias_standard_error`; +- implementation admission: `crates/validation_core/src/bias_se.rs`; +- established fallback: `crates/validation_core/src/bias.rs`; +- public RED: `crates/validation_core/tests/bias_standard_error_four_observation_ratio_sqrt_rounding_contract.rs` at `978c422cbdccff02605b5d220bd2564900a830d5`; +- source/test lineage: `3de7a737...` through `c8032493...`; +- CHANGELOG: `CHANGELOG.d/validation-bias-standard-error-four-observation-ratio-sqrt-rounding.md` at `49c46aa4...`; +- landing vehicle: PR #488; +- predecessor retained: GAP-110 and all inherited Validation Evidence lineages remain in ancestry. diff --git a/docs/research/bias-standard-error-four-observation-reduced-ratio-admission.md b/docs/research/bias-standard-error-four-observation-reduced-ratio-admission.md new file mode 100644 index 000000000..b09970d7b --- /dev/null +++ b/docs/research/bias-standard-error-four-observation-reduced-ratio-admission.md @@ -0,0 +1,88 @@ +# Bias standard error: reduced exact four-observation ratio admission + +## Finding + +GAP-111 added a bounded exact four-observation pair-distance path so that `SE(mean)` is rounded from the exact rational radicand instead of from a binary64-rounded ratio. Its admission still tested the *unreduced* pair-square numerator against the binary64 exact-integer bound. That made proof admission depend on an algebraically irrelevant representation of the same rational number. + +A represented-input counterexample is + +`r = [0, 14_099_687, 16_729_100, 94_045_527]`. + +Every residual and every pairwise difference is exactly represented in binary64. The six squared pair distances are + +`198801173497969`, `279862786810000`, `8844561148707729`, `6913812724569`, `6391337333305600`, and `5977829884046329`, + +which sum exactly to + +`N = 21699306139092196`. + +For four observations, + +`SE(mean)^2 = N / 48`. + +The exact rational reduces by `gcd(N, 48) = 4` to + +`5424826534773049 / 12`. + +The unreduced numerator is greater than `2^53`, so GAP-111 refused its bounded midpoint-square proof and returned to the translated floating path. That fallback returns adjacent-lower bits `0x4174_46e5_76f8_7444`. The *reduced* numerator `5424826534773049` is below `2^53`; applying the same exact midpoint comparison to the identical rational gives the correctly rounded represented-input target `0x4174_46e5_76f8_7445`. + +This is a deterministic Validation Evidence arithmetic defect. No data, estimator target, or probabilistic assumption changes. + +## RED and causal repair + +Public RED `4f2231354fa0ad3e0c646bbb100b3cc83566d033` adds `crates/validation_core/tests/bias_standard_error_four_observation_reduced_ratio_contract.rs`. It fixes several permutations of the exact residual multiset and their sign mirrors at bits `0x4174_46e5_76f8_7445`. + +Causal source repair `ed1a8763c198fbe478de5ec8be72e3436e5918e3` changes only the GAP-111 bounded admission in `crates/validation_core/src/bias_se.rs`. After the exact pair-square sum is constructed in checked `u128`, the implementation computes the integer greatest common divisor with the scientific denominator `48`, divides numerator and denominator by that divisor, and then invokes the existing exact adjacent-midpoint square comparison. The rational radicand is unchanged. + +The admission boundary remains narrow: + +- exactly four observations; +- every represented residual is finite and subtraction-error-free; +- every pairwise residual difference is finite and subtraction-error-free; +- the dyadic pair-distance numerator fits the existing checked `u128` construction; +- after exact rational reduction, numerator and denominator fit the existing bounded binary64-integer proof; +- exact candidate/neighbor midpoint-square comparison can complete without integer overflow. + +Any failed proof still returns to `crates/validation_core/src/bias.rs`. No arbitrary-precision runtime, mutable sibling dependency, or new reusable psychometric arithmetic owner is introduced. + +## Alternatives rejected + +Keeping the unreduced numerator and increasing the `2^53` admission limit was rejected because `numerator as f64` would cease to be exact and would invalidate the proof that the candidate is derived from the represented rational without an extra integer-conversion rounding. + +Special-casing this residual payload was rejected because the defect is rational representation, not the particular values. + +Changing every four-observation evaluation to a different floating formula was rejected because algebraic rearrangement alone does not provide a correct-rounding proof and would widen the rounding surface beyond the established GAP-111 contract. + +Arbitrary-precision production arithmetic was rejected as disproportionate to the finding and outside TEPP's owner boundary. Reusable static psychometric arithmetic remains owned by `fast-mlsirm`; this code is a bounded Validation Evidence admission proof. + +GAP-112 does not claim globally correctly rounded `bias_standard_error` for arbitrary `n > 2`, nor does it admit four-observation samples whose exactness or bounded integer proof cannot be established. + +## Standards and methodological trace + +IEEE 754-2019 remains the published IEEE floating-point standard used for the binary64 destination-format reasoning here. IEEE P754 is the active revision project rather than a published replacement. ISO/IEC 60559:2020 remains the corresponding published international floating-point standard. The relevant engineering consequence is that an exact rational identity does not make two sequences of rounded operations representation-equivalent; proof admission must preserve the exact value being compared with destination-format rounding boundaries. + +The AERA/APA/NCME public testing authority remains the 2014 *Standards for Educational and Psychological Testing* while revision work proceeds. The Validation Evidence interpretation therefore stays tied to the published edition rather than an unpublished revision. + +Morris, White, and Crowther (2019) distinguish deterministic performance-measure calculation from Monte Carlo uncertainty due to finite simulation repetitions. GAP-112 concerns the former: for fixed represented inputs, a one-ULP discrepancy caused by an unnecessary proof fallback is not Monte Carlo error. + +### References + +American Educational Research Association, American Psychological Association, & National Council on Measurement in Education. (2014). *Standards for educational and psychological testing*. American Educational Research Association. + +IEEE. (2019). *IEEE standard for floating-point arithmetic* (IEEE Std 754-2019). + +International Organization for Standardization. (2020). *Information technology—Microprocessor systems—Floating-point arithmetic* (ISO/IEC 60559:2020). + +Morris, T. P., White, I. R., & Crowther, M. J. (2019). Using simulation studies to evaluate statistical methods. *Statistics in Medicine, 38*(11), 2074–2102. https://doi.org/10.1002/sim.8086 + +## Traceability + +- bounded context: Validation Evidence; +- public API: `validation_core::bias_standard_error`; +- exact admission: `crates/validation_core/src/bias_se.rs`; +- established fallback: `crates/validation_core/src/bias.rs`; +- public RED: `crates/validation_core/tests/bias_standard_error_four_observation_reduced_ratio_contract.rs` at `4f2231354fa0ad3e0c646bbb100b3cc83566d033`; +- causal source repair: `ed1a8763c198fbe478de5ec8be72e3436e5918e3`; +- CHANGELOG: `CHANGELOG.d/validation-bias-standard-error-four-observation-reduced-ratio.md` beginning at `28dde0f1e8e3a0f8e8614ed449733b98f2d65c1c`; +- landing vehicle: PR #488; +- predecessor retained: GAP-111 and all inherited Validation Evidence lineages remain in ancestry. diff --git a/docs/research/bias-standard-error-integer-divisor-two-level-count-rounding.md b/docs/research/bias-standard-error-integer-divisor-two-level-count-rounding.md new file mode 100644 index 000000000..ee979e3a8 --- /dev/null +++ b/docs/research/bias-standard-error-integer-divisor-two-level-count-rounding.md @@ -0,0 +1,61 @@ +# Exact integer-divisor two-level bias standard error + +## Problem + +`validation_core::bias_standard_error` already recognizes exactly translated two-level samples before using the general translated second-moment path. GAP-102 preserved the subset whose count factor is a reciprocal power of two. Fresh review found that the scientific identity is broader: some non-singleton two-level count geometries reduce to the reciprocal square of an integer that is not a power of two. + +For two residual levels separated by represented gap `g`, with counts `m` and `n-m`, + +`SE(mean)^2 = g^2 * m(n-m) / (n^2(n-1))`. + +For `n=9`, `m=3`, the count factor is + +`3*6 / (9^2*8) = 18/648 = 1/36`, + +so the represented-input target is exactly `SE(mean) = |g|/6` before the final binary64 rounding. + +The public RED uses `g = 0x1.ffffffffffffdp-1` (three representable steps below `1.0`) with three residuals at `0` and six at `g`. The exact quotient `g/6` rounds to bits `0x3fc5_5555_5555_5553`. The predecessor translated sum/square/FMA/square-root reconstruction returned the adjacent upper value `0x3fc5_5555_5555_5554`. + +## Constraints + +- The repair must remain inside TEPP Validation Evidence performance-measure arithmetic; it does not create a reusable static psychometric estimator. +- Admission must still prove exact anchor-relative residual translation before any two-level shortcut is used. +- Count algebra must be exact and overflow checked. +- The fix must not claim correctly rounded standard errors for arbitrary `n>2` or arbitrary multi-level residual distributions. +- A mathematically nonzero result that cannot be represented in binary64 must fail closed rather than become false perfect recovery. + +## Decision + +RED `d793f7f9ada68d5976effa6539182ba7037bf8d0` adds the nine-observation 3/6 contract, permutation invariance, sign symmetry, and minimum-subnormal false-zero refusal. + +Causal repair `0a4bfcd52defe8912afa8269576395803d451bb3` replaces the power-of-two-only count predicate with an exact integer-divisor predicate. Using checked `u128` arithmetic, TEPP forms `n^2(n-1)`, verifies divisibility by `m(n-m)`, takes the integer square root of the quotient, verifies the square exactly, and admits the shortcut only when the resulting integer divisor is exactly representable as binary64. The final metric is then one division `|g|/divisor`. + +This preserves GAP-102 (`6/10` of `16` gives divisor `8`) and additionally admits exact cases such as `3/6` of `9` giving divisor `6`. Count factors that are not exact reciprocal integer squares continue through the bounded translated second-moment path. + +## Rejected alternatives + +A payload-specific `n=9, m=3` branch was rejected because the scientific condition is the exact count identity, not this fixture. Keeping the power-of-two restriction was rejected because it makes an implementation convenience narrower than the proved estimator algebra. Replacing all two-level samples with a floating count-ratio formula was rejected because it would broaden the changed rounding surface to cases whose square root is irrational or whose count ratio is itself rounded. Arbitrary-precision production arithmetic was rejected because this defect has a narrower exact integer repair and does not justify a new runtime dependency or owner boundary. + +## Risk and follow-up + +This change does not prove global correct rounding of `bias_standard_error`. Two-level factors that do not reduce to an exact reciprocal integer square, and general multi-level samples, retain the current translated moment path and remain candidates for separately reproduced scientific findings. Hosted exact-head Rust, coverage, security, documentation, and independent review evidence remain required after the source mutation. + +## Traceability + +- RED: `d793f7f9ada68d5976effa6539182ba7037bf8d0` +- causal source repair: `0a4bfcd52defe8912afa8269576395803d451bb3` +- release-note fragment: `9651dfd123b71b14c58e34dfca4800a92e298a99` +- module/API: `crates/validation_core/src/bias.rs` → `bias_standard_error` +- executable contract: `crates/validation_core/tests/bias_standard_error_two_level_integer_divisor_rounding_contract.rs` + +IEEE 754-2019 and ISO/IEC 60559:2020 define the binary floating-point arithmetic model used by this deterministic `f64` reference, including division and square root. The relevant engineering point here is not that every composed expression is globally correctly rounded, but that an exact algebraic `g/6` target should not be unnecessarily reconstructed through additional rounded sums, products, and square root operations. + +Morris, White, and Crowther (2019) treat bias and related performance measures as explicit estimand-linked quantities in simulation studies and recommend defining performance measures unambiguously and reporting Monte Carlo uncertainty. TEPP therefore treats a one-ULP change caused by avoidable arithmetic reconstruction as a Validation Evidence defect when the represented-input target is algebraically known. + +## References + +IEEE. (2019). *IEEE standard for floating-point arithmetic* (IEEE Std 754-2019). https://doi.org/10.1109/IEEESTD.2019.8766229 + +International Organization for Standardization. (2020). *ISO/IEC 60559:2020 Information technology—Microprocessor systems—Floating-point arithmetic*. + +Morris, T. P., White, I. R., & Crowther, M. J. (2019). Using simulation studies to evaluate statistical methods. *Statistics in Medicine, 38*(11), 2074–2102. https://doi.org/10.1002/sim.8086 diff --git a/docs/research/bias-standard-error-multi-observation-subtraction-roundoff.md b/docs/research/bias-standard-error-multi-observation-subtraction-roundoff.md new file mode 100644 index 000000000..33cfe77b8 --- /dev/null +++ b/docs/research/bias-standard-error-multi-observation-subtraction-roundoff.md @@ -0,0 +1,65 @@ +# Bias standard error after multi-observation subtraction collapse + +## Problem + +`validation_core::bias_standard_error` admitted only finite `recovered - truth` residuals, but until GAP-093 it treated the rounded binary64 residual as authoritative for samples larger than two observations. That is not sufficient when distinct represented inputs produce the same rounded subtraction result. A standard error of exactly zero then asserts no sampling dispersion even though the represented-input residuals differ. + +The public RED in commit `06a70e1c01629f15f05efef48576a9cadb1f1b98` uses + +- `truth = [2^-54, 2^-55, 0]`, +- `recovered = [1, 1, 1]`. + +The exact represented-input residuals are + +- `r1 = 1 - 2^-54`, +- `r2 = 1 - 2^-55`, +- `r3 = 1`. + +All three binary64 subtractions round to `1.0`, so the predecessor's rounded-residual path produced `SE = 0`. The represented-input mean is exactly `1 - 2^-55`. The deviations are therefore `[-2^-55, 0, 2^-55]`; the sample standard deviation is `2^-55`, and the standard error is `2^-55 / sqrt(3)`. + +The first public contract accidentally encoded `0x3c72_79a7_4590_331d`, which is the result of evaluating `1 / rounded_sqrt(3)` and then restoring the exact power-of-two scale. Exact high-precision evaluation of the represented-input expression shows that the correctly rounded final binary64 value is instead `0x3c72_79a7_4590_331c` (`0x1.279a74590331cp-56`). Oracle correction `6224320410ccabb1cf16d36cc12f88e2b7a05bb1` makes that distinction executable. This exposed GAP-094: the predecessor standard-error helper separately rounded `sqrt(sample_variance)` and `sqrt(n)`, moving the final standard error by one ULP even after GAP-093 restored the missing low-order dispersion. + +The dedicated GAP-094 contract `89fd9ef0add3930346900f75518708978ec9861e` removes subtraction collapse from that second finding entirely: with `truth=[0,0,0]` and exact residuals `recovered=[-2^-55,0,2^-55]`, the same represented standard error must still round to `0x3c72_79a7_4590_331c`. Its sign mirror and an exact two-observation control prevent the repair from being justified only by the GAP-093 special path. + +This is a Validation Evidence defect: zero uncertainty is materially stronger than small but nonzero uncertainty, and a one-ULP numerical boundary is not interchangeable with the represented-input target when the deterministic CPU `f64` reference claims that boundary. Morris, White, and Crowther (2019) treat bias and simulation uncertainty as performance measures whose uncertainty must be reported rather than silently collapsed by implementation arithmetic. + +## Constraints + +The repair must preserve the existing finite-residual admission gate, the GAP-092 two-observation exact-difference path, and the normal scaled estimator when rounded residuals do not collapse. It must not introduce arbitrary-precision production arithmetic, a second psychometric arithmetic owner, or an O(n²) pairwise-difference fallback on the general path. Reusable static psychometric estimation remains owned by `fast-mlsirm`; this function remains TEPP Validation Evidence arithmetic. + +IEEE 754-2019 remains the active IEEE floating-point standard, and ISO/IEC 60559:2020 remains the published international floating-point standard. The repair therefore treats represented binary64 inputs as the executable numerical boundary and avoids an algebraically unnecessary intermediate rounding when the equivalent normalized expression is bounded. + +## Alternatives considered + +A general pairwise-difference variance identity would remove the rounded mean, but evaluating every represented-input pair is O(n²) and unnecessarily widens this repair. Arbitrary-precision rationals would make the oracle straightforward but would add a production dependency and a second numerical path that is not justified by this bounded defect. Returning `InvalidInput` whenever an n>2 subtraction has roundoff would fail closed but would reject a standard error that is both scientifically meaningful and representable. + +The GAP-093 repair uses the error-free subtraction decomposition already implicit in `subtraction_has_roundoff`. When every rounded residual has the same high part `h`, each exact represented-input residual can be written `r_i = h + l_i`, where `l_i` is the error-free low term. Standard deviation and standard error are translation-invariant, so the common `h` cannot contribute to dispersion. Commit `04c62514a23722d63a62bd5d5af6e3a930cc3147` therefore evaluates the scaled standard error from the `l_i` values in O(n), leaving the GAP-092 n=2 identity and non-collapsed estimator unchanged. + +For GAP-094, keeping `sqrt(sample_variance) / sqrt(n)` was rejected because it rounds two square-root operands before the final division. The normalized deviations are bounded by one, so the equivalent `sqrt(sum(d²) / (n * (n - 1)))` can be evaluated without overflow and with one final square root. Commit `8b8f0a21ccc825f355859cadbf20d83f04d2369f` applies that form inside the existing scaled standard-error helper; it does not add a new estimator or weaken fail-closed handling. + +## Acceptance and traceability + +| Evidence | Exact reference | Acceptance meaning | +|---|---|---| +| GAP-093 public RED | `06a70e1c01629f15f05efef48576a9cadb1f1b98` | Three distinct represented-input residuals collapse to rounded `1.0`; predecessor false zero is rejected. | +| GAP-093 source repair | `04c62514a23722d63a62bd5d5af6e3a930cc3147` | Preserve subtraction low terms and use them only when all rounded residual high parts are equal. | +| GAP-094 oracle correction / RED | `6224320410ccabb1cf16d36cc12f88e2b7a05bb1` | Correct expected represented-input SE from `...331d` to `...331c`, exposing the separate square-root double rounding. | +| GAP-094 source repair | `8b8f0a21ccc825f355859cadbf20d83f04d2369f` | Form normalized SE as `sqrt(sum(d²)/(n(n-1)))` before exact scale restoration. | +| GAP-094 direct-residual contract | `89fd9ef0add3930346900f75518708978ec9861e` | Reproduces the one-ULP square-root defect without pairwise subtraction roundoff. | +| GAP-093 CHANGELOG | `56d091544a5780567a6ef772568d62b0fc651747` | Buyer-visible false-zero repair and scope are recorded. | +| GAP-094 CHANGELOG | `184f4e2c109c7904027f782e91b8d9a01a1d46f1` | Buyer-visible one-ULP final-standard-error repair and corrected oracle are recorded. | +| PR authority | `#488` | Validation Evidence landing vehicle; exact current head is maintained in the PR body and queue-authority baseline. | +| Module/API | `crates/validation_core/src/bias.rs::bias_standard_error` | Owner-correct production boundary. | +| Public contracts | `crates/validation_core/tests/bias_standard_error_multi_observation_subtraction_roundoff_contract.rs`, `crates/validation_core/tests/bias_standard_error_sqrt_double_rounding_contract.rs` | False-zero, sign symmetry, corrected final rounding, and exact-control coverage. | + +These repairs do **not** claim globally correctly rounded standard errors for every finite n>2 input. In particular, when rounded residual high parts differ, retained subtraction low terms may still move a nonzero standard error across a final binary64 boundary; that requires an independent represented-input counterexample before widening the production algorithm. + +## References + +American Educational Research Association, American Psychological Association, & National Council on Measurement in Education. (2014). *Standards for educational and psychological testing*. American Educational Research Association. + +IEEE Standards Association. (2019). *IEEE standard for floating-point arithmetic* (IEEE Std 754-2019). https://standards.ieee.org/ieee/754/6210/ + +International Organization for Standardization. (2020). *Information technology—Microprocessor systems—Floating-point arithmetic* (ISO/IEC 60559:2020). https://www.iso.org/standard/80985.html + +Morris, T. P., White, I. R., & Crowther, M. J. (2019). Using simulation studies to evaluate statistical methods. *Statistics in Medicine, 38*(11), 2074–2102. https://doi.org/10.1002/sim.8086 diff --git a/docs/research/bias-standard-error-nine-observation-pair-distance-rounding.md b/docs/research/bias-standard-error-nine-observation-pair-distance-rounding.md new file mode 100644 index 000000000..00504d5eb --- /dev/null +++ b/docs/research/bias-standard-error-nine-observation-pair-distance-rounding.md @@ -0,0 +1,69 @@ +# Nine-observation mean-bias standard error: exact pair-distance rounding + +## Problem + +After GAP-117, the bounded pair-distance/midpoint proof covered four through eight observations. A nine-observation sample with exact represented residuals still fell through to the translated floating second-moment path: + +```text +truth = [0, 0, 0, 0, 0, 0, 0, 0, 0] +recovered = [354161013, 513228884, 592175752, 1188877551, 1313358136, + 1582012649, 1600441891, 1764736057, 1957134661] +``` + +All values are exactly represented binary64 integers and all pairwise differences are exact. The thirty-six squared pair distances sum to + +```text +N = 24907531253053169466. +``` + +For `n=9`, + +```text +SE(mean)^2 = sum_{i2` standard errors. Extending the bound again requires a demonstrated represented-input counterexample plus numerical and performance evidence rather than a speculative increase. + +CHANGELOG `f575fa2143e0e46058dd62d6dcdd6e6c3e6354b1` records the contract. GAP-117 and all preceding Validation Evidence lineages remain inherited. + +## Alternatives rejected + +A payload-specific nine-value branch would encode a fixture rather than the estimator invariant. Replacing the translated fallback with an unconditional pair-distance formula would impose O(n^2) work on ordinary larger samples without evidence that the commercial path needs it. Raising the bound speculatively beyond the demonstrated case would likewise add quadratic work without a scientific defect to justify it. Arbitrary-precision production arithmetic would broaden ownership and dependency surface far beyond the demonstrated defect. The bounded admission instead reuses the existing checked dyadic proof and changes only its evidenced upper sample count. + +## Method and standards trace + +Morris, White, and Crowther (2019) distinguish deterministic performance-measure calculations from Monte Carlo uncertainty; a one-ULP deterministic bias-SE error is therefore repaired in the estimator rather than treated as simulation noise. Published floating-point authority remains IEEE 754-2019 / ISO/IEC 60559:2020. IEEE P754 remains an active PAR approved June 6, 2024 to supersede 754-2019; it is revision work, not a published replacement. ISO continues to list ISO/IEC 60559:2020 as a published International Standard at stage 60.60. AERA, APA, and NCME continue revising the 2014 *Standards for Educational and Psychological Testing*; AERA's task-force roster was current as of August 31, 2026, so the unpublished revision is not treated as normative authority. + +## Traceability + +- Bounded context: Validation Evidence. +- Production module/API: `crates/validation_core/src/bias_se.rs` → `bias_standard_error`. +- Public executable contract: `crates/validation_core/tests/bias_standard_error_nine_observation_pair_distance_contract.rs`. +- RED: `c7b6537763f303069aa11e4ebfa12b7b3093448c`. +- Causal source repair: `bcd2ab5e1e6bd38f0661e7bb47e5d59a78830499`. +- CHANGELOG: `f575fa2143e0e46058dd62d6dcdd6e6c3e6354b1`. +- Scientific invariant: for exact represented residuals admitted by the bounded proof, `SE(mean)^2 = sum_{i 2` case: represented-input subtraction roundoff can still alter dispersion while the present repair intentionally leaves that path unchanged. A follow-up change requires a concrete public counterexample with a materially different representable standard error and a bounded arithmetic strategy that does not regress full-range overflow handling. The current RED is not evidence for a blanket higher-order rewrite. + +## TRACEABILITY + +- RED: `c9c55ea568d27e33d6e522b02e6431ec4c6983d2` +- causal repair: `f6d7da9681022e0df7a60f444e02894605201cd4` +- CHANGELOG: `ab7730ca5246598dd916ff264e5bdf97ecf72754` +- production module: `crates/validation_core/src/bias.rs` +- public contract: `crates/validation_core/tests/bias_standard_error_pairwise_subtraction_roundoff_contract.rs` +- bounded context: Validation Evidence +- owner boundary: reusable static psychometric estimators remain in `fast-mlsirm`; no mutable sibling source is consumed. + +## References + +IEEE. (2019). *IEEE standard for floating-point arithmetic* (IEEE Std 754-2019). https://standards.ieee.org/ieee/754/6210/ + +Morris, T. P., White, I. R., & Crowther, M. J. (2019). Using simulation studies to evaluate statistical methods. *Statistics in Medicine, 38*(11), 2074–2102. https://doi.org/10.1002/sim.8086 diff --git a/docs/research/bias-standard-error-power-scale-rounding.md b/docs/research/bias-standard-error-power-scale-rounding.md new file mode 100644 index 000000000..5baac1e46 --- /dev/null +++ b/docs/research/bias-standard-error-power-scale-rounding.md @@ -0,0 +1,62 @@ +# Bias standard error must preserve dyadic scale through translated dispersion + +## Finding + +GAP-098 isolates a binary64 rounding defect in the exact translated-residual path introduced to avoid rounded-mean dispersion drift. The path proved that anchor-relative residual deltas were exactly representable, but then normalized them by the largest translated magnitude itself. When that magnitude is not a power of two, the final scale restoration can introduce a second rounding that changes `SE(mean)` even though the translated geometry was exact. + +Let `u = 2^-52`, `a = 1 - 4u`, and `b = 1 + u`. With `truth = [0,0,0]` and `recovered = [a,a,b]`, every pairwise residual subtraction is exact. The represented residual gap is + +`d = b - a = 5 * 2^-52`. + +For a three-observation vector `[a,a,a+d]`, translation invariance gives centered deviations `[-d/3,-d/3,2d/3]`, therefore + +`SE(mean) = sqrt(sum((r_i-r_bar)^2)/(3*2)) = d/3`. + +The correctly rounded binary64 result is bits `0x3cba_aaaa_aaaa_aaab`. The predecessor translated to `[0,0,d]`, chose `scale=d`, evaluated the normalized geometry as `[0,0,1]`, rounded the square-root stage to the binary64 approximation of `1/3`, and then multiplied that rounded value by the non-power scale `d`. The result was one ULP low at `0x3cba_aaaa_aaaa_aaaa`. The sign-mirrored sample reproduces the same defect. + +## Constraints + +This remains TEPP Validation Evidence performance-measure arithmetic. It does not create a reusable psychometric estimator, does not move static psychometric ownership from `fast-mlsirm`, and does not consume mutable sibling-repository source. Production remains deterministic Rust `f64`, O(n), and fail closed when a mathematically nonzero requested result cannot be represented. + +The repair must preserve GAP-092 through GAP-097: the exact two-observation identity, common-high subtraction-low recovery, exact translated residual admission, the direct normalized SE expression, and bounded fallback when translated deltas cannot be proved exact. It must not claim globally correctly rounded n>2 dispersion. + +## Alternatives considered + +Keeping the largest translated magnitude as the normalization scale was rejected because the RED proves that an otherwise exact translated geometry can be projected through a rounded normalized result and multiplied by a non-dyadic scale, introducing a second rounding at the scientific result boundary. + +Adding arbitrary-precision rational arithmetic to production was rejected as disproportionate to the demonstrated cause. The input values are already binary64 dyadic rationals, and an exact power-of-two normalization is sufficient to preserve their represented geometry across scale reduction/restoration. + +Special-casing the `[a,a,b]` three-observation identity was rejected because the defect belongs to the normalization policy, not to that sample shape. The existing translated-second-moment path should retain its general bounded admission contract. + +## Selected repair + +`numeric::exact_power_of_two_scale` is made crate-visible and reused by `bias::exact_translated_residual_standard_error`. The helper chooses the power-of-two binade scale already used by Validation Evidence's deterministic mean arithmetic. Dividing a represented dyadic residual delta by that scale changes only its exponent when representable, and multiplying the normalized result by the same scale restores magnitude without an additional non-dyadic rounding. + +For the RED, the translated vector remains an exact dyadic geometry under the power scale. The normalized second moment therefore carries the factor `5` into the square-root ratio instead of collapsing it into a later multiplication by `d`; final power-of-two scale restoration yields the correctly rounded `d/3` result. + +The existing nonzero-to-zero normalization guard remains in force. Exact translated-delta admission is unchanged, and samples that fail that proof still use the predecessor bounded fallback. + +## Evidence and traceability + +| Evidence | Exact reference | Role | +| --- | --- | --- | +| Public RED | `426fc5afd8aae8c2d5f81f53e5db074b480ac8b7` | Adds the exact `[a,a,b]` counterexample and sign mirror with expected bits `0x3cba_aaaa_aaaa_aaab`. | +| Shared numeric prerequisite | `d8b411f5dcf93ff21b18eaa4ff5ccd8d2863a6cd` | Makes the existing exact power-of-two scale helper crate-visible without changing its arithmetic. | +| Causal repair | `d536e3b1082c0f705a6ddd7722bf45c93ac594e3` | Uses the dyadic scale in the exact translated-residual standard-error path and updates the API contract. | +| CHANGELOG | `ebce979fdfb0ab2d94e3ba70145b0076a7ef07ca` | Records the buyer-visible one-ULP uncertainty correction and bounded scope. | +| Module/API | `crates/validation_core/src/bias.rs::bias_standard_error` | Canonical TEPP Validation Evidence producer. | +| Shared helper | `crates/validation_core/src/numeric.rs::exact_power_of_two_scale` | Existing crate-private dyadic normalization owner reused by the producer. | +| Public contract | `crates/validation_core/tests/bias_standard_error_power_scale_rounding_contract.rs` | Exact positive/sign-mirror regression contract. | +| Landing vehicle | PR `#488` | Surviving Validation Evidence head; hosted current-head evidence remains PR-authoritative. | + +## Methodological authority + +Morris, White, and Crowther (2019) treat bias and uncertainty as performance measures evaluated against known simulation truth and emphasize Monte Carlo uncertainty. The floating-point behavior in this repair is interpreted under IEEE 754-2019 / ISO/IEC 60559:2020. The AERA/APA/NCME *Standards for Educational and Psychological Testing* (2014) remains the published testing-standards edition while revision work continues; TEPP therefore ties validation claims to published authority and exact executable evidence rather than unpublished draft language. + +## References + +American Educational Research Association, American Psychological Association, & National Council on Measurement in Education. (2014). *Standards for educational and psychological testing*. American Educational Research Association. + +IEEE. (2019). *IEEE standard for floating-point arithmetic* (IEEE Std 754-2019). https://doi.org/10.1109/IEEESTD.2019.8766229 + +Morris, T. P., White, I. R., & Crowther, M. J. (2019). Using simulation studies to evaluate statistical methods. *Statistics in Medicine, 38*(11), 2074–2102. https://doi.org/10.1002/sim.8086 diff --git a/docs/research/bias-standard-error-rational-scale-subnormal-rounding.md b/docs/research/bias-standard-error-rational-scale-subnormal-rounding.md new file mode 100644 index 000000000..0a9c515e6 --- /dev/null +++ b/docs/research/bias-standard-error-rational-scale-subnormal-rounding.md @@ -0,0 +1,79 @@ +# Bias standard error rational-scale subnormal rounding + +## Problem + +GAP-104 established an exact two-level shortcut when the reduced count factor is a rational square. The surviving implementation still routed that exact rational scale through `deterministic_representable_sum_over_count`, whose same-sign path first normalizes by an exact power of two. That normalization is harmless while the restored quotient remains normal, but it can introduce a second binary64 rounding boundary when the restored scientific result is subnormal. + +The public counterexample uses `n = 33`, with six represented residuals at `0` and 27 at `g = f64::from_bits(0x004a_2c74_6ac3_028e)`. + +For a two-level sample, + +`SE(mean)^2 = g^2 * m(n-m) / (n^2(n-1))`. + +Here + +`6 * 27 / (33^2 * 32) = 162 / 34848 = 9 / 1936 = (3 / 44)^2`, + +so the represented-input target is exactly `3 * |g| / 44`. Correct rounding to binary64 gives bits `0x000e_46cb_22f6_0165`. The GAP-104 predecessor normalizes the rational numerator, rounds the normalized quotient, then restores the power-of-two scale into the subnormal range and returns adjacent lower bits `0x000e_46cb_22f6_0164`. + +This is not a disagreement about the estimand. It is an avoidable floating-point projection error after the exact two-level count geometry has already been proved. + +## Constraints + +- TEPP owns Validation Evidence performance-measure arithmetic; reusable static psychometric estimators remain in `fast-mlsirm`. +- Production numerical arithmetic remains Rust-first and deterministic. +- The repair preserves the existing exact translated-residual admission and does not send arbitrary multi-level or irrational count geometry through a new path. +- A mathematically nonzero result below binary64 range fails closed rather than silently becoming zero. +- No arbitrary-precision production dependency is introduced. + +## Decision + +For exact rational-square two-level geometry, the implementation attempts a bounded exact subnormal projection before the existing represented sum-over-count path. + +A positive finite binary64 magnitude has an integer significand of at most 53 bits. Expressed in units of the minimum positive subnormal, a normal value with encoded exponent `e` has exact unit count `significand * 2^(e-1)`; a subnormal value uses its stored fraction directly. The rational scale therefore has exact unit numerator + +`significand * numerator * 2^(e-1)` + +for normal inputs, or `significand * numerator` for subnormal inputs. On supported targets, the significand and `usize` rational factor fit the bounded `u128` product. The exponent shift is checked; if it cannot fit, the result is outside this subnormal projection and the existing normal overflow-safe path remains authoritative. Division by the exact integer denominator is rounded once with round-to-nearest, ties-to-even. Results above the normal/subnormal boundary fall back to the existing path; a nonzero exact result that rounds below one minimum-subnormal unit returns `ValidationError::InvalidInput`. + +This preserves the existing overflow-safe path for normal results while removing the double-rounding surface at the subnormal boundary. + +## Alternatives rejected + +1. **Keep normalized rational scaling and accept one-ULP drift.** Rejected because the exact count geometry is already known and the drift is an implementation artifact, not simulation uncertainty. +2. **Special-case the `6/27 of 33` payload.** Rejected because the defect is the normal-to-subnormal restoration boundary, not those counts. +3. **Apply arbitrary-precision arithmetic to all Validation Evidence metrics.** Rejected as substantially broader than the causal defect and contrary to the bounded Rust reference-path design. +4. **Replace all standard-error arithmetic with a closed form.** Rejected because general multi-level samples and non-square count factors do not share this exact rational identity. + +## Evidence and traceability + +- Public RED: `8b7995d2320cf256b3a38991ae1f8a230ca00146` +- Causal source repair: `ab0f0df1b8f36647f67239a5c628daed9023210e` +- Public boundary coverage: `4daeb65daee98b25fdc5a29744a710752e229a50` +- Production branch-coverage hardening: `8f6916dda1bc241e6bfd5dab0840e621769f995b` +- CHANGELOG fragment: `900b2091d572c9a984e350d2795ec58bfdd3177c` +- Module/API: `crates/validation_core/src/bias.rs` → `bias_standard_error` +- Public contract: `crates/validation_core/tests/bias_standard_error_rational_scale_subnormal_rounding_contract.rs` +- Expected represented result: `0x000e_46cb_22f6_0165` +- Predecessor result: `0x000e_46cb_22f6_0164` +- Edge contract: a minimum-subnormal gap with the same exact `3/44` count scale is mathematically nonzero but below binary64 range and returns `InvalidInput`. + +The public contract fixes permutation and sign-mirror invariance, both ties-to-even directions in minimum-subnormal units, exact rounding onto `f64::MIN_POSITIVE`, and fail-closed underflow. The crate-private branch tests cover invalid helper admission, normal/subnormal significand decoding, midpoint increment/non-increment, zero refusal, normal-boundary return, above-boundary fallback, and exponent-shift fallback. Hosted exact-head CI remains authoritative for GREEN; branch-local arithmetic proof or predecessor workflow results are not transferred as current-head CI evidence. + +## Standards and methodological basis + +IEEE 754-2019 remains the active IEEE floating-point standard. IEEE P754, approved as a PAR on June 6, 2024, is an active revision project that supersedes 754-2019 only when a replacement standard is actually published. ISO/IEC 60559:2020 remains a published international standard adopting the same floating-point arithmetic model. These sources support explicit control of destination-format rounding and distinguish arithmetic semantics from application-level statistical uncertainty. + +For Validation Evidence, Morris, White, and Crowther (2019) treat bias and empirical standard error as simulation performance measures and require Monte Carlo uncertainty to be reported as simulation uncertainty. A deterministic one-ULP arithmetic projection error in a represented performance measure is therefore not something to absorb into Monte Carlo error. + +The currently published AERA/APA/NCME *Standards for Educational and Psychological Testing* remains the 2014 edition. AERA, APA, and NCME announced a Joint Committee in 2024 to revise that edition; the in-progress revision is not treated here as published normative authority. + +## References + +American Educational Research Association, American Psychological Association, & National Council on Measurement in Education. (2014). *Standards for educational and psychological testing*. American Educational Research Association. https://www.testingstandards.net/open-access-files.html + +Institute of Electrical and Electronics Engineers. (2019). *IEEE standard for floating-point arithmetic* (IEEE Std 754-2019). IEEE Standards Association. https://standards.ieee.org/ieee/754/6210/ + +International Organization for Standardization, & International Electrotechnical Commission. (2020). *Information technology—Microprocessor systems—Floating-point arithmetic* (ISO/IEC 60559:2020). https://www.iso.org/standard/80985.html + +Morris, T. P., White, I. R., & Crowther, M. J. (2019). Using simulation studies to evaluate statistical methods. *Statistics in Medicine, 38*(11), 2074–2102. https://doi.org/10.1002/sim.8086 diff --git a/docs/research/bias-standard-error-rational-square-two-level-count-rounding.md b/docs/research/bias-standard-error-rational-square-two-level-count-rounding.md new file mode 100644 index 000000000..307cd376d --- /dev/null +++ b/docs/research/bias-standard-error-rational-square-two-level-count-rounding.md @@ -0,0 +1,60 @@ +# Exact rational-square two-level bias standard error + +## Problem + +`validation_core::bias_standard_error` already proves exact anchor-relative translation before recognizing two-level residual samples. GAP-103 widened the direct count path from reciprocal powers of two to reciprocal integer squares, but that predicate is still narrower than the estimator algebra: the reduced two-level count factor can be the square of a non-unit rational. + +For two represented residual levels separated by `g`, with counts `m` and `n-m`, + +`SE(mean)^2 = g^2 * m(n-m) / (n^2(n-1))`. + +For `n=10`, `m=2`, the count factor is + +`2*8 / (10^2*9) = 16/900 = 4/225 = (2/15)^2`, + +so the represented-input target is `SE(mean) = 2*|g|/15` before final binary64 rounding. The public RED uses `g = 0x1.ffffffffffffep-1`, with two residuals at `0` and eight at `g`. The exact represented rational target rounds to bits `0x3fc1_1111_1111_1110`; the GAP-103 predecessor rejects the non-unit numerator, falls through to translated sum/square/FMA/square-root reconstruction, and returns adjacent upper bits `0x3fc1_1111_1111_1111`. + +## Constraints + +- The repair remains inside TEPP Validation Evidence performance-measure arithmetic and does not create a reusable static psychometric estimator. +- Exact anchor-relative translation remains a prerequisite; no shortcut is admitted from rounded residual labels alone. +- Count algebra is checked in `u128`; overflow or a non-square reduced factor falls back to the bounded translated second-moment path. +- The repair must preserve GAP-102 and GAP-103 reciprocal-integer cases. +- A mathematically nonzero result below binary64 support fails closed rather than becoming false zero. +- The change does not claim globally correctly rounded standard errors for arbitrary `n>2`, multi-level residuals, or irrational count factors. + +## Decision + +RED `6dc8116c89fa44a7ff1d58a8f9a51c876993d33f` adds the 10-observation 2/8 contract, permutation invariance, sign symmetry, and minimum-subnormal false-zero refusal. + +Causal repair `8f2803c874568877e20e4c0f267ec5ce613daa3d` reduces `m(n-m) / (n^2(n-1))` by the exact integer greatest common divisor, verifies that both reduced numerator and denominator are perfect squares, and returns their integer square roots as the rational scale. When the non-singleton two-level path proves such a scale, TEPP reuses `deterministic_representable_sum_over_count` with `numerator` copies of `|gap|` and the exact integer `denominator`. This preserves a single deterministic represented-rational rounding boundary without multiply-first overflow or divide-first subnormal loss. + +The predecessor cases remain admitted: `3/6` of `9` reduces to `(1/6)^2`, and `6/10` of `16` reduces to `(1/8)^2`. The new 2/8-of-10 case reduces to `(2/15)^2`. Count factors that are not rational squares retain the existing translated second-moment path. + +## Rejected alternatives + +A fixture-specific `n=10,m=2` branch was rejected because the scientific condition is the reduced rational-square identity, not the example. Retaining the reciprocal-integer restriction was rejected because it excludes exact algebraic targets such as `2|g|/15`. A generic floating `sqrt(m(n-m)/(n^2(n-1)))` shortcut was rejected because it changes the rounding surface for irrational count factors and reintroduces the same composed-rounding problem. Direct `(|g| * numerator) / denominator` was rejected because the multiplication can overflow although the final scaled result is representable; direct `(|g| / denominator) * numerator` can lose subnormal mass before the numerator is restored. Arbitrary-precision production arithmetic was rejected because the exact reduced-count predicate and the existing deterministic represented-sum divisor are sufficient for this defect. + +## Risk and follow-up + +This repair proves only the exact rational-square two-level subset after exact residual translation. Rationally non-square two-level factors and general multi-level samples remain on the translated moment implementation and require their own reproduced scientific finding before any broader arithmetic change. Hosted exact-head Rust, coverage, documentation, security, and independent review evidence remain required after this source mutation. + +## Traceability + +- RED: `6dc8116c89fa44a7ff1d58a8f9a51c876993d33f` +- causal source repair: `8f2803c874568877e20e4c0f267ec5ce613daa3d` +- release-note fragment: `f0bb7af91c08b7766f43fec4c8d7984b68a3599f` +- module/API: `crates/validation_core/src/bias.rs` → `bias_standard_error` +- executable contract: `crates/validation_core/tests/bias_standard_error_two_level_rational_scale_rounding_contract.rs` + +IEEE 754-2019 remains the active IEEE floating-point standard, and ISO/IEC 60559:2020 remains the published international adoption. They define the binary floating-point arithmetic model relevant to this deterministic `f64` reference. The engineering conclusion here is narrower than global correct rounding: when the represented-input statistical identity is already an exact rational scale, reconstructing it through additional rounded sums, products, FMA, division, and square root is avoidable. + +Morris, White, and Crowther (2019) frame simulation evaluation around known truth, explicit estimands and performance measures, and Monte Carlo uncertainty. TEPP therefore treats a reproducible one-ULP displacement in a declared Validation Evidence performance measure as a scientific arithmetic defect when the represented-input target is analytically known. + +## References + +IEEE. (2019). *IEEE standard for floating-point arithmetic* (IEEE Std 754-2019). https://doi.org/10.1109/IEEESTD.2019.8766229 + +International Organization for Standardization. (2020). *ISO/IEC 60559:2020 Information technology—Microprocessor systems—Floating-point arithmetic*. + +Morris, T. P., White, I. R., & Crowther, M. J. (2019). Using simulation studies to evaluate statistical methods. *Statistics in Medicine, 38*(11), 2074–2102. https://doi.org/10.1002/sim.8086 diff --git a/docs/research/bias-standard-error-repeated-level-rounding.md b/docs/research/bias-standard-error-repeated-level-rounding.md new file mode 100644 index 000000000..f99d6ea29 --- /dev/null +++ b/docs/research/bias-standard-error-repeated-level-rounding.md @@ -0,0 +1,70 @@ +# Bias standard error: repeated-level three-observation rounding + +## Problem + +Validation Evidence computes the standard error of the mean signed bias from represented binary64 recovery residuals. After GAP-095–098, the exact-translated path preserves represented residual geometry and uses a power-of-two scale, but it still evaluates the general second-moment expression through squared binary64 values and a square root. + +A bounded three-observation counterexample remains. Let + +- `a = next_down(1.0) = 0x1.fffffffffffffp-1 = 1 - 2^-53`, +- `truth = [0, 0, 0]`, and +- `recovered = [0, a, a]`. + +All three represented residuals and anchor-relative translated deltas are exact. For the residual vector `[0, a, a]`, the sample standard error of the mean simplifies algebraically to + +`SE(mean) = |a| / 3`. + +The correctly rounded binary64 result is `0x1.5555555555555p-2` (`0x3fd5_5555_5555_5555`). The predecessor exact-translated implementation normalized on a power-of-two scale, squared the normalized `a` values, formed `n * sum(x^2) - sum(x)^2`, divided, and then took a square root. That sequence returns the adjacent lower float `0x1.5555555555554p-2` for this represented input. The discrepancy is one ULP and is not caused by temporal composition, sampling design, or reusable psychometric estimation; it is Validation Evidence binary64 projection error. + +Public RED: `fbcdb7fac40744c697debbbe6184d4e0ffd5e32a` (`bias_standard_error_repeated_level_rounding_contract.rs`). + +## Constraints + +The repair must preserve the current owner and numerical boundaries: + +- TEPP Validation Evidence owns this performance-measure decision arithmetic. +- reusable static psychometric estimation remains fast-mlsirm-owned; +- no arbitrary-precision production dependency is introduced merely to eliminate a one-ULP bounded projection error; +- exact translated-delta admission and fail-closed behavior remain unchanged; +- a local counterexample does not justify claiming globally correctly rounded `n > 2` standard errors. + +## Alternatives + +### Keep the general second-moment route + +Rejected for the proven shape. The general formula is mathematically valid, but the represented operation sequence introduces avoidable square and square-root projections after the residual geometry is already exact. + +### Replace all larger-sample standard errors with arbitrary precision + +Rejected. It widens the production dependency and performance surface far beyond the demonstrated defect and would duplicate a numerical owner without a demonstrated buyer/scientific need. + +### Add a generic two-level closed form for every sample size + +Not adopted in this repair. For arbitrary counts the coefficient includes a square root and still needs separate rounding analysis. The current counterexample proves only the `n = 3`, two-equal-level identity where the standard error reduces exactly to one represented gap divided by three. + +### Evaluate the exact three-observation two-level identity directly + +Selected. After exact translated-residual admission, any three-observation sample with exactly two equal levels is translation-equivalent to `[0, 0, d]` or `[0, d, d]`. In either case `SE(mean) = |d| / 3`. The gap is already a represented exact translated delta, so one correctly rounded binary64 division is the narrow causal operation required by the scientific identity. + +Causal repair: `e0f2445d825f12817631ca8e5ef5fed77fcd113a`. + +## Risk and follow-up + +This repair does not establish global correct rounding for general translated second moments. Independent counterexamples involving three distinct levels, larger `n`, square accumulation, division, or square root remain separate findings and require their own represented-input RED before the algorithm is widened again. If the direct identity produces zero from a nonzero represented gap, the result remains fail closed as unrepresentable rather than being reported as zero uncertainty. + +## Traceability + +- Bounded context: Validation Evidence. +- Module/API: `crates/validation_core/src/bias.rs` → `bias_standard_error` / `exact_translated_residual_standard_error`. +- Public contract: `crates/validation_core/tests/bias_standard_error_repeated_level_rounding_contract.rs`. +- RED: `fbcdb7fac40744c697debbbe6184d4e0ffd5e32a`. +- Repair: `e0f2445d825f12817631ca8e5ef5fed77fcd113a`. +- Release note: `CHANGELOG.d/validation-bias-standard-error-repeated-level-rounding.md`. + +## References + +IEEE. (2019). *IEEE standard for floating-point arithmetic* (IEEE Std 754-2019). Institute of Electrical and Electronics Engineers. https://doi.org/10.1109/IEEESTD.2019.8766229 + +International Organization for Standardization. (2020). *Information technology—Microprocessor systems—Floating-point arithmetic* (ISO/IEC 60559:2020). + +Morris, T. P., White, I. R., & Crowther, M. J. (2019). Using simulation studies to evaluate statistical methods. *Statistics in Medicine, 38*(11), 2074–2102. https://doi.org/10.1002/sim.8086 diff --git a/docs/research/bias-standard-error-seven-observation-pair-distance-rounding.md b/docs/research/bias-standard-error-seven-observation-pair-distance-rounding.md new file mode 100644 index 000000000..680d10cbe --- /dev/null +++ b/docs/research/bias-standard-error-seven-observation-pair-distance-rounding.md @@ -0,0 +1,60 @@ +# Seven-observation mean-bias standard error: exact pair-distance rounding + +## Problem + +After GAP-115, the bounded pair-distance/midpoint proof covered four through six observations. A seven-observation sample with exact represented residuals still fell through to the translated floating second-moment path: + +```text +truth = [0, 0, 0, 0, 0, 0, 0] +recovered = [912628433, 991631865, 1109185293, 1253685899, 1354619842, 1368206500, 1611374925] +``` + +All values are exactly represented binary64 integers and all pairwise differences are exact. The twenty-one squared pair distances sum to + +```text +N = 2469379766402987422. +``` + +For `n=7`, + +```text +SE(mean)^2 = sum_{i2` standard errors. Extending the bound again requires a demonstrated scientific counterexample plus numerical and performance evidence rather than a speculative increase. + +CHANGELOG `78d5ed3fb81765edbc4ee662dacabe6ec631265f` records the contract. GAP-115 and all preceding Validation Evidence lineages remain inherited. + +## Alternatives rejected + +A payload-specific seven-value branch would encode a fixture rather than the estimator invariant. Replacing the translated fallback with an unconditional pair-distance formula would impose O(n^2) work on ordinary larger samples without evidence that the commercial path needs it. Arbitrary-precision production arithmetic would also broaden ownership and dependency surface far beyond the demonstrated defect. The bounded admission instead reuses the existing checked dyadic proof and changes only its scientifically evidenced upper sample count. + +## Method and standards trace + +Morris, White, and Crowther (2019) distinguish deterministic performance-measure calculations from Monte Carlo uncertainty; a one-ULP deterministic bias-SE error is therefore repaired in the estimator rather than treated as simulation noise. Published floating-point authority remains IEEE 754-2019 / ISO/IEC 60559:2020. IEEE P754 is active revision work, not a published replacement. AERA/APA/NCME public Testing Standards authority remains the 2014 edition while its revision proceeds. + +## Traceability + +- Bounded context: Validation Evidence. +- Production module/API: `crates/validation_core/src/bias_se.rs` → `bias_standard_error`. +- Public executable contract: `crates/validation_core/tests/bias_standard_error_seven_observation_pair_distance_contract.rs`. +- Scientific invariant: for exact represented residuals admitted by the bounded proof, `SE(mean)^2 = sum_{i 2` standard errors. Samples with three or more represented levels, two-level samples without a singleton, and cases that fail the exact translation admission continue through the existing bounded second-moment or fallback paths and require their own represented-input counterexample before any broader change. + +## Alternatives considered + +Applying arbitrary-precision arithmetic to every Validation metric was rejected because it widens the production arithmetic contract far beyond the demonstrated defect and adds a new runtime dependency. Special-casing only the four-row payload was rejected because the algebraic identity is determined by the singleton/two-level structure rather than by `n = 4`. Leaving the generic square/root path unchanged was rejected because the public contract can reproduce a deterministic one-ULP error from exact represented residuals. + +## Traceability + +- Bounded context: Validation Evidence. +- Production API: `validation_core::bias_standard_error`. +- Production module: `crates/validation_core/src/bias.rs`. +- Public regression: `crates/validation_core/tests/bias_standard_error_singleton_level_rounding_contract.rs`. +- RED: `4386d9ace83cd54aa129067ddc589b1a628147a2`. +- Causal repair: `79ad03fae4364d6c364915a062eb0fc8615eaa43`. +- Edge coverage: `7fe4117c5666af08adbfda5d32beb12648f579c6`. +- CHANGELOG: `017ad11ad974219a5a0e1cf91c1ecf55c44524c2`. +- Landing vehicle: PR #488. + +IEEE 754 binary floating-point semantics remain the numerical representation authority. Known-truth recovery metrics remain performance measures rather than LLM judgments; simulation acceptance must continue to report the estimand, bias/dispersion behavior, and Monte Carlo uncertainty. + +## References + +Institute of Electrical and Electronics Engineers. (2019). *IEEE standard for floating-point arithmetic* (IEEE Std 754-2019). IEEE. https://doi.org/10.1109/IEEESTD.2019.8766229 + +Morris, T. P., White, I. R., & Crowther, M. J. (2019). Using simulation studies to evaluate statistical methods. *Statistics in Medicine, 38*(11), 2074–2102. https://doi.org/10.1002/sim.8086 diff --git a/docs/research/bias-standard-error-six-observation-pair-distance-rounding.md b/docs/research/bias-standard-error-six-observation-pair-distance-rounding.md new file mode 100644 index 000000000..b5b532a0c --- /dev/null +++ b/docs/research/bias-standard-error-six-observation-pair-distance-rounding.md @@ -0,0 +1,48 @@ +# Six-observation mean-bias standard error: exact pair-distance rounding + +## Problem + +After GAP-114, the bounded pair-distance/midpoint proof covered four and five observations. A six-observation sample with exact represented residuals still fell through to the translated floating second-moment path: + +```text +truth = [0, 0, 0, 0, 0, 0] +recovered = [1120315269, 1513609015, 1569037659, 1789057504, 1807936669, 1914796738] +``` + +All values are exactly represented binary64 integers and all pairwise differences are exact. The fifteen squared pair distances sum to + +```text +N = 2486413148856729212. +``` + +For `n=6`, + +```text +SE(mean)^2 = sum_{i2` standard errors. A broader admission requires separate numerical and performance evidence. + +CHANGELOG `d709199bfccca6d70f2cd91d7115474a0f8e04cd` records the contract. GAP-114 and all preceding Validation Evidence lineages remain inherited. + +## Method and standards trace + +Morris, White, and Crowther (2019) distinguish deterministic performance-measure calculations from Monte Carlo uncertainty; a one-ULP deterministic bias-SE error is therefore repaired in the estimator rather than treated as simulation noise. Published floating-point authority remains IEEE 754-2019 / ISO/IEC 60559:2020. IEEE P754 is active revision work, not a published replacement. AERA/APA/NCME public Testing Standards authority remains the 2014 edition while its revision proceeds. + +## References + +American Educational Research Association, American Psychological Association, & National Council on Measurement in Education. (2014). *Standards for educational and psychological testing*. American Educational Research Association. + +IEEE. (2019). *IEEE standard for floating-point arithmetic* (IEEE Std 754-2019). https://doi.org/10.1109/IEEESTD.2019.8766229 + +ISO/IEC. (2020). *Information technology—Microprocessor systems—Floating-point arithmetic* (ISO/IEC 60559:2020). + +Morris, T. P., White, I. R., & Crowther, M. J. (2019). Using simulation studies to evaluate statistical methods. *Statistics in Medicine, 38*(11), 2074–2102. https://doi.org/10.1002/sim.8086 diff --git a/docs/research/bias-standard-error-ten-observation-pair-distance-rounding.md b/docs/research/bias-standard-error-ten-observation-pair-distance-rounding.md new file mode 100644 index 000000000..2e25e587f --- /dev/null +++ b/docs/research/bias-standard-error-ten-observation-pair-distance-rounding.md @@ -0,0 +1,69 @@ +# Ten-observation mean-bias standard error: exact pair-distance rounding + +## Problem + +After GAP-118, the bounded pair-distance/midpoint proof covered four through nine observations. A ten-observation sample with exact represented residuals still fell through to the translated floating second-moment path: + +```text +truth = [0, 0, 0, 0, 0, 0, 0, 0, 0, 0] +recovered = [418906532, 431535003, 554793949, 905115878, 1079195036, + 1340223962, 1591821052, 1592008872, 1807262929, 1996099302] +``` + +All values are exactly represented binary64 integers and all pairwise differences are exact. The forty-five squared pair distances sum to + +```text +N = 30398755841753540685. +``` + +For `n=10`, + +```text +SE(mean)^2 = sum_{i2` standard errors. Extending the bound again requires a demonstrated represented-input counterexample plus numerical and performance evidence rather than a speculative increase. + +CHANGELOG `2dacce55a06cc160ae3defcc84b75a8cebd46e18` records the contract. GAP-118 and all preceding Validation Evidence lineages remain inherited. + +## Alternatives rejected + +A payload-specific ten-value branch would encode a fixture rather than the estimator invariant. Replacing the translated fallback with an unconditional pair-distance formula would impose O(n^2) work on ordinary larger samples without evidence that the commercial path needs it. Raising the bound speculatively beyond the demonstrated case would likewise add quadratic work without a scientific defect to justify it. Arbitrary-precision production arithmetic would broaden ownership and dependency surface far beyond the demonstrated defect. The bounded admission instead reuses the existing checked dyadic proof and changes only its evidenced upper sample count. + +## Method and standards trace + +Morris, White, and Crowther (2019) distinguish deterministic performance-measure calculations from Monte Carlo uncertainty; a one-ULP deterministic bias-SE error is therefore repaired in the estimator rather than treated as simulation noise. Published floating-point authority remains IEEE 754-2019 / ISO/IEC 60559:2020. IEEE P754 remains an active PAR approved June 6, 2024 to supersede 754-2019; it is revision work, not a published replacement. ISO continues to list ISO/IEC 60559:2020 as a published International Standard at stage 60.60. AERA, APA, and NCME continue revising the 2014 *Standards for Educational and Psychological Testing*; AERA's task-force roster was current as of August 31, 2026, so the unpublished revision is not treated as normative authority. + +## Traceability + +- Bounded context: Validation Evidence. +- Production module/API: `crates/validation_core/src/bias_se.rs` → `bias_standard_error`. +- Public executable contract: `crates/validation_core/tests/bias_standard_error_ten_observation_pair_distance_contract.rs`. +- RED: `813c97e8e5e13ee7cec7e4290b7bb78d68504cdd`. +- Causal source repair: `1bff5033e2de3095340141da38e9c02387b3a868`. +- CHANGELOG: `2dacce55a06cc160ae3defcc84b75a8cebd46e18`. +- Scientific invariant: for exact represented residuals admitted by the bounded proof, `SE(mean)^2 = sum_{i 2` standard errors. + +Contract completion `8260bc0bff11abae2b05e0a85b5c1c374b8cbd49` covers all six permutations and each sign mirror. CHANGELOG evidence is `5757dda1dc2699618d12c0f8a33913aedde67ad4`. + +## Constraints and rejected alternatives + +A payload-specific branch for the `5/16/19` integer triple was rejected because it would encode the counterexample rather than the scientific invariant. Replacing TEPP Validation Evidence arithmetic with arbitrary-precision production arithmetic was rejected because the defect has a narrower proof boundary and reusable static arithmetic remains owned by `fast-mlsirm`. Applying the closed form to every three-level sample was rejected because inexact products, additions, or irrational roots would create a new unverified rounding surface. Reusing the generic normalized moment merely with a different exact anchor was also rejected: GAP-107 already minimizes anchor dynamic range, but this counterexample remains one ULP wrong even under its best exact anchor. + +## Scientific and standards trace + +IEEE 754-2019 remains the published floating-point arithmetic authority for the binary64 operations used here. IEEE P754 is an active revision project approved on 2024-06-06 and has not replaced the published 2019 standard. ISO/IEC 60559:2020 remains the published international adoption. These sources distinguish the defined rounding of individual operations from the separate numerical-analysis question of whether an algorithm introduces avoidable intermediate rounding boundaries. + +Morris, White, and Crowther (2019) distinguish performance-measure estimation from Monte Carlo uncertainty. The present defect changes the deterministic computation of the standard error of represented signed-bias observations; it must therefore be repaired before Monte Carlo uncertainty can be interpreted as simulation uncertainty rather than arithmetic error. + +The AERA/APA/NCME *Standards for Educational and Psychological Testing* published edition remains the 2014 edition; the sponsoring organizations announced the Joint Committee for revision in 2024. This repair changes numerical fidelity inside Validation Evidence and does not change the construct, intended score interpretation, or validation-policy authority, so no PRD/ADR target change is required. + +## Traceability + +- Bounded context: Validation Evidence. +- Module/API: `crates/validation_core/src/bias.rs` → `validation_core::bias_standard_error`. +- Public contract: `crates/validation_core/tests/bias_standard_error_three_level_rational_scale_rounding_contract.rs`. +- RED: `32dcab8434a9676854f3a470094aadfc4f3f417d`. +- Causal source repair: `bee85e3df044e13a2df6c077cc87706b6cd78402`. +- Permutation/sign-mirror completion: `8260bc0bff11abae2b05e0a85b5c1c374b8cbd49`. +- CHANGELOG: `5757dda1dc2699618d12c0f8a33913aedde67ad4`. +- Owner boundary: no mutable `fast-mlsirm` or unreleased `contextual-orchestrator` source is consumed. +- Promotion boundary: exact-head hosted Rust/coverage/security/documentation evidence and qualifying independent review remain mandatory before merge or release. + +## References + +American Educational Research Association, American Psychological Association, & National Council on Measurement in Education. (2014). *Standards for educational and psychological testing*. American Educational Research Association. + +IEEE Computer Society. (2019). *IEEE standard for floating-point arithmetic* (IEEE Std 754-2019). IEEE. https://doi.org/10.1109/IEEESTD.2019.8766229 + +International Organization for Standardization & International Electrotechnical Commission. (2020). *Information technology—Microprocessor systems—Floating-point arithmetic* (ISO/IEC 60559:2020). + +Morris, T. P., White, I. R., & Crowther, M. J. (2019). Using simulation studies to evaluate statistical methods. *Statistics in Medicine, 38*(11), 2074–2102. https://doi.org/10.1002/sim.8086 diff --git a/docs/research/bias-standard-error-three-level-scale-invariance.md b/docs/research/bias-standard-error-three-level-scale-invariance.md new file mode 100644 index 000000000..a5852963f --- /dev/null +++ b/docs/research/bias-standard-error-three-level-scale-invariance.md @@ -0,0 +1,67 @@ +# GAP-109 — exact three-level bias-SE proof must be scale invariant + +## Problem + +GAP-108 added a bounded exact path for three represented observations whose translated offsets satisfy + +`SE(mean)^2 = (x^2 + y^2 - xy) / 9` + +and whose dispersion numerator is itself an exactly represented square. The proof was nevertheless evaluated on the raw offsets. That made admission depend on magnitude rather than only on represented geometry: multiplying a valid dyadic sample by an exact power of two can overflow `x^2`, `y^2`, or `xy` even when the final standard error is finite and representable. + +The public counterexample is the exact power-of-two scaling of GAP-108 by `2^600`: + +- `truth = [0, 0, 0]` +- `recovered = [0, 5 * 2^590, 21 * 2^590]` +- represented bits: `0x0000000000000000`, `0x64f4000000000000`, `0x6515000000000000` +- minimax exact translation: `[-5 * 2^590, 0, 16 * 2^590]` + +For the unscaled geometry, `x^2 + y^2 - xy = 361 / 2^20 = (19 / 1024)^2`; scaling by `2^600` therefore gives the exact target + +`SE(mean) = (19 / 3072) * 2^600 = 0x1.9555555555555p+592` + +with binary64 bits `0x64f9555555555555`. + +Before GAP-109, the raw square/cross-product proof overflowed and the implementation fell back to the general translated normalized-moment path. That path evaluates the same normalized geometry `[-5/16, 0, 1]` through moment reconstruction and `sqrt`, returning adjacent upper `0x64f9555555555556`. The one-ULP shift is deterministic arithmetic error, not Monte Carlo uncertainty. + +## Constraints + +The repair must not weaken the GAP-108 error-free admission, make arbitrary-precision arithmetic a production dependency, special-case the payload, or move reusable static psychometric estimation out of `fast-mlsirm`. It must preserve permutation/sign symmetry and fail closed whenever the represented geometry cannot be proved exactly reversible. + +## Decision + +When any of the three raw products overflows, `validation_core::bias` now retries the same GAP-108 proof after dividing both offsets by the exact power-of-two binade scale of their maximum magnitude. Admission requires: + +1. both normalized offsets to remain finite and nonzero when their sources are nonzero; +2. multiplying each normalized offset by the scale to reconstruct the original represented offset exactly; +3. normalized squares and cross-product to be finite and FMA-proven error-free; +4. square addition and cross-product subtraction to be error-free; +5. the normalized dispersion numerator to have an exactly represented square root. + +Only then is the normalized root divided by three through the existing deterministic representable-denominator primitive and restored by the same exact power-of-two scale. Raw finite-product cases keep the GAP-108 path unchanged, including its subnormal exact-rational projection. If normalization or any proof fails, the predecessor translated path remains authoritative. + +This is intentionally narrower than a claim that all `n > 2` standard errors are globally correctly rounded. + +## Alternatives rejected + +Payload-specific branching on `5/16/19` would encode the fixture rather than the invariant. Unconditionally normalizing every three-level proof would broaden behavior for low-scale/subnormal cases already covered by GAP-105/GAP-108. Replacing the bounded binary64 proof with arbitrary precision would duplicate numerical ownership and enlarge production cost without evidence that the broader dependency is required. Accepting overflow as proof failure is rejected because exact power-of-two scaling is representation preserving here and would make scientific output depend on unit magnitude. + +## Standards and methodological trace + +IEEE P754 is an active revision PAR approved 2024-06-06 and identifies IEEE 754-2019 as the standard it is intended to supersede. ISO/IEC 60559:2020 remains a published International Standard at stage 60.60, and IEEE/ISO/IEC 60559-2020 is listed as an active adoption of IEEE 754-2019. These authorities support treating the sequence and destination format of binary64 operations as part of the reproducible numerical contract rather than assuming algebraically equivalent floating-point expressions are interchangeable. + +Morris, White, and Crowther (2019, *Statistics in Medicine*, https://doi.org/10.1002/sim.8086) distinguish deterministic performance-measure computation from Monte Carlo error due to a finite number of simulation repetitions. GAP-109 concerns the former: the represented sample is fixed and only the arithmetic path changes. + +AERA, APA, and NCME continue to publish the 2014 *Standards for Educational and Psychological Testing* as the current public edition. This repair is therefore recorded as validity-evidence computation infrastructure; it does not substitute numerical implementation detail for the broader evidentiary requirements governing score interpretation and use. + +## Traceability + +- Public RED: `67fa485248a8673f90bb71a43c2a58865a764383` +- Contract: `crates/validation_core/tests/bias_standard_error_three_level_scale_invariance_contract.rs` +- Causal Rust repair: `dcaf25b37d9860e7956de5429d3ef5894b129b49` +- Production API/module: `validation_core::bias_standard_error` / `crates/validation_core/src/bias.rs` +- CHANGELOG: `31b1aff811703a41e3524e804d095be32621b004` +- Protected base observed before repair: `main@a243f18da4a4ca8a8d068c39922537f1f8ed6ad0` + +## Remaining risk + +The exact three-observation shortcut still applies only when its rational-square identity can be proved in represented binary64 arithmetic. Other three-level and larger-sample geometries remain on the translated second-moment path and may justify separate findings only when a concrete represented-input counterexample demonstrates a scientifically material discrepancy. diff --git a/docs/research/bias-standard-error-three-level-underflow-scale-invariance.md b/docs/research/bias-standard-error-three-level-underflow-scale-invariance.md new file mode 100644 index 000000000..2ee39489e --- /dev/null +++ b/docs/research/bias-standard-error-three-level-underflow-scale-invariance.md @@ -0,0 +1,65 @@ +# GAP-110 — three-level standard error underflow-scale invariance + +## Finding + +`bias_standard_error` could return exact zero for a represented three-observation residual sample with nonzero, representable dispersion when the exact three-level proof evaluated raw products before normalization. + +The public regression sample is + +- `truth = [0, 0, 0]` +- `recovered = [0, f64::MIN_POSITIVE, 2 * f64::MIN_POSITIVE]` +- expected `SE(mean)` bits: `0x0009_3cd3_a2c8_198e` + +The canonical exact translation is `[-m, 0, m]`, where `m = 2^-1022`. For three represented observations `[0, x, y]`, + +`SE(mean)^2 = (x^2 + y^2 - xy) / 9`. + +With `x = -m` and `y = m`, the exact result is `m / sqrt(3)`, which is a nonzero subnormal binary64 value. The predecessor GAP-109 path formed `x*x`, `y*y`, and `x*y` at the raw scale. All three exact products lie below the binary64 minimum-subnormal magnitude and round to signed zero. The subsequent FMA residual checks also round those exact products to zero, so a zero product could be mistaken for an error-free product. The proof then admitted a zero radicand and returned `SE(mean) = 0`. + +This is deterministic performance-measure arithmetic error, not Monte Carlo uncertainty. The represented observations and target estimand are fixed; permutation or sign reflection must not change whether a nonzero standard error survives the arithmetic path. + +## Causal repair + +Public RED: `fe013e7dbd9b6c99371fe28ff3f6aa2cb2915408`. + +Rust repair: `d3660d44f1bc315e2e34ecfcfa74c26b8f1cd257` in `crates/validation_core/src/bias.rs`. + +The repair does not broaden exact-three-level admission. It extends the existing GAP-109 normalization trigger from raw product overflow to raw product loss of represented range: a nonzero square or cross-product that rounds to zero is treated as an inability to prove the identity at that magnitude. The same exactly reversible `exact_power_of_two_scale` retry is used. The normalized products must still be finite, nonzero where the source product is mathematically nonzero, exactly reconstruct the represented offsets after scale restoration, satisfy the existing FMA/error-free sum and subtraction checks, and produce an exactly represented square root before the direct identity can be admitted. + +For the GAP-110 sample, normalization produces offsets `[-1, 1]`. Its radicand is `3`, whose square root is not exactly represented, so the bounded identity correctly declines admission. Control returns to the existing translated second-moment path, which already normalizes the represented geometry before squaring and returns the correctly rounded nonzero subnormal result `0x0009_3cd3_a2c8_198e`. + +The public contract covers all six permutations and their sign mirrors in `crates/validation_core/tests/bias_standard_error_three_level_underflow_scale_contract.rs`. An internal unit contract also asserts that the bounded exact-three-level helper returns `None` for `(-f64::MIN_POSITIVE, f64::MIN_POSITIVE)` instead of manufacturing an exact zero proof. + +## Alternatives rejected + +Returning `InvalidInput` whenever a raw square underflows was rejected because the requested final standard error can remain representable, as it does here. Treating FMA residual zero as sufficient proof was rejected because an exact result below the destination format can round to zero in both the product and the residual check. Applying unconditional normalization to every three-level sample was rejected because GAP-108/GAP-109 already preserve a narrower finite-product fast path with explicit exactness evidence. Arbitrary-precision production arithmetic remains out of scope for this TEPP Validation Evidence repair and would cross the reusable numerical-owner boundary without a demonstrated need. + +## Scientific and standards trace + +IEEE 754-2019 remains the active published IEEE floating-point standard. IEEE P754 is an active revision PAR, approved 2024-06-06, intended to supersede 754-2019 but is not a published replacement as of 2026-09-05. ISO/IEC 60559:2020 remains the published international adoption of IEEE 754-2019. The repair therefore documents destination-format underflow and exact-operation evidence against the current published standard rather than a draft revision. + +Morris, White, and Crowther's ADEMP framework distinguishes the deterministic definition and computation of a performance measure from Monte Carlo standard error caused by a finite number of simulation repetitions. GAP-110 concerns the former: the same represented sample cannot legitimately move from nonzero dispersion to zero because an intermediate product falls outside binary64 range. + +AERA, APA, and NCME continue to publish the 2014 *Standards for Educational and Psychological Testing* while the Joint Committee is revising that edition. TEPP therefore keeps the 2014 edition as the normative testing-standards authority and treats revision materials as development evidence only. + +## Traceability + +- Bounded context: Validation Evidence +- Aggregate/API: `bias_standard_error` +- Production module: `crates/validation_core/src/bias.rs` +- Public contract: `crates/validation_core/tests/bias_standard_error_three_level_underflow_scale_contract.rs` +- RED: `fe013e7dbd9b6c99371fe28ff3f6aa2cb2915408` +- Causal repair: `d3660d44f1bc315e2e34ecfcfa74c26b8f1cd257` +- CHANGELOG: `CHANGELOG.d/validation-bias-standard-error-three-level-underflow-scale.md` +- Predecessor: GAP-109 `1eaac11e715144127048fc20033719777528c692` +- Owner boundary: reusable static psychometric arithmetic remains in `fast-mlsirm`; no mutable `contextual-orchestrator` contract is consumed. + +## References + +American Educational Research Association, American Psychological Association, & National Council on Measurement in Education. (2014). *Standards for educational and psychological testing*. American Educational Research Association. + +IEEE. (2019). *IEEE standard for floating-point arithmetic* (IEEE Std 754-2019). https://doi.org/10.1109/IEEESTD.2019.8766229 + +ISO/IEC. (2020). *Information technology—Microprocessor systems—Floating-point arithmetic* (ISO/IEC 60559:2020). + +Morris, T. P., White, I. R., & Crowther, M. J. (2019). Using simulation studies to evaluate statistical methods. *Statistics in Medicine, 38*(11), 2074–2102. https://doi.org/10.1002/sim.8086 diff --git a/docs/research/bias-standard-error-two-observation-mean-roundoff.md b/docs/research/bias-standard-error-two-observation-mean-roundoff.md new file mode 100644 index 000000000..524760700 --- /dev/null +++ b/docs/research/bias-standard-error-two-observation-mean-roundoff.md @@ -0,0 +1,52 @@ +# Bias standard error: two-observation mean-rounding boundary + +## Problem + +`validation_core::bias_standard_error` already used the exact two-observation identity `SE(mean) = |r₁-r₂| / 2` when either `recovered - truth` subtraction discarded represented low-order mass. Exact pairwise residuals, however, still fell through to the generic path that first rounded the residual mean and then formed deviations around that rounded mean. + +That distinction is scientifically material even when every input subtraction is exact. Let + +- `truth = [0, 0]`, +- `recovered = [1, next_down(1)]`, +- `next_down(1) = 0x1.fffffffffffffp-1`. + +The represented residuals are therefore exactly `[1, 1 - 2^-53]`. Their exact mean is `1 - 2^-54`, which is the midpoint between the two adjacent binary64 residuals and rounds to `1` under round-to-nearest, ties-to-even. Centering on that rounded mean produces deviations `[0, -2^-53]`; the generic scaled second-moment path consequently returns approximately `2^-53 / sqrt(2)` (`0x3c96_a09e_667f_3bcd`). + +For two observations the sample standard error of the mean simplifies algebraically before any mean is needed: + +`SE(mean) = |r₁ - r₂| / 2 = 2^-54`, whose binary64 bits are `0x3c90_0000_0000_0000`. + +The sign-mirrored residuals have the same dispersion, and equal residuals remain exactly zero. + +## Decision + +For every two-observation sample, evaluate `SE(mean) = |r₁-r₂| / 2` before the generic rounded-mean path. + +- If either pairwise `recovered - truth` subtraction has a nonzero error-free low term, retain the predecessor expanded-input difference `[recovered₁, -truth₁, -recovered₂, truth₂]` so discarded subtraction mass is not lost. +- If both residual subtractions are exact, form the half-difference from the represented residuals `[r₁, -r₂]` through `deterministic_representable_sum_over_count(..., 2)`. This preserves the existing cancellation/overflow-safe denominator handling; in particular, opposite extreme finite residuals can still yield a representable standard error without materializing `r₁-r₂` as an overflowing intermediate. +- Take the absolute value only after the signed half-difference is formed. + +This is a bounded algebraic repair, not a claim that the general `n > 2` standard-error path is globally correctly rounded. + +## Rejected alternatives + +Keeping the generic rounded-mean path for exact residuals was rejected because the counterexample shows that exact input subtraction does not imply an exact residual mean, and a rounded two-point mean can change the dispersion geometry substantially rather than by a negligible reporting-only amount. + +Computing `(r₁-r₂).abs()/2` directly was rejected because the raw difference can overflow for opposite-sign extreme finite residuals even when the final halved result is representable. Reusing the repository's deterministic representable sum-over-count boundary keeps the scientific denominator and range policy intact. + +Introducing arbitrary-precision arithmetic for all standard errors was rejected as disproportionate to this proven two-observation identity and outside the current Validation Evidence runtime boundary. + +## Evidence and traceability + +- Public RED: `d3dcd918bece928ae4103dffda8e7dc654927da0`, `crates/validation_core/tests/bias_standard_error_two_observation_mean_roundoff_contract.rs`. +- Minimal causal repair: `02b0a178154ea0ae7da87289756897d7c2f361e3`, `crates/validation_core/src/bias.rs`. +- CHANGELOG: `5603b12d2aedb6dfcc0aa4203d57047a98aee789`. +- API: `validation_core::bias_standard_error`. + +IEEE 754 binary64 round-to-nearest, ties-to-even semantics determine the midpoint behavior in the counterexample. The performance-measure interpretation follows the existing TEPP Validation Evidence trace to known-truth simulation evaluation and Monte Carlo uncertainty rather than treating an LLM judgment as numerical authority. + +## References + +IEEE Computer Society. (2019). *IEEE standard for floating-point arithmetic* (IEEE Std 754-2019). IEEE. https://doi.org/10.1109/IEEESTD.2019.8766229 + +Morris, T. P., White, I. R., & Crowther, M. J. (2019). Using simulation studies to evaluate statistical methods. *Statistics in Medicine, 38*(11), 2074–2102. https://doi.org/10.1002/sim.8086 diff --git a/docs/research/bias-standard-error-wide-linear-admission-bound.md b/docs/research/bias-standard-error-wide-linear-admission-bound.md new file mode 100644 index 000000000..e2bd7ec4f --- /dev/null +++ b/docs/research/bias-standard-error-wide-linear-admission-bound.md @@ -0,0 +1,82 @@ +# Bias standard-error wide-linear admission bound + +## Problem + +Issue #491 originally treated the production `n=4..=16` exact bias-standard-error proof as a sample-count staircase. The accumulated evidence separates three questions: whether represented data admit an exact proof, whether the arithmetic representation is wide enough to carry that proof, and whether the resource cost is acceptable for production. None is resolved by incrementing `n` alone. + +An earlier characterization subtracted the represented minimum and produced nonnegative integer coefficients `c_i` on a common exact dyadic unit. For that historical representation, `P = sum_{i Wide256 O(n) -> pair` route telemetry to the characterization harness. + +Represented-input reachability `5a19b6334487b43fb630abba7e487d7cf4c49960` reaches the wider numerator route at `n=4096` on `{0,1,2^53}` and recovers `P=664289479338799435974172876300357631`. At represented `n=2050`, characterization `a8423173188fa53a26a16d3afdafeb76e114cc1d` finds `P=332306998946228931332463617650984961`, denominator `8_610_922_500`, but 136-bit candidate-square and 140-bit adjacent-midpoint comparison operands. `aab9fe9115cee97225f2aa81e54a55ceafb23336` shows those comparisons can remain bounded as `Wide256` mantissa plus signed dyadic exponent. RED `f7717361ad8c5f0592688c1514c104cc1b4adabe` → repair `e4a85f53a611922be7492fe906d62ce65787c18e` integrates that comparator into production; `1240ace8eb41a01fa72a4bb99df842fd550a1288` fixes both exact tie-to-even parity directions. + +IEEE 754-2019 and ISO/IEC 60559:2020 remain the published floating-point basis. Rechecked on 2026-09-07, ISO lists ISO/IEC 60559:2020 at published stage 60.60, while IEEE lists P754 as an Active PAR approved 2024-06-06 that supersedes 754-2019 when completed; P754 is therefore a revision project, not a published replacement. + +## Represented proof equivalence and admission + +Historical characterization `7a2ab0a1ef7a72d8cc9b9253d7f92c493e578943` compared an O(n²) pair authority with a minimum-anchor Wide256 O(n) identity for represented residual classes `{0,1,2^53}` at `n=4,16,17,65,257,2050`. Wherever both admitted, they produced the same common unit, exact pair numerator, and reduced ratio. That helper ceased to represent production after the conditioned-anchor route was removed. + +Characterization `2bc1d2284d75154e020640adb573c1cfadf005fb` shows why pairwise-f64 exactness cannot define represented-input admission. Residuals `[0,1,2^-54,2]` have exact coordinates from neutral zero, while non-anchor subtraction `1-2^-54` rounds. On common unit `2^-54`, coordinates `[0,2^54,1,2^55]` give `P=3569704090242693886528325169446915`. The generic fallback happens to return the same public result, so this fixture is an admission distinction rather than a public defect. + +Source RED `fd9f9ff2c5c395e4cc13042232f4deef018adb48` then shows a public defect with `[0,1,2,-2^53]`. The represented minimum `-2^53` cannot exactly translate `1`, but neutral zero preserves every represented residual exactly. Signed unit-one coordinates give `P=243388915243820099130562543878155`, denominator `48`, and correctly rounded result `0x4320000000000001`; the predecessor translated floating-moment fallback returns `0x4320000000000000`. The RED Actions runs were cancelled by the immediate successor and are not hosted RED evidence. + +Initial repair `81ba770cc4812c8fbeb4b3529f0a73b41abbed0f` searched every represented residual as a candidate exact anchor. Follow-up RED `9f403194a2ec1636531c2dfe9229cfb34b73d747` proved that set incomplete: with represented residuals `[1,2^-54,2,3]`, no observed residual is a universal exact anchor, while neutral zero preserves every represented residual exactly. On common unit `2^-54`, the coordinates are `[2^54,1,2^55,3*2^54]` and `P=6490371073168534319490338297741315`; correctly rounded public bits are `0x3fe4a7e9cb8a3491`, while the predecessor translated floating-moment fallback returned adjacent upper `0x3fe4a7e9cb8a3492`. + +Repair `6cf30eeb549c0df0377bda1111cf46396e8282a3` therefore expanded the candidate set to neutral zero plus represented residual anchors. That fixed the demonstrated correctness defects but still evaluated pairwise proof first and then scanned the sample for every candidate, leaving production O(n²). + +Neutral zero is not synthetic evidence. It is a deterministic coordinate origin for the translation-invariant pair-distance identity; subtracting `0` from a finite binary64 residual reproduces that represented residual exactly. The source observations remain unchanged. + +## Production route repair + +The production resource defect is repaired on active PR #488 without widening the sample budget. + +Source-level RED `e0b324864e48a503e2aba0d2a487a0b95f5276ed` required a linear neutral-zero proof before quadratic work. Repair `2b62bd46eb0c391327d2285c2244a76f5a1e0449` added `exact_neutral_zero_linear_pair_square_sum`. It scans represented residuals once to select the common dyadic exponent and a second time to accumulate positive/negative integer coefficient mass and `S2`. It then evaluates `n*S2-S1^2` with exact `Wide256` products and subtraction. The proof kernel is O(n) time and O(1) proof storage after the residual vector and allocates no pair records. + +A later retention criterion required the conditioned observed-anchor fallback to demonstrate unique bounded admission after neutral-zero refusal. The checked-in production and unit-test corpus supplied no such represented fixture. Source-level RED `d40bfbf98b36562164d15d505f8f8825fd1c1349` rejected continued production reliance on the unsupported route, and repair `14e7862f4ddccce54f3b93d4dac89adbf047ba77` removed `exact_anchor_linear_pair_square_sum`. Production order is now `neutral_zero_linear -> pairwise_reference -> generic_fallback`. This is a consolidation decision under current evidence, not a theorem that pairwise is globally redundant. + +Commit `da703fc50e136ed79e12262909c9f400a3945621` was an intermediate correction that replaced the removed minimum-anchor helper in the equivalence characterization with a neutral-zero formulation. Review of that test exposed a second methodological defect: the integration test copied `Wide256`, dyadic parsing, scaling, and the neutral-zero kernel from production, so it was not an independent scientific oracle and it described `n=17..2050` as production-aligned even though production exact admission stops at 16. + +Repair `43d208383b765a3424019de45530af77ca484d78` removes the copied exact-route implementation from the integration test. The production-equivalence contract is now restricted to admitted `n=4` and `n=16` represented families and uses direct `i128/u128` integer pair-distance arithmetic as an independent oracle plus audited binary64 output bits. Forward, reversed, and rotated `n=16` inputs must return identical bits. The mixed-sign represented geometry `[-2^53,0,1,2^53]` remains an admission witness: direct binary64 subtraction of `-2^53-1` rounds away the unit contribution, while the independent integer pair oracle retains `P=2^109+3` and the public exact route returns `0x432a20bd700c2c3e`. Wider `n>16` arithmetic remains characterization evidence only and is not called a production-route test. + +Production sample admission remains `n=4..=16`. + +## Resource budget remains unresolved + +Exact pair records are 120 at `n=16`, 136 at `n=17`, 2,096,128 at `n=2048`, and 4,997,541 at `n=3162`. For the older aligned nonnegative diameter characterization `D=2^53`, narrow checked products fit through `n=2047`, exact pair-numerator extremum through `n=4095`, and unreduced `n²(n-1)` stays at or below `2^53` through `n=208064`. GCD reduction and represented geometry mean none is a universal production cutoff. + +The timing vehicle remains `crates/validation_core/examples/bias_se_exact_proof_budget.rs`; it records characterization routes rather than authoritative production execution. No Rust 1.98.0 `--release` raw CPU timing, allocator/RSS, or applicable buyer-path p95 evidence is authoritative yet. Production route telemetry must distinguish `neutral_zero_linear`, `pairwise_reference`, and `generic_fallback`; identical output bits are not route evidence. + +## Decision + +Keep production `validation_core::bias_standard_error` at `n=4..=16`. Use the neutral-zero two-pass exact proof before quadratic proof work inside that budget, keep pairwise O(n²) as the fail-closed comparison/reference path, and fall back to the established general implementation when bounded exact proof refuses. Do not reintroduce observed-anchor scanning without a represented fixture that proves unique scientific admission value. Scientific acceptance tests must use independent oracles or private production-unit tests; do not copy production exact arithmetic into an integration test and then treat agreement between the copies as independent evidence. + +Before widening beyond 16, require exact-head Rust 1.98.0 fmt/clippy/nextest/rustdoc and owned-production 100% line/branch coverage, same-head security/documentation GREEN and qualifying independent review, broad pair/neutral-zero equality wherever both admit, explicit neutral-zero-only represented fixtures, exact candidate stepping/midpoint/tie-to-even and permutation invariance, fail-closed overflow/range behavior, truthful production route telemetry, recorded release-mode raw CPU/allocator/RSS and applicable buyer-path p95 evidence, and current CHANGELOG/TRACEABILITY/TEST_STRATEGY/OPERABILITY/operator baseline. + +## Traceability + +- Issue: #491 +- Landing PR: #488 +- Accumulator-bound characterization: `b7e4da353ac58069afd73ee7c0e8427d49993fdb` +- Wide-product capacity RED / repair: `f74d9ac11cb0acf3eb8fdd9ad79ac3d2e9180993` / `e9a7dee29afb97542bfe2965f850c8ab5a34368e` +- Represented-input Wide256 reachability: `5a19b6334487b43fb630abba7e487d7cf4c49960` +- Represented exact-midpoint width: `a8423173188fa53a26a16d3afdafeb76e114cc1d` +- Exponent-safe scaled comparison: `aab9fe9115cee97225f2aa81e54a55ceafb23336` +- Production scaled-comparison RED / repair: `f7717361ad8c5f0592688c1514c104cc1b4adabe` / `e4a85f53a611922be7492fe906d62ce65787c18e` +- Exact midpoint tie-to-even: `1240ace8eb41a01fa72a4bb99df842fd550a1288` +- Historical minimum-anchor pair/Wide256 equivalence: `7a2ab0a1ef7a72d8cc9b9253d7f92c493e578943` +- Pairwise-f64-strict characterization: `2bc1d2284d75154e020640adb573c1cfadf005fb` +- Non-minimum represented-anchor RED / repair: `fd9f9ff2c5c395e4cc13042232f4deef018adb48` / `81ba770cc4812c8fbeb4b3529f0a73b41abbed0f` +- Neutral-anchor RED / correctness repair: `9f403194a2ec1636531c2dfe9229cfb34b73d747` / `6cf30eeb549c0df0377bda1111cf46396e8282a3` +- Neutral-zero route-order RED / production resource repair: `e0b324864e48a503e2aba0d2a487a0b95f5276ed` / `2b62bd46eb0c391327d2285c2244a76f5a1e0449` +- Unsupported conditioned-anchor retention RED / removal: `d40bfbf98b36562164d15d505f8f8825fd1c1349` / `14e7862f4ddccce54f3b93d4dac89adbf047ba77` +- Intermediate copied neutral-zero characterization: `da703fc50e136ed79e12262909c9f400a3945621` +- Independent production-domain pair oracle repair: `43d208383b765a3424019de45530af77ca484d78` +- Narrow-wide-pair characterization RED / repair: `3136739460ef0c8e13c044a7e5b04891e4f4e23d` / `ce4ed2722e160eb0ca0ee2d636a5eca55e3ff2d5` +- CHANGELOG fragment: `CHANGELOG.d/validation-bias-exact-proof-budget-characterization.md` +- Production module: `crates/validation_core/src/bias_se.rs` +- Route-order contract: `crates/validation_core/tests/bias_standard_error_neutral_zero_route_order_contract.rs` +- Independent production-domain equivalence characterization: `crates/validation_core/tests/bias_standard_error_represented_route_equivalence_characterization.rs` +- Public regression: `crates/validation_core/tests/bias_standard_error_nonminimum_anchor_exact_rounding_contract.rs` +- Exact-proof budget harness: `crates/validation_core/examples/bias_se_exact_proof_budget.rs` diff --git a/docs/research/monte-carlo-percentile-joint-moment-support.md b/docs/research/monte-carlo-percentile-joint-moment-support.md new file mode 100644 index 000000000..6d6b115ae --- /dev/null +++ b/docs/research/monte-carlo-percentile-joint-moment-support.md @@ -0,0 +1,37 @@ +# Joint empirical-percentile moment support + +## Scientific contract + +`MonteCarloSummary` stores a represented binary64 sample mean `m`, sample standard deviation `s` with denominator `n - 1`, and inclusive nearest-rank percentile endpoints selected from the retained replications. The generic carrier is intentionally sign-neutral because it is used for signed metrics such as bias as well as nonnegative metrics such as RMSE. + +For the producer's represented-mean deviations `d_i = x_i - m`, the stored spread satisfies + +`(n - 1) s^2 = sum_i d_i^2`. + +The existing endpoint-support contract correctly requires each retained percentile endpoint to satisfy `|d| <= s * sqrt(n - 1)`. That condition is necessary but not sufficient when the lower and upper endpoints are numerically distinct. Distinct endpoint values must come from distinct retained observations, so both squared deviations consume the same finite sample budget: + +`(percentile_lower - m)^2 + (percentile_upper - m)^2 <= (n - 1) s^2`. + +If the two endpoint values are numerically equal, they may designate the same retained observation or duplicate observations with the same represented value, so admission conservatively counts that value once rather than inventing a second observation. + +The attainable fixture `[0.75, 0.75, 0.75, 1.75]` has `n = 4`, represented mean `1.0`, sample SD `0.5`, SE `0.25`, and distinct endpoints `0.75` and `1.75`. Its joint endpoint contribution is `0.25^2 + 0.75^2 = 0.625`, within the total deviation budget `3 * 0.5^2 = 0.75`. + +By contrast, a summary with the same `n`, mean, SD, and SE but endpoints `0.25` and `1.75` passes the predecessor's separate endpoint checks because each absolute deviation is `0.75 < 0.5 * sqrt(3)`. It is nevertheless impossible: the two observed endpoints alone require `0.75^2 + 0.75^2 = 1.125`, exceeding the entire recorded deviation budget `0.75` before any other retained replication is considered. + +## Numerical implementation + +The admission check avoids raw full-range subtraction and squaring. It divides mean, both endpoints, and SD by one shared finite magnitude, combines the two normalized deviations with binary64 `hypot`, and compares that norm with the normalized `SD * sqrt(n - 1)` support using the same small relative tolerance as the individual endpoint contract. This preserves the represented-mean semantics already adopted by the predecessor repair and does not assume that deviations from the rounded binary64 mean sum to exact real zero. + +## RED, causal repair, and traceability + +- Public RED `c4a13826144708582a73ec4458967bb103287338` adds `crates/validation_core/tests/monte_carlo_percentile_joint_moment_support_contract.rs`. It preserves an attainable joint-support fixture, rejects the individually-valid-but-jointly-impossible `(0.25, 1.75)` endpoints, verifies JSON egress/ingress fail closed, and preserves equal-endpoint admission without double-counting one represented value. +- Causal production repair `cb3f80a2ff3439d238d1bd6e674ef789d25f36c7` extends `MonteCarloSummary::validate` with the distinct-endpoint joint deviation budget while retaining the predecessor's represented-mean individual endpoint rule. +- Changelog trace `f727450d7a68d253254d3f0a8ae8d305a08137eb` records the durable-evidence repair. + +This is Validation Evidence artifact admission in TEPP. It does not create a new psychometric estimator, does not change Longitudinal Modeling composition, and does not relocate reusable static psychometric arithmetic from fast-mlsirm. + +## Methodological reference + +Morris, T. P., White, I. R., & Crowther, M. J. (2019). Using simulation studies to evaluate statistical methods. *Statistics in Medicine, 38*(11), 2074–2102. https://doi.org/10.1002/sim.8086 + +Morris et al. distinguish estimands, performance measures, and Monte Carlo uncertainty and recommend considering performance measures jointly. TEPP's validation boundary therefore treats a summary as a coherent evidence artifact rather than validating each reported scalar in isolation. diff --git a/docs/research/monte-carlo-percentile-moment-support.md b/docs/research/monte-carlo-percentile-moment-support.md new file mode 100644 index 000000000..881d78280 --- /dev/null +++ b/docs/research/monte-carlo-percentile-moment-support.md @@ -0,0 +1,43 @@ +# Monte Carlo percentile moment support + +## Decision + +`MonteCarloSummary` is a reusable Validation Evidence carrier for scalar Monte Carlo metrics. Its empirical percentile endpoints are produced by `summarize_replications` with an inclusive nearest-rank rule, so each endpoint is one of the retained observations rather than an extrapolated quantile estimate. + +Let retained values be `x_1, ..., x_n`, represented binary64 mean be `m`, and the canonical sample standard deviation with denominator `n - 1` be `s`. The producer computes deviations `d_i = x_i - m` and therefore records + +`(n - 1) s^2 = sum_i d_i^2`. + +Every retained observation contributes one nonnegative squared deviation to that sum, so for each `j`, + +`d_j^2 <= (n - 1) s^2`, + +and therefore + +`|x_j - m| <= s * sqrt(n - 1)`. + +Every inclusive nearest-rank percentile endpoint is a retained `x_j`, so `percentile_lower` and `percentile_upper` must satisfy the same support bound. For the public fixture `n=4`, `mean=1`, `SD=0.5`, the support radius is `0.5 * sqrt(3) < 1`; an endpoint of `2.0` is therefore impossible even though the mean, SD, SE, and endpoint are individually finite. + +A stronger textbook bound based on zero-sum deviations was considered and rejected for artifact admission. TEPP deliberately stores a represented binary64 mean, and the mathematical sample mean can lie between adjacent binary64 values. The edge fixture `[1.0, next_up(1.0)]` exercises that projection: validation must use the squared-deviation identity actually implemented by the producer rather than assume that deviations from the represented mean sum to exact real zero. + +The admission comparison normalizes `mean`, endpoint, and `SD` by a shared finite scale before subtraction and multiplication. This prevents opposite-sign full-range binary64 values from overflowing merely because Validation Evidence is being checked. A small relative binary64 tolerance is allowed at the support boundary; the rule does not require cross-language bit-for-bit equality. + +This support law is generic. It does not impose RMSE nonnegativity on `MonteCarloSummary`; signed summaries such as bias remain valid when their retained empirical endpoints fit the represented moments. + +## RED, review, and causal repair + +- Public RED `40acb4f6f51dd9d7074c652fb6448eaa942b95ac` adds `monte_carlo_percentile_moment_support_contract.rs`. The predecessor accepted `n=4`, `mean=1`, `SD=0.5`, `SE=0.25`, `percentile_lower=0.75`, `percentile_upper=2.0` even though the recorded squared-deviation budget cannot contain that endpoint. +- Initial repair `2798e4f92dbb30019e2b1288e59d09564ae73a70` exposed an over-strong zero-sum assumption during immediate self-review. It was not treated as completion evidence. +- Causal correction `c7151b498ccbd562e7945a12a53c55472d93acac` bases admission on the producer's represented-mean squared-deviation identity, `|x - mean| <= SD * sqrt(n - 1)`. +- Edge reinforcement `dbef285b6348cf691bbb72c25350912a5463e11e` proves that adjacent-binary64 samples remain admissible when the represented mean rounds. +- Changelog correction `c38a320c730875919a8bff58de42e2db859c248d` records the final durable-evidence contract. + +This is TEPP Validation Evidence artifact admission. It does not introduce or relocate a psychometric estimator, change Longitudinal Modeling composition, or consume mutable fast-mlsirm source. + +## Methodological trace + +Simulation evidence is useful only when reported performance measures and Monte Carlo uncertainty are interpretable as summaries of realizable retained replications. Rejecting moment-incompatible empirical endpoints keeps the durable artifact aligned with the sample it claims to summarize rather than relying on a downstream renderer or LLM to infer scientific plausibility. + +American Educational Research Association, American Psychological Association, & National Council on Measurement in Education. (2014). *Standards for educational and psychological testing*. American Educational Research Association. + +Morris, T. P., White, I. R., & Crowther, M. J. (2019). Using simulation studies to evaluate statistical methods. *Statistics in Medicine, 38*(11), 2074–2102. https://doi.org/10.1002/sim.8086 diff --git a/docs/research/monte-carlo-summary-uncertainty-coherence.md b/docs/research/monte-carlo-summary-uncertainty-coherence.md new file mode 100644 index 000000000..3b43dd5bf --- /dev/null +++ b/docs/research/monte-carlo-summary-uncertainty-coherence.md @@ -0,0 +1,43 @@ +# Monte Carlo summary uncertainty coherence + +## Problem + +`MonteCarloSummary` is durable Validation Evidence. Its `standard_error` field is the standard error of the retained-replication mean, while `standard_deviation` is the sample SD over those replications. The canonical producer therefore defines `SE = SD / sqrt(n)`. Earlier admission checks rejected several impossible cases but still allowed materially understated or overstated positive SE values whenever they were merely finite, positive, and smaller than SD. For example, `n = 4`, `SD = 0.5`, `SE = 0.2` passed even though the represented summary contract implies `SE = 0.25`. + +That is an artifact-admission defect, not a new estimator. `summarize_replications` already computes the canonical relation and fails closed when a nonzero SD projects to exact-zero SE. The missing boundary was coherence validation for externally constructed or deserialized summaries. + +## Evidence and decision + +Morris, White, and Crowther (2019) treat simulation studies as empirical experiments and require Monte Carlo standard errors to quantify uncertainty from a finite number of simulation repetitions. For the mean of independent replication-level values, the Monte Carlo standard error is the empirical replication SD divided by the square root of the number of replications. The durable summary therefore cannot admit an arbitrary positive SE independently of its represented SD and replication count. + +TEPP keeps `MonteCarloSummary` sign-neutral so it can summarize signed metrics such as bias, while tightening uncertainty coherence: + +- `replication_count` must remain positive; +- SD and SE must remain finite and nonnegative; +- zero SD requires exact-zero SE; +- positive SD requires `n > 1` and a positive representable SE; +- positive SE must agree with `SD / sqrt(n)` within `64 * f64::EPSILON` relative error; +- the tolerance is deliberately wider than bit-for-bit equality so adjacent correctly rounded binary64 results from independent implementations remain admissible, while materially understated or overstated uncertainty is rejected; +- the canonical `n = 1` summary has zero SD and zero SE; +- percentile ordering remains a separate generic invariant. + +The relative comparison is scale-free. If canonical `SD / sqrt(n)` itself becomes exact zero while SD is nonzero, the payload fails closed instead of presenting zero Monte Carlo uncertainty. + +## Traceability + +- Bounded context: Validation Evidence. +- Production module: `crates/validation_core/src/monte_carlo.rs`. +- Public contract: `MonteCarloSummary::validate`, serde ingress/egress through the existing `MonteCarloSummary` implementations. +- Regression: `crates/validation_core/tests/monte_carlo_standard_error_coherence_contract.rs`. +- Earlier isolating RED: `e2d0c057d39b7786dbd96528d4e259775c6c2e01` demonstrated that a multi-replication SE equal to SD was admitted. +- Earlier repair: `0e973b566ec969d0fea8b7403bf09602cdebd4a3` rejected exact-zero SE with positive spread and multi-replication `SE >= SD` without asserting the canonical relationship. +- Canonical-coherence RED: `0a4c242fbd1c5b2f35e71a1ca1665ca9f7861338` demonstrates that `n = 4`, `SD = 0.5`, `SE = 0.2` was still admitted and that zero spread could still carry positive SE. +- Causal repair: `9b53076a53032441623ff487a006ec6d20812030` validates the represented `SD / sqrt(n)` relationship and zero-spread contract. +- Cross-language tolerance regression: `141246cfef7c44b327f7bfbeb22bc51279c6b9f0` admits the adjacent binary64 value to the canonical SE so admission is not a bit-pattern equality gate. +- Changelog: `CHANGELOG.d/validation-monte-carlo-summary-uncertainty-coherence.md`. + +This remains TEPP-owned artifact validation. It does not duplicate reusable static psychometric arithmetic from `fast-mlsirm`, introduce LLM authority, or change the scientific definition of RMSE, bias, coverage, or any longitudinal estimand. + +## Reference + +Morris, T. P., White, I. R., & Crowther, M. J. (2019). Using simulation studies to evaluate statistical methods. *Statistics in Medicine, 38*(11), 2074–2102. https://doi.org/10.1002/sim.8086 diff --git a/docs/research/monte-carlo-two-replication-endpoint-exhaustion.md b/docs/research/monte-carlo-two-replication-endpoint-exhaustion.md new file mode 100644 index 000000000..e543e38ef --- /dev/null +++ b/docs/research/monte-carlo-two-replication-endpoint-exhaustion.md @@ -0,0 +1,37 @@ +# Two-replication nearest-rank sample exhaustion + +## Scientific contract + +`MonteCarloSummary` stores a represented sample mean, sample standard deviation (`n - 1` denominator), standard error of the mean, and inclusive nearest-rank percentile endpoints selected from retained replications. The carrier remains sign-neutral because it can summarize signed metrics such as bias as well as nonnegative metrics such as the RMSE summaries admitted by `ValidationReport`. + +For more than two replications, lower and upper percentile endpoints do not identify the unreported retained values, so TEPP only enforces support that follows from the stored moments. The `n = 2` case is different. When the two nearest-rank endpoint values are numerically distinct, they must designate the two distinct retained observations. There are no remaining replications whose values could alter the represented mean or sample spread. + +Therefore, for `replication_count = 2` and `percentile_lower != percentile_upper`, the retained sample is exactly + +`[percentile_lower, percentile_upper]`. + +The recorded mean and sample SD must be coherent with that exhausted sample. This is stronger than the generic individual endpoint-radius and joint squared-deviation budget, but only in the finite-sample case where the artifact itself identifies every retained value. + +The public RED uses endpoints `[-0.5, 0.5]`. The canonical producer returns represented mean `0.0` and sample SD `sqrt(0.5)`. A payload that records the same two endpoint values with `SD = 1.0` and `SE = 1/sqrt(2)` passes the predecessor's standard-error coherence, individual endpoint support, and joint deviation-budget checks, yet no two-observation retained sample can produce it. A second fixture records mean `0.25`; it likewise satisfies the looser moment budget but cannot be the mean of the only two retained values. + +Equal numeric endpoints are not treated as sample exhaustion because nearest-rank lower and upper requests may select the same retained rank, or two retained observations may share the same represented value. Without percentile probabilities or rank multiplicity in the artifact, inferring two distinct observations from one numeric endpoint would overconstrain valid summaries. + +## Numerical implementation + +For the exhausted two-value case, admission reuses the same deterministic represented-mean and scaled sample-SD references as `summarize_replications`. The recorded and reconstructed values are compared after normalization by their own maximum magnitude, with the existing empirical-support relative tolerance. This avoids raw full-range subtraction and does not scale a near-zero mean comparison by the much larger endpoint magnitudes, which would otherwise admit a materially wrong near-zero mean. + +The normalized operands are finite and bounded by one, so the final relative-distance calculation cannot create a non-finite validation-only intermediate. The implementation therefore does not add an unreachable finite-check branch that would weaken owned branch-coverage evidence. + +## RED, causal repair, and traceability + +- Public RED `81bf0d9e2f1a28947b1343244002d6762b703f8a` adds `crates/validation_core/tests/monte_carlo_two_replication_endpoint_exhaustion_contract.rs` and demonstrates a two-replication summary that passes the predecessor's looser support checks but is not realizable from the exposed endpoint values. +- Causal implementation was corrected and stabilized at `d48f8fef08e77b8fa654f2852814c25c5d1baa79` after immediate source review of the first contents update; `fb314d8afccdf19f0074c64f6277d2edb290e907` adds explicit represented-mean coverage, and `440b78d86908fe1464e65bdbc4ceb5f9f6606c9f` removes an unreachable non-finite comparison branch while retaining the same scientific contract. +- Changelog trace `45116498f29c0d3421192d452e26182975b114ae` records the durable-evidence repair. + +This remains TEPP Validation Evidence artifact admission. It does not add a psychometric estimator, change Longitudinal Modeling composition, or copy mutable arithmetic from fast-mlsirm. + +## Methodological reference + +Morris, T. P., White, I. R., & Crowther, M. J. (2019). Using simulation studies to evaluate statistical methods. *Statistics in Medicine, 38*(11), 2074–2102. https://doi.org/10.1002/sim.8086 + +The ADEMP framing separates estimands, methods, data-generating mechanisms, and performance measures. TEPP applies that discipline at the evidence boundary: a reported performance summary is admitted only when the stored finite-sample fields can coexist under the producer contract actually used to generate them. diff --git a/docs/research/standard-error-acceptance-both-overflow-exact-comparison.md b/docs/research/standard-error-acceptance-both-overflow-exact-comparison.md new file mode 100644 index 000000000..410d2f0b2 --- /dev/null +++ b/docs/research/standard-error-acceptance-both-overflow-exact-comparison.md @@ -0,0 +1,50 @@ +# Standard-error acceptance both-overflow exact comparison + +## Problem and scientific contract + +TEPP's Validation bounded context exposes `accept_within_standard_errors(estimate, target, standard_error, k)` as the deterministic CPU `f64` reference for the decision + +`|estimate - target| <= k * standard_error`. + +The preceding repair made finite represented residuals and finite represented `k × SE` bounds direct-first, but retained scale normalization when both direct operations overflowed. Fresh represented-input review found that the remaining fallback can round both normalized sides to the same binary64 value even when the exact rational values represented by the original four binary64 inputs are strictly ordered. + +Public RED `a9a45714eb6266f6b922086112b313060e77c522` uses these exact binary64 payloads: + +- `estimate = 0x1.e446cf80dddbcp+1023` (`0x7fee446cf80dddbc`), +- `target = -0x1.2d7e397966af3p+1023` (`0xffe2d7e397966af3`), +- `standard_error = 0x1.20ad22ddb6f38p+823` (`0x73620ad22ddb6f38`), and +- `k = 0x1.5c69ac1c7a9edp+201` (`0x4c85c69ac1c7a9ed`). + +Both `estimate - target` and `k * standard_error` overflow to positive infinity in magnitude. The predecessor then normalized by the largest magnitude and obtained the same rounded binary64 value for the residual and bound (`0x1.9f6056b74e5cap+0`), so it accepted. An exact rational comparison of the represented inputs shows the bound is smaller than the residual; the difference is approximately `3.2267077731482595e292`. The immediately adjacent multiplier `0x1.5c69ac1c7a9eep+201` crosses the represented-input boundary and must remain accepted. + +## Causal repair and constraints + +Causal repair `425e89638d594fdb2f3586d73f021b60a530b456` keeps the existing direct-first cases and changes only the both-overflow branch. It decodes each finite binary64 magnitude into its exact integer significand and power-of-two exponent. The opposite-sign residual becomes an exact sum of two at-most-53-bit significands; `k × SE` becomes an exact at-most-106-bit significand product. The comparison then aligns those integer values by powers of two inside `u128`, avoiding another floating-point normalization step. + +This bounded method is possible because direct finite subtraction can overflow only for opposite-sign inputs near the top of binary64 range. Their exact significands therefore require at most one 53-bit-width alignment shift. The product of two binary64 significands needs at most 106 bits, so the complete comparison remains allocation-free and deterministic in Rust. + +Always retaining the scale-normalized fallback was rejected because it is the demonstrated cause of the false acceptance. Log-domain comparison was rejected because transcendental rounding is unnecessary for values that already have exact binary decompositions. Arbitrary-precision runtime arithmetic was also rejected: the relevant exact integers fit `u128`, so a heap-allocated big-number dependency would add cost and supply-chain surface without improving this bounded decision. + +The nearby adjacent-multiplier contract prevents the repair from turning the both-overflow region into a blanket rejection. CHANGELOG trace: `e62706291ad556c6d9078a2bc50dcf3bca5a6feb`. + +## Ownership, validation, and non-claims + +This is TEPP Validation decision semantics, not reusable static psychometric estimation. It does not move arithmetic owned by `fast-mlsirm`, does not consume mutable sibling source, and does not depend on an unreleased `contextual-orchestrator` contract. + +The RED and adjacent control are public Rust contract tests. An independent exact-rational search was used only to discover and verify the counterexample; it is not production arithmetic, does not replace the Rust decision path, and is not scientific acceptance evidence by itself. + +This repair claims exact represented-input comparison only for the existing both-overflow branch of `accept_within_standard_errors`. It does not claim that unrelated Wilson, Monte Carlo summary, strict-interior interval, or other validation formulas are globally correctly rounded. + +## Standards trace + +IEEE. (2019). *IEEE standard for floating-point arithmetic* (IEEE Std 754-2019). https://standards.ieee.org/ieee/754/6210/ + +IEEE 754-2019 remains an Active Standard as checked on 2026-09-04. IEEE P754, approved as a PAR on 2024-06-06, is an Active PAR intended to supersede 754-2019 and is not treated here as a published replacement. https://standards.ieee.org/ieee/754/11684/ + +International Organization for Standardization. (2020). *Information technology—Microprocessor systems—Floating-point arithmetic* (ISO/IEC 60559:2020). https://www.iso.org/standard/80985.html + +ISO/IEC 60559:2020 remains Published, stage 60.60, as checked on 2026-09-04. + +American Educational Research Association, American Psychological Association, & National Council on Measurement in Education. (2014). *Standards for educational and psychological testing*. American Educational Research Association. + +AERA, APA, and NCME announced the Joint Committee charged with revising the 2014 edition on 2024-06-12. As checked on 2026-09-04, that announcement still describes the committee as revising the 2014 edition; TEPP therefore continues to use the 2014 published edition rather than treating an unpublished revision as normative authority. https://www.aera.net/Newsroom/Members-of-the-Joint-Committee-for-the-Revision-of-the-Standards-for-Educational-and-Psychological-Testing-Named diff --git a/docs/research/standard-error-acceptance-equal-nonzero-correction-projection.md b/docs/research/standard-error-acceptance-equal-nonzero-correction-projection.md new file mode 100644 index 000000000..eaf244e6e --- /dev/null +++ b/docs/research/standard-error-acceptance-equal-nonzero-correction-projection.md @@ -0,0 +1,66 @@ +# SE-aware acceptance: equal nonzero correction projection + +## Problem + +`accept_within_standard_errors` decides the Validation Evidence predicate + +`|estimate - target| <= k * standard_error` + +from finite binary64 inputs. GAP-081 and GAP-082 recover first-order rounding terms when the directly rounded residual and bound are equal; GAP-083 falls back to an exact dyadic product comparison when both correction projections are zero. A remaining case existed when the subtraction correction and the FMA product correction both rounded to the same **nonzero** binary64 value even though the exact product correction lay strictly on one side of the exact subtraction correction. Falling through to the directly rounded equality could therefore change the scientific decision. + +This is Validation Evidence admission policy in TEPP. It is not a reusable psychometric estimator and does not move arithmetic ownership from `fast-mlsirm`. + +## Exact represented-input RED + +Public RED commit `35ea85ba5c049e3736e8549445bc799638cc6555` adds `crates/validation_core/tests/standard_error_acceptance_equal_nonzero_correction_projection_contract.rs`. + +Strict-rejection payload: + +- `estimate = f64::from_bits(0x0210_2814_5144_3c99)` +- `target = -f64::from_bits(0x0000_0000_6398_c737)` +- `k = f64::from_bits(0x20d9_5434_7757_68c7)` +- `standard_error = f64::from_bits(0x2124_696e_33e2_baaa)` + +The direct subtraction and direct product round to the same finite binary64 value. The error-free subtraction low term and `mul_add` product low term also project to the same positive subnormal. However, the represented-input exact product correction is slightly smaller than the represented subtraction correction; therefore the exact represented-input residual is strictly greater than `k * standard_error` and the correct decision is rejection. + +The same contract contains an adjacent acceptance control with `estimate=0x01c3_f43e_c52b_4312`, subtraction correction `0x0000_0000_003b_9da9`, `k=0x20c8_7ace_8d72_9746`, and `standard_error=0x20ea_1585_cc49_24ca`. There the exact product correction lies above the subtraction correction, so a blanket rejection of equal nonzero projections would also be wrong. + +Edge commit `b463a991adc6ff98fa09f91eb428cda9e0ff1255` adds the sign-complementary projection boundary: both first-order corrections can be the same negative subnormal while the exact represented product remains slightly farther below the rounded bound than the exact residual. This keeps the comparator's signed ordering executable rather than testing only positive product roundoff. + +## Causal repair + +Commit `7d597a18e043f3619b893981823b9be15ddb823c` keeps the existing decision hierarchy and changes only the unresolved equal-nonzero finite-tie branch. `represented_correction_le_exact_product_roundoff` decodes the represented factors and rounded product into integer significands and powers of two, forms the exact represented product and its exact signed roundoff in `u128`, and compares that exact roundoff with the represented subtraction correction. + +Self-review refinement `ca8c2b86a81cfa6e99a10bdcc141fc10d49ec011` removes generic zero/sign-mismatch branches that the equal-nonzero caller cannot reach. The helper now encodes those caller invariants as debug assertions and retains only the positive/negative signed orderings exercised by public contracts, avoiding a synthetic branch-coverage obligation with no production state behind it. + +The chosen repair avoids a second public decision authority, arbitrary-precision runtime dependency, decimal conversion, scale normalization, or a source copy from another CWL repository. The product significand is at most 106 bits, so the exact product and alignment required by this branch fit the existing `u128` numerical boundary. + +## Alternatives rejected + +Treating equal projected corrections as equality was rejected because the RED proves that projection equality does not imply exact represented-input equality. Rejecting every equal nonzero correction pair was rejected because the companion control proves that a valid acceptance exists on the other side of the same projection boundary. Replacing every finite comparison with a general exact-rational engine was rejected as unnecessary scope expansion: ordinary unequal direct results and the existing GAP-080/081/082/083 branches already have narrower causal rules. + +## Scope and residual risk + +The repair claims only the finite case where the directly rounded residual and bound are equal and the two nonzero correction projections are also equal. It does not claim globally correctly rounded Monte Carlo estimation, does not alter numerical estimation, and does not authorize scientific acceptance without the surrounding TEPP Validation Evidence contract and exact-head gates. Independent counterexamples remain the criterion for expanding the exact comparator into another branch. + +## Traceability + +- Bounded context: Validation Evidence +- Module/API: `crates/validation_core/src/monte_carlo.rs` / `accept_within_standard_errors` +- Public RED: `35ea85ba5c049e3736e8549445bc799638cc6555` +- Causal repair: `7d597a18e043f3619b893981823b9be15ddb823c` +- Signed edge coverage: `b463a991adc6ff98fa09f91eb428cda9e0ff1255` +- Branch-scope refinement: `ca8c2b86a81cfa6e99a10bdcc141fc10d49ec011` +- Contract test: `crates/validation_core/tests/standard_error_acceptance_equal_nonzero_correction_projection_contract.rs` +- CHANGELOG fragment: `CHANGELOG.d/validation-standard-error-acceptance-equal-nonzero-correction-projection.md` +- Landing vehicle: PR #488; only its current exact head after documentation commits is authoritative for hosted checks and review. + +## Normative and methodological references + +Institute of Electrical and Electronics Engineers. (2019). *IEEE standard for floating-point arithmetic* (IEEE Std 754-2019). IEEE Standards Association. https://doi.org/10.1109/IEEESTD.2019.8766229 + +International Organization for Standardization, International Electrotechnical Commission, & Institute of Electrical and Electronics Engineers. (2020). *Information technology—Microprocessor systems—Floating-point arithmetic* (ISO/IEC 60559:2020). + +American Educational Research Association, American Psychological Association, & National Council on Measurement in Education. (2014). *Standards for educational and psychological testing*. American Educational Research Association. + +As of 2026-09-05, IEEE 754-2019 remains an active published standard and P754 remains an active revision project rather than a published replacement. ISO/IEC 60559:2020 remains published. The AERA/APA/NCME Joint Committee is revising the 2014 testing Standards; the unpublished revision is not treated as current normative authority. diff --git a/docs/research/standard-error-acceptance-finite-rounding-tie.md b/docs/research/standard-error-acceptance-finite-rounding-tie.md new file mode 100644 index 000000000..0b98c5be5 --- /dev/null +++ b/docs/research/standard-error-acceptance-finite-rounding-tie.md @@ -0,0 +1,47 @@ +# Standard-error acceptance finite rounding tie + +## Problem and scientific contract + +TEPP Validation exposes `accept_within_standard_errors(estimate, target, standard_error, k)` for the deterministic decision + +`|estimate - target| <= k * standard_error`. + +The preceding GAP-079/GAP-080 repairs covered scale-underflow and both-overflow failures. Fresh represented-input review found a separate finite/finite boundary: both direct operations can remain finite yet round to the same binary64 value even when the exact dyadic values represented by the original inputs are strictly ordered. + +Public RED `23c3262c824609d79dc14d45bb0acb5a54e99a51` uses `estimate = 1`, `target = 0`, `standard_error = 0x1.ffffffc000000p-1 = 1 - 2^-27`, and `k = 0x1.0000002000000p+0 = 1 + 2^-27`. The subtraction is exact, so the residual is exactly 1. The exact represented product is + +`(1 - 2^-27)(1 + 2^-27) = 1 - 2^-54`, + +which is strictly smaller than the residual. Binary64 multiplication rounds that midpoint to the even value `1.0`, so the predecessor compared `1.0 <= 1.0` and falsely accepted. + +The adjacent multiplier `0x1.0000002000001p+0` remains accepted. A second control uses factors whose exact product lies slightly above 1 while the rounded product is still 1, ensuring the discriminator does not turn rounded ties into blanket rejection. + +## Causal repair and constraints + +Causal repair `6a2add488cfa6bb5ac3cc854a107f287b61bbed5` keeps every non-tie finite comparison and both-overflow exact comparator unchanged. On a nonzero finite rounded tie only, TEPP computes the error-free low term of `estimate - target`. If subtraction was exact, it uses fused multiply-add `k.mul_add(SE, -rounded_bound)` to expose the sign of multiplication roundoff. A negative nonzero product residual proves the exact represented bound is below the tied residual and rejects; a positive residual accepts. A zero product residual or an inexact subtraction stays on the predecessor rounded comparison rather than pretending that this bounded repair proves a more general exact inequality. + +This is deliberately narrower than replacing all finite comparisons with arbitrary-precision arithmetic. A general sparse-dyadic comparator was rejected for this finding because the demonstrated defect needs only the product-rounding sign when subtraction itself is exact. Log-domain comparison was rejected because transcendental rounding would replace an exact binary boundary with another approximation. Treating every rounded tie as rejection was rejected because exact products can lie on either side of the same rounded value. + +Coverage/edge contract `3656deb2f2abc0a83027618a823497a829d9227e` adds the strict-rejection RED, the adjacent accepted multiplier, a positive product-roundoff tie, an exactly represented tie, and an inexact-subtraction tie that must remain on the conservative rounded path. CHANGELOG trace: `090f5124588cb184fc2ef644d91355f2b2c2082b`. + +## Ownership and non-claims + +This is TEPP Validation decision semantics in `validation_core`. It is not reusable static psychometric estimation owned by `fast-mlsirm`, does not alter Longitudinal Modeling composition, and does not consume mutable `contextual-orchestrator` source. + +The public Rust contract is the production authority. Exact dyadic arithmetic was used to establish the counterexample and controls; it does not replace scientific acceptance, hosted CI, or current-head review evidence. + +This repair does not claim globally exact comparison for every finite rounded tie. In particular, when subtraction itself rounded or the FMA product residual is zero, the function preserves the existing finite rounded decision. A future widening requires an independent represented-input counterexample and a bounded exact method that does not create a second writer. + +## Standards trace + +Institute of Electrical and Electronics Engineers. (2019). *IEEE standard for floating-point arithmetic* (IEEE Std 754-2019). IEEE. https://standards.ieee.org/ieee/754/6210/ + +IEEE 754-2019 was rechecked on 2026-09-04 and remains an Active Standard. IEEE P754 remains an Active PAR approved 2024-06-06 to supersede 754-2019; it is not treated as a published replacement. https://standards.ieee.org/ieee/754/11684/ + +International Organization for Standardization. (2020). *Information technology—Microprocessor systems—Floating-point arithmetic* (ISO/IEC 60559:2020). ISO. https://www.iso.org/standard/80985.html + +ISO/IEC 60559:2020 was rechecked on 2026-09-04 and remains Published at stage 60.60. + +American Educational Research Association, American Psychological Association, & National Council on Measurement in Education. (2014). *Standards for educational and psychological testing*. American Educational Research Association. + +AERA continues to publish the 2014 edition, while the Joint Committee announced in 2024 remains charged with revising that edition. TEPP therefore does not treat an unpublished revision as current normative authority. diff --git a/docs/research/standard-error-acceptance-scale-underflow.md b/docs/research/standard-error-acceptance-scale-underflow.md new file mode 100644 index 000000000..3ace21396 --- /dev/null +++ b/docs/research/standard-error-acceptance-scale-underflow.md @@ -0,0 +1,42 @@ +# Standard-error acceptance scale-underflow repair + +## Problem and scientific contract + +TEPP's Validation bounded context exposes `accept_within_standard_errors(estimate, target, standard_error, k)` as the deterministic CPU `f64` reference for the decision + +`|estimate - target| <= k * standard_error`. + +The predecessor normalized every operand before comparison. That avoided overflow for opposite-sign full-range estimates, but the order of operations could erase a finite positive uncertainty allowance. With + +- `estimate = 1.0e308`, +- `target = next_down(estimate)`, +- `standard_error = 2.2e-16`, and +- `k = 1.0e308`, + +the represented direct residual is finite (`1.99584030953472e292`) and the represented direct bound is finite (`2.2e292`), so the recovery is admissible. The predecessor nevertheless computed `standard_error / scale == 0.0` before multiplying by `k`, converted the positive bound to zero, and rejected the result. This is an operation-order underflow in the decision implementation, not evidence that the scientific uncertainty is zero. + +Public RED `76067efce44419c27687b8673cb76092c90fb5a5` fixes the admissible case and a nearby rejection (`standard_error = 1.8e-16`) so repairing the positive bound cannot widen the decision arbitrarily. + +## Constraints and alternatives + +The repair must retain the existing exact-recovery semantics for `standard_error == 0` or `k == 0`, must remain deterministic binary64, and must still handle an overflowing opposite-sign residual without accepting merely because both sides materialized as infinity. + +Always evaluating the normalized expression was rejected because it is the demonstrated cause of the false rejection. Always comparing `abs(estimate - target)` with `k * standard_error` was also rejected because either side can overflow for finite inputs. Log-domain comparison was not selected because it introduces transcendental rounding into a gate whose operands already admit an arithmetic decision. + +Causal repair `4ffdf3665b3bbafd3b0bbf06b599fe71498169ab` therefore uses direct represented subtraction and multiplication whenever those results are finite. If only the finite positive bound overflows, any finite residual is inside it; if only the residual overflows, a finite bound cannot cover it. Scale normalization remains only for the both-overflow case. The nearby rejection in the public contract preserves the original decision boundary. CHANGELOG trace: `d822c0ba7fa548c3283e462323c56d8f5705de31`. + +## Ownership and risk + +This is TEPP Validation decision semantics. It does not introduce reusable static psychometric estimation into TEPP, does not move arithmetic owned by `fast-mlsirm`, and does not depend on an unreleased contextual-orchestrator contract. The change is confined to the existing `accept_within_standard_errors` Domain Service and its public contract. + +Residual risk remains in binary64 tie behavior when both the direct residual and direct bound overflow and the normalized fallback must decide the comparison. No broader correctly-rounded claim is made without a separate represented-input counterexample and RED. + +## Standards trace + +IEEE. (2019). *IEEE standard for floating-point arithmetic* (IEEE Std 754-2019). https://standards.ieee.org/ieee/754/6210/ + +International Organization for Standardization. (2020). *Information technology—Microprocessor systems—Floating-point arithmetic* (ISO/IEC 60559:2020). https://www.iso.org/standard/80985.html + +American Educational Research Association, American Psychological Association, & National Council on Measurement in Education. (2014). *Standards for educational and psychological testing*. American Educational Research Association. + +AERA, APA, and NCME announced the Joint Committee charged with revising the 2014 edition on June 12, 2024; as of 2026-09-04 the 2014 edition remains the published edition used by this trace. https://www.aera.net/Newsroom/Members-of-the-Joint-Committee-for-the-Revision-of-the-Standards-for-Educational-and-Psychological-Testing-Named diff --git a/docs/research/standard-error-acceptance-subnormal-product-rounding.md b/docs/research/standard-error-acceptance-subnormal-product-rounding.md new file mode 100644 index 000000000..f6c336b6e --- /dev/null +++ b/docs/research/standard-error-acceptance-subnormal-product-rounding.md @@ -0,0 +1,29 @@ +# Standard-error acceptance below correction resolution + +## Problem + +TEPP Validation evaluates `|estimate - target| <= k * standard_error` from represented binary64 inputs. GAP-081 and GAP-082 repaired finite rounded ties when multiplication or subtraction retained a nonzero error term. A narrower correction-resolution boundary remains: the exact product error can fall below half of the minimum subnormal, so the FMA correction projects to signed zero even though the rounded finite bound has crossed the scientific decision boundary. + +Public RED `b55c5473c8ee7a70ec2508d14d0755c2aeb38191` uses an exact residual equal to the minimum positive subnormal, `estimate = 0x0.0000000000001p-1022`, `target = 0`, `k = 0x1.8p-538`, and `SE = 0x1p-537`. The exact dyadic product of the represented factors is `3/4` of the minimum subnormal, so the scientific inequality is false. Binary64 multiplication rounds that product up to the minimum subnormal, while `fma(k, SE, -rounded_bound)` rounds the `-1/4`-subnormal correction to signed zero. The predecessor therefore observed equal rounded residual/bound and equal zero correction projections and falsely accepted. The exact-equality control uses `k = 1` and `SE = minimum_subnormal`. + +Initial repair `210cebc4980c861ddfd6d098bf1c9d66c8449e72` correctly closed that RED but restricted exact dyadic comparison to a *subnormal rounded bound*. A concurrent follow-up RED `c77ac440971abc649e6ede35874e5a7439e96797` demonstrated that the restriction was too narrow. At `estimate = f64::MIN_POSITIVE`, `target = 0`, `SE = (1 - 2^-27) * 2^-511`, and `k = (1 + 2^-27) * 2^-511`, the exact product is `(1 - 2^-54) * 2^-1022`: one quarter of a minimum-subnormal ULP below the minimum normal. Multiplication rounds to `f64::MIN_POSITIVE` and the negative quarter-subnormal FMA correction again projects to signed zero. The exact represented residual is the minimum normal, so the scientific inequality is still false even though the rounded bound itself is normal. The adjacent exact minimum-normal equality remains an acceptance control. + +## Causal repair + +Corrected causal repair `772ad8ed3be8d105975bf776e9f9d898774a4a23` stays inside the existing `validation_core` decision writer and removes the inappropriate rounded-bound-class condition. On a nonzero finite rounded tie, nonzero correction projections continue to use the GAP-081/GAP-082 low-term ordering. If both subtraction and product correction projections are zero, the zero subtraction correction means the represented residual is exact at that rounded value, while the product correction may have fallen below binary64 resolution. TEPP therefore decodes the exact integer significands and powers of two already used by the both-overflow comparator and compares that exact represented residual magnitude directly with the exact dyadic product of represented `k` and `SE`. + +This covers both minimum-subnormal and minimum-normal boundary REDs without claiming a global exact comparator for finite ties with equal nonzero projected corrections. Ordinary finite non-ties, one-sided overflow handling, and the both-overflow exact comparator remain unchanged. No arbitrary-precision dependency, scale normalization, or alternate estimator is introduced. + +Alternatives rejected: keeping the subnormal-bound guard fails the minimum-normal RED; blanket rejection of zero-projection ties breaks exact equality controls; relying on FMA alone cannot distinguish exact product equality from product error below binary64 correction resolution; broad arbitrary precision would widen the production surface beyond the demonstrated boundary. + +CHANGELOG correction: `ceb68825f2759637034c2aaea935e50771aa6afc` supersedes the narrower wording introduced at `338c270c65c10d305432029c455b9ac37c28c0f1`. + +## Ownership and standards + +This is TEPP Validation Evidence decision semantics, not reusable static psychometric estimation. It does not move work into or copy source from fast-mlsirm and does not consume mutable contextual-orchestrator behavior. + +Institute of Electrical and Electronics Engineers. (2019). *IEEE standard for floating-point arithmetic* (IEEE Std 754-2019). IEEE. + +International Organization for Standardization. (2020). *Information technology—Microprocessor systems—Floating-point arithmetic* (ISO/IEC 60559:2020). ISO. + +American Educational Research Association, American Psychological Association, & National Council on Measurement in Education. (2014). *Standards for educational and psychological testing*. American Educational Research Association. diff --git a/docs/research/standard-error-acceptance-subtraction-rounding.md b/docs/research/standard-error-acceptance-subtraction-rounding.md new file mode 100644 index 000000000..25bcec9ac --- /dev/null +++ b/docs/research/standard-error-acceptance-subtraction-rounding.md @@ -0,0 +1,35 @@ +# Standard-error acceptance subtraction rounding + +## Problem and represented-input contract + +TEPP Validation exposes `accept_within_standard_errors(estimate, target, standard_error, k)` for the deterministic decision `|estimate - target| <= k * standard_error`. + +GAP-081 corrected a finite rounded tie when subtraction was exact and multiplication alone rounded across the decision boundary. A fresh review found the complementary finite case: subtraction itself can round onto the finite bound and erase a strict rejection. + +Public RED `58cbc03253997865b7c8ec19fb501fc89c22c851` uses `estimate = 1`, `target = -2^-54` (`0xbc90_0000_0000_0000`), `standard_error = 1`, and `k = 1`. The exact dyadic residual represented by the inputs is `1 + 2^-54`, strictly greater than the exact unit bound, while binary64 subtraction rounds the residual to `1.0`. The predecessor therefore fell through to `1.0 <= 1.0` and falsely accepted. The existing positive-min-subnormal target remains an acceptance control because its exact residual is below the unit bound even though its subtraction also rounds to `1.0`. + +## Causal repair + +Causal repair `68a6fd98b4f661ad5d4c3c35dc8b9074f2c59281` remains inside the existing `validation_core` writer. On a nonzero finite rounded equality only, it obtains the error-free low term of `estimate - target`, changes that low-term sign when the rounded difference is negative so it describes the absolute residual, and compares it with the FMA low term of `k * standard_error`. If those two correction projections differ, their order determines the represented-input decision. If they are equal, TEPP preserves the predecessor rounded decision rather than claiming a global exact comparator. + +This preserves the earlier finite-direct, scale-underflow, one-sided overflow, and both-overflow paths. It also preserves the GAP-081 multiplication-only boundary as the special case whose residual correction is zero. Edge-coverage commit `b2c06512b6d795555269862c1c3d7e4bc67f3f18` adds the sign-symmetric negative rounded-difference case so the absolute-residual correction sign is exercised explicitly rather than left to branch inference. + +Alternatives rejected: blanket rejection of rounded ties would break valid below-bound cases; replacing all finite comparisons with arbitrary precision would widen the owner and implementation surface beyond the demonstrated defect; scale or logarithmic comparison would introduce another rounded approximation at a boundary already expressible with binary floating-point error terms. + +CHANGELOG trace: `c4f752f30121901bfbf4a96b03fdef0ccd9f4bf3`. + +## Restack and ownership + +Before this RED, protected `main` advanced through #489 to `b18bca1c69ef8d1799fcd3af6bf4412498e007c3`. The Validation branch was one commit behind and diverged from that protected head. It was repaired without force push or destructive rebase by merge-restack commit `a2754592987be9e88941d220565c45e34f621a4e`, retaining all Validation ancestry while inheriting #489's workflow-load repair. + +This remains TEPP Validation decision semantics. It is not reusable static psychometric estimation owned by `fast-mlsirm`, does not alter Longitudinal Modeling composition, and does not consume mutable `contextual-orchestrator` source. + +## Standards trace + +Institute of Electrical and Electronics Engineers. (2019). *IEEE standard for floating-point arithmetic* (IEEE Std 754-2019). IEEE. + +International Organization for Standardization. (2020). *Information technology—Microprocessor systems—Floating-point arithmetic* (ISO/IEC 60559:2020). ISO. + +American Educational Research Association, American Psychological Association, & National Council on Measurement in Education. (2014). *Standards for educational and psychological testing*. American Educational Research Association. + +The published standards above govern the current trace. Unpublished revisions are not treated as current normative authority. diff --git a/docs/research/standard-error-acceptance-zero-multiplier.md b/docs/research/standard-error-acceptance-zero-multiplier.md new file mode 100644 index 000000000..ae0b777c1 --- /dev/null +++ b/docs/research/standard-error-acceptance-zero-multiplier.md @@ -0,0 +1,60 @@ +# Standard-error exact-recovery boundaries + +## Scope + +`validation_core::accept_within_standard_errors` evaluates the existing Validation Evidence rule + +`|estimate - target| <= k * standard_error`. + +These repairs do not change the estimand or introduce a new acceptance rule. They fix binary64 execution only at the exact-recovery boundary reached when `k = 0` or `standard_error = 0`. + +## Zero-multiplier scale-collapse finding + +For any finite nonnegative standard error, `k = 0` makes the mathematical acceptance bound exactly zero. The gate is therefore an exact-recovery comparison: a nonzero estimate-target residual must be rejected regardless of the magnitude of the supplied standard error. + +The predecessor implementation scaled `estimate`, `target`, and `standard_error` by one shared magnitude before comparing them. That protects the ordinary positive-`k` path from opposite-sign overflow, but it is not sound for the zero-multiplier boundary. With + +- `estimate = f64::from_bits(1)` (the minimum positive binary64 subnormal), +- `target = 0.0`, +- `standard_error = f64::MAX`, and +- `k = 0.0`, + +the scientific residual is nonzero and the acceptance bound is exactly zero. After predecessor scale reduction, however, `estimate / f64::MAX` rounds to `0.0`; the scaled residual and scaled bound both become zero and the gate incorrectly accepts. + +Public RED `bd8a7c8a6d93ec262f5634cec199265521308dfd` fixes this contract through `crates/validation_core/tests/standard_error_acceptance_zero_multiplier_contract.rs`, including positive and negative minimum-subnormal residuals and an exact-equality control. + +Causal repair `00ef2d90580e01494370e48ef68afbe4d0819ba8` handles `k == 0.0` in the same exact-recovery branch already used for `standard_error == 0.0`, before any scale reduction. Positive-`k` scaling and non-finite/configuration refusal remain unchanged. + +## Signed-zero identity finding + +Fresh review of the exact-recovery branch found a separate semantic defect. The predecessor used `f64::total_cmp` for equality. `total_cmp` intentionally distinguishes IEEE `-0.0` from `+0.0`, but the acceptance rule is numerical: both zeros satisfy `estimate - target = 0` exactly and neither sign bit denotes a distinct recovery error. + +Public RED `379e65258c5675ae9fee6d84d369803f1e8a1ae3` requires `-0.0` versus `+0.0` to be accepted in both exact-recovery entry paths: zero multiplier with finite positive SE and zero SE with positive multiplier. Causal repair `55876e60b5ae553bcc4c1c41a793861b3d7e9cc8` replaces bit-order equality with finite numeric equality (`estimate == target`) only in the exact-recovery branch. NaN and infinities remain rejected before that comparison, and every nonzero finite residual remains non-equal. + +This is not blanket signed-zero canonicalization. The API returns a Boolean acceptance decision, so the relevant invariant is that mathematically equal finite zero-valued estimates and targets produce one decision. Private numerical paths remain free to retain signed zero where it carries diagnostic information. + +## Scientific and DDD boundary + +The repair belongs to Validation Evidence because it changes only execution fidelity of an acceptance predicate over already-computed estimate, target, and uncertainty values. It is not a psychometric estimator, longitudinal composition rule, or reusable static psychometric primitive, so no fast-mlsirm source or mutable sibling dependency is introduced. + +Morris, White, and Crowther treat simulation performance measures and Monte Carlo uncertainty as quantities whose definitions and evaluation rules must be explicit. Here the rule already defines the exact-recovery endpoint; binary64 normalization or representational ordering must not silently change that endpoint. + +IEEE/ISO/IEC 60559-2020 remains the floating-point authority for binary64 behavior exercised by the regressions. Minimum subnormal values are valid finite inputs, and IEEE signed zeros compare numerically equal even though total ordering distinguishes their encodings. + +## Traceability + +- Public API: `validation_core::accept_within_standard_errors`. +- Scale-collapse RED: `bd8a7c8a6d93ec262f5634cec199265521308dfd`. +- Scale-collapse repair: `00ef2d90580e01494370e48ef68afbe4d0819ba8`. +- Signed-zero RED: `379e65258c5675ae9fee6d84d369803f1e8a1ae3`. +- Signed-zero causal repair: `55876e60b5ae553bcc4c1c41a793861b3d7e9cc8`. +- Test: `crates/validation_core/tests/standard_error_acceptance_zero_multiplier_contract.rs`. +- Production module: `crates/validation_core/src/monte_carlo.rs`. +- Landing vehicle: PR #488. +- Required delivery evidence: exact-head Rust, documentation, security/supply-chain, owned line/branch coverage, and qualifying independent review; predecessor-head evidence does not transfer. + +## References + +Morris, T. P., White, I. R., & Crowther, M. J. (2019). Using simulation studies to evaluate statistical methods. *Statistics in Medicine, 38*(11), 2074–2102. https://doi.org/10.1002/sim.8086 + +IEEE Computer Society. (2020). *IEEE/ISO/IEC 60559-2020: ISO/IEC/IEEE international standard—Floating-point arithmetic*. IEEE Standards Association. https://standards.ieee.org/ieee/60559/10226/ diff --git a/docs/research/task-11-recovery-metrics-foundations.md b/docs/research/task-11-recovery-metrics-foundations.md index 33a946105..a1f049da4 100644 --- a/docs/research/task-11-recovery-metrics-foundations.md +++ b/docs/research/task-11-recovery-metrics-foundations.md @@ -35,10 +35,51 @@ Manning, C. D., Raghavan, P., & Schütze, H. (2008). *Introduction to informatio - **Coverage** is the closed-interval hit rate; Wilson bounds use the normal critical value `z` (for example 1.96). - **Edge precision/recall** operate on normalized undirected edge identities. - **Temporal-order accuracy** scores pairwise sign agreement, treating exact ties as a distinct class. -- **Monte Carlo** percentiles use inclusive nearest-rank on sorted finite replications. +- **Monte Carlo** percentiles use inclusive nearest-rank on sorted finite replications; mean, sample standard deviation, and standard error are deterministic CPU `f64` evidence and must remain representable whenever the final metric is representable. + +## 2026-09-03 bias arithmetic hardening + +Fresh Validation Evidence review found that the protected-main implementation formed mean signed bias by adding raw finite residuals before dividing by `n`. That makes the numerical procedure stricter than the estimand: two finite residuals equal to `f64::MAX` have a representable mean of `f64::MAX`, but their raw sum overflows. The same raw-sum dependency also made the standard error of a constant extreme bias fail even though its sampling variance is exactly zero. + +RED `c5ec42e40307f3645c18b0d73114b73e01745a20` fixes this contract through the public `mean_bias` and `bias_standard_error` APIs. Causal repair `7499042f7451b2e3d5e9f83843aeea82c4f5ff06` validates each signed residual, normalizes by the largest residual magnitude, uses deterministic compensated summation, and restores scale only after dividing by the replication count. Exact cancellation is canonical `+0.0`; a represented non-zero normalized mean that becomes `0.0` only when scaled back fails closed rather than being reported as zero bias. + +A subsequent full-range cancellation review found that largest-magnitude normalization itself was not sufficient for mixed signs. With minimum-subnormal unit `u = f64::from_bits(1)`, residuals `[f64::MAX, 2u, 2u, -f64::MAX]` have exact sum `4u` and exact mean `u`, which is representable. The predecessor divided both `2u` residuals by `f64::MAX` before the opposing extremes cancelled, turning those terms into zero and returning false zero bias. Public RED `b60847500e9e7f3866f446302964627de009961c` fixes this represented-endpoint contract. Causal repair `227921d99676a420c94df402fc29153718ae8d2c` partitions finite residuals by sign, cancels opposite signs at their represented magnitudes before any scale reduction, and then applies an exact power-of-two normalization to the remaining one-sign mass while retaining the original recovery count as denominator. Contract hardening `77dcf38c9d356b0c72454b335944f5bdb2be52e3` adds the negative mirror, mixed-sign transport permutation, and full-range exact-cancellation cases. Shared-support refactor `edaafaca68b2d85fb88f0f2fb7f1ec22618d720c` moves only this deterministic representable-mean primitive into private `validation_core::numeric`, and `f22315c796123c75749a9de2fd6bcb744b410a97` makes bias consume it without moving bias-specific SEM or recovery semantics. This distinguishes a representable non-zero bias after extreme cancellation from both genuine exact cancellation and a positive real mean below binary64 range. + +Review of the initial overflow repair also exposed a second avoidable intermediate: bias SE still squared unscaled deviations and accumulated those raw squares. A final SEM can be representable even when the raw sum of squared deviations or the intermediate sample variance is not. Public RED `7de0ef90944925ae7b232a8280f5bf9096df6502` uses signed residuals `[1e154, -1e154, 0]`: the raw square sum overflows, while the intended sample SEM is finite at approximately `1e154 / sqrt(3)`. Causal repair `cad231620679d8f912bded36c654446032b45e57` scales finite deviations before squaring and forms the SEM directly, avoiding unnecessary materialization of an overflowing variance. If subtraction from the finite bias mean itself overflows, the reference falls back to a scale-normalized deviation calculation. Oracle/edge refinements `8a6cc346d0b058340285c4172bc14c42c0cdbfa5` and `28d96c2315c58db5336292e000c9f6132cff2621` retain a one-ULP-tolerant public oracle while covering constant extreme bias, `f64::MAX` opposite residuals, direct-deviation overflow, exact cancellation, and non-zero mean underflow. + +These are Validation Evidence implementation repairs, not changes to the bias estimand, estimator target, or longitudinal domain semantics, so they do not require a new PRD target or ADR. Morris, White, and Crowther's ADEMP guidance treats bias and uncertainty as explicitly defined simulation performance measures against known truth; avoidable intermediate overflow or scale erasure must not silently redefine whether those measures exist. + +## 2026-09-03 RMSE arithmetic hardening + +The same review was applied independently to generic RMSE rather than copying the bias estimator. Protected-main `root_mean_square_error` squared each finite absolute residual before normalization. A residual of `f64::MAX` is itself representable, and two identical such residuals have a representable RMSE of exactly `f64::MAX`, but the predecessor rejected the result because `f64::MAX²` overflowed. The inverse boundary was also unsound: a minimum-subnormal residual can square to zero before averaging, allowing a representable non-zero RMSE to be reported as exact perfect recovery. + +Public RED `dd41ff5323cbc6aa3f2da8a0fb6af540e38c582e` requires constant `f64::MAX` residuals to produce RMSE `f64::MAX` and exact-zero RMSE SE. Boundary RED refinement `f4e19991bfe7b83cdce767a7214193c9e53e0b2b` requires one minimum-subnormal residual among two observations to preserve the representable minimum-subnormal RMSE, while one such residual among four observations fails closed because the positive real RMSE lies at the binary64 half-ULP boundary and would otherwise become false zero. + +Causal repair `6b182107376b9b7dec66570d21a1ea6b002266f3` normalizes absolute residuals by their largest finite magnitude before squaring, deterministically accumulates normalized squares, and restores scale only after taking the square root. RMSE SE uses the same normalized squared-residual domain for its sample variance and applies the residual scale once at the end. This preserves representable extreme RMSE and RMSE SE without materializing avoidably overflowing raw squares or squared deviations. Exact all-zero residuals remain exact zero; a non-zero normalized RMSE or RMSE SE that scales below binary64 range fails closed instead of becoming perfect recovery. + +## 2026-09-03 Monte Carlo moment hardening + +Fresh review of `summarize_replications` found that sorting the replication vector did not make Welford's raw `delta * delta2` update safe across the full binary64 range. For samples `[f64::MAX, 0, 0, -f64::MAX]`, the exact mean is zero, the sample standard deviation is `f64::MAX * sqrt(2/3)`, and the standard error is half of that value; all three final moments are representable. The predecessor nevertheless overflowed a Welford squared-deviation intermediate and rejected the summary. The same raw-product path can underflow minimum-subnormal variation to an exact zero uncertainty estimate. + +Public RED `41faba0d184e9f0a3067f6df74c74f74798fa839` fixes both boundaries through `summarize_replications`: it requires the representable symmetric extreme summary to survive, and requires sixteen alternating minimum-subnormal replications to fail closed because their real sampling uncertainty is nonzero while the final standard error lies below binary64 range. Private support refactor `edaafaca68b2d85fb88f0f2fb7f1ec22618d720c` provides the same deterministic cancellation-safe represented mean used by bias. Causal Monte Carlo repair `91b4a93d160efa68942fcb8a38fddab10fb97caf` removes Welford accumulation from the public recovery path, computes deviations from the deterministic represented mean, normalizes before squaring, restores the sample-standard-deviation scale once, and rejects a nonzero standard deviation whose standard error becomes exact zero. If a finite mean makes a direct represented deviation overflow, the same deviation geometry is evaluated after outer-scale normalization. + +The shared helper is numerical support inside the Validation Evidence bounded context, not a psychometric estimator. Mean signed bias, bias SEM, Monte Carlo sample variance/SE, RMSE, and their respective denominators and refusal rules remain separately named scientific contracts. Morris, White, and Crowther's simulation-study guidance requires performance measures and their Monte Carlo uncertainty to be defined and reported explicitly; numerical intermediate behavior must not silently change whether that uncertainty is zero or unavailable. + +This is Validation Evidence numerical execution policy for TEPP's existing recovery metrics, not a new psychometric estimator or a Longitudinal Modeling primitive. IEEE/ISO/IEC 60559-2020 remains the published international adoption of IEEE 754-2019 referenced by this implementation; a later revision must not be treated as published authority until an actual replacement standard is issued. + +Morris, T. P., White, I. R., & Crowther, M. J. (2019). Using simulation studies to evaluate statistical methods. *Statistics in Medicine, 38*(11), 2074–2102. https://doi.org/10.1002/sim.8086 + +IEEE Computer Society. (2020). *IEEE/ISO/IEC 60559-2020: ISO/IEC/IEEE international standard—Floating-point arithmetic*. IEEE Standards Association. https://standards.ieee.org/ieee/60559/10226/ ## Verification -- unit oracle tests for every metric, including empty/unequal/non-finite inputs, inverted intervals, overflow RMSE, single-replication MC, and SE-aware accept/reject; +- unit oracle tests for every metric, including empty/unequal/non-finite inputs, inverted intervals, extreme/subnormal RMSE, single-replication MC, and SE-aware accept/reject; - foundation recovery study unit test with known loadings, intervals, temporal order, edges, and report serialization; -- workspace line and branch coverage gates must remain complete for production modules. +- `crates/validation_core/tests/bias_overflow_safe_mean_contract.rs` exercises both the representable `f64::MAX` constant-bias case and a representable SEM whose predecessor raw square sum overflows through public APIs; +- `crates/validation_core/tests/bias_cancellation_subnormal_contract.rs` exercises positive and negative minimum-subnormal mean bias after `f64::MAX` cancellation, mixed-sign input permutation, and full-range exact cancellation through the public `mean_bias` API; +- `crates/validation_core/tests/rmse_overflow_safe_contract.rs` exercises representable `f64::MAX` RMSE, exact-zero RMSE SE for constant extreme residuals, representable minimum-subnormal RMSE, and false-perfect underflow refusal through public APIs; +- `crates/validation_core/tests/monte_carlo_extreme_moments_contract.rs` exercises a representable full-range symmetric Monte Carlo mean/SD/SE and the false-zero uncertainty boundary through public `summarize_replications`; +- exact cancellation and non-zero-bias underflow are distinct contracts: cancellation remains zero, while an unrepresentable positive mean fails closed; +- exact all-zero recovery and non-zero RMSE underflow are distinct contracts: exact zero remains zero, while a positive real RMSE that would round to false zero fails closed; +- exact zero Monte Carlo uncertainty is admitted only for a degenerate replicated metric; nonzero replication variability whose final SE is below binary64 range is unavailable evidence, not zero uncertainty; +- exact-head hosted workspace line and branch coverage gates remain required before the Draft repair can be promoted. diff --git a/docs/research/validation-bias-standard-error-exact-proof-budget.md b/docs/research/validation-bias-standard-error-exact-proof-budget.md new file mode 100644 index 000000000..4a47b1cf7 --- /dev/null +++ b/docs/research/validation-bias-standard-error-exact-proof-budget.md @@ -0,0 +1,126 @@ +# Bias-SE exact-proof budget characterization + +## Scope + +Issue #491 replaces the sample-count staircase in GAP-111 through GAP-125 with an evidence-based resource budget. This note records bounded characterization evidence. It does **not** widen production admission beyond `n=16`, does not claim buyer-path p95, and does not make a benchmark result from an unmeasured environment authoritative. + +## New represented-input boundary evidence + +The seventeen-observation residual multiset + +`[38_557_579, 48_779_805, 63_558_649, 106_352_599, 139_863_777, 142_786_819, 267_163_239, 275_103_292, 375_678_558, 454_709_869, 484_300_224, 623_646_610, 989_643_121, 1_027_595_814, 1_520_220_488, 1_569_903_156, 1_805_452_085]` + +is exactly representable in binary64. Its 136 squared pair distances sum to + +`N = 92_549_865_125_191_410_206`. + +For `n=17`, the scientific denominator is `17^2(17-1)=4_624`. `gcd(N,4_624)=2`, giving the reduced exact radicand + +`46_274_932_562_595_705_103 / 2_312`. + +The current public API intentionally remains on the established translated floating fallback at `n=17`; for this fixture it returns bits `0x41a0_dd77_9ac3_8e98`. Independent high-precision rational-square-root evaluation gives the adjacent correctly rounded binary64 target `0x41a0_dd77_9ac3_8e99`. This extends the demonstrated failure class beyond the current cutoff, but it is evidence for a systemic budget decision rather than justification for another one-count production patch. + +`crates/validation_core/tests/bias_standard_error_exact_proof_budget_characterization.rs` fixes the exact pair-square sum, reduced ratio, current fallback boundary, algebraic equivalence, pair-record counts, distinct checked-integer envelopes, dyadic-unit normalization, the admitted/refused-set relation between the current pair reference and the candidate O(n) accumulator, and a dependency-free wider-product reference for the narrow-intermediate refusal boundary. The test intentionally makes any future widening of production admission update this characterization rather than silently inheriting a stale fallback assumption. + +## O(n²) reference versus O(n) exact accumulator + +For exact dyadic coefficients `c_i` on one shared unit, + +`sum_{i` before the second accumulation pass. The number of stored pair records is exactly `n(n-1)/2`: 120 at `n=16`, 136 at `n=17`, 2,096,128 at `n=2,048`, and **4,997,541** at `n=3,162`. The predecessor note incorrectly recorded the last count as 4,997,500; the Rust characterization now locks the exact integer count. + +Exact byte cost is target-layout dependent and must not be inferred by adding field widths. The measurement harness obtains `size_of::>()` on the executing target, records the actual `Vec` element capacity after `with_capacity`, and reports their product as scratch payload bytes. Allocator bookkeeping and whole-process RSS are still outside that number and must be recorded separately if they become release evidence. + +A two-pass O(n²) reference can remove the pair-record allocation without changing the pair-enumeration proof shape: the first pass establishes the common dyadic unit, and the second recomputes each pair record and accumulates the checked square. The harness measures this allocation-free quadratic alternative alongside the buffered pair layout. The normalized O(n) identity removes pair enumeration as well, but the admission-set characterization shows that its checked-`u128` form must remain a sufficient fast path with pairwise fallback unless wider intermediates are justified. + +## Measurement harness + +`crates/validation_core/examples/bias_se_exact_proof_budget.rs` is a standard-library-only release-mode characterization harness. It compares five kernels: + +- `quadratic_buffered`: production-layout-shaped `Vec>` pair records plus aligned checked-square accumulation; +- `quadratic_two_pass`: the same pair enumeration and dyadic alignment without pair-record storage; +- `linear`: minimum-anchor coefficients, normalized by their largest shared power-of-two unit, then checked-`u128` `n*sum(c_i^2) - (sum c_i)^2`; +- `linear_wide_product_reference`: the same normalized coefficient/square accumulators, but the two final products and cancellation use dependency-free two-limb 256-bit arithmetic before the exact result is required to fit the existing `u128` pair-numerator domain; +- `hybrid`: the viable production-shape candidate, using the narrow normalized checked O(n) accumulator when it admits and otherwise falling back to the production-layout-shaped buffered pair proof. + +Before timing, the harness restores the dyadic unit and requires every applicable kernel to equal the same exact pair-square numerator. Compact deterministic fixtures at 16, 64, 256, 1,024, and 2,047 observations exercise ordinary admitting geometry. Three boundary geometries separate normalization from true narrow-intermediate refusal: + +1. `power_of_two_normalized_admit`: `n=65`, `D=2^58`. The narrow linear, wider-product reference, and hybrid paths must factor the common dyadic unit and admit; pair fallback here would reproduce the predecessor characterization defect. +2. `odd_boundary_admit`: `n=64`, `D=2^58+1`. With no removable common dyadic factor, normalized narrow O(n) still fits. +3. `odd_boundary_pair_fallback`: `n=65`, `D=2^58+1`. The exact pair numerator fits, normalized narrow O(n) checked intermediates refuse, the wider-product reference still recovers the exact numerator, and the production-shape hybrid must execute the buffered pair fallback. + +The CSV schema remains + +`geometry,sample_count,kernel,p95_ns,timing_samples,unit_exponent,scratch_records,scratch_payload_bytes,pair_record_size_bytes,used_pairwise_fallback`. + +`unit_exponent` is material evidence in the corrected harness: it distinguishes the common-power geometry, where the linear path reports exponent 58 and a small aligned numerator, from the odd-diameter geometry, where the canonical unit exponent is zero. `used_pairwise_fallback` records whether a hybrid row actually exercised the expensive proof path instead of inferring that fact from sample count. The `kernel` field now makes the wider-product reference directly comparable to the narrow O(n), pair, and hybrid timing rows in the same release-mode run. + +The original hybrid harness landed in `c6b237e0bb1388cccd7bcb71a0df5cbf837a07c5`; the dyadic-normalization correction is `4a3d988702593b2b8d59be6dcfb1601ca1a0d610`; the wider-product measurement extension is `0bd805d4b0304cf1f76344ae14b7f079b3dade17`. It remains measurement tooling only. No release-mode timing result is recorded in this document yet because the current execution environment does not provide Rust 1.98.0 and the hosted exact-head jobs have not produced measurement artifacts. A valid timing record must include CPU, OS, Rust toolchain, exact commit, release build mode, raw sample count, raw CSV, allocator/RSS evidence, and the cold/warm procedure. Kernel timing cannot substitute for an applicable API buyer-path p95 measurement. + +## Decision and rejected alternatives + +Production admission stays `n=4..=16`. Extending to `n=17` alone is rejected because GAP-111 through GAP-125 plus the seventeen-observation evidence show that the integer cutoff is not a scientific boundary. Removing the cutoff entirely is rejected because the current production implementation still enumerates and stores O(n²) pair evidence. + +The former `D=2^58, n=65` raw-scale refusal is explicitly rejected as admission evidence because it disappears under the same common-power dyadic normalization already required by the proposed O(n) proof. Retaining it would make the resource budget representation-dependent. The odd `D=2^58+1` boundary replaces it as the normalized refusal fixture. + +Treating `n<=2_047`, `n<=4_095`, or the unreduced denominator threshold as the production budget is also rejected because arithmetic representability is not latency or memory evidence. A two-pass O(n²) allocation-removal path remains a candidate because it can preserve the current pair-proof admission shape while eliminating pair-record storage. The normalized O(n) identity remains the stronger CPU-scaling candidate, but its checked-`u128` form is still a strict sufficient subset of the pair reference. Replacing the pair proof with that kernel alone is rejected because it would silently narrow exact-proof admission. The hybrid harness measures the viable bounded shape—normalized O(n) fast admission with buffered O(n²) fallback—without making it production behavior. + +The new two-limb wider-product reference is retained only as characterization evidence. Promoting it directly to production is rejected for now because its bounded construction deliberately assumes the normalized coefficient sum and sum of squares already fit `u128`, and because no Rust 1.98.0 release-mode timing, broader admitted/refused-set sweep, coverage evidence, or independent review exists for it yet. Arbitrary-precision production arithmetic remains deferred pending measured benefit, supply-chain review, and an explicit owner/resource decision. + +## Traceability + +| Item | Evidence | +|---|---| +| Domain owner | TEPP Validation Evidence | +| Systemic issue | #491 | +| Predecessor scientific repair | GAP-125; RED `5da82b2d651706c191ca191c6c077d916cbfda25`; repair `a509ae9e46c8ffc2cc3ef4f0e904774ad2516e1f` | +| Characterization RED | `4f1bd2c343cf2d54905a07c257a570a89dc575d3` — common `2^58` scale must normalize before O(n) overflow admission | +| Characterization repair | `d423b57797b6f7f127e61e0679f9ee9841525c77` — normalized test kernel plus odd-diameter refusal fixture | +| Narrow-reference overflow hardening | `96f17c02edba0792f61e0e92167703a6ae4e40d0` — checked restored-scale multiplication | +| Wider-product characterization | `081000289f5a52e94863026d55696ee2a4daf923` — dependency-free two-limb products/cancellation and odd-boundary recovery | +| Wider-product harness | `0bd805d4b0304cf1f76344ae14b7f079b3dade17` — release-mode comparison row for the wider intermediate reference | +| CHANGELOG fragment | `8ec2092872edc2867652ce98313ebad9deabd5ba` | +| Current production module | `crates/validation_core/src/bias_se.rs` | +| Public API | `validation_core::bias_standard_error` | +| Exact characterization | `crates/validation_core/tests/bias_standard_error_exact_proof_budget_characterization.rs` | +| CPU/layout/hybrid harness | `crates/validation_core/examples/bias_se_exact_proof_budget.rs` | +| Resource/merge rule | Production cutoff remains `n<=16` pending measured exact-proof budget | + +## Follow-up evidence required by #491 + +Run the five-kernel harness in release mode on a recorded CPU/toolchain and retain raw timing CSV. Record allocator/RSS evidence in addition to the harness's exact pair-record payload layout. Extend the wider-product comparison across sample count, canonical dyadic exponent spread/diameter, and coefficient distributions so a narrower refusal cannot be mistaken for scientific refusal and a wider-product admission cannot be mistaken for full-domain equivalence. Only after those results establish a resource budget should production admission change; any such change needs a realistic public RED, exact-head Rust/rustdoc/coverage/security evidence, independent current-head review, and applicable buyer-path p95 evidence. diff --git a/docs/research/validation-bias-standard-error-fifteen-observation-pair-distance.md b/docs/research/validation-bias-standard-error-fifteen-observation-pair-distance.md new file mode 100644 index 000000000..b6ddfc3fa --- /dev/null +++ b/docs/research/validation-bias-standard-error-fifteen-observation-pair-distance.md @@ -0,0 +1,50 @@ +# Exact represented-input mean-bias standard error for fifteen observations + +## Problem and represented-input evidence + +TEPP's Validation Evidence layer treats the represented binary64 inputs as the numerical observation contract. For an exact fifteen-observation residual sample + +`[224_611_356, 291_740_781, 326_373_353, 461_196_594, 686_313_913, 812_468_689, 889_538_573, 1_089_098_727, 1_093_012_981, 1_094_199_400, 1_387_143_595, 1_412_604_591, 1_556_072_759, 1_847_457_618, 1_990_087_657]`, + +the 105 exact squared pair distances sum to `N = 65_163_338_527_647_814_596`. With `n = 15`, + +`SE(mean)^2 = N / [15^2(15-1)] = N / 3150`. + +`gcd(N,3150)=18`, so the reduced exact radicand is `3_620_185_473_758_211_922 / 175`. The predecessor bounded pair-distance proof stopped at fourteen observations. Its translated floating second-moment/`sqrt` fallback returns `0x41a1_254f_de99_720c`; exact dyadic midpoint-square comparison places the target above the midpoint between that value and its upper neighbor and below the next midpoint, so the correctly rounded represented result is `0x41a1_254f_de99_720d`. + +Public RED `1c8a7cedd4ff846d3f3ab226cb4fa25b79650c58` adds `crates/validation_core/tests/bias_standard_error_fifteen_observation_pair_distance_contract.rs` with original order, reverse order, an independent permutation, and sign mirrors. + +## Causal repair + +Repair `3cc041ee1aa5f9871619c483059f5930a056f41a` changes only `crates/validation_core/src/bias_se.rs` admission from `n=4..=14` to `n=4..=15`, updates its rustdoc, admits exact-zero `n=15`, and moves the explicit fallback boundary to `n=16`. + +The proof conditions are unchanged: residual subtraction and every pairwise residual difference must be error-free; dyadic coefficient construction and pair-square accumulation must fit checked `u128`; the scientific denominator remains `n^2(n-1)` and is GCD-reduced; binary64 ratio/`sqrt` is only a candidate seed; exact candidate-square and adjacent-midpoint comparisons authorize the result. Any proof failure stays on the established general path. + +This is not a payload-specific branch and does not claim a globally correctly rounded standard error for arbitrary `n`. An unconditional O(n^2) reference path, speculative admission beyond the demonstrated boundary, weakening exactness checks, and arbitrary-precision production arithmetic remain rejected. The repeated one-ULP sequence through `n=15` is evidence that the numerical class persists, but a broader production-bound change still requires explicit cost/overflow evidence rather than an unmeasured cutoff removal. + +## Traceability + +| Item | Exact evidence | +|---|---| +| Domain owner | Validation Evidence | +| Public RED | `1c8a7cedd4ff846d3f3ab226cb4fa25b79650c58` | +| Causal repair | `3cc041ee1aa5f9871619c483059f5930a056f41a` | +| Production module/API | `crates/validation_core/src/bias_se.rs` / `validation_core::bias_standard_error` | +| Public test | `crates/validation_core/tests/bias_standard_error_fifteen_observation_pair_distance_contract.rs` | +| Predecessor doctoring | `docs/research/validation-bias-standard-error-twelve-through-fourteen-observation-pair-distance.md` | + +## Methodological and standards basis + +IEEE 754-2019 and ISO/IEC 60559:2020 remain the published floating-point arithmetic authorities for binary64 behavior. Exact midpoint comparison is used so a rounded ratio followed by `sqrt` cannot silently become authoritative when the represented inputs admit an exact bounded rational proof. + +Morris, White, and Crowther (2019) distinguish a performance measure's deterministic calculation from Monte Carlo uncertainty due to finite simulation replications. This defect is deterministic for a fixed represented input and belongs to the former layer. AERA, APA, and NCME's *Standards for Educational and Psychological Testing* provide the broader validity-evidence basis; an LLM judgment does not replace the numerical estimator contract or scientific acceptance evidence. + +## References + +American Educational Research Association, American Psychological Association, & National Council on Measurement in Education. (2014). *Standards for educational and psychological testing*. American Educational Research Association. + +Institute of Electrical and Electronics Engineers. (2019). *IEEE standard for floating-point arithmetic* (IEEE Std 754-2019). https://doi.org/10.1109/IEEESTD.2019.8766229 + +International Organization for Standardization, & International Electrotechnical Commission. (2020). *ISO/IEC 60559:2020 Information technology—Microprocessor systems—Floating-point arithmetic*. + +Morris, T. P., White, I. R., & Crowther, M. J. (2019). Using simulation studies to evaluate statistical methods. *Statistics in Medicine, 38*(11), 2074–2102. https://doi.org/10.1002/sim.8086 diff --git a/docs/research/validation-bias-standard-error-sixteen-observation-pair-distance.md b/docs/research/validation-bias-standard-error-sixteen-observation-pair-distance.md new file mode 100644 index 000000000..c59659f55 --- /dev/null +++ b/docs/research/validation-bias-standard-error-sixteen-observation-pair-distance.md @@ -0,0 +1,54 @@ +# Exact represented-input mean-bias standard error for sixteen observations + +## Problem and represented-input evidence + +TEPP's Validation Evidence layer treats the represented binary64 inputs as the numerical observation contract. The exact sixteen-observation residual sample + +`[314_270_929, 327_661_307, 371_854_441, 398_522_837, 413_483_290, 416_184_956, 565_808_551, 682_627_163, 724_514_517, 731_058_943, 740_662_035, 970_233_120, 1_141_566_755, 1_320_628_283, 1_526_331_271, 1_992_574_092]` + +has 120 pairwise distances and an exact squared-distance sum `N = 56_762_922_330_032_131_548`. With `n = 16`, + +`SE(mean)^2 = N / [16^2(16-1)] = N / 3_840`. + +`gcd(N,3_840)=12`, so the reduced exact radicand is `4_730_243_527_502_677_629 / 320`. The predecessor bounded pair-distance proof stopped at fifteen observations. Its translated floating second-moment/`sqrt` fallback returns `0x419c_fcbb_b78d_2ad4`; exact dyadic midpoint-square comparison returns correctly rounded `0x419c_fcbb_b78d_2ad5`. + +The public RED lineage is `65d3965fd9ce8e4667f235fe33ce0f8b38ec5f6c` followed by fixture correction `5da82b2d651706c191ca191c6c077d916cbfda25`; the latter is the authoritative RED fixture. `crates/validation_core/tests/bias_standard_error_sixteen_observation_pair_distance_contract.rs` exercises original order, reverse order, an independent permutation, and sign mirrors without changing the represented multiset. + +## Causal repair + +Repair `a509ae9e46c8ffc2cc3ef4f0e904774ad2516e1f` changes only `crates/validation_core/src/bias_se.rs` admission from `n=4..=15` to `n=4..=16`, updates its rustdoc, admits exact-zero `n=16`, and moves the explicit fallback boundary to `n=17`. + +The proof conditions are unchanged: residual subtraction and every pairwise residual difference must be error-free; dyadic coefficient construction and pair-square accumulation must fit checked `u128`; the scientific denominator remains `n^2(n-1)` and is GCD-reduced; binary64 ratio/`sqrt` is only a candidate seed; exact candidate-square and adjacent-midpoint comparisons authorize the result. Any proof failure remains on the established general path. + +This is not a payload-specific branch and does not claim globally correctly rounded standard errors for arbitrary `n`. However, demonstrated one-ULP counterexamples now span every bounded sample size from `n=4` through `n=16`. Treating the next integer sample count as the scientific boundary is therefore no longer a satisfactory long-term design. The next owner work is to replace the staircase cutoff with an evidence-based proof budget: characterize O(n^2) cost, checked-`u128` overflow/refusal behavior, realistic Validation Evidence sample sizes, and whether a mathematically equivalent O(n) or wider-integer exact accumulator can preserve the same midpoint authority. Until that evidence exists, `n=17` remains fail-closed to the established translated path rather than being admitted speculatively. + +## Alternatives rejected + +A fixture-specific `n=16` formula is rejected because the defect is a repeated exact-rational double-rounding class. Removing the bound outright is rejected because it would silently impose quadratic work on arbitrary buyer inputs without a measured latency/resource envelope. Weakening subtraction exactness, checked integer construction, or midpoint proof would turn the reference path into an approximation and is rejected. Arbitrary-precision production arithmetic is not introduced without a DDD/performance/release decision because the current demonstrated case fits checked `u128` after GCD reduction. + +## Traceability + +| Item | Exact evidence | +|---|---| +| Domain owner | Validation Evidence | +| Authoritative public RED | `5da82b2d651706c191ca191c6c077d916cbfda25` | +| Causal repair | `a509ae9e46c8ffc2cc3ef4f0e904774ad2516e1f` | +| Production module/API | `crates/validation_core/src/bias_se.rs` / `validation_core::bias_standard_error` | +| Public test | `crates/validation_core/tests/bias_standard_error_sixteen_observation_pair_distance_contract.rs` | +| Predecessor doctoring | `docs/research/validation-bias-standard-error-fifteen-observation-pair-distance.md` | + +## Methodological and standards basis + +IEEE 754-2019 and ISO/IEC 60559:2020 remain the published floating-point arithmetic authorities for binary64 behavior. Exact midpoint comparison is used so a rounded ratio followed by `sqrt` cannot silently become authoritative when represented inputs admit an exact bounded rational proof. + +Morris, White, and Crowther (2019) distinguish deterministic calculation of a performance measure from Monte Carlo uncertainty due to finite simulation replications. This defect is deterministic for a fixed represented input and belongs to the former layer. AERA, APA, and NCME's *Standards for Educational and Psychological Testing* provide the broader validity-evidence basis; an LLM judgment does not replace the numerical estimator contract or scientific acceptance evidence. + +## References + +American Educational Research Association, American Psychological Association, & National Council on Measurement in Education. (2014). *Standards for educational and psychological testing*. American Educational Research Association. + +Institute of Electrical and Electronics Engineers. (2019). *IEEE standard for floating-point arithmetic* (IEEE Std 754-2019). https://doi.org/10.1109/IEEESTD.2019.8766229 + +International Organization for Standardization, & International Electrotechnical Commission. (2020). *ISO/IEC 60559:2020 Information technology—Microprocessor systems—Floating-point arithmetic*. + +Morris, T. P., White, I. R., & Crowther, M. J. (2019). Using simulation studies to evaluate statistical methods. *Statistics in Medicine, 38*(11), 2074–2102. https://doi.org/10.1002/sim.8086 diff --git a/docs/research/validation-bias-standard-error-twelve-through-fourteen-observation-pair-distance.md b/docs/research/validation-bias-standard-error-twelve-through-fourteen-observation-pair-distance.md new file mode 100644 index 000000000..af0b7bf3c --- /dev/null +++ b/docs/research/validation-bias-standard-error-twelve-through-fourteen-observation-pair-distance.md @@ -0,0 +1,77 @@ +# Exact represented-input mean-bias standard error for 12–14 observations + +## Decision scope + +TEPP's Validation Evidence layer treats the represented binary64 inputs as the numerical observation contract. For small samples whose residual subtraction and every pairwise residual difference are proven exact, the identity + +\[ +SE(\bar r)^2 = \frac{\sum_{i repair `035cf392...` | +| GAP-122 | Validation Evidence | `crates/validation_core/src/bias_se.rs` | `crates/validation_core/tests/bias_standard_error_thirteen_observation_pair_distance_contract.rs` | RED `369af46d...` -> repair `0b8727e7...` | +| GAP-123 | Validation Evidence | `crates/validation_core/src/bias_se.rs` | `crates/validation_core/tests/bias_standard_error_fourteen_observation_pair_distance_contract.rs` | RED `db403976...` -> repair `58efe80d...` | + +No ADR or PRD target changes are required: the estimator target remains standard error of mean signed bias under the existing represented-input contract. These changes repair numerical realization and evidence, not latent-variable meaning or service authority. + +## References + +American Educational Research Association, American Psychological Association, & National Council on Measurement in Education. (2014). *Standards for educational and psychological testing*. American Educational Research Association. + +Institute of Electrical and Electronics Engineers. (2019). *IEEE standard for floating-point arithmetic* (IEEE Std 754-2019). https://doi.org/10.1109/IEEESTD.2019.8766229 + +International Organization for Standardization, & International Electrotechnical Commission. (2020). *ISO/IEC 60559:2020 Information technology—Microprocessor systems—Floating-point arithmetic*. + +Morris, T. P., White, I. R., & Crowther, M. J. (2019). Using simulation studies to evaluate statistical methods. *Statistics in Medicine, 38*(11), 2074–2102. https://doi.org/10.1002/sim.8086 diff --git a/docs/research/validation-report-rmse-summary-domain.md b/docs/research/validation-report-rmse-summary-domain.md new file mode 100644 index 000000000..674b80490 --- /dev/null +++ b/docs/research/validation-report-rmse-summary-domain.md @@ -0,0 +1,64 @@ +# Validation report RMSE-summary domain invariant + +## Finding + +`MonteCarloSummary` is intentionally metric-neutral: its mean and percentile endpoints may be negative when it summarizes a signed metric such as bias. `ValidationReport::monte_carlo_rmse`, however, gives that same wire shape a narrower scientific meaning. Every RMSE replication is nonnegative, so a negative Monte Carlo RMSE mean or percentile endpoint is finite but scientifically impossible evidence. + +A second support constraint follows from the same typed meaning. Let retained RMSE replications be `x_i >= 0`, let `n` be the replication count, and let `m` be their sample mean. For a fixed nonnegative sum `sum(x_i) = n m`, the largest possible squared deviation occurs when one replication carries the whole sum and the remaining `n - 1` values are zero. Therefore + +`SD <= sqrt(n) * m` + +for the sample standard deviation, and because the stored Monte Carlo standard error is `SD / sqrt(n)`, + +`SE(mean) <= m`. + +The bound is attainable: for four replications `[0, 0, 0, 4]`, `mean = 1`, sample `SD = 2`, and `SE = 1`. A generic summary such as `n = 4`, `mean = 1`, `SD = 3`, `SE = 1.5` is internally coherent as a sign-neutral carrier because `SE = SD / sqrt(n)`, but it cannot have been produced by four nonnegative RMSE replications. + +Before these repairs, `ValidationReport::validate()` delegated the nested object only to the generic `MonteCarloSummary::validate()` contract and then checked the RMSE-specific sign domain. A caller could therefore construct or deserialize a report whose field was explicitly named `monte_carlo_rmse` while carrying negative RMSE evidence or a positive mean/spread combination outside nonnegative sample support, and the report could pass canonical JSON and human-summary projection. + +## Decision + +Keep `MonteCarloSummary` sign-neutral because it is reusable for signed recovery metrics. Enforce the narrower domain only at the `ValidationReport::monte_carlo_rmse` ownership boundary: + +- `mean >= 0`; +- `percentile_lower >= 0`; +- `percentile_upper >= 0`; +- exact-zero RMSE mean requires zero spread, zero SE, and zero percentile support; +- positive RMSE mean requires `SE(mean) <= mean`, with the same small binary64 relative tolerance used at the point-RMSE support boundary; +- all existing generic Monte Carlo count, finiteness, uncertainty, percentile-order, and zero-spread support invariants remain mandatory. + +Checking `SE / mean` at the typed boundary avoids duplicating the generic `SD / sqrt(n)` coherence calculation while still enforcing the nonnegative-support theorem. If the ratio overflows or is otherwise non-finite, the durable artifact fails closed. The boundary case remains admissible within the explicit floating-point tolerance. + +This is an artifact-admission invariant, not a new estimator and not reusable static psychometric arithmetic. The change therefore remains in TEPP `validation_core`; it does not move arithmetic into or copy source from `fast-mlsirm`. + +## RED -> repair trace + +- Public RED `3cd6e41ddeffbb41e0a6179a65bc3dd9b60f41d8`: direct validation, canonical JSON, human projection, and JSON ingress must reject negative values when a generic Monte Carlo summary is embedded specifically as RMSE evidence. +- Causal sign-domain repair `0090259d01ee00ad0de35ba0c4c9cb7a37c0b13c`: `ValidationReport::validate()` first applies the generic summary validator and then enforces the nonnegative RMSE-specific mean/percentile domain. +- Generic zero-spread support later made an older zero-mean fixture self-contradictory; test repair `e201a2f46953152255345a7b3abc35f05ea9e33c` aligns the fixture with the stronger upstream summary contract rather than weakening that contract. +- Public RED `43a7dec1dbc848435ca099aea80db46c8cbd97e5`: a generic-valid `n=4, mean=1, SD=3, SE=1.5` summary must fail when embedded as RMSE evidence, while the attainable `[0,0,0,4]` boundary summary (`mean=1, SD=2, SE=1`) must remain valid. +- Causal nonnegative-support repair `2f78954eb21a08c316d0d6b70f659685fb283a0b`: positive Monte Carlo RMSE summaries fail when `standard_error / mean > 1 + 64*EPSILON`; zero mean retains the exact-perfect-recovery rule. +- Changelog trace `4ce54959a5c70ccae8212a6494d346ceef0ff35f`. + +Owned module/API/test: + +- `crates/validation_core/src/report.rs` +- `validation_core::ValidationReport::validate` +- `validation_core::ValidationReport::to_json` +- `validation_core::ValidationReport::to_human_summary` +- serde ingress/egress for `ValidationReport` +- `crates/validation_core/tests/validation_report_rmse_summary_domain_contract.rs` +- `crates/validation_core/tests/validation_report_rmse_summary_nonnegative_support_contract.rs` +- `crates/validation_core/tests/validation_report_zero_mean_rmse_summary_contract.rs` + +## Methodological basis + +Morris, White, and Crowther (2019) treat simulation performance measures as explicitly defined quantities tied to their estimands and recommend coding and execution checks. RMSE is the square root of a mean squared error and therefore has a nonnegative range; negative Monte Carlo RMSE evidence or a mean/spread combination that no nonnegative replication sample can realize is not an alternative convention but an artifact-domain violation. + +The 2014 *Standards for Educational and Psychological Testing* remain the current published AERA/APA/NCME edition while revision is underway. TEPP uses that validity framework to require coherent interpretation and reporting of evidence, rather than accepting a payload solely because each scalar is machine-representable. + +## References + +American Educational Research Association, American Psychological Association, & National Council on Measurement in Education. (2014). *Standards for educational and psychological testing*. American Educational Research Association. https://www.testingstandards.net/open-access-files.html + +Morris, T. P., White, I. R., & Crowther, M. J. (2019). Using simulation studies to evaluate statistical methods. *Statistics in Medicine, 38*(11), 2074–2102. https://doi.org/10.1002/sim.8086 diff --git a/docs/research/validation-report-rmse-summary-percentile-support.md b/docs/research/validation-report-rmse-summary-percentile-support.md new file mode 100644 index 000000000..f58783b0a --- /dev/null +++ b/docs/research/validation-report-rmse-summary-percentile-support.md @@ -0,0 +1,27 @@ +# Monte Carlo RMSE percentile support + +## Decision + +`ValidationReport::monte_carlo_rmse` is a typed Validation Evidence slot for retained RMSE replications, so every underlying replication is nonnegative even though the reusable `MonteCarloSummary` carrier must remain sign-neutral for other metrics such as bias. + +For retained values `x_i >= 0`, replication count `n`, and represented mean `m`, the finite sample sum is `sum(x_i) = n*m` in the mathematical estimand. Since no nonnegative member can exceed the sum, every retained value satisfies `x_i <= n*m`. `summarize_replications` uses inclusive nearest-rank endpoints selected from the sorted retained values rather than an extrapolating quantile model, so every stored empirical percentile endpoint must satisfy the same support bound. + +The artifact-admission check evaluates the upper endpoint as + +`percentile_upper / mean <= replication_count` + +for positive mean, with a small binary64 relative tolerance. This form avoids requiring the finite product `n*mean` to be representable merely to validate an otherwise finite endpoint. Exact-zero mean remains governed by the stronger perfect-recovery invariant that spread, standard error, and percentile support are all numeric zero. The bound is attainable: `[0, 0, 0, 4]` has `n=4`, `mean=1`, and maximum/100th-percentile endpoint `4 = n*mean`. + +## RED and causal repair + +- RED `84a200ee451247c8f75fcce322aa7fc558f38c43` adds `validation_report_rmse_summary_percentile_support_contract.rs`. A generic-valid summary with `n=4`, `mean=1`, coherent `SD=0.5` / `SE=0.25`, and empirical upper endpoint `5` is impossible for nonnegative replications but predecessor report admission accepted it. +- Causal repair `04c9cdd4b89d37145853316bb419943735830c79` adds the typed percentile-support admission rule without narrowing `MonteCarloSummary` globally. +- Changelog trace `00b9278a2c1bc1b912ed7c945391afe71f56bf55` records the buyer-visible durable-evidence correction. + +This is TEPP Validation Evidence artifact admission and projection. It does not introduce a psychometric estimator, change Longitudinal Modeling composition, or copy reusable arithmetic from fast-mlsirm. + +## Methodological trace + +Monte Carlo performance summaries need their reported measures and Monte Carlo uncertainty to remain coherent with the simulated estimand and retained replications. The typed support check operationalizes that requirement at TEPP's durable-artifact boundary rather than asking an LLM or downstream report renderer to infer whether a finite payload is scientifically possible. + +Morris, T. P., White, I. R., & Crowther, M. J. (2019). Using simulation studies to evaluate statistical methods. *Statistics in Medicine, 38*(11), 2074–2102. https://doi.org/10.1002/sim.8086 diff --git a/docs/research/validation-report-scientific-invariants.md b/docs/research/validation-report-scientific-invariants.md new file mode 100644 index 000000000..6daa64294 --- /dev/null +++ b/docs/research/validation-report-scientific-invariants.md @@ -0,0 +1,65 @@ +# Validation report scientific-invariant boundary + +## Finding + +`validation_core::ValidationReport` previously treated finiteness as sufficient payload validity. That allowed finite but impossible Validation Evidence to be serialized or deserialized: negative RMSE or standard errors, empirical coverage and temporal-order accuracy outside `[0, 1]`, Wilson endpoints outside the probability domain, inverted Wilson intervals, or an empirical coverage value outside the Wilson interval stored beside it. + +After the serde boundary was hardened, the human-readable projection remained a separate bypass: `to_human_summary()` returned a `String` without validating the public report fields. A manually constructed invalid report could therefore be refused by JSON while still being rendered as apparently normal human-facing Validation Evidence. + +The same boundary review found that `MonteCarloSummary` already rejected impossible values on explicit validation and serde ingress, but its direct serde serializer wrote public fields without applying that contract. A caller could therefore serialize a negative standard deviation or otherwise invalid summary directly even though the same payload would be rejected on re-ingress. + +These defects are evidence-admission/projection problems rather than new estimators. The report and Monte Carlo summary are durable boundaries over already-computed recovery metrics, so their responsibility is to preserve the metric domains and cross-field relationships established by the producing functions. Mean signed bias is intentionally not sign-restricted. + +## Decision + +The report boundary now enforces the following invariants on explicit validation, canonical JSON egress, direct serde serialization, JSON ingress, and human-readable projection: + +- `rmse >= 0`, `rmse_standard_error >= 0`, and `bias_standard_error >= 0`; +- empirical coverage, Wilson endpoints, and temporal-order accuracy are each in `[0, 1]`; +- `coverage_wilson_lower <= coverage_wilson_upper`; +- `coverage_wilson_lower <= interval_coverage <= coverage_wilson_upper`; +- all numeric fields remain finite and an embedded Monte Carlo summary must satisfy its existing validation contract. + +`ValidationReport::to_human_summary()` now returns `Result` and validates before rendering. Custom `Serialize` and `Deserialize` implementations validate `ValidationReport` at both wire boundaries. `MonteCarloSummary` direct serialization also invokes its existing validator before writing fields. Callers therefore cannot select a weaker projection path to turn impossible finite values into durable or human-facing scientific evidence. + +## RED -> repair trace + +- Public RED `e9414e6f7b824c2dc508f335d355b97b7e399b9`: impossible finite metric domains and incoherent Wilson evidence must fail `ValidationReport::validate()` / `to_json()`. +- First causal repair `28924b0d82bc2d4663f5ba1317cc0c3d94a4833b`: enforce metric-domain and Wilson coherence invariants on the report object. +- Ingress RED `6d190cd783a00f8ce917e37b63ae87053a929ae1`: serde deserialization must not bypass the same scientific contract. +- Ingress repair `f70a6fc0e58c2ae419c3b3bac322db5f35efe538`: custom `Deserialize` constructs then validates the report before admission. +- Direct-report-egress RED `fb28f959297a50cf50e26ea14dc9bfb5ee10ea89`: `serde_json::to_string(&report)` must not bypass report validation. +- Direct-report-egress repair `f7e58ccdb1864ce775ef6797f7fd88596dff1269`: custom `Serialize` validates before writing any report field. +- Direct-Monte-Carlo-egress RED `b5a8ae1fbd1344b09a9fbb3c65a3c6d0cdc38c28`: an invalid `MonteCarloSummary` must not serialize directly despite its public fields. +- Direct-Monte-Carlo-egress repair `18374c35061fb0b98bfe09079d4967baef7697b4`: the existing summary validator is applied before serde writes fields. +- Human-projection RED `541273f10d2085680073fb8ca9dc72d3cbe1e62b`: the human summary must not render a report rejected by the canonical Validation Evidence contract. +- Human-projection repair `53607f8033ee03b409c8fce4ee464797e8cb7be1`: human summary projection validates and returns `Result` instead of rendering invalid evidence. +- Changelog trace `d1f1b85ac1441ef573bbb84478bacec1c7424266`. + +Owned module/API/tests: + +- `crates/validation_core/src/report.rs` +- `validation_core::ValidationReport::validate` +- `validation_core::ValidationReport::to_json` +- `validation_core::ValidationReport::to_human_summary` +- `serde::Serialize` / `serde::Deserialize` for `ValidationReport` +- `serde::Serialize` / existing `serde::Deserialize` for `MonteCarloSummary` +- `crates/validation_core/tests/validation_report_scientific_invariants_contract.rs` + +## Methodological basis + +The 2014 *Standards for Educational and Psychological Testing* remain the current published AERA/APA/NCME edition as of 2026-09-04; the sponsoring organizations announced a revision process in 2024 rather than a replacement published edition. The Standards' validity framework makes interpretation and use of reported scores/evidence contingent on appropriate evidence and coherent reporting, which supports fail-closed handling of impossible metric artifacts rather than treating mere machine representability as scientific validity. + +Morris, White, and Crowther (2019) frame simulation performance measures as quantities tied to explicit estimands/targets, emphasize unambiguous definitions, coverage, bias, and Monte Carlo uncertainty, and recommend checks during coding and execution. TEPP therefore treats metric-domain constraints as part of the Validation Evidence contract, not cosmetic post-processing. + +ISO/IEC 25012:2008 defines a data-quality model for structured data and was last reviewed and confirmed in 2025, so it remains current as of 2026-09-04. TEPP uses that standard only as a data-quality support for enforcing validity/consistency requirements at the artifact boundary; it does not replace psychometric validity theory. + +## References + +American Educational Research Association, American Psychological Association, & National Council on Measurement in Education. (2014). *Standards for educational and psychological testing*. American Educational Research Association. https://www.testingstandards.net/open-access-files.html + +American Educational Research Association. (2024, June 12). *Members of the Joint Committee for the Revision of the Standards for Educational and Psychological Testing named*. https://www.aera.net/Newsroom/Members-of-the-Joint-Committee-for-the-Revision-of-the-Standards-for-Educational-and-Psychological-Testing-Named + +International Organization for Standardization. (2008). *ISO/IEC 25012:2008 Software engineering—Software product Quality Requirements and Evaluation (SQuaRE)—Data quality model*. https://www.iso.org/standard/35736.html + +Morris, T. P., White, I. R., & Crowther, M. J. (2019). Using simulation studies to evaluate statistical methods. *Statistics in Medicine, 38*(11), 2074–2102. https://doi.org/10.1002/sim.8086 diff --git a/docs/research/validation-report-wilson-all-covered-positive-lower.md b/docs/research/validation-report-wilson-all-covered-positive-lower.md new file mode 100644 index 000000000..591d3badf --- /dev/null +++ b/docs/research/validation-report-wilson-all-covered-positive-lower.md @@ -0,0 +1,41 @@ +# Wilson all-covered lower-endpoint support in durable Validation Evidence + +## Decision + +`ValidationReport` must reject exact all-covered (`interval_coverage == 1.0`) Wilson evidence whose stored lower endpoint is numeric zero. + +The existing endpoint-pair identity is necessary for strict-interior coverage but degenerates at `p = 1`: with `U = 1`, the eliminated identity is satisfied for any `L`. That makes `[0, 1]` look algebraically coherent even though TEPP's canonical all-covered producer cannot emit it. + +## Producer invariant + +`wilson_coverage_interval` handles exact all-covered evidence as + +`L = n / (n + z²)`, `U = 1`, + +where the input slice is non-empty (`n >= 1`) and the represented `z²` must be finite. Therefore `n + z²` is finite and positive and the represented lower endpoint is strictly positive. If a positive `z` squares to binary64 zero, the producer yields `L = 1`, not `0`; that existing underflow behavior does not weaken this invariant. + +An extreme finite fixture with `n = 1` and `z = 1e154` keeps `z²` finite and produces a positive lower endpoint near the smallest normal/subnormal transition, exercising the intended full-range support without inventing a denominator or critical value that the report does not store. + +## RED → repair trace + +- Public RED: `ce714f077fe1575b50f1b97131e1857ad0c69b1e`, `crates/validation_core/tests/validation_report_wilson_all_covered_positive_lower_contract.rs`. +- The RED proves the canonical extreme-`z` all-covered lower endpoint remains positive and requires `+0.0` and `-0.0` lower endpoints to fail explicit validation, JSON egress, human projection, and serde ingress. +- During the source edit, two intermediate commits introduced unrelated transcription defects. They were not treated as valid evidence and were fully neutralized by `184990522287e254a8e4c1995c02bce20aaa288e`, which restores the exact predecessor `report.rs` blob while preserving the RED file and branch ancestry. No force push or destructive rebase was used. +- Causal source repair: `72e9d9546e4bf98a63544d6e76a92116da5bf670`. The only surviving source delta splits the degenerate `p = 1` case from `p = 0` and requires `coverage_wilson_lower > 0.0` for exact all-covered evidence. +- Changelog trace: `e3a2f4a27a9355c77c2aa9aad92793d254536439`. + +## Scope and owner boundary + +This is TEPP Validation Evidence artifact admission for the existing Wilson coverage producer. It does not redefine the Wilson estimator, does not add reusable static psychometric arithmetic, and does not move owner responsibility from `fast-mlsirm`. It also does not claim full Wilson provenance: `ValidationReport` still lacks the empirical denominator and critical-value/confidence-level semantics required for exact recomputation. + +The rule is intentionally asymmetric at the boundaries. For `p = 0`, the current producer can emit an upper endpoint of zero when positive `z` squares to represented zero, so admission does not invent an `upper > 0` rule. For `p = 1`, however, the producer's `n / (n + z²)` path remains strictly positive for every accepted configuration. + +## Standards and primary research trace + +Wilson's score interval remains the primary statistical source. The current published *Standards for Educational and Psychological Testing* is the 2014 edition jointly issued by AERA, APA, and NCME; the sponsoring organizations have a Joint Committee revising that 2014 edition, so an unpublished revision is not treated as current normative authority. + +### References + +American Educational Research Association, American Psychological Association, & National Council on Measurement in Education. (2014). *Standards for educational and psychological testing*. American Educational Research Association. + +Wilson, E. B. (1927). Probable inference, the law of succession, and statistical inference. *Journal of the American Statistical Association, 22*(158), 209–212. https://doi.org/10.1080/01621459.1927.10502953 diff --git a/docs/research/validation-report-wilson-boundary-peer-root.md b/docs/research/validation-report-wilson-boundary-peer-root.md new file mode 100644 index 000000000..264edf639 --- /dev/null +++ b/docs/research/validation-report-wilson-boundary-peer-root.md @@ -0,0 +1,57 @@ +# Wilson boundary peer-root admission + +## Problem + +`ValidationReport` stores empirical coverage `p` and the two Wilson endpoints, but not the producer's sample count or critical value. The existing admission check therefore eliminates the unrecorded `a = z² / n` and checks the Wilson root identity. That identity is a necessary interior-pair condition, but its complement form can become numerically insensitive when a minority-coverage lower endpoint is replaced by exact zero. + +A concrete represented-input case is one covered observation out of `n = 100_000_000` with `z = 10_000`. The canonical producer gives: + +- `p = 0x3e45_798e_e230_8c3a` (`1.0e-8` as represented binary64); +- lower `L = 0x3c9c_d2b2_8e2c_a873` (`9.999999800000005e-17`); +- upper `U = 0x3fe0_0000_055e_63b8` (`0.5000000099999999`). + +Replacing only `L` with `0.0` leaves the old complement-form comparison numerically equal on both sides, so the forged pair was admitted even though the peer endpoint implies a positive, ordinarily representable lower root. This is an artifact-admission defect: it does not change the Wilson producer or estimate a new psychometric quantity. + +## Necessary boundary relation + +Wilson's two roots obey + +`L U = p² / (1 + a)` + +and + +`L + U = 1 + (2p - 1) / (1 + a)`. + +Eliminating `a` and solving directly for the minority lower root gives + +`L = p²(1 - U) / [p² + (1 - 2p)U]`, for `0 < p < 0.5`. + +The implementation evaluates the same relation as + +`p * ((p / denominator) * (1 - U))` + +so `p²` is not forced to underflow before the final lower root itself becomes unrepresentable. If a stored lower endpoint is exact zero while this implied peer root remains positive in binary64, the pair cannot be admitted. For `p > 0.5`, TEPP applies the same check to the complement interval `(1-U, 1-L)`. + +The check remains deliberately boundary-local. It does not reconstruct `n` or `z`, does not claim globally correctly rounded Wilson endpoints, and does not replace the existing ordinary interior-pair tolerance. An extreme `[0, 1]` pair remains admissible when the reconstructed minority peer root also rounds to zero; the durable report lacks enough producer provenance to make a stronger claim there. + +## Decision and rejected alternatives + +The selected repair is a necessary peer-root representability gate inside the existing Validation Evidence single writer. Tightening the global absolute tolerance was rejected because the counterexample is cancellation-conditioned rather than merely too loosely tolerated, and a global tolerance change would alter ordinary interior admission without a demonstrated defect. Requiring every nonzero empirical coverage to have a strictly positive lower endpoint was rejected because a finite extreme Wilson configuration can legitimately project a positive mathematical root to binary64 zero. Reconstructing a unique sample count or critical value from the durable report was rejected because those values are not fields of `ValidationReport`; inventing them would create false provenance. + +## Executable traceability + +- Public RED: `c9d612f2964eb89ae6070e707c77115a12be84b9`, `crates/validation_core/tests/validation_report_wilson_boundary_pair_coherence_contract.rs`. +- Minimal causal repair: `88512417d931e54ec4eb41581caa0ef20b71df5b`, `crates/validation_core/src/report.rs`. +- Branch-complete boundary implementation: `6a4f89da52cf18f63b2818545f15f72f43099a3f`. +- Symmetry, representable-control, extreme-boundary, and serde contract hardening: `0ce7639bd9f0ab5df2ecd6b24cd57a467e975d26`. +- CHANGELOG evidence: `407b3645233e8855cf30487250155ac903d6a4cb`. + +Hosted exact-head GREEN, independent review, protected-main merge, release, and downstream release evidence remain separate delivery gates. + +## References + +IEEE. (2019). *IEEE standard for floating-point arithmetic* (IEEE Std 754-2019). IEEE Standards Association. https://doi.org/10.1109/IEEESTD.2019.8766229 + +ISO/IEC/IEEE. (2020). *Floating-point arithmetic* (ISO/IEC/IEEE 60559:2020). International Organization for Standardization, International Electrotechnical Commission, & IEEE. + +Wilson, E. B. (1927). Probable inference, the law of succession, and statistical inference. *Journal of the American Statistical Association, 22*(158), 209–212. https://doi.org/10.1080/01621459.1927.10502953 diff --git a/docs/research/validation-report-wilson-pair-coherence.md b/docs/research/validation-report-wilson-pair-coherence.md new file mode 100644 index 000000000..dd6e7328a --- /dev/null +++ b/docs/research/validation-report-wilson-pair-coherence.md @@ -0,0 +1,56 @@ +# ValidationReport Wilson endpoint-pair coherence + +## Problem + +`ValidationReport` previously required `coverage_wilson_lower <= interval_coverage <= coverage_wilson_upper`, but containment alone does not establish that the two stored endpoints came from one Wilson score interval for the same empirical coverage. For example, `p = 0.5`, `L = 0.2`, `U = 0.9` is ordered and contains `p`, yet no Wilson score interval can produce that pair: at `p = 0.5` the Wilson roots are symmetric about `0.5` for every finite positive `z² / n`. + +This matters at the Validation Evidence boundary because a durable artifact must not combine independently plausible numbers into a scientifically unrealizable interval. It does not change the Wilson estimator or psychometric model. + +## Algebraic invariant + +Let `p` be the empirical coverage and `a = z² / n > 0`. The Wilson roots satisfy + +`L * U = p² / (1 + a)` + +and + +`L + U = 1 + (2p - 1) / (1 + a)`. + +Eliminating `a` gives the necessary pair identity + +`p² * (L + U - 1) = (2p - 1) * L * U`. + +For `p < 0.5`, TEPP evaluates the equivalent identity on the uncovered proportion `q = 1 - p`: + +`q² * (1 - L - U) = (1 - 2p) * (1 - L) * (1 - U)`. + +Using the complementary form avoids needlessly squaring a tiny `p`. Every term is probability-scaled, so the admission comparison cannot overflow finite binary64 inputs. A `64 * EPSILON` absolute tolerance admits normal endpoint-rounding error while rejecting materially incoherent pairs such as `[0.2, 0.9]` at `p = 0.5`. + +This is a necessary, not sufficient, provenance check. The current report still does not retain the coverage denominator or the critical value `z`; therefore this repair does not claim full recomputation or denominator provenance. The existing producer can also admit a positive `z` whose squared binary64 representation underflows to zero, so this artifact-level identity is deliberately not strengthened into a non-degeneracy rule that the producer itself does not yet guarantee. Those are separate producer/schema questions rather than facts inferred from unavailable evidence. + +## Dependent-fixture review + +Adding a cross-field invariant changed the validity of old branch fixtures that used arbitrary Wilson-looking probabilities while testing unrelated RMSE/Monte Carlo behavior. Self-review therefore replaced those incidental values with the exactly coherent `p = 0.5`, `[0.2, 0.8]` pair, which corresponds to a positive finite `z² / n = 0.5625`. The intended RED condition in each test remains unchanged. + +The same review rechecked typed RMSE fixtures against the already-landed generic `MonteCarloSummary` moment contracts. Where an older typed test had become generic-invalid, its sample statistics were repaired so the generic carrier is valid and the typed RMSE boundary remains the sole reason for refusal. In particular, the nonnegative percentile RED now uses `n = 4`, `mean = 1`, `SD = 2`, `SE = 1`, and `upper = 4.1`: it fits the generic individual/joint moment support but exceeds the typed nonnegative RMSE sample-sum bound `n * mean = 4`. + +These fixture changes are test doctoring, not evidence-gate weakening. They remove confounding failure causes introduced by stronger predecessor contracts. + +## Executable trace + +- Public RED: `a839c606fb2329ce1b339eb235c79e02abf40e16`, `crates/validation_core/tests/validation_report_wilson_pair_coherence_contract.rs`. +- Causal source repair: `38c5b8e83fe2433167afb6ece13e72b6608ceb03`, `crates/validation_core/src/report.rs`. +- Complementary-identity coverage: `c1cb16a78499648c10d6d5a8dad5e212a267064a`. +- Changelog trace: `6af0821bc2fca3e9f101cfa4ad36048ecfaa6ddd`. +- Dependent fixture doctoring includes `acf573526f955a7700ebd753b83e0baad120628d`, `ccd30ede3b6e90e0ddff0c0c9ef764f320c2cec1`, `fee458966a9dfb64da2aba01f242fe9e2e613540`, `b9eb9465b155b0fd3f44d0ff88b429485f038c8e`, and `59e40b4f0ebbb3b991c84724ded157ffe974abcd`. +- Owner: TEPP Validation Evidence. No reusable static psychometric estimator is introduced; fast-mlsirm remains untouched. No semantic LLM path is involved. + +## Methodological trace + +Wilson's score interval is the primary statistical source for the interval family used by `validation_core::wilson_coverage_interval`. The AERA/APA/NCME *Standards for Educational and Psychological Testing* remain the current published testing standards while the sponsoring organizations revise the 2014 edition; this repair follows the evidence-integrity principle by refusing a durable summary whose reported components are mutually inconsistent. + +### References + +American Educational Research Association, American Psychological Association, & National Council on Measurement in Education. (2014). *Standards for educational and psychological testing*. American Educational Research Association. + +Wilson, E. B. (1927). Probable inference, the law of succession, and statistical inference. *Journal of the American Statistical Association, 22*(158), 209–212. https://doi.org/10.1080/01621459.1927.10502953 diff --git a/docs/research/validation-report-zero-mean-rmse-summary.md b/docs/research/validation-report-zero-mean-rmse-summary.md new file mode 100644 index 000000000..8249e9f04 --- /dev/null +++ b/docs/research/validation-report-zero-mean-rmse-summary.md @@ -0,0 +1,33 @@ +# Zero-mean Monte Carlo RMSE summary admission + +## Problem + +`MonteCarloSummary` is deliberately sign-neutral because the same carrier can summarize signed quantities such as bias. `ValidationReport::monte_carlo_rmse` is narrower: every retained replication is an RMSE and is therefore nonnegative. The report boundary already rejected negative means and percentile endpoints, but it still admitted an exact-zero Monte Carlo RMSE mean together with positive spread, positive standard error, or positive empirical percentile endpoints. + +For a finite set of nonnegative represented RMSE replications, an arithmetic mean of exactly zero implies that every retained replication is exactly zero. The sample standard deviation, standard error of the mean, and every empirical percentile are consequently zero. Treating a zero mean with positive uncertainty/support as valid durable evidence would describe perfect average recovery and non-perfect replications at the same time. + +## Decision + +Keep `MonteCarloSummary` generic and sign-neutral. Enforce the stronger invariant only when the summary occupies `ValidationReport::monte_carlo_rmse`: if `mean == 0.0`, `standard_deviation`, `standard_error`, `percentile_lower`, and `percentile_upper` must all equal numerical zero. IEEE signed zero is one zero-valued scientific result, so `-0.0` is accepted wherever numerical equality to zero holds. + +The change also repairs the pre-existing report round-trip fixture to use the generic summary contract's canonical Monte Carlo standard error, `SD / sqrt(n)`, rather than the approximate literal `0.003` for `SD = 0.01` and `n = 10`. + +## Alternatives rejected + +- Reject zero-mean/positive-spread summaries in `MonteCarloSummary` globally: rejected because a generic signed metric can have zero mean with positive spread. +- Require the Monte Carlo RMSE mean to lie between stored percentile endpoints: rejected because the stored percentile levels are not part of the payload, and skewed nonnegative samples can legitimately have means outside a selected percentile interval. +- Reconstruct hidden replications from summary fields: impossible and outside the evidence contract. + +## Traceability + +- Public RED: `a17dfe1bd9d845356b757ec98e930b5d927eab8f`, `crates/validation_core/tests/validation_report_zero_mean_rmse_summary_contract.rs`. +- Causal repair: `d17d803415333f79bf6291efed9206a630979adf`, `ValidationReport::validate` in `crates/validation_core/src/report.rs`. +- Release note: `42be85748c4e4136c54fd1f918dfa89533376451`, `CHANGELOG.d/validation-report-zero-mean-rmse-summary.md`. +- Edge contract: `944fa96e89e31123e97f95f6909835026c0bd6fa`; exact signed-zero perfect recovery remains admissible while zero-mean positive spread/support fails closed. +- Owner: TEPP Validation Evidence. No reusable static psychometric estimator or mutable sibling implementation is introduced. + +## Methodological basis + +Morris, T. P., White, I. R., & Crowther, M. J. (2019). Using simulation studies to evaluate statistical methods. *Statistics in Medicine, 38*(11), 2074–2102. https://doi.org/10.1002/sim.8086. The ADEMP framework separates estimands and performance measures and treats Monte Carlo uncertainty as uncertainty of an explicitly defined performance measure. That supports rejecting artifacts whose stored moments cannot jointly represent the declared RMSE performance measure. + +American Educational Research Association, American Psychological Association, & National Council on Measurement in Education. (2014). *Standards for educational and psychological testing*. American Educational Research Association. The 2014 edition remains the current published edition; this repair concerns the integrity and interpretability of validation evidence rather than replacing substantive validity arguments with an arithmetic gate. diff --git a/docs/research/validation-report-zero-rmse-standard-error.md b/docs/research/validation-report-zero-rmse-standard-error.md new file mode 100644 index 000000000..fe76439ba --- /dev/null +++ b/docs/research/validation-report-zero-rmse-standard-error.md @@ -0,0 +1,47 @@ +# Point RMSE and RMSE standard-error coherence + +## Problem + +`ValidationReport` originally validated `rmse` and `rmse_standard_error` independently as finite, nonnegative numbers. The first repair closed the exact-perfect case: a durable artifact with exact-zero RMSE and a positive RMSE standard error contradicted the canonical `validation_core::rmse_standard_error` producer. + +A broader support invariant follows from the same producer. Let `x_i = r_i^2 >= 0`, `m = mean(x) = RMSE^2`, and let `s_x` be the sample standard deviation with denominator `n - 1`. For fixed nonnegative sample mean, the maximum sample variance occurs when one observation carries all mass (`x_1 = n m`) and the remaining observations are zero. Then `s_x = sqrt(n) m`. The crate's delta-method definition + +`SE(RMSE) = s_x / (2 * RMSE * sqrt(n))` + +therefore satisfies `SE(RMSE) <= RMSE / 2` for every admissible finite squared-residual sample. The boundary is attained, for example, by two residual magnitudes `[0, 1]`. A report such as `rmse = 0.2`, `rmse_standard_error = 0.11` is individually finite and nonnegative but cannot arise from the declared producer. + +## Decision + +Keep the RMSE arithmetic and standard-error estimator unchanged. Enforce joint support only at `ValidationReport` admission: + +- exact-zero RMSE still requires numerical-zero RMSE standard error; +- positive RMSE requires `rmse_standard_error / rmse <= 0.5 + 64 * EPSILON` so a represented boundary result is not rejected by cross-operation binary64 rounding; +- positive RMSE with zero standard error remains valid when squared residuals are exactly constant. + +IEEE `-0.0` and `+0.0` remain one zero-valued scientific state. The relative check also fails closed if a positive standard error divided by a tiny positive RMSE is not representable. + +This is Validation Evidence artifact coherence, not a reusable psychometric estimator. No fast-mlsirm source or mutable sibling dependency is introduced. + +## Alternatives rejected + +- Make every positive RMSE require positive standard error: rejected because equal nonzero residual magnitudes produce positive RMSE with exactly zero squared-residual spread. +- Store an arbitrary uncertainty field beside RMSE: rejected because the field is explicitly named `rmse_standard_error` and the crate exposes one canonical squared-residual delta-method producer for it. +- Require bit-exact `SE <= RMSE / 2` with no tolerance: rejected because the mathematical boundary is produced through separate binary64 operations and a valid boundary artifact must survive harmless last-bit differences. +- Infer hidden residuals from the report: rejected because the report intentionally stores summary evidence rather than raw residual vectors. + +## Traceability + +- Exact-zero public RED: `f7b018c5f10d11c7cc21a3430242e37c2d7a1056`, `crates/validation_core/tests/validation_report_zero_rmse_standard_error_contract.rs`. +- Exact-zero causal repair: `4c5999186141dafd8d6293d5da66ab8e19693f5c`, `ValidationReport::validate` in `crates/validation_core/src/report.rs`. +- Exact-zero release note: `6ae5b254669868162428fc5c85538e9f5052ac6c`, `CHANGELOG.d/validation-report-zero-rmse-standard-error.md`. +- Positive-RMSE support RED: `a2aca5b077665433aad0e5531d53360b599b64b3`, `crates/validation_core/tests/validation_report_rmse_standard_error_upper_bound_contract.rs`. +- Positive-RMSE causal repair: `32f094029732e2333b35ad3a11521c4c3d956798`, `ValidationReport::validate` in `crates/validation_core/src/report.rs`. +- Positive-RMSE release note: `7b61c107d1bc6391651ce2341e6ad27c2cada17a`, `CHANGELOG.d/validation-report-rmse-standard-error-support.md`. +- Producer contract: `root_mean_square_error` and `rmse_standard_error` in `crates/validation_core/src/rmse.rs`. +- Owner: TEPP Validation Evidence. + +## Methodological basis + +Morris, T. P., White, I. R., & Crowther, M. J. (2019). Using simulation studies to evaluate statistical methods. *Statistics in Medicine, 38*(11), 2074–2102. https://doi.org/10.1002/sim.8086. ADEMP requires performance measures and their Monte Carlo or sampling uncertainty to be defined coherently; a stored point metric and uncertainty field that cannot jointly arise from the declared estimator should fail admission rather than become durable evidence. + +American Educational Research Association, American Psychological Association, & National Council on Measurement in Education. (2014). *Standards for educational and psychological testing*. American Educational Research Association. The 2014 edition remains the current published edition while revision is underway. AERA's Joint Committee is revising that edition; as of 31 August 2026 AERA also publishes a current Task Force roster for the Standards work. This repair strengthens internal consistency of validation evidence and does not substitute arithmetic checks for substantive validity arguments. diff --git a/docs/research/wilson-all-covered-exact-count-large-z-rounding.md b/docs/research/wilson-all-covered-exact-count-large-z-rounding.md new file mode 100644 index 000000000..cb0c0a3b3 --- /dev/null +++ b/docs/research/wilson-all-covered-exact-count-large-z-rounding.md @@ -0,0 +1,65 @@ +# Wilson all-covered exact-count large-z denominator absorption + +## Problem and scientific boundary + +TEPP's Validation Evidence owner computes the Wilson score interval from retained coverage counts and a finite positive standard-normal critical value. For an all-covered sample, Wilson's lower endpoint is + +`L = n / (n + z²)`. + +The estimator, sidedness, retained counts, and critical-value meaning do not change here. The defect is binary64 evaluation of that same represented-input endpoint when `n` is exactly representable but `z²` is so large that forming `n + z²` rounds back to `z²`. + +For `n = 3` and represented `z = 0x1.fffffffffffffp+29`, binary64 multiplication produces represented `z² = 0x1.ffffffffffffep+59`. The rounded denominator `n + z²` equals `z²`, so the predecessor evaluates a quotient with the finite sample-count contribution missing from its denominator. The resulting lower endpoint is `0x1.8000000000002p-59`. The exact rational endpoint formed from the exact integer `3` and the represented binary64 `z²` rounds instead to `0x1.8000000000001p-59`, one ULP lower. + +This is not a new confidence-interval estimator and does not move reusable psychometric arithmetic into TEPP. It is numerical representation of TEPP Validation Evidence around the canonical Wilson producer in `validation_core`; reusable static psychometric estimation remains fast-mlsirm-owned. + +## RED and causal repair + +Public RED `3f3c9f2e16303791eaa0554979366dc68e2e63ff` adds `crates/validation_core/tests/wilson_all_covered_exact_count_large_z_rounding_contract.rs` and fixes the expected lower endpoint to bits `0x3c48000000000001` for the `n = 3` counterexample. + +Causal repair `07766cb3df3a4c788c95fa6ec13bfd3de072185b` keeps the existing exact-count direct path and its separate near-one complementary repair. Only when the rounded denominator equals `z²` exactly does the implementation recover the lost addition term with a TwoSum-style residual. It then computes the quotient residual with `f64::mul_add` and applies the residual correction to the direct quotient. The branch is therefore limited to complete denominator absorption rather than imposing a new global evaluation order on ordinary Wilson endpoints. + +Boundary reinforcement `3a6b243e3dae786fcd154674dffe9a890a7099f5` adds `z = 2^30` at the same `n = 3` absorption scale. In that neighboring case the ordinary quotient already has the correct represented endpoint `0x1.8000000000000p-59`; residual compensation must preserve that value rather than force an unconditional one-ULP decrement. + +CHANGELOG trace: `8a2f6cee2dd8b490d2124a6f4255897925fd7cc0`. + +## Decision record + +Problem: an exactly representable sample count could be lost when a large represented `z²` absorbed it in `n + z²`, and that intermediate rounding could move the final all-covered Wilson lower endpoint by one ULP. + +Constraints: + +- preserve Wilson's score interval and the existing standard-normal/two-sided evidence contract; +- preserve exact retained `u64` count provenance; +- keep `coverage.rs` as the single Wilson arithmetic writer; +- do not replace the already-correct near-one or inexact-count paths; +- do not claim globally correctly rounded Wilson arithmetic beyond the demonstrated boundary. + +Alternatives considered: + +- Always rewrite the endpoint as `1 / (1 + z² / n)`. Rejected because it introduces an additional rounded division and addition and does not, by itself, recover the exact represented-input rational endpoint. +- Always evaluate `(n / z²) / (1 + n / z²)`. Rejected for the same double-rounding reason; the exposed counterexample remains one ULP high under that simple rearrangement. +- Introduce arbitrary-precision arithmetic for every Wilson endpoint. Rejected as disproportionate to the demonstrated defect and inconsistent with the smallest causal repair requirement for the Rust `f64` reference path. +- Recover the lost denominator term and quotient residual only when `n + z² == z²`. Selected because it directly repairs the demonstrated intermediate-rounding failure while leaving ordinary evaluation unchanged. + +Risk: the repair does not prove global correct rounding for every exactly represented `n` and finite `z²`. It closes the demonstrated complete-denominator-absorption boundary. A different counterexample outside that boundary requires its own exact represented-input oracle and RED before the evaluation contract is broadened. + +## Traceability + +- Bounded context: Validation Evidence +- Production writer: `crates/validation_core/src/coverage.rs` +- Public contract: `crates/validation_core/tests/wilson_all_covered_exact_count_large_z_rounding_contract.rs` +- Durable consumers: `WilsonCoverageEvidenceV1` and `ValidationEvidenceV1` continue to consume the canonical Wilson writer rather than duplicate endpoint arithmetic. +- RED: `3f3c9f2e16303791eaa0554979366dc68e2e63ff` +- Causal fix: `07766cb3df3a4c788c95fa6ec13bfd3de072185b` +- Boundary reinforcement: `3a6b243e3dae786fcd154674dffe9a890a7099f5` +- CHANGELOG: `8a2f6cee2dd8b490d2124a6f4255897925fd7cc0` + +## References + +Wilson, E. B. (1927). Probable inference, the law of succession, and statistical inference. *Journal of the American Statistical Association, 22*(158), 209–212. https://doi.org/10.1080/01621459.1927.10502953 + +IEEE. (2019). *IEEE Standard for Floating-Point Arithmetic (IEEE Std 754-2019).* https://standards.ieee.org/ieee/754/6210/ + +ISO/IEC. (2020). *ISO/IEC 60559:2020 Information technology—Microprocessor systems—Floating-point arithmetic.* https://www.iso.org/standard/80985.html + +As checked in September 2026, IEEE 754-2019 is an active standard and ISO/IEC 60559:2020 remains published; IEEE P754 is an active revision project rather than a published replacement. diff --git a/docs/research/wilson-all-covered-exact-count-partial-denominator-rounding.md b/docs/research/wilson-all-covered-exact-count-partial-denominator-rounding.md new file mode 100644 index 000000000..a965af39b --- /dev/null +++ b/docs/research/wilson-all-covered-exact-count-partial-denominator-rounding.md @@ -0,0 +1,61 @@ +# Wilson all-covered exact-count partial-denominator rounding + +## Problem + +For an all-covered binomial sample with an exactly representable retained count `n`, the Wilson lower endpoint reduces to + +\[ +L = \frac{n}{n + z^2}. +\] + +The prior exact-count boundary repairs covered two special cases: a tiny positive `z²` whose denominator addition collapses to `n`, and a large `z²` that completely absorbs `n`. Fresh exact-rational review found a third state in the ordinary path: `n + z²` can round to a nearby finite binary64 value without either operand being completely absorbed, and the subsequent correctly rounded hardware division is then correct for the *rounded denominator* rather than for the represented-input denominator sum. + +The public RED uses `n = 1` and represented `z = 3 * 2^-28` (`0x1.8p-27`). Binary64 multiplication gives `z² = 9 * 2^-56` (`0x1.2p-53`). The exact represented-input endpoint is `1 / (1 + 9 * 2^-56)`, whose nearest binary64 value is `0x1.fffffffffffffp-1`. Forming the denominator first rounds `1 + z²` upward to `0x1.0000000000001p+0`, and direct division then produces `0x1.ffffffffffffep-1`, one ULP below the correct represented-input result. + +This is numerical evidence corruption, not an estimator-target change. A durable Validation Evidence artifact must not depend on a denominator rounding state that is distinguishable from the exact arithmetic of its already represented inputs. + +## Constraints and rejected alternatives + +`coverage.rs` remains the canonical Wilson writer. The repair must not duplicate Wilson arithmetic in a report/projection layer, move reusable static psychometric arithmetic into TEPP, consume mutable code from `fast-mlsirm`, or use an LLM as numerical authority. + +Always switching to `1 - z² / (n + z²)` was rejected. That algebraic form is useful at the near-one boundary but has its own rounding behavior outside that boundary. Replacing all Wilson evaluation with an arbitrary alternative formula would change stable paths without causal evidence. + +Treating hardware division as sufficient was also rejected. IEEE 754 makes the basic operation deterministic for the floating-point operands presented to it; it does not restore information already lost when `n + z²` was rounded before division. + +## Selected repair + +For the exactly representable-count, all-covered path, TEPP now obtains the error-free TwoSum residual of the denominator addition: + +\[ +D = \operatorname{fl}(n + z^2), \qquad n + z^2 = D + \delta_D. +\] + +When `δ_D != 0`, the direct quotient `q = fl(n / D)` is corrected with a fused residual for `n - qD` and the denominator residual contribution `-qδ_D`. The correction reuses the same compensated quotient mechanism already justified by the complete large-`z²` absorption repair; this change broadens its causal trigger from complete absorption to any demonstrably inexact exact-count denominator sum. Exact denominator sums remain on the direct path. The earlier false-exact-one complementary branch remains boundary-local because it also protects the case in which the quotient has already collapsed to `1.0`. + +The change does **not** claim that every possible floating-point rearrangement of the Wilson interval is globally correctly rounded. It closes the demonstrated partial-denominator state and retains bit-level public contracts for the near-one, partial-rounding, large-`z²`, and correctly rounded control boundaries. The control added after the repair uses `z = 3 * 2^-27`: its denominator sum is likewise inexact, but the direct quotient is already correctly rounded, and the compensation must preserve that same endpoint rather than force a one-ULP move. + +## Evidence and traceability + +- Public RED: `06e556538e171e675c4d8a8287d75052ffc2c4c3`, `crates/validation_core/tests/wilson_all_covered_exact_count_partial_denominator_rounding_contract.rs`. +- Causal production repair: `6c084dbe607e6c415288c77fa41a4270947cd51e`, `crates/validation_core/src/coverage.rs`. +- Research decision record introduced: `25cc19436085881602356e7f2609b697b575540c`. +- Dedicated changelog fragment: `5acc894b8a4d42cd7af8cdc22a02b61a063f16aa`. +- Predecessor large-`z²` changelog made code-current after the broadened trigger: `f89e36d1f2a048befb983327c83f5696baf530cc`. +- Inexact-denominator control retained: `80a0a0ade16f516bf63907f2a8d5105dbcd9c438`. +- Canonical API: `validation_core::wilson_coverage_interval`. +- Predecessor complete-absorption contract retained: `crates/validation_core/tests/wilson_all_covered_exact_count_large_z_rounding_contract.rs`. +- Owner boundary: TEPP Validation Evidence numerical representation/admission. No reusable static psychometric estimator was added. + +## Standards and primary literature status checked 2026-09-04 + +Wilson's original score-interval source remains the primary statistical reference. IEEE 754-2019 is currently listed by IEEE SA as an Active Standard; P754 is an Active PAR to revise/supersede it, not a published replacement. ISO/IEC 60559:2020 remains a published International Standard adopting the floating-point arithmetic specification. The current published AERA/APA/NCME *Standards for Educational and Psychological Testing* remains the 2014 edition; the sponsoring organizations' Joint Committee is revising that edition, and AERA lists the Standards task-force roster as of 2026-08-31. These statuses are recorded without treating an unpublished revision as current normative authority. + +## References + +American Educational Research Association, American Psychological Association, & National Council on Measurement in Education. (2014). *Standards for educational and psychological testing*. American Educational Research Association. + +IEEE Computer Society. (2019). *IEEE standard for floating-point arithmetic* (IEEE Std 754-2019). IEEE. https://doi.org/10.1109/IEEESTD.2019.8766229 + +International Organization for Standardization & International Electrotechnical Commission. (2020). *Information technology—Microprocessor systems—Floating-point arithmetic* (ISO/IEC 60559:2020). + +Wilson, E. B. (1927). Probable inference, the law of succession, and statistical inference. *Journal of the American Statistical Association, 22*(158), 209–212. https://doi.org/10.1080/01621459.1927.10502953 diff --git a/docs/research/wilson-all-covered-exact-count-residual-midpoint.md b/docs/research/wilson-all-covered-exact-count-residual-midpoint.md new file mode 100644 index 000000000..cc1f6eded --- /dev/null +++ b/docs/research/wilson-all-covered-exact-count-residual-midpoint.md @@ -0,0 +1,43 @@ +# Exact-count Wilson residual midpoint selection + +## Problem + +For an all-covered sample whose retained count is exactly representable in binary64, the Wilson lower endpoint reduces algebraically to + +\[ +L = \frac{n}{n + z^2}. +\] + +`coverage.rs` already preserves the TwoSum residual of `n + z²` and an FMA quotient residual. The previous repair converted that residual into an additive binary64 quotient correction. That is not sufficient for correct final rounding: when the exact represented-input quotient differs from the direct hardware quotient by less than the midpoint to the adjacent binary64 value, rounding the correction separately can still produce a one-ULP step. + +Public RED `d076d344cb3cd3a768e7a5c2d8d7bd9039c657e9` fixes a concrete finite case: `n = 3`, represented `z = 0x1.6a09e667f3bcdp+492`, and represented `z² = 0x1.0000000000001p+985`. The rounded denominator is exactly `z²`, so the count contribution is present only in the TwoSum residual. The direct quotient is `0x1.7ffffffffffffp-984`; the exact rational formed from the already represented inputs `3 / (3 + z²)` rounds to that same binary64 value. The predecessor additive correction instead returned `0x1.7fffffffffffep-984`, one ULP too low. + +## Constraints and alternatives + +The repair stays in TEPP Validation Evidence because it governs representation of Wilson evidence emitted by the canonical `validation_core::coverage` producer. It does not introduce reusable static psychometric estimation and does not copy fast-mlsirm source. + +Always applying the additive correction was rejected because the RED demonstrates a double-rounding failure at the quotient scale. Always keeping the direct quotient was rejected because earlier exact-count cases demonstrate the opposite state: denominator rounding can move the exact represented-input endpoint across an adjacent binary64 midpoint. Replacing the whole Wilson implementation with a second arbitrary-precision writer was rejected because it would violate the single-writer boundary and widen the causal surface. + +## Selected repair + +Causal repair `32314239754204158f228ec67a0771abf4d39b45` keeps the rounded denominator, its TwoSum residual, and the FMA quotient residual. When the denominator residual is nonzero, it uses the residual sign only to identify the adjacent candidate and compares residual magnitude with the exact-denominator midpoint distance represented as + +\[ +\tfrac12\,\operatorname{ulp}_{direction}(q)\,(D + \delta D). +\] + +The dominant `ulp * D` product is evaluated through FMA with the residual term. The quotient changes by one adjacent binary64 value only when the represented-input rational lies beyond that midpoint; an exact midpoint follows ties-to-even from the direct quotient significand. The existing `direct_lower == 1.0` complementary-miss boundary remains separate because the adjacent uncertainty can be lost before a usable direct quotient exists. + +Dedicated CHANGELOG evidence is `a75940c955633d5eb92f227d79e64acd0ef46ea8`. + +## Scope and risk + +This closes the demonstrated exact-count all-covered residual-overcorrection state. It does not claim globally correctly rounded Wilson endpoints for strict-interior coverage, inexact durable counts, or algebraically different Wilson forms. Those paths require their own represented-input counterexamples before any gate is strengthened. + +The main remaining risk is midpoint comparison at binary64 spacing transitions and tie states. Existing predecessor tests cover a large-`z²` case where one-ULP movement is required, a power-of-two large-`z` case where it is not, near-one exact-count recovery, and partial denominator rounding. The new RED covers the opposite extreme-scale state where a real denominator residual is below final quotient resolution. + +## Methodological trace + +Wilson, E. B. (1927). Probable inference, the law of succession, and statistical inference. *Journal of the American Statistical Association, 22*(158), 209–212. https://doi.org/10.1080/01621459.1927.10502953 + +IEEE Std 754-2019 and ISO/IEC 60559:2020 remain the published floating-point authorities used for binary64 round-to-nearest, ties-to-even reasoning. The scientific acceptance policy remains anchored to the published AERA/APA/NCME *Standards for Educational and Psychological Testing* while the successor revision is still under development. diff --git a/docs/research/wilson-all-covered-exact-count-small-z-rounding.md b/docs/research/wilson-all-covered-exact-count-small-z-rounding.md new file mode 100644 index 000000000..45ca57cde --- /dev/null +++ b/docs/research/wilson-all-covered-exact-count-small-z-rounding.md @@ -0,0 +1,58 @@ +# Wilson all-covered exact-count near-one rounding + +## Problem and scientific boundary + +TEPP's Validation Evidence owner computes the Wilson score interval from retained coverage counts and a finite positive standard-normal critical value. For an all-covered sample, Wilson's lower endpoint is algebraically + +`L = n / (n + z²) = 1 - z² / (n + z²)`. + +The estimator and sidedness do not change here. The defect is binary64 evaluation of the same endpoint when `n` is exactly representable and `z²` is small but not so small that the final endpoint should round to exact one. + +With `n = 1` and represented `z = 0x1.0000000000001p-27`, binary64 multiplication produces represented `z² = 0x1.0000000000002p-54`. The correctly rounded represented-input endpoint is `0x1.fffffffffffffp-1` (`next_down(1.0)`). Evaluating `n + z²` first rounds that denominator back to `1.0`, so the predecessor direct expression `n / (n + z²)` emitted false exact `1.0` and erased representable finite-sample uncertainty. + +This is not a new confidence-interval estimator. It is numerical conditioning of Wilson's all-covered score endpoint and remains owned by TEPP `validation_core` Validation Evidence. Reusable static psychometric estimation remains owned by fast-mlsirm. + +## Causal repair + +Public RED `e0c4ec81bb455d230259489dc71e23fe33704b1d` adds `crates/validation_core/tests/wilson_all_covered_exact_count_small_z_rounding_contract.rs` and fixes the expected lower endpoint to bits `0x3fefffffffffffff`. + +Causal repair `c9dcb9df363999bbcbb6fffdc8b6a6d9ae5e762c` keeps the ordinary exact-count direct path. Only when that path returns exact `1.0` while represented `z² > 0` does it evaluate the algebraically equivalent miss mass `z² / (n + z²)` and subtract that from one. This closes the demonstrated false-one boundary without globally replacing the all-covered formula with a different rearrangement. + +Boundary reinforcement `6140080d2257d0550be479d71371d70e2255c3d0` fixes `z = 2^-28`, `z² = 2^-56`, where the exact Wilson lower endpoint differs from one by less than half the binary64 spacing immediately below one. Exact `1.0` is therefore the correct represented endpoint, and the repair must not manufacture a lower representable value. + +CHANGELOG trace: `09ebb482851fe7836e738a74395e6424621da9bf`. + +## Decision record + +Problem: exactly representable coverage denominators could still lose a representable all-covered miss mass through denominator absorption. + +Constraint: preserve the existing Wilson estimator, standard-normal/two-sided semantics, `u64` count provenance, and `coverage.rs` single-writer ownership. Do not claim global correct rounding for every algebraic rearrangement. + +Alternatives considered: + +- Always evaluate `1 - z² / (n + z²)`. Rejected because large `z²/n` can make the miss fraction round to exact one and create the opposite false-zero boundary already addressed on the inexact-count path. +- Always evaluate `1 / (1 + z²/n)`. Rejected because forming `1 + z²/n` is exactly the near-one absorption mechanism exposed by this RED. +- Add a boundary-local complementary evaluation only after the direct exact-count result has collapsed to `1.0`. Selected because it is the minimum causal change for the demonstrated public contract and leaves ordinary direct evaluation unchanged. + +Risk: the repair does not prove every possible exact-count Wilson endpoint is globally correctly rounded. Its claim is narrower: it removes the demonstrated false exact-one state while retaining the correctly rounded exact-one state below binary64 resolution. + +## Traceability + +- Bounded context: Validation Evidence +- Production writer: `crates/validation_core/src/coverage.rs` +- Public contract: `crates/validation_core/tests/wilson_all_covered_exact_count_small_z_rounding_contract.rs` +- Durable consumers: `WilsonCoverageEvidenceV1` and `ValidationEvidenceV1` continue to consume the canonical Wilson writer rather than duplicate endpoint arithmetic. +- RED: `e0c4ec81bb455d230259489dc71e23fe33704b1d` +- Causal fix: `c9dcb9df363999bbcbb6fffdc8b6a6d9ae5e762c` +- Edge reinforcement: `6140080d2257d0550be479d71371d70e2255c3d0` +- CHANGELOG: `09ebb482851fe7836e738a74395e6424621da9bf` + +## References + +Wilson, E. B. (1927). Probable inference, the law of succession, and statistical inference. *Journal of the American Statistical Association, 22*(158), 209–212. https://doi.org/10.1080/01621459.1927.10502953 + +IEEE. (2019). *IEEE Standard for Floating-Point Arithmetic (IEEE Std 754-2019).* https://standards.ieee.org/ieee/754/6210/ + +ISO/IEC. (2020). *ISO/IEC 60559:2020 Information technology—Microprocessor systems—Floating-point arithmetic.* https://www.iso.org/standard/80985.html + +As checked in September 2026, IEEE 754-2019 and IEEE/ISO/IEC 60559-2020 are published active standards; IEEE P754 is an active revision project and is not treated as a published replacement. diff --git a/docs/research/wilson-all-covered-inexact-count-extreme-z.md b/docs/research/wilson-all-covered-inexact-count-extreme-z.md new file mode 100644 index 000000000..7071dd96d --- /dev/null +++ b/docs/research/wilson-all-covered-inexact-count-extreme-z.md @@ -0,0 +1,65 @@ +# Wilson all-covered exact-count scaling under extreme finite critical values + +## Problem + +`WilsonCoverageEvidenceV1` retains `sample_count` and `covered_count` as exact `u64` provenance and recomputes the canonical Wilson score endpoints from those counts. The reciprocal-scale path introduced for sample counts above binary64's exact-integer range correctly avoids pre-rounding the durable denominator. Its all-covered branch, however, always evaluated the lower endpoint through the complementary miss mass + +`1 - (z² / n) / (1 + z² / n)`. + +That algebraic form is stable when `z² / n` is small because it keeps a finite miss mass that would otherwise disappear when `1 + z² / n` rounds to `1.0`. It is not stable when `z² / n` is large: the miss fraction itself rounds to exact `1.0`, so subtracting it produces a false exact-zero lower endpoint even when the Wilson lower endpoint is an ordinary positive binary64 value. + +## Public RED + +Commit `059ce70d3dd497f486214137bca6a1f1b2e8b3cd` adds `crates/validation_core/tests/wilson_all_covered_inexact_count_extreme_z_contract.rs`. + +The contract fixes: + +- `sample_count = covered_count = 2^53 + 1 = 9_007_199_254_740_993`; +- `z = 1e20`, so represented `z²` is finite; +- empirical coverage `1.0`; +- represented Wilson lower endpoint `0x1.16c262777579dp-80`, decimal `9.007199254740993e-25`. + +The predecessor reciprocal path computes `z² / n`, then `(z² / n)/(1 + z² / n)`. At this scale the latter rounds to exact `1.0`; subtracting from one therefore returns `0.0` and destroys representable finite-sample uncertainty. + +## Repair lineage and self-review RCA + +Commit `0f4783929b8d067eecb91696e1ad5761cd315b1e` first repaired the catastrophic boundary collapse by switching algebraic projection for the all-covered lower endpoint when the durable `u64` denominator is not exactly representable as binary64: + +- if `z² / n <= 1`, evaluate `1 - (z² / n)/(1 + z² / n)` so tiny uncertainty below one survives; +- if `z² / n > 1`, evaluate `1/(1 + z² / n)` so a large complementary miss mass cannot round to one before subtraction. + +Commit `8e2058f2fc6ab42af8f732f03e7ae1dcee3e873d` then removed a transient duplicated implementation from `coverage_evidence.rs`, restoring the Wilson projection to the single canonical writer in `coverage.rs`. + +Pre-GREEN exact-oracle review found that this was still incomplete. The first scale-switch implementation formed `z² / n` as `z² * round(1/n)`. For the RED case that yields `0x1.d6329f1c35ca5p+79` and the direct reciprocal lower endpoint `0x1.16c262777579cp-80`, one ULP below the public oracle. The catastrophic false zero was gone, but the exact durable denominator had still been rounded once before multiplication. + +Commit `93d5d2089db1fcf4c6167930adb362333a2ed809` is the causal correction for that residual double rounding. For the inexact-`u64` all-covered path, `coverage.rs` now decodes represented finite `z²` into its exact binary significand and power-of-two exponent, computes the significand/`sample_count` unit ratio through the existing integer ties-to-even routine, and restores the power-of-two scale. For the RED case this produces `z²/n = 0x1.d6329f1c35ca4p+79`, after which the large-scale reciprocal yields the required `0x1.16c262777579dp-80` endpoint. + +The new private helper is restricted to this exact-count projection path. It does not redefine a psychometric estimator or make a claim that every arbitrary algebraic rearrangement of the Wilson formula is globally correctly rounded. The public contract is the exposed scientific boundary: exact durable count provenance must not be replaced by a pre-rounded denominator or a rounded-reciprocal product that changes the represented endpoint in the tested case. + +## Boundary and owner decision + +The existing `n = 2^55 + 3, z = 1.96` durable all-covered contract exercises the small-`z²/n` side and still requires `next_down(1.0)`. The extreme-`z` contract exercises the large side and requires the exact public-oracle bit pattern rather than false exact zero or the one-ULP-low intermediate repair. The transition at `z²/n = 1` changes only which algebraically equivalent expression is numerically conditioned; it does not alter interval sidedness, critical-value semantics, retained counts, or the Wilson score estimand. + +This remains TEPP Validation Evidence provenance/projection behavior. No reusable static psychometric estimator is added, so there is no fast-mlsirm migration. No LLM is involved, and no contextual-orchestrator dependency is introduced. + +## Traceability + +- Public RED: `059ce70d3dd497f486214137bca6a1f1b2e8b3cd` +- Boundary scale-switch repair: `0f4783929b8d067eecb91696e1ad5761cd315b1e` +- Single-writer cleanup: `8e2058f2fc6ab42af8f732f03e7ae1dcee3e873d` +- Exact-denominator quotient correction: `93d5d2089db1fcf4c6167930adb362333a2ed809` +- Changelog correction: `43b6562c4c2ad3af51970040c127c784cccff228` +- Module: `crates/validation_core/src/coverage.rs` +- Durable carrier: `crates/validation_core/src/coverage_evidence.rs` +- Public contract: `crates/validation_core/tests/wilson_all_covered_inexact_count_extreme_z_contract.rs` +- Landing vehicle: PR #488 + +## References + +Institute of Electrical and Electronics Engineers. (2019). *IEEE standard for floating-point arithmetic* (IEEE Std 754-2019). https://doi.org/10.1109/IEEESTD.2019.8766229 + +International Organization for Standardization. (2020). *Information technology—Microprocessor systems—Floating-point arithmetic* (ISO/IEC 60559:2020). + +Wilson, E. B. (1927). Probable inference, the law of succession, and statistical inference. *Journal of the American Statistical Association, 22*(158), 209–212. https://doi.org/10.1080/01621459.1927.10502953 + +IEEE P754 is an active revision project superseding IEEE 754-2019; it is not treated here as a published replacement standard. diff --git a/docs/research/wilson-coverage-evidence-provenance.md b/docs/research/wilson-coverage-evidence-provenance.md new file mode 100644 index 000000000..599c82005 --- /dev/null +++ b/docs/research/wilson-coverage-evidence-provenance.md @@ -0,0 +1,87 @@ +# Wilson coverage evidence provenance + +## Problem + +`ValidationReport` historically retained only the empirical coverage proportion and two Wilson score endpoints. That compact projection is useful for presentation but is not sufficient provenance for a durable validation artifact: the empirical denominator and the caller-supplied Wilson critical value are lost. Endpoint-pair algebra can reject many impossible combinations, but it cannot reconstruct the exact finite-sample calculation and becomes deliberately non-identifying at exact boundary coverage. + +For a coverage study with `k` covered intervals among `n` admitted interval/truth triples, TEPP's canonical producer uses `p = k / n` and a caller-supplied standard-normal critical value `z`. Two reports can therefore show the same represented `p` while having different denominators, and the same `p` can produce different Wilson bounds under different `z` values. A durable evidence record that omits `n`, `k`, or the critical-value semantics cannot independently recompute the interval it claims to preserve. + +## Versioned carrier and envelope + +`validation_core::WilsonCoverageEvidenceV1` is the versioned provenance carrier for the calculation. Its JSON contract emits `schema = "tepp.wilson_coverage_evidence.v1"`, fixed-width `u64` `sample_count` and `covered_count`, `critical_value_kind = "standard_normal_z"`, `interval_sidedness = "two_sided"`, the caller-supplied `normal_critical_value`, represented empirical coverage, and the canonical Wilson lower and upper endpoints. + +The type does not infer or invent a nominal confidence-level label. The current producer accepts a numeric standard-normal critical value directly and evaluates the symmetric lower/upper Wilson roots, so v1 records that scientific input, its scale, and two-sided interpretation. A downstream product may display `z = 1.96` as a nominal two-sided 95% convention only when that claim is supplied by its own validated contract; the Validation Evidence carrier does not reverse-engineer a confidence label that the producer was never given. + +`ValidationEvidenceV1` is the durable outer envelope. It retains the existing `ValidationReport` as a backward-compatible compact projection and nests `WilsonCoverageEvidenceV1` as its recomputable coverage provenance. Admission validates both artifacts and requires the report's empirical coverage, Wilson lower endpoint, and Wilson upper endpoint to equal the nested carrier exactly. Existing callers can continue using `ValidationReport`; durable v1 evidence can no longer pair a valid-looking projection with a different denominator or critical value. + +## Canonical recomputation + +The coverage implementation has one crate-private count-based Wilson authority. `wilson_coverage_interval` still accepts interval/truth triples, but it counts covered observations once and delegates the numeric interval calculation to `wilson_coverage_interval_from_counts`. `WilsonCoverageEvidenceV1` uses the same helper for construction and validation. This avoids a second copy of Wilson arithmetic and makes the durable carrier recomputable from the evidence it stores. + +Artifact admission is exact for the represented binary64 contract. The carrier recomputes represented empirical coverage and both Wilson endpoints with the canonical producer and requires numeric equality. TEPP's JSON serializer emits round-trip-safe binary64 decimals, so a serialized artifact produced by this crate decodes to the same represented values. A changed denominator, covered count, critical value, projected coverage, or endpoint fails closed. Unknown fields, an unsupported schema version, a critical-value kind other than `standard_normal_z`, or sidedness other than `two_sided` also fail deserialization rather than being silently reinterpreted. + +This exact recomputation contract is intentionally stronger than `ValidationReport`'s legacy endpoint-pair admission. The legacy report lacks enough provenance to reproduce the original interval and therefore uses necessary algebraic support checks; the versioned carrier has the missing denominator and critical value, so it can call the actual canonical producer instead of relying on a loose identity. + +## Exact count-ratio representation + +Durable integer provenance creates a numerical obligation that the in-memory interval API normally cannot reach in practice. Binary64 has 53 bits of significand precision, so every integer is exactly representable only through `2^53`. At `n = 2^53 + 1` and `k = 2^53`, converting `k` and `n` independently to binary64 rounds both to `2^53`; the naive expression `(k as f64) / (n as f64)` therefore becomes exact `1.0` even though one admitted interval is uncovered. The same collapse also steers a count-based Wilson implementation onto an all-covered numerical path and materially changes the representable lower endpoint. + +The first repair preserved the smaller side of the binomial partition for Wilson evaluation. Near the all-covered boundary, Wilson endpoints are evaluated on the uncovered proportion and reflected by score-interval complement symmetry, while exact integer equality `covered_count == sample_count` alone owns the all-covered branch. At `n = 9,007,199,254,740,993`, `k = 9,007,199,254,740,992`, and `z = 1.96`, the durable contract records represented coverage `0x3fefffffffffffff` (`0.9999999999999999`) and Wilson lower endpoint `0x3feffffffffffffa` (`0.9999999999999993`), rather than falsely treating the count state as all-covered. The upper endpoint rounds to `1.0`, which is representable and does not erase the retained uncovered count because the integer provenance remains authoritative. + +Complement selection alone does not make every `u64/u64` proportion correctly rounded. A second realistic count pair exposes the remaining defect: `n = 9,007,199,254,740,993` and `k = 4,503,599,627,370,396`. The exact rational `k/n` rounds to binary64 `0x1.fffffffffff37p-2` (`0.49999999999998884`), but independently converting both integers and then dividing yields `0x1.fffffffffff38p-2`, one ULP higher. That discrepancy is a projection error in durable empirical coverage even though neither count is near an exact 0/1 boundary. + +`correctly_rounded_unit_ratio` now derives the binary exponent from integer bit lengths and comparisons, forms the exact scaled significand in `u128`, and applies round-to-nearest, ties-to-even before constructing the binary64 bits. Every valid `u64/u64` unit ratio lies between `2^-64` and `1`, so its nonzero binary64 result is normal and the required scaled integer fits in `u128`; no bigint or Python validation path is needed. `represented_coverage_from_counts` uses this exact integer-ratio projection directly. `wilson_coverage_interval_from_counts` uses the same correctly rounded proportion and still evaluates the smaller uncovered side when complement symmetry avoids all-covered cancellation. + +The v1 JSON count fields are `u64`, not `usize`. A durable schema must not change its numeric domain with the pointer width of the Rust process that reads it. The in-memory slice constructor safely widens its `usize` lengths/counts to `u64`; deserialization can therefore preserve the same versioned count artifact on 32-bit and 64-bit consumers even when the count could not be materialized as one process-resident slice. + +IEEE 754-2019 remains the published active IEEE floating-point standard, and IEEE/ISO/IEC 60559-2020 is its active international adoption. IEEE P754 is an active revision project intended to supersede IEEE 754-2019, not a published replacement. This repair therefore grounds binary64 representation claims in the published 2019/2020 standards and does not treat the active P754 project as current normative authority. + +## RED → repair trace + +The first test draft `9fc96345f67ee0d6e6e8b62903b9994f13932a1d` contained a bad fixture and placeholder assertion and is not scientific evidence. Non-force correction `6f6e06d2446cc459cc29879c3e4bc34a2fff8e82` is the first valid RED for denominator/covered-count retention, standard-normal critical-value semantics, JSON round-trip, and fail-closed tampering. + +The initial implementation lineage is: + +- `ca517ed3755a11b4574f8909acd6965273cf69e9`: factor interval hit counting and count-based Wilson recomputation into one crate-private numeric authority while preserving public interval APIs; +- `31e1ab2bbaf9ce40cf74bed2110a310116e7a80a`: add `WilsonCoverageEvidenceV1` with validated construction, exact recomputation, schema-tagged manual serde, and fail-closed unknown-field behavior; +- `b6714d2365bbdbad0f127631edd8463f1829f0e2`: export the coverage carrier from `validation_core`; +- `02c8763fd0d921189b51fc437016d04505182e1b`: expand edge contracts for tampered counts/endpoints/probabilities, serializer refusal, invalid inputs, and square-overflowing `z`; +- `e9c6392604140b819a192bc2969636f62e770a0b` and `fdd24a1a9d3c9d22802f6a8ec6379e687009afe7`: bind and test `two_sided` interval semantics rather than leaving one-sided reinterpretation implicit; +- `07766cb0b475da3d5610b42577dd9a5e91038bc9`: public RED requiring a versioned durable envelope to bind `ValidationReport` coverage fields to the provenance carrier; +- `a16f22e674a0493fc0d49589e66e7ad66ea1e543`: add `ValidationEvidenceV1` with nested validation, exact projection identity, schema-tagged serde, and unknown-field refusal; +- export edit `4e2381f31a43d753cd8c37bb153aac701876c1ef` accidentally changed unrelated claim-authority wording and is not accepted repair evidence; non-force correction `57d8bf57687c5157c7c06c941a113071e22c5430` restores that wording while retaining only the intended module/export delta; +- `6e5467428b9de851f075cb251f9dc38a4ec6a728`: update release-facing change documentation for the carrier/envelope contract. + +The large-count repair lineage is: + +- `29d710a53d4988b46a37667b4ff03352d520b3c7`: public RED showing that a durable one-uncovered count state above `2^53` must decode to a non-all-covered represented coverage/Wilson artifact; +- `29968c807368fe3fe19bef3013af9d577d5f6025`: make count-based Wilson evaluation reflect the smaller uncovered proportion near the all-covered boundary and stop using a rounded proportion to decide all-covered status; +- `91d9a3bb54b47605377e5159763a55f3386efcf9`: make v1 durable counts fixed-width `u64` and route carrier construction/validation through the same count-preserving authority; +- `d908ce2a37685bdf915cc513be9f3f9dd36aae19`: add release-facing documentation for the count-precision and schema-portability repair; +- `63ddfdd615de23c50910ce54cff60fbb537a45d4`: public RED showing a non-boundary `u64/u64` count ratio whose independent integer-to-binary64 conversions misround durable empirical coverage by one ULP; +- `11323c579b499dc136ec564f8b04ca91c5d7076b`: add exact `u128` ratio projection with ties-to-even coverage and route represented coverage/Wilson proportions through it; +- `7cad2f45771eab2e3ac2f498e27b3665406fe739`: add release-facing documentation for the exact count-ratio projection repair. + +Every later source or documentation commit on PR #488 invalidates predecessor exact-head workflow evidence; only the current surviving head's hosted gates and independent review count for landing. + +## DDD and owner boundary + +This is Validation Evidence provenance and projection policy. It does not define a new psychometric estimator, change the Wilson score estimand, move longitudinal/time-varying composition into `validation_core`, or copy mutable arithmetic from fast-mlsirm. The count-based and exact-ratio helpers are private to the existing TEPP coverage producer so there remains one numeric authority inside this bounded context. + +The carrier and envelope do not involve semantic LLM execution. contextual-orchestrator remains the owner of model routing; no unreleased orchestrator source, direct provider route, or provider credential is introduced here. + +## Standards and primary sources + +Wilson's original score-interval paper remains the primary statistical source for the interval family used by this producer. The current published AERA/APA/NCME testing standards remain the 2014 edition; AERA, APA, and NCME have convened a Joint Committee to revise that edition, and AERA's Task Force roster was current as of August 31, 2026. An unpublished revision is not treated as present normative authority. + +American Educational Research Association, American Psychological Association, & National Council on Measurement in Education. (2014). *Standards for educational and psychological testing*. American Educational Research Association. + +Institute of Electrical and Electronics Engineers. (2019). *IEEE standard for floating-point arithmetic* (IEEE Std 754-2019). IEEE. https://standards.ieee.org/ieee/754/6210/ + +International Organization for Standardization, & International Electrotechnical Commission. (2020). *Information technology—Microprocessor systems—Floating-point arithmetic* (ISO/IEC 60559:2020). ISO. https://www.iso.org/standard/80985.html + +Wilson, E. B. (1927). Probable inference, the law of succession, and statistical inference. *Journal of the American Statistical Association, 22*(158), 209–212. https://doi.org/10.1080/01621459.1927.10502953 + +## Verification contract + +`crates/validation_core/tests/wilson_coverage_evidence_v1_contract.rs` covers the provenance carrier. `crates/validation_core/tests/wilson_coverage_count_precision_contract.rs` covers the all-covered-adjacent large-count boundary, and `crates/validation_core/tests/wilson_coverage_ratio_rounding_contract.rs` covers non-boundary exact count-ratio rounding. `crates/validation_core/tests/validation_evidence_v1_coverage_provenance_contract.rs` covers the durable report/provenance envelope. Exact-head Rust tests, rustdoc/docstring checks, owned line/branch coverage, documentation validation, security/SAST/supply-chain checks, and qualifying independent review remain required before the Draft landing vehicle can be promoted or merged. diff --git a/docs/research/wilson-coverage-extreme-endpoint.md b/docs/research/wilson-coverage-extreme-endpoint.md new file mode 100644 index 000000000..832db0ffa --- /dev/null +++ b/docs/research/wilson-coverage-extreme-endpoint.md @@ -0,0 +1,70 @@ +# Wilson coverage endpoint stability + +## Decision + +`validation_core::wilson_coverage_interval` remains the Validation Evidence authority for a Wilson score interval around empirical interval-coverage proportions. The public estimand is unchanged. The numerical implementation must not turn a mathematically interior, binary64-representable Wilson endpoint into exact `0.0` or `1.0`, or substitute a floating-point cancellation residue for that endpoint, through avoidable intermediate rounding. + +For an all-covered sample (`p̂ = 1`), the ordinary Wilson lower endpoint simplifies algebraically to + +`n / (n + z²)`. + +Evaluating the generic center-minus-margin form first can subtract two nearly equal `O(z²)` quantities. With one covered replication and finite `z = 1e154`, `z² = 1e308` is still finite and the exact simplified lower endpoint is approximately `1e-308`, which is representable in binary64. The predecessor generic expression rounded the numerator cancellation to exact zero and therefore reported a stronger boundary statement than the represented inputs justify. + +The same defect exists away from the endpoint. For strict-interior empirical coverage `0 < p̂ < 1`, write the Wilson lower root as + +`(A - B) / (2(n + z²))`, + +where `A = z² + 2np̂` and `B = z sqrt(z² + 4np̂(1-p̂))`. Rationalizing the numerator gives the exactly equivalent form + +`2np̂² / (A + B)`. + +Dividing numerator and denominator through by `z²` avoids both the `A - B` cancellation and an avoidable `A + B` overflow: + +`(2np̂² / z²) / (1 + 2np̂ / z² + sqrt(1 + 4np̂(1-p̂) / z²))`. + +With two replications, one covered (`p̂ = 0.5`), and finite `z = 1e154`, the generic predecessor path produces an exact-zero lower endpoint because its center and margin both round to `2.5e307`. The rationalized Wilson lower endpoint is approximately `5e-309`, still representable in binary64. + +An exact-zero fallback is not sufficient. With three replications, one covered (`p̂ = 1/3`), and finite `z = 1e11`, binary64 rounds the predecessor center and margin to adjacent values whose subtraction leaves the nonzero residue `262144`. Dividing that residue by the Wilson denominator produces approximately `7.86432e-17`, while the algebraically equivalent rationalized endpoint is approximately `3.3333333333333333e-23`. The predecessor result is therefore exactly 2,359,296 times larger even though it never reaches zero and would bypass an exact-zero-only fallback. + +The repair evaluates every nonzero strict-interior lower endpoint through the rationalized positive root. For `z² >= 1`, it uses the `z²`-normalized expression above. For `z² < 1`, dividing through by `z²` can itself create avoidable overflow, so the same rationalized root is evaluated on its natural scale as + +`2np̂² / (z² + 2np̂ + z sqrt(z² + 4np̂(1-p̂)))`. + +This is one algebraic estimand with scale-aware evaluation, not a confidence-policy change. + +The upper endpoint has the complementary identity + +`U(p̂) = 1 - L(1 - p̂)`. + +That identity matters when the generic `center + margin` numerator and denominator round to the same binary64 value. With two uncovered replications and `z = 2^27`, `z² / n = 2^53` exactly. The predecessor evaluates the all-uncovered upper endpoint as exact `1.0`, although the represented Wilson value correctly rounds to `next_down(1.0)`. The same false-one collapse occurs for strict-interior coverage; with one covered replication out of eight and `z = 2^28`, the represented upper endpoint also rounds to `next_down(1.0)`, not `1.0`. + +The repair therefore keeps the ordinary upper calculation when it remains below one. Only when that direct path reaches exact `1.0` while uncovered mass is nonzero does TEPP evaluate the positive lower endpoint of the complementary uncovered proportion with the same rationalized form and subtract it from one. This preserves a representable nonunit endpoint without perturbing ordinary cases or redefining true boundary coverage. + +## RED → repair trace + +- All-covered lower public RED: `f84e5918acc81ca8bf3708f3cce2004c67675b78`, `crates/validation_core/tests/wilson_all_covered_extreme_z_contract.rs`. +- All-covered lower causal repair: `fe9b9c8a5b94a01cd8416efd613503569b98ac1a`, `crates/validation_core/src/coverage.rs`. +- Strict-interior exact-zero lower public RED: `9d45f482854037d96d5dff38964fd3844335a39b`, `crates/validation_core/tests/wilson_interior_extreme_z_contract.rs`. +- Strict-interior exact-zero causal repair: `4f259f6e5c98ade2e4a34125430de872f32c1589`, `crates/validation_core/src/coverage.rs`. +- Strict-interior nonzero cancellation-residue RED: `1a24fac71569334b0c0185d013574ec5ccdcc58d`, `crates/validation_core/tests/wilson_interior_cancellation_residual_contract.rs`. +- Scale-aware rationalized-lower causal repair: `f7e20ddc59399f2e014a3ac91809bb57ba73460f`, `crates/validation_core/src/coverage.rs`. +- Small-`z` branch coverage and oracle reinforcement: `39e5e087cba22d6df85b9ad5901cda0194e3da93`, same public contract file. +- All-uncovered upper public RED: `c070da269aa257fc9c9fa9eae17231a51ec63b74`, `crates/validation_core/tests/wilson_all_uncovered_upper_endpoint_contract.rs`. +- Strict-interior upper RED expansion: `344081bfd98ee9bc70a3bf8fdebc795a9090e692`, same public contract file. +- Complementary rationalized upper repair: `9a2fdd05c2994f51f6c72030fe39e695ba5a876d`, `crates/validation_core/src/coverage.rs`. +- Release trace: `2dbdf330af21ea6263fd5d03eb6cb8defa867254`, `CHANGELOG.d/validation-wilson-extreme-endpoint.md`. +- API: `validation_core::wilson_coverage_interval`. + +Interval-admission rules and rejection of non-finite/non-positive `z` or overflowing `z²` remain unchanged. The new path is endpoint-representation repair, not a change to nominal coverage policy. + +## Scientific boundary + +This is Validation Evidence execution arithmetic, not a psychometric estimator and not Longitudinal Modeling composition. A value of `z` is caller-supplied configuration; finite positive values remain admitted under the existing API contract. If future product policy restricts supported confidence levels, that is a separate configuration/PRD decision and must not be smuggled in as a numerical workaround. + +Wilson's score construction is the methodological authority for the interval form. IEEE/ISO/IEC 60559 binary floating-point semantics explain why algebraically equivalent expressions can have different endpoint behavior in finite precision; TEPP therefore uses algebraically equivalent forms that preserve representable scientific evidence across the admitted binary64 domain. As of 2026-09-04, IEEE/ISO/IEC 60559-2020 remains an active published floating-point standard, while IEEE P754 is an active revision project superseding IEEE 754-2019; an unpublished revision is not treated as current normative text. + +## References + +IEEE. (2020). *IEEE/ISO/IEC 60559-2020: ISO/IEC/IEEE International Standard—Floating-point arithmetic*. https://standards.ieee.org/ieee/60559/10226/ + +Wilson, E. B. (1927). Probable inference, the law of succession, and statistical inference. *Journal of the American Statistical Association, 22*(158), 209–212. https://doi.org/10.1080/01621459.1927.10502953 diff --git a/docs/research/wilson-sample-count-scale.md b/docs/research/wilson-sample-count-scale.md new file mode 100644 index 000000000..b7576ffa6 --- /dev/null +++ b/docs/research/wilson-sample-count-scale.md @@ -0,0 +1,51 @@ +# Wilson sample-count scale in durable coverage evidence + +## Problem + +`WilsonCoverageEvidenceV1` stores `sample_count` and `covered_count` as fixed-width `u64` provenance and validates its projected Wilson score interval by recomputation. The preceding large-count repairs made `covered_count / sample_count` correctly rounded and stopped one uncovered observation above `2^53` from collapsing into an all-covered state. One denominator path nevertheless remained lossy: `wilson_coverage_interval_from_counts` converted the exact `sample_count` to binary64 before using it in the Wilson scale terms. + +That conversion is harmless only when the integer count itself is exactly representable. For `sample_count = 9_007_199_254_740_1013`, `sample_count as f64` is a neighboring even binary64 integer rather than the retained denominator. With `covered_count = 1_286_742_750_677_287` and `z = 1.96`, the exact integer ratio still projects to ordinary finite coverage `0.1428571428571428`, but using the rounded denominator moves the Wilson lower endpoint one binary64 ULP upward. The represented endpoint obtained from the exact denominator is `0x1.2492482c43beap-3` (`0.14285713563046382`). + +The same loss is buyer-visible at a boundary. For all-covered evidence with `sample_count = 2^55 + 3 = 36_028_797_018_963_971` and `z = 1.96`, the exact Wilson lower root `n / (n + z²)` rounds to `next_down(1.0)`. Materializing the count as `f64` first can make the endpoint exact `1.0` and therefore erase representable finite-sample uncertainty even though the durable carrier still owns the exact denominator. + +## Repair + +The canonical count-based Wilson authority now distinguishes whether the fixed-width denominator is exactly representable in binary64. Exact counts keep the established `n: f64` path so existing small-count and extreme-`z` contracts retain their evaluated arithmetic. Counts that are not exactly representable use `correctly_rounded_unit_ratio(1, sample_count)` and evaluate the same Wilson score algebra through the reciprocal scale `1 / n`, avoiding a pre-rounded integer denominator. + +For a positive strict-interior represented proportion `p`, the rationalized lower root is evaluated as + +`2 p² / [z²/n + 2p + z sqrt(z²/n² + 4p(1-p)/n)]`. + +The upper root uses the same reciprocal scale in its denominator, center, and radical. Near all-covered evidence still uses score-interval complement symmetry with the smaller uncovered proportion. For exact all-covered evidence on the inexact-denominator path, the implementation evaluates the complementary miss mass `(z²/n) / (1 + z²/n)` and subtracts that from one; this avoids losing a representable `next_down(1.0)` endpoint when `1 + z²/n` itself rounds to one. + +This is not a new confidence-interval estimand and does not introduce arbitrary higher-precision output. The durable contract already retained exact integer provenance. The repair prevents an avoidable binary64 pre-rounding of that provenance before the existing Wilson score projection. Public results remain binary64 and deterministic. + +## RED → repair trace + +- `f89e246790a835ca1a520c8071401a1e4fd6892f`: public RED for the strict-interior denominator case, fixing exact `u64` counts and the one-ULP lower-endpoint oracle. +- `1a5180b22c755c793d3878e644192e7d7cbbff47`: strengthen the RED with the exact all-covered `2^55 + 3` boundary whose finite-sample lower endpoint must remain `next_down(1.0)`. +- `6254c4989a5fb8922e88bff3d6e5d5b42b4f88e0`: correct the predecessor non-boundary large-count fixture; its empirical count-ratio oracle was already exact, but its Wilson lower endpoint still encoded rounded-denominator arithmetic. +- `73bbb5cf262f710d35b2fffa793076ae6a173947`: causal production repair using an exact-integer representability check and reciprocal-scale Wilson evaluation only when the retained denominator cannot be represented exactly. +- `f2b5768b1632d647959c72a1799160bf16cab5c6`: release-facing CHANGELOG fragment. + +Every later source or documentation push invalidates predecessor exact-head workflow/review evidence. Landing still requires current-head hosted Rust, owned line/branch coverage, documentation, security/SAST/supply-chain checks, resolved review threads, and a qualifying independent approval. + +## Bounded-context ownership + +This change belongs to TEPP Validation Evidence. `WilsonCoverageEvidenceV1` is a durable provenance/projection carrier around the Wilson coverage producer and its exact fixed-width counts; the issue is not reusable static psychometric estimation. No fast-mlsirm source is copied, no Longitudinal Modeling semantics move into `validation_core`, and no contextual-orchestrator behavior is involved. + +## Standards and primary source + +Wilson's original paper remains the primary source for the score-interval family. Binary64 representation and round-to-nearest behavior are grounded in the currently published IEEE 754-2019 and its ISO/IEC 60559:2020 adoption. IEEE P754 is an active revision project rather than a published replacement. The current published AERA/APA/NCME testing standards remain the 2014 edition while the Joint Committee proceeds with revision. + +American Educational Research Association, American Psychological Association, & National Council on Measurement in Education. (2014). *Standards for educational and psychological testing*. American Educational Research Association. + +Institute of Electrical and Electronics Engineers. (2019). *IEEE standard for floating-point arithmetic* (IEEE Std 754-2019). IEEE. https://standards.ieee.org/ieee/754/6210/ + +International Organization for Standardization, & International Electrotechnical Commission. (2020). *Information technology—Microprocessor systems—Floating-point arithmetic* (ISO/IEC 60559:2020). ISO. https://www.iso.org/standard/80985.html + +Wilson, E. B. (1927). Probable inference, the law of succession, and statistical inference. *Journal of the American Statistical Association, 22*(158), 209–212. https://doi.org/10.1080/01621459.1927.10502953 + +## Verification contract + +`crates/validation_core/tests/wilson_sample_count_rounding_contract.rs` fixes both the strict-interior and exact all-covered large-denominator oracles. `crates/validation_core/tests/wilson_coverage_ratio_rounding_contract.rs` protects the corrected earlier ratio case. Private unit coverage verifies binary64 integer-representability classification across spacing changes. Existing small-denominator, extreme-`z`, complement, endpoint-cancellation, carrier-serde, and durable-envelope contracts remain required and are not replaced by these tests. diff --git a/tests/quality/test_prepare_agent_pr_message.py b/tests/quality/test_prepare_agent_pr_message.py index cccc0cc37..dd7aeaa46 100644 --- a/tests/quality/test_prepare_agent_pr_message.py +++ b/tests/quality/test_prepare_agent_pr_message.py @@ -4,7 +4,6 @@ import os import stat -import sys import tempfile import unittest import unittest.mock @@ -288,15 +287,8 @@ def test_module_entrypoint_invokes_main(self) -> None: str(body), ], ): - loaded_module = sys.modules.pop("scripts.prepare_agent_pr_message", None) - try: - with self.assertRaises(SystemExit) as raised: - runpy.run_module( - "scripts.prepare_agent_pr_message", run_name="__main__" - ) - finally: - if loaded_module is not None: - sys.modules["scripts.prepare_agent_pr_message"] = loaded_module + with self.assertRaises(SystemExit) as raised: + runpy.run_module("scripts.prepare_agent_pr_message", run_name="__main__") self.assertEqual(raised.exception.code, 0) def test_read_without_nofollow_flag_when_unavailable(self) -> None: diff --git a/tests/quality/test_workflow_queue_hygiene.py b/tests/quality/test_workflow_queue_hygiene.py deleted file mode 100644 index 431687af5..000000000 --- a/tests/quality/test_workflow_queue_hygiene.py +++ /dev/null @@ -1,67 +0,0 @@ -"""Queue-isolation contracts for repository-local GitHub Actions.""" - -from pathlib import Path -import unittest - - -WORKFLOW_DIRECTORY = Path(".github/workflows") - - -class WorkflowQueueHygieneTests(unittest.TestCase): - """Keep local validation isolated by repository and pull request.""" - - def test_pull_request_workflows_cancel_only_older_same_pr_runs(self) -> None: - """CI groups include workflow, repository, and PR identity.""" - - expected_groups = { - "ci.yml": ( - "tepp-rust-foundation-${{ github.repository }}-" - "${{ github.event.pull_request.number || github.run_id }}" - ), - "docs-quality.yml": ( - "docs-quality-${{ github.repository }}-" - "${{ github.event.pull_request.number || github.run_id }}" - ), - } - for workflow_name, expected_group in expected_groups.items(): - workflow = (WORKFLOW_DIRECTORY / workflow_name).read_text(encoding="utf-8") - self.assertIn(f"group: {expected_group}", workflow) - self.assertIn( - "cancel-in-progress: ${{ github.event_name == 'pull_request' }}", - workflow, - ) - - def test_central_scheduler_replaces_local_hourly_caller(self) -> None: - """A local timer must not duplicate central PR scheduling.""" - - self.assertFalse((WORKFLOW_DIRECTORY / "hourly-pr-maintenance.yml").exists()) - - def test_documentation_workflow_does_not_duplicate_crate_ci(self) -> None: - """Crate changes already run documentation contracts in Rust CI.""" - - workflow = (WORKFLOW_DIRECTORY / "docs-quality.yml").read_text( - encoding="utf-8" - ) - self.assertNotIn(' - "crates/**"', workflow) - - def test_rust_ci_reuses_one_runner_for_both_coverage_reports(self) -> None: - """Coverage keeps both gates while avoiding a second runner boot.""" - - workflow = (WORKFLOW_DIRECTORY / "ci.yml").read_text(encoding="utf-8") - self.assertIn(" production-coverage:\n", workflow) - self.assertNotIn(" line-coverage:\n", workflow) - self.assertNotIn(" branch-coverage:\n", workflow) - self.assertIn("id: line-report", workflow) - self.assertIn("id: branch-report", workflow) - self.assertIn("if: ${{ !cancelled() }}", workflow) - self.assertNotIn( - "cargo check --workspace --all-targets --all-features", workflow - ) - self.assertIn( - "cargo clippy --workspace --all-targets --all-features -- -D warnings", - workflow, - ) - - -if __name__ == "__main__": # pragma: no cover - unittest.main()