Record audited damping and eligibility results through six Newton updates

This commit is contained in:
2026-09-18 12:13:53 -04:00
parent 18762298c1
commit db854c0f7c
3 changed files with 839 additions and 2 deletions

View File

@@ -6,6 +6,16 @@ bounded damping of one fully coupled correction, and reserving that correction
solve for a trial whose merit is reasonably close to its affine prediction.
The previous full-step policy remains available and the default solver is unchanged.
The combined opt-in policy gives useful measured progress, but does not yet
establish convergence. On the matched six-update n=1.5,J=0 trajectory it reaches
F=1.24583e-5 versus 1.49720e-5 for gravity correction alone: 16.79% lower residual
for 22.08% more Newton iteration time. After their identical first update, the
residual reductions are 22.85% versus 7.29%, giving 2.53 times greater decrease
per iteration-second over that bounded interval. Both runs hit the six-update
cap. Endpoint enthalpy-based virials remain below 1e-5, while negative density
and local EOS defects remain unresolved. The default solver remains unchanged.
## Algorithm and architecture
At accepted state x, capture F0=F(x), the actual Newton direction p and J0 p.
@@ -65,5 +75,285 @@ commit refresh, artifact output and physical replay; search wall time
also includes artifact output. Callback vector copies remain in the measured
transaction. The no-correction control captures the same tangent for a comparable
screen, so capture cost is reported separately and is not ordinary Newton work
for that control. All builds and numerical cases run serially; artifact audits
run after timed FEM work.
for that control. Builds, timed numerical cases and artifact audits do not overlap; numerical
cases run one at a time.
## Frozen sequential trial results
The independent artifact audit passed all nine cases in `screen_v1` (see
`screen_v1_audit.json` in the evidence directory below). This audits the recorded transaction algebra and true-defect certificates
from saved vectors, provenance, and recorded restoration evidence; it does not
certify a converged or physically admissible stellar model.
### What improved
The late-state screen uses the same saved state and Newton direction, a halving
line search beginning at alpha=1, one shared correction-solve budget, and the
first accepted trial. The initial full residual norm is 1.3966991044e-5.
| Path | Policy | Accepted alpha | Residual decrease | Search operation time |
| --- | --- | ---: | ---: | ---: |
| Raw | No coupled correction | 1/512 | 0.116541% | 49.04 s |
| Raw | Full correction, every rejection eligible | 1/512 | 0.116541% | 69.00 s |
| Raw | Damped correction, every rejection eligible | 1/512 | 0.116541% | 95.33 s |
| Raw | Damped correction, near-acceptance eligibility | 1/256 | 0.253536% | 80.11 s |
| Gravity-prepared | No coupled correction | 1/512 | 0.047127% | 116.06 s |
| Gravity-prepared | Full correction, every rejection eligible | 1/512 | 0.047127% | Excluded: clock anomaly |
| Gravity-prepared | Damped correction, every rejection eligible | 1/512 | 0.047127% | 168.80 s |
| Gravity-prepared | Damped correction, near-acceptance eligibility | 1/256 | 0.189199% | 128.75 s |
These are original-screen operation timings, excluding artifact writes,
initialization, physical replay, and the separately measured base capture.
The three timing repeats below use the identical diagnostic binary. The excluded
original gravity/full timing is retained as evidence, without rescaling.
The demonstrated accepted-step gain comes from **eligibility timing**. The near
gate defers the prepared rejections at 1/32, 1/64, and 1/128, preserving the sole
solve for 1/256. Both accepted corrections use beta=1 and stop after one
candidate. Damping is therefore not responsible for their accepted improvement.
Without this gate, the correction budget is spent at 1/32, and the eventual
accepted state equals the corresponding baseline. Gravity damping does improve
the rejected 1/32 candidate at beta=1/2, but it remains unacceptable.
The nominal accepted alpha improves **twofold, not fourfold**. The earlier
independent-alpha screen omitted 1/512 and did not reproduce a shared-budget
search. The present residual decreases and costs describe one frozen search;
they establish neither sustained convergence nor a nonlinear speedup. They also
do not establish that the chosen eligibility threshold is effective at other
states, polytropic indices, or rotation rates.
### Seed control
The seed's independently verified outer defect is 0.0281993. Its first trial,
alpha=1, already passes after existing gravity preparation: the residual falls
from 2.736592176e-4 to 1.614841549e-5, a 94.0991% decrease. The new policy performs
zero coupled solves, beta evaluations, or deferrals. This confirms nonintervention
at this sampled seed; it is not a complete trajectory or performance control.
### Physical interpretation
All selected snapshots replay exactly, with accepted buffers, direction,
normalization factors, residual, and Jacobian action restored. The q18
enthalpy-pressure energy virial errors are 4.059–4.068e-6 for the late candidates
and 4.3646e-6 for the seed candidate. Force virial errors are 2.565–2.572e-6 and
2.8455e-6, respectively. Raising quadrature from q14 to q18 changes either
enthalpy-based virial diagnostic by only 1.828–1.834e-9. Mass errors are
3.13–3.71e-6, Jz is zero, and sampled mapping determinants remain above 0.9858.
However, every selected snapshot retains negative density: q18 minima range
from -8.25e-4 to -8.64e-4. Density-derived pressure is not defined everywhere,
and its reported virial diagnostics are NaN. The q18 negative-density mass
fraction is about 2.90e-5, **29–30% larger** than its q14 estimate. The sampled
density-minimum magnitude increases 15–17%, and the maximum scaled EOS mismatch
increases about 17%; the RMS scaled EOS mismatch changes about 0.21%.
Thus the enthalpy-based global virial diagnostic meets 1e-5 and is stable under
this quadrature comparison, while local density/EOS defects remain unresolved.
This comparison is neither a mesh-convergence study nor a pointwise positivity
certificate. Frozen Armijo acceptance must not be presented as model convergence
or complete physical validation.
## Timing repeats
The first gravity/full case had inconsistent elapsed clocks: 531.747 s UTC
versus 236.576 s Python monotonic, with a 325.024 s C++ auxiliary solve. It is
excluded from performance comparisons. The runner now records child user/system
CPU time, UTC elapsed time and monotonic elapsed time independently. A temporary
idle-sleep guard covers each subsequent campaign; no phase time is rescaled.
| Repeat | Search operation time | Total elapsed / child CPU | Result |
| --- | ---: | ---: | --- |
| Gravity/full | 149.17 s | 227.30 / 226.58 s | Same baseline endpoint; 53 auxiliary iterations |
| Raw/none | 45.76 s | 123.67 / 123.57 s | Same alpha=1/512 endpoint |
| Raw/near | 74.90 s | 153.11 / 153.05 s | Same alpha=1/256, beta=1 endpoint |
UTC and monotonic times differ by at most 0.0053 s in these repeats. Every saved
vector file has the same hash as its original run (68, 55 and 54 files,
respectively), and the independent vector audit passes all three. The raw
progress per operation-second ratio is 1.332 initially and 1.329 in the paired
repeat. These support about 33% greater fractional residual decrease per measured
operation-second at this frozen state, despite an approximately 7% timing shift
between campaigns. The gravity near/none ratio is 3.619 in the
one original pair; it has not received an additional paired timing repeat.
These ratios use fractional residual decrease, not merit decrease. They compare
one fixed state/direction and exclude ordinary outer linear solve, commit
refresh, initialization and physical replay. They are not Newton speedup factors.
The correction also changes the actual displacement, so a factor-two nominal
alpha is not a factor-two displacement claim.
## Matched live Newton follow-up
The paired six-update runs use the same seed, mesh, n=1.5,J=0 specification,
no central constraint, gravity-Schur preconditioner, restart 160 and outer true
defect tolerance 0.03. Only the trial policy changes: `curvature` versus
`curvature-backtracked-coupled`. Initial physical observations and layout hashes
match exactly, as do the first accepted residual, alpha, linear diagnostics and
block residuals. First-step state vectors were not saved, so these are recorded
diagnostic matches rather than a separate bitwise state comparison.
| Accepted update | Control alpha | Control F | New alpha | New F | Cumulative Newton seconds, control / new |
| ---: | ---: | ---: | ---: | ---: | ---: |
| 1 | 1 | 1.614842e-5 | 1 | 1.614842e-5 | 39.25 / 39.11 |
| 2 | 1/16 | 1.579569e-5 | 1/8 | 1.614552e-5 | 173.68 / 194.99 |
| 3 | 1/16 | 1.553144e-5 | 1/4 | 1.595479e-5 | 248.91 / 249.76 |
| 4 | 1/16 | 1.514564e-5 | 1/2 | 1.574591e-5 | 324.13 / 305.06 |
| 5 | 1/32 | 1.499075e-5 | 1/2 | 1.273240e-5 | 419.19 / 431.45 |
| 6 | 1/32 | 1.497197e-5 | 1/8 | 1.245830e-5 | 502.21 / 613.09 |
Both runs complete six accepted updates and report nonconvergence at the
iteration limit (exit 3). The original strict residual tolerance is unchanged.
The combined policy produces 16.79% lower final residual, at 22.08% more
iteration time. Excluding the identical first update, residual decreases from
that common value are 7.285% in 462.95 s (control) and 22.851% in 573.98 s (new),
a 2.530 ratio of residual decrease per second over this fixed interval. This is
a bounded progress-rate comparison, not a speedup to a converged model.
The new method initially lags. First recorded crossings of fixed residual
thresholds illustrate the tradeoff without interpolating between states:
| F threshold | Control Newton seconds | New Newton seconds |
| ---: | ---: | ---: |
| 1.60e-5 | 173.68 | 249.76 |
| 1.55e-5 | 324.13 | 431.45 |
| 1.50e-5 | 419.19 | 431.45 |
| 1.40e-5 | Not reached within six updates | 431.45 |
Total process elapsed/child CPU times are 561.92/561.22 s (control) and
671.98/671.86 s (new). UTC and monotonic elapsed times differ by 0.0014 s and
-0.0318 s, respectively. The total process includes initialization, endpoint
physical diagnostics and artifact output; the iteration totals exclude these.
There is one live timing pair, not a statistical performance distribution.
### What the live corrections actually did
The control uses 354 outer Krylov iterations; the new run uses 341 outer plus
163 auxiliary iterations across three correction solves. The new gate defers
five trials, and auxiliary preparation adds 11 preparations, including one
best-candidate replay. Auxiliary solve/preparation/verification/refresh counters
are 84.35/64.03/0.67/4.77 s; these are nested work already included in iteration
time and must not be added to it. Ten beta candidates are evaluated.
At update 2 (zero-based iteration 1), alpha=1/8 has an eligible excess ratio
0.35155. The unscaled solve passes its true defect gate (relative 0.02570) and
cap (correction/cap=0.63369). Beta=1 worsens the merit; beta=1/2 passes ordinary
acceptance. It reduces residual by only 0.01795%, versus 2.18426% from the
control's next ordinary backtrack at alpha=1/16. Thus an accepted rescue need
not have the best immediate progress. It realizes only 0.1533% of the nominal
affine predicted merit decrease, despite satisfying Armijo by a factor 28.7.
Unlike the successful frozen rescues, this accepted live correction genuinely
uses damping. It changes the following trajectory: updates 3 and 4 accept
ordinary alpha=1/4 and 1/2 trials, and update 5 achieves a large residual drop.
At updates 5 and 6 the auxiliary searches keep their best improvements at
beta=1/8 and 1/4, respectively, but both still fail outer acceptance. The next
ordinary trial is accepted instead. Therefore three `kept` diagnostic events
mean only **one committed coupled rescue**. No later rejected correction is
carried into the accepted state.
A minimum realized-versus-predicted decrease guard is a plausible separate
investigation, not an established improvement: it would reject the initially
weak rescue that precedes this better final trajectory. A beta-aware prediction
F(y)+beta J(y)c would distinguish a damped candidate from the full target L
without another FEM action. Any guard must control final candidate admission
and fallback, not just beta-loop stopping. Comparing with one ordinary further
backtrack is another useful control, at one extra preparation cost. Neither
change is implemented or claimed beneficial here.
### Live endpoint physics
| q18 observation | Control | New |
| --- | ---: | ---: |
| Enthalpy-pressure energy virial error | 4.22590e-6 | 3.78554e-6 |
| Enthalpy-pressure force virial error | 2.71367e-6 | 2.31894e-6 |
| Absolute mass error | 3.41685e-6 | 2.53899e-6 |
| Absolute J error | 0 | 0 |
| Minimum sampled mapping determinant | 0.989275 | 0.989068 |
| Minimum density | -7.73185e-4 | -7.85522e-4 |
| Negative-density mass fraction | 2.89683e-5 | 2.88557e-5 |
| Scaled RMS EOS density mismatch | 1.27948e-4 | 1.27721e-4 |
| Scaled maximum EOS density mismatch | 9.63952e-4 | 9.73858e-4 |
Both endpoints meet the primary enthalpy-based virial milestone at q14 and q18;
the quadrature change is about 1.83–1.84e-9. Density-derived pressure and its
virials remain undefined because of negative samples. The new endpoint improves
the global measures while its sampled density minimum and maximum EOS defect
are slightly worse. No positivity, mesh convergence, or complete local-physics
claim follows from the virial result. The separate conservative solver/defined-
virial checker returns its expected nonpassing result for both endpoints.
### Implication and next experiment
Keep the new policy opt-in. The implementation and its cost bounds are tested;
the combined policy now has evidence of beneficial nonlinear trajectory changes
as well as frozen-step improvement. It has not yet demonstrated sustained
convergence or any higher-n/rotating result. The next useful test is a longer
matched n=1.5 trajectory from the same seed, preserving the physical diagnostics
and work accounting, before expanding to higher n and J. Do not promote the
policy or tune a new quality threshold solely from the early marginal rescue.
## Validation
Production and experiment targets build successfully with the configured release
Clang/C++23 module build. Existing duplicate-library linker warnings remain.
- Core correction tests: 23 serial cases / 1,129 assertions; the same 23 cases
on two MPI ranks pass 1,134 assertions per rank.
- Broader Newton, numerical-strategy, gravity and FGMRES regression tests:
35 cases / 357,016 assertions. Together the serial groups cover 58 cases.
- Python artifact-checker tests: 25 frozen-search cases, 27 prior cases and
4 live-checker regressions, all 56 passing.
- Actual frozen campaigns: nine original cases and three repeats, all passing
the independent recorded-vector/provenance audit.
- Matched live pair: 1,083 scalar-decision/provenance checks pass. These check
recorded true-defect observations rather than independently reapplying FEM.
Coverage includes smaller-beta acceptance after full-step rejection, preparation
failure followed by success, first-accepted stopping, best-candidate replay,
complete rejection, later-candidate/replay exceptions, restoration, near-gate
budget preservation, zero predicted decrease, already accepted originals, invalid
and rank-mismatched options, rank-local rejection, and typed recipe composition.
Real-FEM production tests cover the original and new policies on raw and gravity
paths, preservation of the outer linear report, and checkpoint restoration.
The checker reconstructs the affine prediction, auxiliary RHS, unscaled full-J
defect and cap, beta state updates and remainder identity from saved vectors.
It verifies shared budget use and first-accept stopping, checks recorded fresh
selected-state replay and restoration evidence, and checks accepted-state and
normalization invariants and input/output hashes. It does
not independently rerun FEM or physical quadrature.
## Evidence and reproduction
Evidence root (outside the source tree):
`/Users/tboudreaux/.codex/.chatgpt-projects/g-p-6a6b3f2194ec8191bebcd59075897f1d/diagnostics/damped_coupled_2026-09-18`
The original frozen screen binds to source `25816d4`; the repeats and matched
Newton cases bind to `1876229`. Their diagnostic binary hashes are recorded in
each campaign's `provenance.json`. The repeats use the same binary hash as the
screen. Test provenance and logs are in the evidence root. Tests were compiled
at `422c46b`; the subsequent source change `25816d4` only hardened the frozen
experiment's finite-metric guards.
Versioned cases:
- `damped_coupled_screen_2026-09-18.json`
- `damped_coupled_repeat_2026-09-18.json`
- `damped_coupled_newton_2026-09-18.json`
Use `run_plateau_campaign.py` with a clean committed source, its configured build,
`--mesh`, `--cases`, and a fresh `--output`. The build copies an immutable
executable before any case runs. Case manifests currently name the retained
absolute snapshot paths; relocate those explicitly when reproducing elsewhere.
After timed cases finish, run:
```sh
python3 experiments/check_damped_coupled_trial.py EVIDENCE/screen_v1 --output EVIDENCE/screen_v1_audit.json
python3 experiments/check_damped_coupled_trial.py EVIDENCE/repeat_v1 --output EVIDENCE/repeat_v1_audit.json
python3 experiments/check_damped_coupled_newton.py EVIDENCE/newton_v1 --output EVIDENCE/newton_v1_review.json
```
`TIMING_NOTE.md`, `repeat_comparison.json`, the per-case run records and all raw
CSVs/vectors preserve the timing exclusion and reproducibility evidence.

