Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 14 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -125,6 +125,20 @@ All notable changes to EigenScript are documented here.
by pointing the instrument at itself.

### Fixed
- **`report_value` no longer calls a monotone converging sequence
`diverging` (#674).** The value channel's raw-step divergence test (#422)
judged a window's steps "non-vanishing" when the recent half's mean
magnitude was at least *half* the older half's — admitting any geometric
decay with ratio `r^5 >= 0.5`, i.e. `r ≳ 0.871`, so `x *= 0.90` /
`x *= 0.95` (every real iterative solver's operating regime) read
`diverging` while converging to zero. A geometric decay's steps DO vanish
— their sum converges — so the bar is now "not shrinking": the recent
half must be at least the older half (minus 1e-9 of fp-rounding slack).
Linear runaways (constant steps) and polynomial/geometric growth (growing
steps) still read `diverging`; decaying approaches fall through to the
relative verdicts and settle as `converged`. Regression test
`test_report_value_convergence.eigs` (suite [50j3]) pins the issue repro
(ratios 0.88/0.90/0.95 converge) against the genuine-divergence cases.
- **`file_exists` / `ls` / `mkdir` / `getcwd` / `exe_path` are now
trace-recorded (#585).** These fs builtins predated the tape and ran live
under `EIGS_REPLAY`: a program branching on `file_exists of p` replayed
Expand Down
3 changes: 2 additions & 1 deletion docs/OBSERVER.md
Original file line number Diff line number Diff line change
Expand Up @@ -142,7 +142,8 @@ binding). Use `report` to ask *how determined*; use `report_value` to ask
**The raw-step signal (#422).** Relative normalization erases exactly two
classes, so the value channel also keeps the window of *raw* steps `Δv` and
asks one structural question of it: are the steps **non-vanishing** (the
recent half's mean magnitude at least half the older half's, above an
recent half's mean magnitude not below the older half's — a geometric
decay's steps shrink with the value and DO vanish, #674 — above an
fp-noise floor of `4·ε·(1+|x|)`)? Non-vanishing **same-sign** steps sum
without bound — an additive or polynomial runaway (`x → x + c` seeded large)
whose `Δv/|x|` vanishes below the deadband is `diverging`, not `converged`.
Expand Down
23 changes: 16 additions & 7 deletions src/eigenscript.c
Original file line number Diff line number Diff line change
Expand Up @@ -473,12 +473,21 @@ static double observer_slot_vr_get(const ObserverSlot *s, size_t offset_back) {
* the fp-noise floor (a settled double wobbles by ULPs whose signs are
* meaningless — 4·ε·(1+|v|) scales the floor to the value's magnitude), and
* both hinge on the same question the relative channel cannot ask: are the
* raw steps NON-VANISHING (recent-half mean magnitude ≥ half the older-half
* mean)? Non-vanishing same-sign steps sum without bound → diverging, no
* matter how large |v| already is; non-vanishing sign-alternating steps are
* a perpetual oscillation, no matter how small the deadband-relative step.
* A damped (decaying-step) trajectory fails non-vanishing and falls through
* to the relative verdicts — converging approaches stay 'converged'. */
* raw steps NON-VANISHING (recent-half mean magnitude not below the
* older-half mean)? Non-vanishing same-sign steps sum without bound →
* diverging, no matter how large |v| already is; non-vanishing
* sign-alternating steps are a perpetual oscillation, no matter how small
* the deadband-relative step. A damped (decaying-step) trajectory fails
* non-vanishing and falls through to the relative verdicts — converging
* approaches stay 'converged'.
* #674: the bar is "not shrinking", not "shrinking slower than 2x per
* half-window". The old 0.5 factor admitted any geometric decay with ratio
* r^half ≥ 0.5 (r ≳ 0.871 for a 10-window) as "non-vanishing", so a
* monotone approach to zero — every real iterative solver — read
* 'diverging'. A geometric decay's steps DO vanish (their sum converges);
* only non-shrinking steps (a linear runaway's constant Δv, a polynomial
* or geometric runaway's growing Δv) sum without bound. The 1e-9 slack
* absorbs fp rounding in the two half-sums for exactly-constant steps. */
static int observer_slot_raw_nonvanishing(const ObserverSlot *s) {
if (s->v_window_count < OBSERVER_WINDOW_N) return 0;
double floor_eps = 4.0 * DBL_EPSILON * (1.0 + fabs(s->last_value));
Expand All @@ -489,7 +498,7 @@ static int observer_slot_raw_nonvanishing(const ObserverSlot *s) {
recent /= (double)half;
older /= (double)(OBSERVER_WINDOW_N - half);
if (older <= floor_eps || recent <= floor_eps) return 0;
return recent >= 0.5 * older;
return recent >= older * (1.0 - 1e-9);
}

static int observer_slot_raw_diverging(const ObserverSlot *s) {
Expand Down
5 changes: 5 additions & 0 deletions tests/run_all_tests.sh
Original file line number Diff line number Diff line change
Expand Up @@ -2071,6 +2071,11 @@ check_eigs_suite "value-channel diverging/sub-deadband oscillation, trajectory-o
"test_trajectory.eigs" "TRAJECTORY_ALL_PASS" 1
echo ""

echo "[50j3] report_value convergence classification (#674) (6 checks)"
check_eigs_suite "slow geometric decay converges (not diverging), geometric/linear growth still diverging" \
"test_report_value_convergence.eigs" "REPORT_VALUE_CONVERGENCE_ALL_PASS" 1
echo ""

echo "[50k] UTF-8 codepoints (16 checks)"
check_eigs_suite "utf8: decode/len/at/char_at over byte strings + structural validation (published vectors)" \
"test_utf8.eigs" "UTF8_ALL_PASS" 1
Expand Down
60 changes: 60 additions & 0 deletions tests/test_report_value_convergence.eigs
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
# #674 — report_value must not call a monotone CONVERGING sequence 'diverging'.
#
# The value channel's raw-step divergence test (#422) is meant for runaways
# whose steps do not vanish (linear/polynomial/geometric growth — the sum of
# same-sign steps has no bound). A geometric decay x *= r with r in (0,1)
# converges to zero for EVERY r; its steps shrink with it, so it must never
# read 'diverging' no matter how close r is to 1. The old "recent >= 0.5 *
# older" bar admitted any r ≳ 0.871 as "non-vanishing", covering essentially
# every real iterative solver. These checks pin the issue repro plus the
# genuine-divergence cases the raw-step test exists for.
# All-pass prints REPORT_VALUE_CONVERGENCE_ALL_PASS.

fails is 0
define check(tag, got, want) as:
if got == want:
print of f" PASS: {tag}"
if got != want:
print of f" FAIL: {tag} (got {str of got}, want {str of want})"
fails is fails + 1

# 1. The #674 repro: slow geometric decays to zero. 300 iterations, exactly
# as the issue's repro runs them — x is still well above machine epsilon
# for r = 0.95, yet the sequence is converging.
rates is [0.88, 0.90, 0.95]
for i in range of (len of rates):
r is rates[i]
x is 1.0
for j in range of 300:
x is (x * r) + 0.0
check of [f"decay ratio {r} converges (not diverging)", report_value of x, "converged"]

# 2. Genuine divergence must still read 'diverging': geometric GROWTH
# (ratio >= 1, growing magnitude, growing steps).
g is 1.0
s is 0
loop while s < 40:
g is g * 1.05
s is s + 1
check of ["geometric growth (r=1.05) is diverging", report_value of g, "diverging"]

# 3. Linear runaway: constant non-vanishing same-sign steps (the #422 case).
a is 1.0
s2 is 0
loop while s2 < 40:
a is a + 7.0
s2 is s2 + 1
check of ["linear runaway is diverging", report_value of a, "diverging"]

# 4. Fast decay keeps its existing label (no regression on r <= 0.84).
h is 1.0
s3 is 0
loop while s3 < 60:
h is h * 0.5
s3 is s3 + 1
check of ["fast decay (r=0.5) stays converged", report_value of h, "converged"]

if fails == 0:
print of "REPORT_VALUE_CONVERGENCE_ALL_PASS"
if fails != 0:
print of f"REPORT_VALUE_CONVERGENCE_FAILED ({str of fails})"
Loading