Record audited damping and eligibility results through six Newton updates
This commit is contained in:
@@ -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.
|
||||
|
||||
382
experiments/check_damped_coupled_newton.py
Normal file
382
experiments/check_damped_coupled_newton.py
Normal 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())
|
||||
165
experiments/test_damped_coupled_newton_checker.py
Normal file
165
experiments/test_damped_coupled_newton_checker.py
Normal 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()
|
||||
Reference in New Issue
Block a user