View File

@@ -0,0 +1,382 @@
#!/usr/bin/env python3
"""Audit the paired six-step live experiment without loading state vectors.
Hashes bind the producer's observations. Scalar identities check those recorded
observations; they do not supply a fresh FEM action, a pointwise certificate, or
an unavailable corrected displacement. Nonconvergence and physical defects are
reported separately from artifact/decision consistency.
"""
import argparse
import csv
import datetime
import hashlib
import json
import math
from pathlib import Path
import sys
EPS = sys.float_info.epsilon
ARMIJO = 1.e-4 # The live driver's default; this is not a configurable CLI flag.
COUNTERS = ('attempts', 'kept', 'fallbacks', 'iterations', 'preparations', 'restorations',
'solve_seconds', 'verification_seconds', 'preparation_seconds', 'refresh_seconds')
REQUIRED = {'metadata.txt', 'layout.csv', 'initial_physical.csv', 'final_physical.csv',
'newton.csv', 'blocks.csv', 'trials.csv', 'coupled_trials.csv', 'coupled_search.csv',
'coupled_candidates.csv', 'trial_state_consistency.csv', 'numerical_strategies.csv',
'accepted_state.txt'}
def digest(path):
result = hashlib.sha256()
with path.open('rb') as stream:
for chunk in iter(lambda: stream.read(1024 * 1024), b''):
result.update(chunk)
return result.hexdigest()
def rows(path):
with path.open() as stream:
result = list(csv.DictReader(stream))
if any(None in row or any(value is None for value in row.values()) for row in result):
raise ValueError(f'Incomplete CSV row: {path}')
return result
def close(a, b, scale=0.):
return math.isfinite(a) and math.isfinite(b) and abs(a-b) <= 1.e-10*max(abs(a), abs(b)) + 256*EPS*abs(scale)
def safe(value):
if isinstance(value, float) and not math.isfinite(value):
return None
if isinstance(value, dict):
return {key: safe(item) for key, item in value.items()}
if isinstance(value, (list, tuple)):
return [safe(item) for item in value]
return value
def key(row):
return int(row['iteration']), int(row['trial'])
def indexed(records):
result = {key(row): row for row in records}
if len(result) != len(records):
raise ValueError('Duplicate iteration/trial observations')
return result
def physical(path, metadata, check):
data = rows(path)
values = {(int(row['quadrature']), row['metric']): float(row['value']) for row in data}
check(path.name+':unique_metrics', len(values) == len(data))
check(path.name+':quadratures', {q for q, _ in values} == {14, 18})
result = {}
for q in (14, 18):
m = {metric: value for (order, metric), value in values.items() if order == q}
label = f'{path.name}:{q}:'
check(label+'model', m['polytropic_index'] == float(metadata['n']) and m['polytropic_constant'] == float(metadata['K']))
complete = m['complete_volume_measurement'] == 1
if complete:
check(label+'mass_error', close(m['mass_absolute_error'], abs(m['mass']-float(metadata['M'])), abs(m['mass'])))
check(label+'angular_momentum_error', close(m['angular_momentum_absolute_error'], abs(m['angular_momentum_z']-float(metadata['J'])), 1.))
virials = {}
for pressure_prefix, defined_key, pressure_key in (
('', 'pressure_from_density_defined_everywhere', 'pressure_integral'),
('enthalpy_', 'pressure_from_enthalpy_defined_everywhere', 'enthalpy_pressure_integral')):
defined = m[defined_key] == 1 and complete and math.isfinite(m['binding_energy']) and m['binding_energy'] < 0
for force, binding_key in (('', 'binding_energy'), ('force_', 'force_binding_energy')):
name = pressure_prefix+force+'virial_'
signed, error = m[name+'signed'], m[name+'error']
if defined:
numerator = 2*m['kinetic_energy']+m[binding_key]+3*m[pressure_key]
scale = (2*abs(m['kinetic_energy'])+abs(m[binding_key])+3*abs(m[pressure_key]))/abs(m['binding_energy'])
check(label+name+'identity', close(signed, numerator/abs(m['binding_energy']), scale) and close(error, abs(signed), scale))
else:
check(label+name+'undefined', math.isnan(signed) and math.isnan(error))
virials[name+'within_1e5'] = error <= 1.e-5 if defined and math.isfinite(error) else None
result[str(q)] = {'metrics': m, 'virial_milestones': virials,
'pressure_from_density_defined': m['pressure_from_density_defined_everywhere'] == 1,
'sampled_negative_density': m['negative_density_samples'] > 0,
'sampled_negative_enthalpy': m['negative_enthalpy_samples'] > 0,
'global_enthalpy_certificate': 'not_measured'}
return result
def inspect_case(folder, record, check):
metadata = dict(line.split('=', 1) for line in (folder/'metadata.txt').read_text().splitlines() if '=' in line)
history, trials = rows(folder/'newton.csv'), rows(folder/'trials.csv')
coupled, searches = indexed(rows(folder/'coupled_trials.csv')), indexed(rows(folder/'coupled_search.csv'))
consistency = indexed(rows(folder/'trial_state_consistency.csv'))
trial_map = indexed(trials)
check('trial_joins', set(trial_map) == set(coupled) == set(searches) == set(consistency))
candidates = {}
for row in rows(folder/'coupled_candidates.csv'):
candidates.setdefault(key(row), []).append(row)
check('candidate_trial_coverage', set(candidates) <= set(trial_map))
strategy_rows = rows(folder/'numerical_strategies.csv')
strategy = {row['metric']: float(row['value']) for row in strategy_rows}
check('unique_strategy_summary', len(strategy) == len(strategy_rows))
initial = physical(folder/'initial_physical.csv', metadata, check)
endpoint = physical(folder/'final_physical.csv', metadata, check)
before = initial['18']['metrics']['normalized_bordered_residual']
initial_residual = before
check('six_step_control', metadata['max_newton'] == '6' and float(metadata['linear_tol']) == .03)
check('single_auxiliary_budget', metadata['coupled_attempts_per_iteration'] == '1')
check('outer_iteration_sequence', [int(row['iteration']) for row in history] == list(range(len(history))) and len(history) <= 6)
block_data = rows(folder/'blocks.csv')
residual_blocks = {row['block'] for row in rows(folder/'layout.csv') if row['kind'] == 'residual'}
blocks = {(int(row['iteration']), row['stage'], row['block']): float(row['normalized_norm']) for row in block_data}
check('unique_block_observations', len(blocks) == len(block_data))
expected_blocks = {(int(row['iteration']), stage, block) for row in history for stage in ('before', 'after') for block in residual_blocks}
check('block_coverage', set(blocks) == expected_blocks)
previous = dict.fromkeys(COUNTERS, 0.)
previous_deferred = 0.
accepted_count = 0
cumulative_seconds = 0.
cumulative_linear_iterations = 0
step_results = []
used_trials = set()
for row in history:
iteration = int(row['iteration'])
iteration_trials = [trial for trial in trials if int(trial['iteration']) == iteration]
check(f'{iteration}:trial_sequence', [int(trial['trial']) for trial in iteration_trials] == list(range(len(iteration_trials))))
check(f'{iteration}:trial_count', int(row['trials']) == len(iteration_trials))
for stage, value in (('before', before), ('after', float(row['F']))):
block_norm = math.hypot(*(blocks[(iteration, stage, block)] for block in residual_blocks))
check(f'{iteration}:{stage}:block_norm', close(block_norm, value))
check(f'{iteration}:linear_rhs_matches_base', close(float(row['linear_rhs_norm']), before))
outer_gate = math.isfinite(float(row['linear_true'])) and float(row['linear_true']) <= .03
step_accepted = row['accepted'] == '1'
check(f'{iteration}:accepted_boolean', row['accepted'] in ('0', '1'))
accepted_trials = [trial for trial in iteration_trials if trial['disposition'] == '0']
check(f'{iteration}:accepted_trial_count', len(accepted_trials) == int(step_accepted))
if step_accepted:
accepted_count += 1
check(f'{iteration}:accepted_linear_gate', row['linear_status'] == '0' and outer_gate)
check(f'{iteration}:strict_residual_decrease', 0 <= float(row['F']) < before)
check(f'{iteration}:accepted_trial_binding', accepted_trials[0] == iteration_trials[-1] and
close(float(accepted_trials[0]['F']), float(row['F'])) and accepted_trials[0]['alpha'] == row['alpha'])
else:
check(f'{iteration}:failed_state_unchanged', close(float(row['F']), before))
increments = dict.fromkeys(COUNTERS, 0.)
deferred_increment = 0.
observations = []
for trial in iteration_trials:
address = key(trial)
used_trials.add(address)
label = f'{iteration}:{address[1]}:'
c, search, roundtrip = coupled[address], searches[address], consistency[address]
alpha = float(trial['alpha'])
check(label+'alpha_binding', 0 < alpha <= 1 and float(c['alpha']) == alpha and float(search['alpha']) == alpha)
check(label+'diagnostics_available', c['diagnostics_available'] == '1' and c['status'] == search['status'])
check(label+'observer_roundtrip', math.isfinite(float(roundtrip['relative_error'])) and
0 <= float(roundtrip['relative_error']) <= 64*EPS and math.isfinite(float(roundtrip['normalized_physical_roundtrip_error'])) and
float(roundtrip['normalized_physical_roundtrip_error']) >= 0)
change = {}
for counter in COUNTERS:
current = float(c[counter+'_total'])
check(label+counter+':monotone', math.isfinite(current) and current >= previous[counter])
change[counter] = current-previous[counter]
increments[counter] += change[counter]
previous[counter] = current
deferred = float(search['deferred_total'])
check(label+'deferred_monotone', math.isfinite(deferred) and deferred >= previous_deferred)
deferred_change = deferred-previous_deferred
deferred_increment += deferred_change
previous_deferred = deferred
has_solve = c['has_solve'] == '1'
status = int(c['status'])
check(label+'known_status', 0 <= status <= 9 and c['has_solve'] in ('0', '1'))
check(label+'attempt_accounting', change['attempts'] == int(has_solve) and change['iterations'] == int(c['linear_iterations']))
check(label+'selection_accounting', change['kept'] == int(status == 3) and
change['fallbacks'] == int(has_solve and status != 3) and deferred_change == int(status == 9))
proposals = candidates.get(address, [])
check(label+'candidate_count', len(proposals) == int(search['candidate_count']) and len(proposals) <= int(metadata['coupled_beta_steps']))
check(label+'candidate_sequence', [int(item['candidate']) for item in proposals] == list(range(len(proposals))))
merit_limit = (1-ARMIJO*alpha)*.5*before*before
if trial['disposition'] == '0':
check(label+'ordinary_merit_acceptance', math.isfinite(float(trial['F'])) and .5*float(trial['F'])**2 <= merit_limit)
for index, proposal in enumerate(proposals):
beta, merit = float(proposal['beta']), float(proposal['merit'])
check(label+f'beta{index}:grid', close(beta, float(metadata['coupled_beta_reduction'])**index))
check(label+f'beta{index}:status', proposal['status'] in ('0', '1', '2') and proposal['accepted'] in ('0', '1'))
valid = proposal['status'] == '0'
check(label+f'beta{index}:metric', math.isfinite(merit) and merit >= 0 if valid else math.isnan(merit))
accepted = valid and merit < float(c['original_merit']) and merit <= merit_limit
check(label+f'beta{index}:ordinary_acceptance', (proposal['accepted'] == '1') == accepted)
if accepted:
check(label+'first_accepted_beta_stops_search', index == len(proposals)-1)
check(label+f'beta{index}:time', math.isfinite(float(proposal['preparation_seconds'])) and float(proposal['preparation_seconds']) >= 0)
if has_solve:
rhs, threshold, verified = float(c['rhs_norm']), float(c['threshold']), float(c['verified_true_norm'])
check(label+'auxiliary_work_bound', 0 <= int(c['linear_iterations']) <= int(metadata['coupled_iterations']))
if math.isfinite(rhs) and math.isfinite(threshold):
check(label+'auxiliary_threshold', close(threshold, max(1.e-14, float(metadata['coupled_tolerance'])*rhs)))
if math.isfinite(verified) and math.isfinite(threshold):
check(label+'auxiliary_true_gate', (c['verified_gate'] == '1') == (verified <= threshold))
if status == 3:
selected_beta = float(search['selected_beta'])
chosen = [item for item in proposals if float(item['beta']) == selected_beta]
check(label+'retained_auxiliary_certificate', has_solve and c['linear_status'] == '0' and c['verified_gate'] == '1' and
math.isfinite(float(c['verified_true_norm'])) and float(c['verified_true_norm']) <= float(c['threshold']) and
0 <= float(c['correction_norm']) <= float(c['maximum_correction_norm']))
check(label+'retained_beta_observed', len(chosen) == 1 and chosen[0]['status'] == '0')
if len(chosen) == 1:
check(label+'retained_metric_replay', close(float(chosen[0]['merit']), float(c['corrected_merit'])))
check(label+'retained_merit_improves', 0 <= float(c['corrected_merit']) < float(c['original_merit']))
check(label+'retained_residual_binding', close(.5*float(trial['F'])**2, float(c['corrected_merit'])))
else:
check(label+'no_stale_selection', math.isnan(float(search['selected_beta'])))
if not has_solve:
check(label+'no_stale_auxiliary_report', c['linear_status'] == '-1' and c['verified_gate'] == '0' and not proposals)
if status == 9 or has_solve:
if metadata['strategy'] == 'curvature-backtracked-coupled':
base_merit, predicted = float(search['base_merit']), float(search['predicted_merit'])
drop = float(search['predicted_decrease'])
check(label+'prediction_base_binding', close(base_merit, .5*before*before))
check(label+'predicted_decrease_identity', close(drop, base_merit-predicted, base_merit))
if drop > 0:
ratio = (float(c['original_merit'])-base_merit)/drop
check(label+'excess_ratio_identity', close(float(search['excess_ratio']), ratio, 1.))
check(label+'near_gate_decision', (ratio <= float(metadata['coupled_excess_ratio'])) == has_solve)
else:
check(label+'nonpositive_prediction_defers', status == 9 and not has_solve)
observations.append({'trial': address[1], 'alpha': alpha, 'disposition': int(trial['disposition']),
'rejection_source': trial['source'], 'F': float(trial['F']), 'coupled_status': status,
'selected_beta': float(search['selected_beta']), 'auxiliary_changes': change,
'deferrals': deferred_change, 'candidates': proposals})
check(f'{iteration}:auxiliary_attempt_budget', increments['attempts'] <= 1)
for field in ('seconds', 'linear_seconds', 'prepare_seconds', 'refresh_seconds'):
check(f'{iteration}:{field}', math.isfinite(float(row[field])) and float(row[field]) >= 0)
cumulative_seconds += float(row['seconds'])
cumulative_linear_iterations += int(row['linear_iterations'])
step_results.append({'iteration': iteration, 'accepted': step_accepted, 'F_before': before, 'F': float(row['F']),
'alpha': float(row['alpha']), 'outer_true_relative': float(row['linear_true']), 'outer_true_gate': outer_gate,
'outer_linear_iterations': int(row['linear_iterations']), 'iteration_seconds': float(row['seconds']),
'cumulative_iteration_seconds': cumulative_seconds, 'cumulative_outer_linear_iterations': cumulative_linear_iterations,
'linear_seconds': float(row['linear_seconds']), 'reported_trial_preparation_seconds': float(row['prepare_seconds']),
'outer_refresh_seconds': float(row['refresh_seconds']), 'auxiliary_changes': increments,
'auxiliary_deferrals': deferred_increment, 'trials': observations})
before = float(row['F'])
check('all_trials_accounted', used_trials == set(trial_map))
check('accepted_steps_metadata', accepted_count == int(metadata['accepted_steps']))
converged = metadata['converged'] == '1'
check('termination_record', metadata['converged'] in ('0', '1') and record['exit_code'] == (0 if converged else 3) and
(converged or bool(metadata.get('failure'))))
check('bounded_outcome', accepted_count == 6 or converged or bool(metadata.get('failure')))
for counter, total in previous.items():
check('final_auxiliary_'+counter, close(strategy['coupled_'+counter], total))
check('final_deferred_count', strategy['coupled_deferred_attempts'] == previous_deferred)
for q in ('14', '18'):
check('endpoint_residual_'+q, close(endpoint[q]['metrics']['normalized_bordered_residual'], before))
return {'metadata': metadata, 'converged': converged, 'failure': metadata.get('failure'), 'accepted_steps': accepted_count,
'initial_residual': initial_residual, 'final_residual': before, 'steps': step_results,
'summed_iteration_seconds': math.fsum(float(row['seconds']) for row in history),
'summed_outer_linear_iterations': sum(int(row['linear_iterations']) for row in history),
'auxiliary_totals': previous, 'auxiliary_deferrals_total': previous_deferred,
'initial_physical': initial, 'endpoint_physical': endpoint,
'actual_corrected_displacement': 'not_logged; alpha and beta do not determine its norm',
'first_accepted_record': next((row for row in history if row['accepted'] == '1'), None),
'first_after_blocks': [row for row in block_data if row['iteration'] == '0' and row['stage'] == 'after']}
def audit(campaign):
campaign = campaign.resolve()
checks, cases = [], {}
def check(name, value):
checks.append({'check': name, 'passed': bool(value)})
try:
provenance = json.loads((campaign/'provenance.json').read_text())
specifications = provenance['cases']
check('paired_manifest', len(specifications) == 2 and len({case['id'] for case in specifications}) == 2 and
specifications == json.loads((campaign/'cases.json').read_text()))
check('immutable_executable', digest(campaign/'plateau_diagnostic') == provenance['binary_sha256'])
check('mesh_identity', digest(Path(provenance['mesh'])) == provenance['mesh_sha256'])
for specification in specifications:
name, arguments = specification['id'], specification['arguments']
folder = campaign/name
record_path = campaign/(name+'_run.json')
record = json.loads(record_path.read_text())
case_check = lambda label, value: check(name+':'+label, value)
case_check('completed_observation', bool(record.get('finished_utc')) and not record.get('timed_out', False) and record.get('exit_code') in (0, 3))
case_check('command_identity', record['id'] == name and record['command'] ==
[str(campaign/'plateau_diagnostic'), *arguments, '--mesh', provenance['mesh'], '--output', str(folder)])
for path, expected in record['input_state_hashes'].items():
case_check('input_hash:'+path, digest(Path(path)) == expected)
outputs = record['output_hashes']
case_check('required_hashes', REQUIRED <= outputs.keys())
observed_files = {str(path.relative_to(folder)) for path in folder.rglob('*') if path.is_file() and path.suffix in ('.txt', '.csv', '.json')}
case_check('complete_hash_manifest', observed_files == set(outputs))
for path, expected in outputs.items():
case_check('output_hash:'+path, digest(folder/path) == expected)
result = inspect_case(folder, record, case_check)
metadata = result['metadata']
for option, field in {'--index': 'n', '--angular-momentum': 'J', '--max-newton': 'max_newton', '--restart': 'restart',
'--max-linear': 'max_linear', '--linear-tol': 'linear_tol', '--rtol': 'rtol', '--atol': 'atol',
'--schur-sweeps': 'schur_correction_sweeps', '--schur-relaxation': 'schur_relaxation'}.items():
if option in arguments:
case_check('requested_'+field, float(metadata[field]) == float(arguments[arguments.index(option)+1]))
for option, field in (('--strategy', 'strategy'), ('--preconditioner-variant', 'preconditioner_variant')):
case_check('requested_'+field, metadata[field] == arguments[arguments.index(option)+1])
case_check('no_central', '--no-central' in arguments and metadata['central_phase'] == '0')
elapsed = (datetime.datetime.fromisoformat(record['finished_utc'])-datetime.datetime.fromisoformat(record['started_utc'])).total_seconds()
case_check('utc_elapsed_identity', close(record['utc_elapsed_seconds'], elapsed))
case_check('clock_difference_identity', close(record['utc_minus_monotonic_seconds'], elapsed-record['seconds'], elapsed))
case_check('child_cpu_identity', close(record['child_cpu_seconds'], record['child_user_cpu_seconds']+record['child_system_cpu_seconds']))
for field in ('seconds', 'utc_elapsed_seconds', 'child_user_cpu_seconds', 'child_system_cpu_seconds', 'child_cpu_seconds'):
case_check('timing_'+field, math.isfinite(record[field]) and record[field] >= 0)
result['timing'] = {field: record[field] for field in ('started_utc', 'finished_utc', 'seconds', 'utc_elapsed_seconds',
'utc_minus_monotonic_seconds', 'child_user_cpu_seconds', 'child_system_cpu_seconds', 'child_cpu_seconds')}
result['timing']['clock_disagreement_observed'] = abs(record['utc_minus_monotonic_seconds']) > max(.5, .01*record['seconds'])
result['run_record_sha256'] = digest(record_path)
result['output_hashes'] = outputs
cases[name] = result
first, second = specifications
def without_strategy(arguments):
values = list(arguments)
position = values.index('--strategy')
del values[position:position+2]
return values
check('pair:only_strategy_differs', without_strategy(first['arguments']) == without_strategy(second['arguments']))
a, b = cases[first['id']], cases[second['id']]
check('pair:strategies', {a['metadata']['strategy'], b['metadata']['strategy']} == {'curvature', 'curvature-backtracked-coupled'})
excluded = {'strategy', 'converged', 'accepted_steps', 'failure'}
check('pair:matching_metadata', {k: v for k, v in a['metadata'].items() if k not in excluded} ==
{k: v for k, v in b['metadata'].items() if k not in excluded})
for path in ('initial_physical.csv', 'layout.csv'):
check('pair:exact_'+path, a['output_hashes'][path] == b['output_hashes'][path])
both_first = a['first_accepted_record'] is not None and b['first_accepted_record'] is not None
if both_first:
fields = ('iteration', 'F', 'alpha', 'trials', 'linear_status', 'linear_iterations', 'linear_true',
'central_border', 'unbordered_F', 'linear_initial_residual', 'linear_rhs_norm', 'linear_initial_over_rhs')
check('pair:first_accepted_record_exact', all(a['first_accepted_record'][k] == b['first_accepted_record'][k] for k in fields))
check('pair:first_accepted_blocks_exact', a['first_after_blocks'] == b['first_after_blocks'])
passed = all(item['passed'] for item in checks)
return safe({'passed': passed, 'observations_bound_to_valid_artifacts': passed, 'checks': checks, 'cases': cases,
'source_commit': provenance['commit'], 'provenance_sha256': digest(campaign/'provenance.json'),
'analysis_script_sha256': digest(Path(__file__)), 'both_first_accepted_steps_observed': both_first,
'scope': 'Recorded decision/scalar-identity audit, with streaming artifact hashes. Outer and auxiliary true defects are producer observations, not recomputed FEM actions. Matching first-step diagnostics are not bitwise state equality. Numerical and physical outcomes are separate; no speedup-to-convergence claim. Auxiliary phase counters are differenced, not summed repeatedly; nested phase timings must not be added to total iteration time.'})
except (OSError, ValueError, KeyError, TypeError, IndexError, OverflowError) as error:
check('complete_readable_campaign', False)
return safe({'passed': False, 'checks': checks, 'error': str(error), 'cases': cases,
'interpretation_valid': False})
def main():
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument('campaign', type=Path)
parser.add_argument('--output', type=Path, required=True)
args = parser.parse_args()
if args.output.resolve() == args.campaign.resolve() or args.campaign.resolve() in args.output.resolve().parents:
parser.error('Write analysis outside the immutable campaign.')
result = audit(args.campaign)
args.output.write_text(json.dumps(result, indent=2, allow_nan=False)+'\n')
print('PASS' if result['passed'] else 'FAIL', args.output)
for name, case in result['cases'].items():
print(name, 'accepted=', case['accepted_steps'], 'converged=', case['converged'], 'final F=', case['final_residual'])
return 0 if result['passed'] else 1
if __name__ == '__main__':
raise SystemExit(main())

