Audit retained convergence evidence and distinguish compatible physical families
This commit is contained in:
@@ -395,6 +395,7 @@ add_executable(tests
|
||||
tests/solver/gravity_reduction.cpp
|
||||
tests/solver/coupled_trial_correction.cpp
|
||||
tests/solver/stellar_numerical_strategies.cpp
|
||||
tests/solver/continuation_snapshot.cpp
|
||||
)
|
||||
|
||||
target_link_libraries(tests PRIVATE mean_field test_mod Catch2::Catch2 Boost::boost)
|
||||
|
||||
@@ -113,5 +113,15 @@ The exact continuum statement must not be applied uncritically to the
|
||||
present discrete equations: independent rho and P(h), weak EOS consistency,
|
||||
negative density, gravity approximation and central forcing can bypass its
|
||||
assumptions. Numerical Lane–Emden integration also makes the driver's K only
|
||||
approximately critical. A small discrete virial in such a case would require
|
||||
careful interpretation. No mass/K change has been made in this campaign.
|
||||
approximately critical. Independent Lane–Emden integration gives
|
||||
M_LE=0.9999999918534539 for the recorded K, allowing only
|
||||
T/|W| <= 2.7155e-9 under those continuum assumptions. Thus the actual rounded
|
||||
parameters permit negligible, rather than literally zero, rotational support.
|
||||
A small discrete virial in such a case would require careful interpretation.
|
||||
|
||||
The initial screen keeps its original mass/K parameters. A separate physical
|
||||
family control is specified in [N3_COMPATIBLE_FAMILY_2026-09-19.md](N3_COMPATIBLE_FAMILY_2026-09-19.md)
|
||||
and isolated on `research/n3-compatible-family-20260919`: reduce K by one
|
||||
percent, leave M=1, omit the central border and prescribe J=.035. This tests
|
||||
a necessary physical compatibility condition, not a preconditioner speedup
|
||||
or a guarantee of equilibrium or a particular fraction of critical rotation.
|
||||
|
||||
85
experiments/check_n3_mass_compatibility.py
Normal file
85
experiments/check_n3_mass_compatibility.py
Normal file
@@ -0,0 +1,85 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Independent n=3 Lane–Emden integration; no FEM or production code changes."""
|
||||
from fractions import Fraction
|
||||
from pathlib import Path
|
||||
import json
|
||||
import math
|
||||
import sys
|
||||
|
||||
import scipy
|
||||
from scipy.integrate import solve_ivp
|
||||
|
||||
|
||||
def central_series(x, terms=10):
|
||||
# Recurrence from theta'' + 2 theta'/x + theta^3 = 0 avoids
|
||||
# a singular initial-value evaluation and a hand-transcribed series.
|
||||
coeff = [Fraction(1)]
|
||||
for k in range(terms - 1):
|
||||
cube = sum(
|
||||
coeff[i] * coeff[j] * coeff[k-i-j]
|
||||
for i in range(k+1) for j in range(k-i+1)
|
||||
)
|
||||
coeff.append(-cube / ((2*k+2)*(2*k+3)))
|
||||
theta = math.fsum(float(a)*x**(2*k) for k, a in enumerate(coeff))
|
||||
mu = -math.fsum(2*k*float(a)*x**(2*k+1) for k, a in enumerate(coeff) if k)
|
||||
return theta, mu
|
||||
|
||||
|
||||
def solve(rtol, atol, maximum_step, start):
|
||||
def rhs(x, y):
|
||||
theta, mu = y
|
||||
return -mu / x**2, x**2 * theta**3
|
||||
|
||||
def zero(x, y):
|
||||
return y[0]
|
||||
zero.terminal = True
|
||||
zero.direction = -1
|
||||
|
||||
result = solve_ivp(rhs, (start, 10.0), central_series(start),
|
||||
events=zero, method="DOP853", rtol=rtol, atol=atol,
|
||||
max_step=maximum_step)
|
||||
if not result.success or len(result.t_events[0]) != 1:
|
||||
raise RuntimeError(result.message)
|
||||
xi = float(result.t_events[0][0])
|
||||
mu = float(result.y_events[0][0, 1])
|
||||
kcrit = math.pi / (4.0*math.pi*mu)**(2.0/3.0)
|
||||
rho_for_unit_radius = xi**3/(4.0*math.pi*mu)
|
||||
mass = 4.0*math.pi*(driver_k/math.pi)**1.5*mu
|
||||
radius = xi*math.sqrt(driver_k/math.pi)*driver_rho**(-1.0/3.0)
|
||||
return dict(rtol=rtol, atol=atol, maximum_step=maximum_step, start=start,
|
||||
steps=len(result.t)-1, function_evaluations=result.nfev,
|
||||
xi1=xi, mu1=mu, K_for_mass_one=kcrit,
|
||||
rho_c_for_mass_and_radius_one=rho_for_unit_radius,
|
||||
M_LE_at_driver_K=mass, M_target_minus_M_LE=1.0-mass,
|
||||
R_LE_at_driver_K_and_rho_c=radius,
|
||||
K_driver_relative_to_critical_minus_one=driver_k/kcrit-1.0,
|
||||
virial_bound_T_over_absW_max=0.5*(1.0-mass**(2.0/3.0)))
|
||||
|
||||
|
||||
metadata = Path(sys.argv[1]).resolve()
|
||||
values = dict(line.split("=", 1) for line in metadata.read_text().splitlines() if "=" in line)
|
||||
if float(values["n"]) != 3.0 or float(values["M"]) != 1.0:
|
||||
raise ValueError("This diagnostic uses the campaign's n=3, M=1, G=1 convention")
|
||||
driver_k = float(values["K"])
|
||||
driver_rho = float(values["rho_c"])
|
||||
checks = [solve(3e-12, 1e-14, 0.2, 1e-3),
|
||||
solve(3e-13, 1e-15, 0.1, 1e-3),
|
||||
solve(3e-14, 1e-16, 0.05, 1e-3),
|
||||
solve(3e-14, 1e-16, 0.025, 1e-2),
|
||||
solve(3e-14, 1e-16, 0.025, 1e-4)]
|
||||
mu = checks[-1]["mu1"]
|
||||
def mass_for_k(k):
|
||||
return 4.0*math.pi*(k/math.pi)**1.5*mu
|
||||
|
||||
out = dict(metadata=str(metadata), python=sys.version, scipy=scipy.__version__,
|
||||
convention="G=1; theta'= -mu/xi^2; mu'=xi^2 theta^3; first theta zero",
|
||||
driver={name: values[name] for name in ("n", "K", "rho_c", "M", "J", "xi1", "mu1")},
|
||||
checks=checks,
|
||||
mass_check_spread=max(x["M_LE_at_driver_K"] for x in checks)-min(x["M_LE_at_driver_K"] for x in checks),
|
||||
full_metadata_K_binary64_ulp=math.ulp(driver_k),
|
||||
M_LE_for_K_adjacent_binary64=[mass_for_k(math.nextafter(driver_k, -math.inf)),
|
||||
mass_for_k(math.nextafter(driver_k, math.inf))],
|
||||
K_quoted_12_decimal_rounding_interval=[0.3639390599515, 0.3639390599525],
|
||||
M_LE_for_K_quoted_rounding_interval=[mass_for_k(0.3639390599515), mass_for_k(0.3639390599525)],
|
||||
limitation="Tolerance/step/start sensitivity check, not an interval-arithmetic error certificate.")
|
||||
print(json.dumps(out, indent=2))
|
||||
684
experiments/summarize_coupled_convergence.py
Normal file
684
experiments/summarize_coupled_convergence.py
Normal file
@@ -0,0 +1,684 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Verify completed campaign evidence and summarize coupled/linear convergence.
|
||||
|
||||
This reads saved artifacts, not FEM operators. A verified record may describe
|
||||
nonconvergence. Pending records never acquire a final numerical classification.
|
||||
Run serially with numerical campaigns: hashing and vector checks are real work.
|
||||
"""
|
||||
import argparse
|
||||
from collections import Counter, defaultdict
|
||||
import csv
|
||||
import hashlib
|
||||
import json
|
||||
import math
|
||||
from pathlib import Path
|
||||
import sys
|
||||
import tempfile
|
||||
import unittest
|
||||
|
||||
import check_preconditioner_benchmark as benchmark
|
||||
|
||||
|
||||
def require(condition, message):
|
||||
if not condition:
|
||||
raise ValueError(message)
|
||||
|
||||
|
||||
def finite(value):
|
||||
result = float(value)
|
||||
require(math.isfinite(result), f'Nonfinite required scalar: {value}')
|
||||
return result
|
||||
|
||||
|
||||
def near(left, right, label, rtol=3e-10, atol=1e-26):
|
||||
require(math.isfinite(left) and math.isfinite(right) and
|
||||
abs(left-right) <= atol + rtol * max(abs(left), abs(right)), f'{label}: {left} != {right}')
|
||||
|
||||
|
||||
def clean(value):
|
||||
if isinstance(value, float) and not math.isfinite(value):
|
||||
return None
|
||||
if isinstance(value, dict):
|
||||
return {k: clean(v) for k, v in value.items()}
|
||||
if isinstance(value, (list, tuple)):
|
||||
return [clean(v) for v in value]
|
||||
return value
|
||||
|
||||
|
||||
def rows(path):
|
||||
with path.open() as stream:
|
||||
return list(csv.DictReader(stream))
|
||||
|
||||
|
||||
def metadata(path):
|
||||
result = {}
|
||||
for line in path.read_text().splitlines():
|
||||
if not line:
|
||||
continue
|
||||
key, separator, value = line.partition('=')
|
||||
require(separator and key and key not in result, 'Malformed or repeated metadata key')
|
||||
result[key] = value
|
||||
return result
|
||||
|
||||
|
||||
def norm(values):
|
||||
return math.sqrt(math.fsum(v*v for v in values))
|
||||
|
||||
|
||||
class Hashes:
|
||||
def __init__(self):
|
||||
self.consumed = {}
|
||||
|
||||
def digest(self, path):
|
||||
path = path.resolve()
|
||||
key = str(path)
|
||||
if key not in self.consumed:
|
||||
digest = hashlib.sha256()
|
||||
with path.open('rb') as stream:
|
||||
for chunk in iter(lambda: stream.read(1024*1024), b''):
|
||||
digest.update(chunk)
|
||||
self.consumed[key] = digest.hexdigest()
|
||||
return self.consumed[key]
|
||||
|
||||
def verify(self, path, expected):
|
||||
require(self.digest(path) == expected, f'Hash mismatch: {path}')
|
||||
|
||||
|
||||
def physical(path):
|
||||
result = {}
|
||||
for row in rows(path):
|
||||
q = row['quadrature']
|
||||
measurements = result.setdefault(q, {})
|
||||
require(row['metric'] not in measurements, 'Repeated physical metric')
|
||||
measurements[row['metric']] = float(row['value'])
|
||||
for measurements in result.values():
|
||||
required = {'complete_volume_measurement', 'pressure_from_enthalpy_defined_everywhere',
|
||||
'invalid_stellar_samples', 'valid_stellar_samples', 'invalid_enthalpy_eos_samples',
|
||||
'volume', 'binding_energy', 'force_binding_energy', 'enthalpy_pressure_integral',
|
||||
'kinetic_energy', 'enthalpy_virial_error', 'enthalpy_force_virial_error'}
|
||||
require(required <= measurements.keys(), 'Incomplete physical virial accounting')
|
||||
for name in ('complete_volume_measurement', 'pressure_from_enthalpy_defined_everywhere'):
|
||||
require(measurements[name] in (0., 1.), 'Invalid physical validity flag')
|
||||
for name in ('invalid_stellar_samples', 'valid_stellar_samples', 'invalid_enthalpy_eos_samples'):
|
||||
count = finite(measurements[name])
|
||||
require(count >= 0 and count.is_integer(), 'Invalid physical sample count')
|
||||
complete = measurements['complete_volume_measurement'] == 1.
|
||||
if complete:
|
||||
require(measurements['invalid_stellar_samples'] == 0 and
|
||||
measurements['valid_stellar_samples'] > 0 and finite(measurements['volume']) > 0,
|
||||
'Complete physical measurement contradicts sample validity')
|
||||
valid_pressure = complete and measurements['invalid_enthalpy_eos_samples'] == 0
|
||||
require((measurements['pressure_from_enthalpy_defined_everywhere'] == 1.) == valid_pressure,
|
||||
'Enthalpy-pressure validity contradicts sample accounting')
|
||||
# Independently reconstruct both h-pressure virial scalars. This checks
|
||||
# their accounting, not the underlying FEM quadrature or physical model.
|
||||
w, p, t = (measurements[name] for name in
|
||||
('binding_energy', 'enthalpy_pressure_integral', 'kinetic_energy'))
|
||||
if complete:
|
||||
finite(w)
|
||||
finite(t)
|
||||
finite(measurements['force_binding_energy'])
|
||||
if valid_pressure:
|
||||
require(finite(p) >= 0, 'Invalid complete enthalpy pressure')
|
||||
valid_virial = valid_pressure and math.isfinite(w) and w < 0
|
||||
for binding, label in [('binding_energy', 'enthalpy_virial_error'),
|
||||
('force_binding_energy', 'enthalpy_force_virial_error')]:
|
||||
if valid_virial:
|
||||
require(finite(measurements[label]) >= 0, 'Invalid reported virial error')
|
||||
expected = abs(2*t + measurements[binding] + 3*p) / abs(w)
|
||||
# The producer accumulates in long double, but serializes each
|
||||
# cancelling term as double. Scale rounding by those terms.
|
||||
rounding = 64*sys.float_info.epsilon*(abs(2*t)+abs(measurements[binding])+3*abs(p))/abs(w)
|
||||
near(measurements[label], expected, label, atol=rounding)
|
||||
else:
|
||||
# Invalid physics is a retained numerical outcome. A finite
|
||||
# virial in this situation would falsely certify missing terms.
|
||||
require(math.isnan(measurements[label]), 'Undefined physical virial reported as finite')
|
||||
return result
|
||||
|
||||
|
||||
def both_h_virials_below_threshold(measurements, threshold=1e-5):
|
||||
required = ('volume', 'binding_energy', 'force_binding_energy', 'enthalpy_pressure_integral',
|
||||
'kinetic_energy', 'enthalpy_virial_error', 'enthalpy_force_virial_error')
|
||||
if not all(name in measurements and math.isfinite(measurements[name]) for name in required):
|
||||
return False
|
||||
return (measurements.get('complete_volume_measurement') == 1. and
|
||||
measurements.get('pressure_from_enthalpy_defined_everywhere') == 1. and
|
||||
measurements.get('invalid_stellar_samples') == 0. and
|
||||
measurements.get('invalid_enthalpy_eos_samples') == 0. and
|
||||
measurements.get('valid_stellar_samples', 0.) > 0. and
|
||||
measurements['volume'] > 0. and measurements['binding_energy'] < 0. and
|
||||
measurements['enthalpy_pressure_integral'] >= 0. and measurements['kinetic_energy'] >= 0. and
|
||||
all(0. <= measurements[name] <= threshold for name in
|
||||
('enthalpy_virial_error', 'enthalpy_force_virial_error')))
|
||||
|
||||
|
||||
DISPOSITIONS = {0: 'accepted', 1: 'inadmissible_state', 2: 'non_finite_state',
|
||||
3: 'non_finite_residual', 4: 'insufficient_decrease', 5: 'auxiliary_solve_failure'}
|
||||
|
||||
|
||||
def trial_summary(table, history, initial, check_merit):
|
||||
grouped = defaultdict(list)
|
||||
counts = Counter()
|
||||
for row in table:
|
||||
iteration, trial, code = (int(row[k]) for k in ('iteration', 'trial', 'disposition'))
|
||||
alpha = finite(row['alpha'])
|
||||
require(iteration >= 0 and code in DISPOSITIONS and 0 < alpha <= 1, 'Invalid trial coordinates')
|
||||
require(trial == len(grouped[iteration]), 'Missing/repeated trial')
|
||||
if grouped[iteration]:
|
||||
previous = grouped[iteration][-1]
|
||||
require(int(previous['disposition']) != 0 and alpha < float(previous['alpha']),
|
||||
'Trial after acceptance or nondecreasing backtracking')
|
||||
if code == 0:
|
||||
require(row['source'] == '', 'Accepted trial has a rejection source')
|
||||
if code == 4:
|
||||
require(row['source'] == 'residual metric', 'Merit failure source mismatch')
|
||||
grouped[iteration].append(row)
|
||||
counts[(row['source'], code)] += 1
|
||||
require(set(grouped) <= {int(r['iteration']) for r in history}, 'Trial has no completed iteration')
|
||||
previous = initial
|
||||
accepted_alphas = []
|
||||
for row in history:
|
||||
trials = grouped[int(row['iteration'])]
|
||||
require(len(trials) == int(row['trials']), 'Trial count differs from iteration record')
|
||||
accepted = [r for r in trials if int(r['disposition']) == 0]
|
||||
require(len(accepted) == int(row['accepted']), 'Trial/iteration acceptance mismatch')
|
||||
for trial in trials:
|
||||
code = int(trial['disposition'])
|
||||
if code in (0, 4):
|
||||
value, alpha = finite(trial['F']), finite(trial['alpha'])
|
||||
require(value >= 0 and finite(trial['min_jacobian']) > 0, 'Invalid evaluated trial')
|
||||
if check_merit:
|
||||
limit = previous * math.sqrt(1 - 1e-4*alpha)
|
||||
allowance = 3e-12 * max(previous, value)
|
||||
require(value <= limit + allowance if code == 0 else value >= limit - allowance,
|
||||
'Recorded trial contradicts squared-merit acceptance')
|
||||
if accepted:
|
||||
require(accepted[0] is trials[-1], 'Accepted trial must be last')
|
||||
near(float(accepted[0]['F']), float(row['F']), 'Accepted trial residual')
|
||||
near(float(accepted[0]['alpha']), float(row['alpha']), 'Accepted alpha')
|
||||
accepted_alphas.append(float(row['alpha']))
|
||||
else:
|
||||
near(float(row['F']), previous, 'Failed iteration restored residual')
|
||||
previous = float(row['F'])
|
||||
return {'total': len(table), 'minimum_accepted_alpha': min(accepted_alphas, default=None),
|
||||
'source_disposition_counts': [dict(source=source, disposition=DISPOSITIONS[code],
|
||||
disposition_code=code, count=count) for (source, code), count in sorted(counts.items())]}
|
||||
|
||||
|
||||
def nonlinear(folder, meta):
|
||||
history, blocks, trials = (rows(folder / name) for name in ('newton.csv', 'blocks.csv', 'trials.csv'))
|
||||
initial_physical, final_physical = physical(folder/'initial_physical.csv'), physical(folder/'final_physical.csv')
|
||||
require('18' in initial_physical and '18' in final_physical, 'Missing q18 physical diagnostics')
|
||||
layout = rows(folder/'layout.csv')
|
||||
residual_layout = [r for r in layout if r['kind'] == 'residual']
|
||||
names = {r['block'] for r in residual_layout}
|
||||
require(bool(names) and len(names) == len(residual_layout), 'Invalid residual block catalog')
|
||||
offset = 0
|
||||
for block in residual_layout:
|
||||
require(int(block['offset']) == offset and int(block['size']) > 0, 'Noncontiguous residual layout')
|
||||
offset += int(block['size'])
|
||||
block_groups = defaultdict(dict)
|
||||
for row in blocks:
|
||||
key = (int(row['iteration']), row['stage'])
|
||||
require(row['block'] not in block_groups[key], 'Repeated iteration/block observation')
|
||||
value = finite(row['normalized_norm'])
|
||||
require(value >= 0, 'Negative block norm')
|
||||
block_groups[key][row['block']] = value
|
||||
require(set(block_groups) == {(i, stage) for i in range(len(history)) for stage in ('before','after')},
|
||||
'Incomplete or extra iteration/block stages')
|
||||
require(all(set(group) == names for group in block_groups.values()), 'Incomplete residual block observations')
|
||||
initial = finite(initial_physical['18']['normalized_bordered_residual'])
|
||||
if (0, 'before') in block_groups:
|
||||
before = norm(block_groups[(0, 'before')].values())
|
||||
if meta.get('strategy') in ('reduced', 'positivity-reduced'):
|
||||
initial = before # Gravity completion precedes the solver's initial metric.
|
||||
else:
|
||||
near(before, initial, 'Initial block/physical residual binding')
|
||||
require(initial >= 0, 'Negative initial norm')
|
||||
accepted, previous, rates = 0, initial, []
|
||||
for i, row in enumerate(history):
|
||||
require(int(row['iteration']) == i and row['accepted'] in ('0', '1'), 'Invalid iteration sequence')
|
||||
value = finite(row['F'])
|
||||
require(value >= 0, 'Negative nonlinear norm')
|
||||
if (i, 'before') in block_groups:
|
||||
near(norm(block_groups[(i, 'before')].values()), previous, 'Before block norm')
|
||||
if (i, 'after') in block_groups:
|
||||
near(norm(block_groups[(i, 'after')].values()), value, 'After block norm')
|
||||
for key in ('seconds', 'linear_seconds', 'prepare_seconds', 'refresh_seconds'):
|
||||
require(finite(row[key]) >= 0, 'Invalid iteration timing')
|
||||
require(int(row['linear_iterations']) >= 0, 'Invalid Krylov count')
|
||||
if row['accepted'] == '1':
|
||||
require(0 < finite(row['alpha']) <= 1, 'Invalid accepted step')
|
||||
require(row['linear_status'] == '0', 'Accepted Newton step has failed linear solve')
|
||||
require(0 <= finite(row['linear_true']) <= finite(meta['linear_tol']),
|
||||
'Accepted Newton step fails its reported linear gate')
|
||||
accepted += 1
|
||||
rates.append(value/previous if previous > 0 else None)
|
||||
previous = value
|
||||
near(previous, finite(final_physical['18']['normalized_bordered_residual']), 'Final residual binding')
|
||||
require(accepted == int(meta['accepted_steps']) and meta['converged'] in ('0', '1'), 'Invalid completion metadata')
|
||||
rtol, atol = finite(meta['rtol']), finite(meta['atol'])
|
||||
require(rtol >= 0 and atol >= 0, 'Invalid nonlinear tolerances')
|
||||
threshold = max(atol, rtol*initial)
|
||||
declared = meta['converged'] == '1'
|
||||
require(not declared or previous <= threshold, 'Convergence flag exceeds actual initial-dependent threshold')
|
||||
stages = trial_summary(trials, history, initial, meta.get('strategy') in ('baseline', 'curvature', 'reduced'))
|
||||
final_blocks = block_groups.get((len(history)-1, 'after'), {})
|
||||
strategies = {r['metric']: float(r['value']) for r in rows(folder/'numerical_strategies.csv')}
|
||||
return {'kind': 'newton', 'outcome': 'converged' if declared else 'not_converged',
|
||||
'failure': meta.get('failure'), 'initial_residual': initial, 'final_residual': previous,
|
||||
'final_over_initial': previous/initial if initial else None,
|
||||
'rtol': rtol, 'atol': atol, 'actual_threshold': threshold,
|
||||
'final_residual_within_threshold': previous <= threshold,
|
||||
'accepted_updates': accepted, 'attempted_updates': len(history),
|
||||
'krylov_iterations': sum(int(r['linear_iterations']) for r in history),
|
||||
'newton_seconds': math.fsum(float(r['seconds']) for r in history),
|
||||
'linear_seconds': math.fsum(float(r['linear_seconds']) for r in history),
|
||||
'prepare_seconds': math.fsum(float(r['prepare_seconds']) for r in history),
|
||||
'refresh_seconds': math.fsum(float(r['refresh_seconds']) for r in history),
|
||||
'accepted_residual_ratios': rates, 'trials': stages,
|
||||
'final_block_squared_fractions': {k: v*v/(previous*previous) if previous else None for k,v in final_blocks.items()},
|
||||
'initial_physical': initial_physical, 'final_physical': final_physical,
|
||||
'numerical_strategies': strategies, 'history': history,
|
||||
'linear_report_scope': 'reduced_or_auxiliary; consult lifted full check' if 'reduced' in meta.get('strategy','') else 'full_system',
|
||||
'initial_physical_scope': 'before optional gravity completion; first before-row defines completed initial norm'}
|
||||
|
||||
|
||||
def linear_trials(folder, summary):
|
||||
table = rows(folder/'linear_direction_trials.csv')
|
||||
rhs = benchmark.vector(folder/'benchmark_normalized_rhs.txt')
|
||||
f = [-v for v in rhs]
|
||||
f_norm = norm(f)
|
||||
result = {}
|
||||
for role in ('baseline', 'candidate'):
|
||||
p = benchmark.vector(folder/f'{role}_normalized_correction.txt')
|
||||
action = benchmark.vector(folder/f'{role}_trial_jacobian_action.txt')
|
||||
saved_defect = benchmark.vector(folder/f'{role}_normalized_linear_defect.txt')
|
||||
require(len(f) == len(p) == len(action) == len(saved_defect), 'Trial action vector dimensions differ')
|
||||
defect = [a+b for a,b in zip(action,f)]
|
||||
error = norm([a-b for a,b in zip(defect,saved_defect)])
|
||||
require(error <= 2e-12*(norm(action)+f_norm), 'Saved full-J defect disagrees with re-applied action')
|
||||
relative = norm(defect)/f_norm
|
||||
slope = math.fsum(a*b for a,b in zip(f,action))
|
||||
recorded = summary['comparison'][role]
|
||||
near(relative, float(recorded['true_relative']), 'Linear gate relative defect', atol=error/f_norm)
|
||||
gate = norm(defect) <= float(recorded['relative_tolerance'])*f_norm
|
||||
require(gate == (recorded['true_gate_pass'] == '1'), 'Trial direction gate mismatch')
|
||||
samples = [row for row in table if row['role'] == role]
|
||||
require([float(r['alpha']) for r in samples] == [1., .25, .0625, .015625, .00390625, .0009765625, .000244140625],
|
||||
'Incomplete or changed frozen trial grid')
|
||||
observations = []
|
||||
for row in samples:
|
||||
alpha = finite(row['alpha'])
|
||||
for key, expected in [('base_norm', f_norm), ('linear_defect_relative', relative),
|
||||
('direction_norm', norm(p)), ('action_norm', norm(action)), ('merit_slope', slope)]:
|
||||
near(float(row[key]), expected, key)
|
||||
require(row['prepared'] in ('0','1') and row['decrease_screen'] in ('0','1'), 'Invalid trial flags')
|
||||
if row['prepared'] == '1':
|
||||
require(row['reason'] == '-1' and row['stage'] == '-1', 'Prepared trial has rejection codes')
|
||||
predicted = norm([x+alpha*y for x,y in zip(f,action)])
|
||||
trial, remainder, projection = (finite(row[k]) for k in
|
||||
('trial_norm','remainder_norm','signed_remainder_projection'))
|
||||
require(trial >= 0 and remainder >= 0, 'Negative trial/remainder norm')
|
||||
near(float(row['predicted_norm']), predicted, 'Affine predicted norm')
|
||||
near(float(row['relative_norm']), trial/f_norm, 'Trial relative norm')
|
||||
near(float(row['remainder_over_alpha']), remainder/alpha, 'Scaled remainder')
|
||||
identity = trial*trial - predicted*predicted - 2*projection - remainder*remainder
|
||||
scale = trial*trial + predicted*predicted + 2*abs(projection) + remainder*remainder
|
||||
allowance = 3e-10*scale + 1e-28
|
||||
require(abs(identity) <= allowance and abs(identity-float(row['merit_identity_error'])) <= allowance,
|
||||
'Scalar squared-merit identity contradicts reported norms')
|
||||
require(abs(projection) <= predicted*remainder + allowance, 'Impossible signed remainder projection')
|
||||
screen = trial*trial <= (1 - 1e-4*alpha)*f_norm*f_norm
|
||||
require(screen == (row['decrease_screen'] == '1'), 'Squared-merit screen mismatch')
|
||||
else:
|
||||
require(int(row['reason']) >= 0 and int(row['stage']) >= 0 and row['decrease_screen'] == '0',
|
||||
'Unprepared trial claims evaluation or merit success')
|
||||
require(all(math.isnan(float(row[k])) for k in ('trial_norm','relative_norm','remainder_norm',
|
||||
'remainder_over_alpha','predicted_norm','signed_remainder_projection','merit_identity_error')),
|
||||
'Unprepared trial has fabricated finite measurements')
|
||||
observations.append(row)
|
||||
result[role] = {'verified_linear_gate': gate, 'relative_true_defect': relative, 'merit_slope': slope,
|
||||
'largest_sampled_decreasing_alpha': max((float(r['alpha']) for r in samples if r['decrease_screen']=='1'), default=None),
|
||||
'observations': observations}
|
||||
require({r['role'] for r in table} == {'baseline','candidate'}, 'Unknown trial role')
|
||||
invariants = {r['metric']: r['value'] for r in rows(folder/'linear_direction_trial_invariants.csv')}
|
||||
require(invariants == {'accepted_unchanged':'1','base_residual_restored':'1'}, 'Trial restoration invariant failed')
|
||||
return {'scope': 'Jp+F and slope reconstructed from saved vectors; trial remainder identities checked only from saved scalars. No trial F/remainder vectors or independent FEM evaluation. Sampled grid is not the live halving line search; failed linear gates remain failed.',
|
||||
'roles': result}
|
||||
|
||||
|
||||
def linear_history(folder, assessment):
|
||||
table = rows(folder/'preconditioner_history.csv')
|
||||
require({r['role'] for r in table} == {'baseline','candidate'}, 'Missing or unknown linear history role')
|
||||
result = {}
|
||||
for role in ('baseline','candidate'):
|
||||
samples = [r for r in table if r['role'] == role]
|
||||
record = assessment['comparison'][role]
|
||||
iterations = [int(r['iteration']) for r in samples]
|
||||
require(iterations == sorted(iterations) and iterations[0] >= 0, 'Nonsequential recurrence history')
|
||||
require(all(r['variant'] == record['variant'] and r['final'] in ('0','1') for r in samples),
|
||||
'Linear history variant/flag mismatch')
|
||||
require(all(finite(r['reported_norm']) >= 0 for r in samples), 'Invalid recurrence norm')
|
||||
result[role] = {'samples':samples, 'minimum_reported_over_rhs':
|
||||
min(float(r['reported_norm']) for r in samples)/float(record['rhs_norm']),
|
||||
'scope':'Recorded Krylov recurrence norms; not independently recomputed true residuals or a proved floor.'}
|
||||
return result
|
||||
|
||||
|
||||
def frozen_audit_scope(folder, assessment):
|
||||
variants = {'surface-border-schur', 'gravity-schur-surface-border-schur'}
|
||||
roles = [role for role in ('baseline', 'candidate')
|
||||
if assessment['comparison'][role]['variant'] in variants]
|
||||
files = ('preconditioner_surface_border_schur.csv',
|
||||
'preconditioner_surface_border_coordinates.csv',
|
||||
'preconditioner_surface_border_matrices.csv')
|
||||
if roles:
|
||||
require(all((folder/name).is_file() for name in files), 'Missing surface/scalar Schur observations')
|
||||
return {
|
||||
'endpoint_scope': 'Saved full-system linear defects, block norms, gates and reported timing sums are audited.',
|
||||
'surface_border_schur_roles': roles,
|
||||
'surface_border_schur_internal_audit': 'not_performed' if roles else 'not_applicable',
|
||||
'surface_border_schur_artifact_scope':
|
||||
'Presence and producer hashes only; coarse matrices, coordinates, counters and condition estimates are not independently checked.'
|
||||
if roles else None,
|
||||
'operator_work_scope':
|
||||
'Outer Krylov J counts exclude one internal J action per surface/scalar Schur application; inverse time includes it. No total operator-work comparison is certified.'
|
||||
if roles else 'Application counters have the scope stated by the producing benchmark.'}
|
||||
|
||||
|
||||
def verify_run_binding(campaign, spec, record, provenance):
|
||||
command = record['command']
|
||||
require(command[1:-4] == spec['arguments'], 'Command differs from declared case')
|
||||
require(command[-4] == '--mesh' and command[-2] == '--output', 'Command has unexpected argument layout')
|
||||
require(Path(command[0]).resolve() == campaign/'plateau_diagnostic', 'Case did not use the retained executable')
|
||||
require(Path(command[-3]).resolve() == Path(provenance['mesh']).resolve(), 'Case mesh differs from provenance')
|
||||
require(Path(command[-1]).resolve() == campaign/spec['id'], 'Command output is not the case directory')
|
||||
inputs = {str(Path(p).resolve()) for p in record['input_state_hashes']}
|
||||
arguments = spec['arguments']
|
||||
for i, word in enumerate(arguments):
|
||||
if word not in ('--probe-state','--probe-direction','--replay-candidate','--density-snapshot','--restart-snapshot'):
|
||||
continue
|
||||
path = Path(arguments[i+1])
|
||||
path = (path if path.is_absolute() else Path(provenance['source'])/path).resolve()
|
||||
if word in ('--density-snapshot','--restart-snapshot'):
|
||||
snapshot = path/'accepted_state.txt' if path.is_dir() else path
|
||||
prior = snapshot.parent
|
||||
if not (prior/'metadata.txt').is_file():
|
||||
prior = prior.parent
|
||||
expected = {snapshot, prior/'metadata.txt', prior/'layout.csv',
|
||||
prior.parent/'provenance.json', prior.parent/(prior.name+'_run.json')}
|
||||
require({str(p) for p in expected} <= inputs, 'Restart input metadata/provenance is not hash-bound')
|
||||
prior_run = json.loads((prior.parent/(prior.name+'_run.json')).read_text())
|
||||
prior_provenance = json.loads((prior.parent/'provenance.json').read_text())
|
||||
require(prior_provenance['mesh_sha256'] == provenance['mesh_sha256'], 'Restart mesh hash differs')
|
||||
for saved in (snapshot, prior/'metadata.txt', prior/'layout.csv'):
|
||||
require(prior_run['output_hashes'][str(saved.relative_to(prior))] == record['input_state_hashes'][str(saved)],
|
||||
'Restart input differs from its producing record')
|
||||
else:
|
||||
require(str(path) in inputs, 'Supplied input vector has no recorded hash')
|
||||
|
||||
|
||||
def audit_case(campaign, spec, hashes, provenance=None):
|
||||
name, folder = spec['id'], campaign/spec['id']
|
||||
run_path = campaign/f'{name}_run.json'
|
||||
result = {'id': name, 'campaign': str(campaign), 'complete': False, 'artifact_passed': None}
|
||||
if not run_path.exists():
|
||||
return {**result, 'outcome': 'not_started'}
|
||||
try:
|
||||
record = json.loads(run_path.read_text())
|
||||
except json.JSONDecodeError:
|
||||
return {**result, 'outcome': 'incomplete_run_record'}
|
||||
if 'finished_utc' not in record:
|
||||
return {**result, 'outcome': 'running_or_interrupted'}
|
||||
result.update(complete=True, run=record)
|
||||
try:
|
||||
hashes.digest(run_path)
|
||||
require(record['id'] == name, 'Run/case ID mismatch')
|
||||
verify_run_binding(campaign, spec, record, provenance)
|
||||
listed = record['output_hashes']
|
||||
actual = {str(p.relative_to(folder)) for p in folder.rglob('*') if p.is_file() and p.suffix in ('.txt','.csv','.json')}
|
||||
require(set(listed) == actual, 'Retained output set differs from run record')
|
||||
for relative, expected in listed.items():
|
||||
path = folder/relative
|
||||
require(path.resolve().is_relative_to(folder), 'Output hash path escapes case directory')
|
||||
hashes.verify(path, expected)
|
||||
require(record['inputs_unchanged'] is True, 'Runner recorded mutated inputs')
|
||||
for path, expected in record['input_state_hashes'].items():
|
||||
hashes.verify(Path(path), expected)
|
||||
if record.get('timed_out') or record['exit_code'] not in (0,3):
|
||||
return {**result, 'artifact_passed': True, 'kind':'execution_failure', 'outcome':'timed_out' if record.get('timed_out') else 'execution_failure'}
|
||||
meta = metadata(folder/'metadata.txt')
|
||||
result['metadata'] = meta
|
||||
if (folder/'preconditioner_summary.csv').exists():
|
||||
assessment = benchmark.inspect_case(folder)
|
||||
require(assessment['passed'], 'Frozen benchmark audit failed: ' + str([c for c in assessment['checks'] if not c['passed']]))
|
||||
result.update(kind='benchmark', outcome=assessment['comparison']['outcome'], benchmark=assessment)
|
||||
result['benchmark_audit_scope'] = frozen_audit_scope(folder, assessment)
|
||||
result['linear_history'] = linear_history(folder, assessment)
|
||||
if (folder/'linear_direction_trials.csv').exists():
|
||||
result['linear_trials'] = linear_trials(folder, assessment)
|
||||
else:
|
||||
result.update(nonlinear(folder, meta))
|
||||
require((record['exit_code'] == 0) == (result['outcome'] == 'converged'), 'Exit code/convergence mismatch')
|
||||
result['artifact_passed'] = True
|
||||
except (OSError, ValueError, KeyError, TypeError, IndexError, OverflowError, ZeroDivisionError) as error:
|
||||
result.update(artifact_passed=False, outcome='invalid_artifacts', error=str(error))
|
||||
return result
|
||||
|
||||
|
||||
def audit_campaign(path, hashes):
|
||||
campaign = path.resolve()
|
||||
provenance_path = campaign/'provenance.json'
|
||||
provenance = json.loads(provenance_path.read_text())
|
||||
hashes.digest(provenance_path)
|
||||
if 'binary_sha256' not in provenance:
|
||||
return {'path':str(campaign), 'complete':False, 'artifact_passed':None, 'outcome':'build_incomplete', 'cases':[]}
|
||||
hashes.verify(campaign/'plateau_diagnostic', provenance['binary_sha256'])
|
||||
hashes.verify(Path(provenance['mesh']), provenance['mesh_sha256'])
|
||||
hashes.digest(campaign/'cases.json')
|
||||
require(json.loads((campaign/'cases.json').read_text()) == provenance['cases'], 'Cases file differs from provenance')
|
||||
ids = [spec['id'] for spec in provenance['cases']]
|
||||
require(bool(ids) and len(ids) == len(set(ids)) and all(Path(name).name == name and name not in ('.','..') for name in ids),
|
||||
'Invalid or repeated case IDs')
|
||||
cases = [audit_case(campaign, spec, hashes, provenance) for spec in provenance['cases']]
|
||||
complete = all(c['complete'] for c in cases)
|
||||
return {'path':str(campaign), 'producer_commit':provenance['commit'], 'mesh_sha256':provenance['mesh_sha256'],
|
||||
'complete':complete, 'artifact_passed':complete and all(c['artifact_passed'] for c in cases), 'cases':cases}
|
||||
|
||||
|
||||
def flat_rows(campaigns):
|
||||
result = []
|
||||
for campaign in campaigns:
|
||||
for case in campaign['cases']:
|
||||
meta, run = case.get('metadata',{}), case.get('run',{})
|
||||
row = {k:case.get(k) for k in ('id','kind','outcome','complete','artifact_passed','failure','initial_residual',
|
||||
'final_residual','final_over_initial','rtol','atol','actual_threshold','final_residual_within_threshold',
|
||||
'accepted_updates','attempted_updates','krylov_iterations',
|
||||
'newton_seconds','linear_seconds','prepare_seconds','refresh_seconds')}
|
||||
row.update(campaign=campaign['path'], **{k:meta.get(k) for k in
|
||||
('n','K','rho_c','M','J','n3_K_factor','seed_analytic_mass_estimate','central_phase',
|
||||
'density_order','surface_prescription','surface_maximum_order','surface_degree',
|
||||
'strategy','preconditioner_variant')})
|
||||
row.update({k:run.get(k) for k in ('seconds','utc_elapsed_seconds','utc_minus_monotonic_seconds','child_cpu_seconds','exit_code')})
|
||||
q18 = case.get('final_physical',{}).get('18',{})
|
||||
for key in ('enthalpy_virial_error','enthalpy_force_virial_error','normalized_unbordered_residual',
|
||||
'central_border','normalized_border_action','hydrostatic_gradient_rms','mass_absolute_error','angular_momentum_absolute_error'):
|
||||
row[key] = q18.get(key)
|
||||
row['both_h_virials_below_1e-5'] = both_h_virials_below_threshold(q18) if q18 else None
|
||||
row['minimum_accepted_alpha'] = case.get('trials',{}).get('minimum_accepted_alpha')
|
||||
for stage in ('pressure force','residual metric'):
|
||||
row[stage.replace(' ','_')+'_rejections'] = sum(r['count'] for r in
|
||||
case.get('trials',{}).get('source_disposition_counts',[]) if r['source'] == stage and r['disposition_code'] != 0)
|
||||
if 'benchmark' in case:
|
||||
row['surface_border_schur_internal_audit'] = case['benchmark_audit_scope']['surface_border_schur_internal_audit']
|
||||
for role in ('baseline','candidate'):
|
||||
summary = case['benchmark']['comparison'][role]
|
||||
for key in ('variant','true_relative','true_gate_pass','iterations','krylov_seconds','construction_seconds','setup_solve_and_true_check_seconds'):
|
||||
row[role+'_'+key] = summary[key]
|
||||
result.append(row)
|
||||
return result
|
||||
|
||||
|
||||
class TinyChecks(unittest.TestCase):
|
||||
def test_pending_is_not_success(self):
|
||||
with tempfile.TemporaryDirectory() as directory:
|
||||
result = audit_case(Path(directory), {'id':'pending','arguments':[]}, Hashes())
|
||||
self.assertFalse(result['complete'])
|
||||
self.assertIsNone(result['artifact_passed'])
|
||||
|
||||
def test_squared_merit_and_failed_stage(self):
|
||||
trials = [dict(iteration='0',trial='0',alpha='1',disposition='1',source='pressure force',F='nan',min_jacobian='1'),
|
||||
dict(iteration='0',trial='1',alpha='.5',disposition='0',source='',F='.8',min_jacobian='1')]
|
||||
history = [dict(iteration='0',trials='2',accepted='1',F='.8',alpha='.5')]
|
||||
result = trial_summary(trials, history, 1., True)
|
||||
self.assertEqual(result['minimum_accepted_alpha'], .5)
|
||||
self.assertEqual(len(result['source_disposition_counts']), 2)
|
||||
trials[-1]['F'] = '1.1'
|
||||
with self.assertRaises(ValueError):
|
||||
trial_summary(trials, history, 1., True)
|
||||
|
||||
def test_duplicate_metadata_and_hash_corruption(self):
|
||||
with tempfile.TemporaryDirectory() as directory:
|
||||
path = Path(directory)/'metadata.txt'
|
||||
path.write_text('n=1\nn=2\n')
|
||||
with self.assertRaises(ValueError): metadata(path)
|
||||
with self.assertRaises(ValueError): Hashes().verify(path, '0'*64)
|
||||
|
||||
def test_command_must_use_retained_binary_and_mesh(self):
|
||||
with tempfile.TemporaryDirectory() as directory:
|
||||
campaign = Path(directory).resolve()
|
||||
spec = dict(id='case', arguments=['--n','2'])
|
||||
provenance = dict(source=directory, mesh=str(campaign/'mesh'))
|
||||
record = dict(command=[str(campaign/'plateau_diagnostic'),'--n','2','--mesh',
|
||||
str(campaign/'mesh'),'--output',str(campaign/'case')], input_state_hashes={})
|
||||
verify_run_binding(campaign, spec, record, provenance)
|
||||
record['command'][0] = str(campaign/'other_binary')
|
||||
with self.assertRaises(ValueError): verify_run_binding(campaign, spec, record, provenance)
|
||||
|
||||
def test_virial_cancellation_and_corruption(self):
|
||||
with tempfile.TemporaryDirectory() as directory:
|
||||
path = Path(directory)/'physical.csv'
|
||||
values = dict(complete_volume_measurement=1., pressure_from_enthalpy_defined_everywhere=1.,
|
||||
invalid_stellar_samples=0., valid_stellar_samples=8., invalid_enthalpy_eos_samples=0., volume=1.,
|
||||
binding_energy=-3., force_binding_energy=-3.+1e-8,
|
||||
enthalpy_pressure_integral=1., kinetic_energy=0., enthalpy_virial_error=1e-17,
|
||||
enthalpy_force_virial_error=1e-8/3.)
|
||||
def save():
|
||||
path.write_text('quadrature,metric,value\n' + ''.join(f'18,{k},{v}\n' for k,v in values.items()))
|
||||
save()
|
||||
self.assertTrue(both_h_virials_below_threshold(physical(path)['18']))
|
||||
values['enthalpy_force_virial_error'] = 1e-5
|
||||
save()
|
||||
with self.assertRaises(ValueError): physical(path)
|
||||
|
||||
def test_invalid_physics_is_retained_without_virial_success(self):
|
||||
with tempfile.TemporaryDirectory() as directory:
|
||||
path = Path(directory)/'physical.csv'
|
||||
values = dict(complete_volume_measurement=0., pressure_from_enthalpy_defined_everywhere=0.,
|
||||
invalid_stellar_samples=1., valid_stellar_samples=7., invalid_enthalpy_eos_samples=0.,
|
||||
volume=math.nan, binding_energy=math.nan, force_binding_energy=math.nan,
|
||||
enthalpy_pressure_integral=math.nan, kinetic_energy=math.nan,
|
||||
enthalpy_virial_error=math.nan, enthalpy_force_virial_error=math.nan)
|
||||
def save():
|
||||
path.write_text('quadrature,metric,value\n' + ''.join(f'18,{k},{v}\n' for k,v in values.items()))
|
||||
save()
|
||||
self.assertFalse(both_h_virials_below_threshold(physical(path)['18']))
|
||||
values['enthalpy_virial_error'] = values['enthalpy_force_virial_error'] = 0.
|
||||
self.assertFalse(both_h_virials_below_threshold(values))
|
||||
save()
|
||||
with self.assertRaises(ValueError): physical(path)
|
||||
del values['binding_energy']
|
||||
save()
|
||||
with self.assertRaises(ValueError): physical(path)
|
||||
|
||||
def test_flat_summary_retains_physical_family(self):
|
||||
meta = dict(n='3', K='.3603', rho_c='12.74', M='1', J='.035', n3_K_factor='.99',
|
||||
central_phase='0', surface_prescription='harmonics', surface_maximum_order='4')
|
||||
result = flat_rows([dict(path='/example', cases=[dict(metadata=meta)])])[0]
|
||||
for name, value in meta.items():
|
||||
self.assertEqual(result[name], value)
|
||||
|
||||
def test_new_schur_scope_is_explicit_and_requires_retained_observations(self):
|
||||
with tempfile.TemporaryDirectory() as directory:
|
||||
folder = Path(directory)
|
||||
assessment = {'comparison': {'baseline': {'variant':'independent'},
|
||||
'candidate': {'variant':'surface-border-schur'}}}
|
||||
with self.assertRaises(ValueError): frozen_audit_scope(folder, assessment)
|
||||
for name in ('preconditioner_surface_border_schur.csv',
|
||||
'preconditioner_surface_border_coordinates.csv',
|
||||
'preconditioner_surface_border_matrices.csv'):
|
||||
(folder/name).write_text('retained,observation\n')
|
||||
scope = frozen_audit_scope(folder, assessment)
|
||||
self.assertEqual(scope['surface_border_schur_internal_audit'], 'not_performed')
|
||||
self.assertEqual(scope['surface_border_schur_roles'], ['candidate'])
|
||||
assessment['comparison']['candidate']['variant'] = 'gravity-schur'
|
||||
self.assertEqual(frozen_audit_scope(folder, assessment)['surface_border_schur_internal_audit'],
|
||||
'not_applicable')
|
||||
|
||||
def test_completed_failure_requires_matching_retained_outputs(self):
|
||||
with tempfile.TemporaryDirectory() as directory:
|
||||
campaign = Path(directory).resolve()
|
||||
folder = campaign/'case'
|
||||
folder.mkdir()
|
||||
spec = dict(id='case', arguments=[])
|
||||
provenance = dict(source=directory, mesh=str(campaign/'mesh'))
|
||||
record = dict(id='case', finished_utc='2026-09-19T00:00:00+00:00', exit_code=1,
|
||||
command=[str(campaign/'plateau_diagnostic'),'--mesh',str(campaign/'mesh'),'--output',str(folder)],
|
||||
output_hashes={}, input_state_hashes={}, inputs_unchanged=True)
|
||||
(campaign/'case_run.json').write_text(json.dumps(record))
|
||||
result = audit_case(campaign, spec, Hashes(), provenance)
|
||||
self.assertTrue(result['artifact_passed'])
|
||||
self.assertEqual(result['outcome'],'execution_failure')
|
||||
(folder/'unrecorded.csv').write_text('value\n1\n')
|
||||
self.assertFalse(audit_case(campaign, spec, Hashes(), provenance)['artifact_passed'])
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(description=__doc__)
|
||||
parser.add_argument('campaigns', nargs='*', type=Path)
|
||||
parser.add_argument('--output', type=Path)
|
||||
parser.add_argument('--self-test', action='store_true')
|
||||
args = parser.parse_args()
|
||||
if args.self_test:
|
||||
suite = unittest.defaultTestLoader.loadTestsFromTestCase(TinyChecks)
|
||||
return 0 if unittest.TextTestRunner(verbosity=2).run(suite).wasSuccessful() else 1
|
||||
if not args.campaigns or args.output is None:
|
||||
parser.error('Provide campaign directories and --output analysis.json')
|
||||
hashes = Hashes()
|
||||
campaigns = []
|
||||
for path in args.campaigns:
|
||||
try:
|
||||
campaigns.append(audit_campaign(path, hashes))
|
||||
except (OSError, ValueError, KeyError, TypeError, IndexError) as error:
|
||||
campaigns.append({'path':str(path.resolve()), 'complete':False, 'artifact_passed':False,
|
||||
'outcome':'invalid_campaign_provenance', 'error':str(error), 'cases':[]})
|
||||
result = {'artifact_passed':all(c['artifact_passed'] for c in campaigns),
|
||||
'complete':all(c['complete'] for c in campaigns), 'campaigns':campaigns,
|
||||
'auditor_sha256':hashes.digest(Path(__file__)),
|
||||
'benchmark_checker_sha256':hashes.digest(Path(benchmark.__file__)), 'consumed_sha256':hashes.consumed,
|
||||
'scope':'Provenance and retained-file hashes; recorded Newton/trial/block bindings; saved-vector final linear defects and scalar trial identities. Numerical failure is an outcome, not an artifact failure.',
|
||||
'limitations':['No independent FEM reassembly or physical-validity certificate.',
|
||||
'Linear histories are recurrence residuals; only saved endpoints have independently checked true defects. No proved residual floor.',
|
||||
'New surface/scalar Schur wrapper artifacts are retained and hash-bound, but their matrices, coordinates, counters and conditioning are outside this audit; endpoint gates and timing sums are checked.',
|
||||
'Trial remainder vectors are not saved. Scalar checks cannot verify the underlying FEM evaluations.',
|
||||
'Virial outcomes refer to the reported quadrature and h-derived pressure; they do not establish nonlinear convergence.',
|
||||
'Normalized residuals and rtol thresholds belong to their own discrete systems; Krylov counts are not equal work units.',
|
||||
'Newton time excludes observers/setup; benchmark setup/solve costs have separate scope. Clock discrepancies are retained without rescaling.']}
|
||||
args.output.parent.mkdir(parents=True, exist_ok=True)
|
||||
args.output.write_text(json.dumps(clean(result), indent=2, allow_nan=False)+'\n')
|
||||
flat = flat_rows(campaigns)
|
||||
with args.output.with_suffix('.csv').open('w') as stream:
|
||||
writer = csv.DictWriter(stream, fieldnames=list(dict.fromkeys(k for row in flat for k in row)))
|
||||
writer.writeheader()
|
||||
writer.writerows(clean(flat))
|
||||
print(json.dumps({'complete':result['complete'], 'artifact_passed':result['artifact_passed'],
|
||||
'cases':len(flat), 'output':str(args.output)}))
|
||||
invalid = any(c.get('outcome') == 'invalid_campaign_provenance' or
|
||||
any(case['artifact_passed'] is False for case in c['cases']) for c in campaigns)
|
||||
return 1 if invalid else 0 if result['artifact_passed'] else 2
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
sys.exit(main())
|
||||
Reference in New Issue
Block a user