Files
MeanField/experiments/check_damped_coupled_newton.py

383 lines
26 KiB
Python

#!/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())