View File

@@ -0,0 +1,165 @@
#!/usr/bin/env python3
"""Tiny recorded-trial examples distinguish artifact validity from success.
These fixtures exercise live scalar joins and counter accounting, without
pretending to contain FEM states or independently computed Jacobian actions.
"""
import csv
import math
from pathlib import Path
import tempfile
import unittest
import check_damped_coupled_newton as audit
def write_rows(path, records):
with path.open('w', newline='') as stream:
writer = csv.DictWriter(stream, fieldnames=list(records[0]))
writer.writeheader()
writer.writerows(records)
class LiveCoupledCheckerTests(unittest.TestCase):
def setUp(self):
self.temporary = tempfile.TemporaryDirectory()
self.addCleanup(self.temporary.cleanup)
self.folder = Path(self.temporary.name)
def fixture(self, accepted=True, false_gate=False):
"""One bounded iteration with two beta observations and one solve."""
final = .9 if accepted else 1.
trial_f = .9 if accepted else 1.05
corrected_merit = .5*trial_f**2
metadata = {
'n': '1.5', 'K': '1', 'M': '1', 'J': '0', 'max_newton': '6', 'linear_tol': '.03',
'coupled_attempts_per_iteration': '1', 'coupled_beta_steps': '2', 'coupled_beta_reduction': '.5',
'coupled_iterations': '160', 'coupled_tolerance': '.03', 'coupled_excess_ratio': '1',
'strategy': 'curvature-backtracked-coupled', 'accepted_steps': str(int(accepted)),
'converged': '0', 'failure': 'Explicit synthetic bounded failure.'}
(self.folder/'metadata.txt').write_text('\n'.join(f'{k}={v}' for k, v in metadata.items()))
self.write('newton', [dict(iteration=0, accepted=int(accepted), F=final, alpha=1 if accepted else 0,
trials=1, linear_status=0, linear_iterations=2, linear_true=.02, seconds=2, linear_seconds=.5,
prepare_seconds=.8, refresh_seconds=.1, linear_rhs_norm=1)])
self.write('layout', [dict(kind='residual', block='balance', offset=0, size=1, scale=1)])
self.write('blocks', [dict(iteration=0, stage=stage, block='balance', normalized_norm=value)
for stage, value in [('before', 1), ('after', final)]])
self.write('trials', [dict(iteration=0, trial=0, alpha=1, disposition=0 if accepted else 4,
source='' if accepted else 'residual metric', F=trial_f)])
totals = dict(attempts=1, kept=1, fallbacks=0, iterations=2, preparations=2, restorations=0,
solve_seconds=.2, verification_seconds=.01, preparation_seconds=.5, refresh_seconds=.05)
self.write('coupled_trials', [dict(iteration=0, trial=0, alpha=1, diagnostics_available=1, status=3,
**{key+'_total': value for key, value in totals.items()}, original_merit=.6,
corrected_merit=corrected_merit, has_solve=1, linear_status=0, linear_iterations=2, rhs_norm=.5,
reported_true_norm=.01, verified_true_norm=.01, threshold=.015, verified_gate=0 if false_gate else 1,
correction_norm=.8, maximum_correction_norm=1)])
self.write('coupled_search', [dict(iteration=0, trial=0, alpha=1, status=3, deferred_total=0,
selected_beta=.5, candidate_count=2, base_merit=.5, predicted_merit=.25,
predicted_decrease=.25, excess_ratio=.4)])
self.write('coupled_candidates', [dict(iteration=0, trial=0, candidate=i, beta=beta, status=0,
merit=merit, accepted=int(passed), preparation_seconds=.2)
for i, beta, merit, passed in [(0, 1, .8, False), (1, .5, corrected_merit, accepted)]])
self.write('trial_state_consistency', [dict(iteration=0, trial=0,
normalized_physical_roundtrip_error=0, relative_error=0)])
self.write('numerical_strategies', [dict(metric='coupled_'+key, value=value)
for key, value in {**totals, 'deferred_attempts': 0}.items()])
for stage, residual in [('initial', 1.), ('final', final)]:
measurements = []
for quadrature in (14, 18):
metrics = dict(polytropic_index=1.5, polytropic_constant=1, complete_volume_measurement=1,
mass_absolute_error=0, mass=1, angular_momentum_z=0, angular_momentum_absolute_error=0,
binding_energy=-3, force_binding_energy=-3, kinetic_energy=0, pressure_integral=math.nan,
enthalpy_pressure_integral=1, pressure_from_density_defined_everywhere=0,
pressure_from_enthalpy_defined_everywhere=1, negative_density_samples=1,
negative_enthalpy_samples=0, normalized_bordered_residual=residual)
for pressure in ('', 'enthalpy_'):
for force in ('', 'force_'):
metrics[pressure+force+'virial_signed'] = 0 if pressure else math.nan
metrics[pressure+force+'virial_error'] = 0 if pressure else math.nan
measurements.extend(dict(quadrature=quadrature, metric=key, value=value)
for key, value in metrics.items())
self.write(stage+'_physical', measurements)
def write(self, name, records):
write_rows(self.folder/(name+'.csv'), records)
def inspect(self):
checks = []
result = audit.inspect_case(self.folder, {'exit_code': 3},
lambda name, value: checks.append((name, bool(value))))
return result, [name for name, passed in checks if not passed]
def test_two_beta_observations_still_describe_one_auxiliary_solve(self):
self.fixture()
result, failed = self.inspect()
self.assertFalse(failed, failed)
self.assertEqual(result['accepted_steps'], 1)
self.assertEqual(result['auxiliary_totals']['attempts'], 1)
self.assertEqual(result['auxiliary_totals']['iterations'], 2)
self.assertEqual(result['auxiliary_totals']['preparations'], 2)
self.assertEqual(result['steps'][0]['cumulative_iteration_seconds'], 2)
self.assertFalse(result['converged'])
def test_two_iterations_difference_cumulative_counters_instead_of_summing_them(self):
self.fixture()
for name in ('newton', 'trials', 'coupled_trials', 'coupled_search', 'coupled_candidates', 'trial_state_consistency', 'blocks'):
records = audit.rows(self.folder/(name+'.csv'))
second = [{**row, 'iteration': 1} for row in records]
for row in second:
if name in ('newton', 'trials'):
row['F'] = .81
if name == 'newton':
row['linear_rhs_norm'] = .9
if name == 'blocks':
row['normalized_norm'] = .9 if row['stage'] == 'before' else .81
if name == 'coupled_trials':
for counter in audit.COUNTERS:
row[counter+'_total'] = 2*float(row[counter+'_total'])
row['original_merit'] = .6*.81
row['corrected_merit'] = .5*.81**2
if name == 'coupled_search':
for metric in ('base_merit', 'predicted_merit', 'predicted_decrease'):
row[metric] = .81*float(row[metric])
if name == 'coupled_candidates':
row['merit'] = float(row['merit'])*.81
self.write(name, records+second)
summary = audit.rows(self.folder/'numerical_strategies.csv')
for row in summary:
row['value'] = 2*float(row['value'])
self.write('numerical_strategies', summary)
final = audit.rows(self.folder/'final_physical.csv')
for row in final:
if row['metric'] == 'normalized_bordered_residual':
row['value'] = .81
self.write('final_physical', final)
metadata = self.folder/'metadata.txt'
metadata.write_text(metadata.read_text().replace('accepted_steps=1', 'accepted_steps=2'))
result, failed = self.inspect()
self.assertFalse(failed, failed)
self.assertEqual(result['auxiliary_totals']['attempts'], 2)
self.assertEqual(result['auxiliary_totals']['iterations'], 4)
self.assertEqual(result['steps'][1]['auxiliary_changes']['attempts'], 1)
self.assertEqual(result['steps'][1]['cumulative_iteration_seconds'], 4)
def test_retained_but_outer_rejected_correction_is_honest_failure_evidence(self):
self.fixture(accepted=False)
result, failed = self.inspect()
self.assertFalse(failed, failed)
self.assertEqual(result['accepted_steps'], 0)
self.assertEqual(result['auxiliary_totals']['kept'], 1)
self.assertFalse(result['converged'])
physical = result['endpoint_physical']['18']
self.assertFalse(physical['pressure_from_density_defined'])
self.assertIsNone(physical['virial_milestones']['virial_within_1e5'])
self.assertTrue(physical['virial_milestones']['enthalpy_virial_within_1e5'])
self.assertEqual(physical['global_enthalpy_certificate'], 'not_measured')
def test_false_retained_auxiliary_gate_is_rejected(self):
self.fixture(false_gate=True)
_, failed = self.inspect()
self.assertIn('0:0:retained_auxiliary_certificate', failed)
self.assertIn('0:0:auxiliary_true_gate', failed)
if __name__ == '__main__':
unittest.main()