#!/usr/bin/env python3 """Audit a sequential frozen line search, separately from numerical success. Only saved-vector arithmetic is independent here: no FEM action or physical quadrature is repeated. A shared attempt budget and first-accept stopping rule are checked explicitly; these frozen searches are not Newton trajectories. """ import argparse import json import math from pathlib import Path from check_nonlinear_step import EPS, close, digest, dot, layout_for, minus, norm, rows, sanitize, vector from check_coupled_trial import inspect_physical, near_vector P = 'damped_coupled_' STATUS = ('not_attempted', 'original_accepted', 'budget_exhausted', 'kept', 'linear_failure', 'norm_limit', 'non_finite', 'inadmissible', 'not_improved', 'prediction_deferred') BASE = {'metadata.txt', 'coupled_trial_physical.csv'} | { P + name + '.csv' for name in ('summary', 'trials', 'candidates', 'evaluations', 'layout')} | { P + name + '.txt' for name in ('base', 'base_physical', 'base_residual', 'direction', 'base_action', 'state_factors', 'residual_factors', 'restored_residual', 'restored_action')} def same_number(a, b, scale=0.): return (math.isnan(a) and math.isnan(b)) or (not math.isfinite(a) and a == b) or close(a, b, scale) def merit(values): return .5 * math.fsum(value * value for value in values) def eligibility(base_merit, prediction_merit, original_merit, maximum_excess): decrease = base_merit - prediction_merit ratio = (original_merit - base_merit) / decrease if math.isfinite(decrease) and decrease > 0. else math.nan return math.isfinite(ratio) and ratio <= maximum_excess, decrease, ratio def correction_mechanism(prediction, before, corrected, action, rhs, beta): remainder = minus(before, prediction) defect = minus(action, rhs) own = [z - y - beta * a for z, y, a in zip(corrected, before, action)] after = minus(corrected, prediction) reconstructed = [(1. - beta) * r + beta * d + e for r, d, e in zip(remainder, defect, own)] return {'remainder_before_norm': norm(remainder), 'remainder_after_norm': norm(after), 'scaled_linear_defect_norm': abs(beta) * norm(defect), 'own_taylor_remainder_norm': norm(own), 'decomposition_error_norm': norm(minus(after, reconstructed))} def inspect_case(folder): try: return inspect_case_artifacts(Path(folder)) except (OSError, ValueError, KeyError, TypeError, IndexError, OverflowError, ZeroDivisionError) as error: return {'passed': False, 'observations_valid': False, 'required_artifacts': sorted(BASE), 'checks': [{'check': 'complete_readable_artifacts', 'passed': False, 'error': str(error)}], 'trials': [], 'physical_observations': []} def inspect_case_artifacts(folder): checks, required = [], set(BASE) def check(name, condition): checks.append({'check': name, 'passed': bool(condition)}) def flag(row, key, label): value = float(row[key]); check(label + key + ':boolean', value in (0., 1.)); return value == 1. def get(name, available=True, prefix=P): path = folder / (prefix + name + '.txt') if available: required.add(path.name); return vector(path) check(name + ':unavailable_file_absent', not path.exists()); return None summary_rows = rows(folder / (P + 'summary.csv')) summary = {row['metric']: row['value'] if row['metric'] == 'mode' else float(row['value']) for row in summary_rows} check('unique_summary', len(summary) == len(summary_rows)) metadata = dict(line.split('=', 1) for line in (folder / 'metadata.txt').read_text().splitlines() if '=' in line) mode = summary['mode']; check('mode', mode in ('none', 'full', 'damped', 'near')) check('metadata_scope', metadata['damped_coupled_probe'] == '1' and metadata['coupled_trial_alphas_are_independent'] == '0' and metadata['coupled_trial_admissibility'] == 'sampled_enthalpy_without_repair' and metadata['coupled_search'] == mode) for key, source in {'outer_linear_tolerance': 'linear_tol', 'relative_tolerance': 'coupled_tolerance', 'maximum_iterations': 'coupled_iterations', 'restart': 'coupled_restart', 'maximum_correction_ratio': 'coupled_maximum_ratio', 'attempt_budget': 'coupled_attempts_per_iteration', 'beta_reduction': 'coupled_beta_reduction', 'maximum_excess_ratio': 'coupled_excess_ratio'}.items(): check('metadata:' + key, summary[key] == float(metadata[source])) for key, expected in {'schema_version': 1, 'attempt_budget': 1, 'shared_iteration_budget': 1, 'accepted_commits': 0, 'probe_exception': 0, 'accepted_buffers_bitwise_identical': 1, 'direction_buffers_bitwise_identical': 1, 'normalization_factors_bitwise_identical': 1, 'fresh_operator_and_inverse_current': 1}.items(): check(key, summary[key] == expected) for name in ('original_accepted', 'budget_exhausted', 'kept', 'prediction_deferred'): check('status_mapping:' + name, summary['status_' + name] == STATUS.index(name)) for value, name in enumerate(('evaluated', 'inadmissible', 'non_finite')): check('candidate_status:' + name, summary['candidate_status_' + name] == value) gravity_path = flag(summary, 'gravity_path', 'summary:') check('gravity_policy', gravity_path == metadata['strategy'].startswith('curvature-')) maximum_trials = int(summary['maximum_trials']) maximum_steps = int(summary['maximum_beta_steps']) reduction = summary['beta_reduction'] check('outer_grid_control', 1 <= maximum_trials <= 30 and maximum_trials == summary['maximum_trials']) check('beta_grid_control', maximum_steps >= 1 and maximum_steps == summary['maximum_beta_steps'] and 0 < reduction < 1) check('full_step_policy', mode not in ('none', 'full') or maximum_steps == 1) check('beta_steps_metadata', mode in ('none', 'full') or maximum_steps == int(metadata['coupled_beta_steps'])) check('finite_controls', all(math.isfinite(summary[key]) and summary[key] > 0. for key in ('relative_tolerance', 'maximum_correction_ratio', 'maximum_excess_ratio', 'outer_linear_tolerance'))) check('nonnegative_absolute_tolerance', math.isfinite(summary['absolute_tolerance']) and summary['absolute_tolerance'] >= 0.) check('sufficient_decrease', 0 < summary['sufficient_decrease'] < 1) x, physical, f, p, j, sf, rf = [get(name) for name in ('base', 'base_physical', 'base_residual', 'direction', 'base_action', 'state_factors', 'residual_factors')] size = len(x) check('dimensions', all(len(value) == size for value in (physical, f, p, j, sf, rf))) check('positive_normalization', all(value > 0. for value in sf + rf)) check('base_normalization', near_vector(x, [value * factor for value, factor in zip(physical, sf)], norm(x))) layout = rows(folder / (P + 'layout.csv')) value_layout = layout_for(layout, 'value', size); residual_layout = layout_for(layout, 'residual', size) gravity_values = {i for name, (offset, count) in value_layout.items() if name in ('gravity_gradient', 'gravity_potential') for i in range(offset, offset + count)} gravity_rows = [i for name, (offset, count) in residual_layout.items() if name in ('gravity_gradient_relation', 'poisson_balance') for i in range(offset, offset + count)] fn, pn, jn, m0 = norm(f), norm(p), norm(j), merit(f) check('finite_nonzero_base', math.isfinite(fn) and fn > 0. and math.isfinite(pn) and pn > 0.) defect = [a + b for a, b in zip(f, j)] check('outer_true_gate', norm(defect) <= summary['outer_linear_tolerance'] * fn and summary['outer_verified_gate'] == 1) for key, value in {'base_norm': fn, 'base_merit': m0, 'outer_relative_defect': norm(defect) / fn}.items(): check(key, close(summary[key], value, value)) for name, expected in (('residual', f), ('action', j)): restored = get('restored_' + name) error = norm(minus(restored, expected)) check('restored_' + name, near_vector(restored, expected, norm(expected)) and close(summary['restored_' + name + '_error'], error, norm(expected))) trial_rows = rows(folder / (P + 'trials.csv')) candidate_rows = rows(folder / (P + 'candidates.csv')) evaluation_rows = rows(folder / (P + 'evaluations.csv')) check('sequential_trials', [int(row['trial']) for row in trial_rows] == list(range(len(trial_rows))) and 1 <= len(trial_rows) <= maximum_trials) candidate_map = {(int(row['trial']), int(row['candidate'])): row for row in candidate_rows} evaluation_map = {(int(row['trial']), int(row['evaluation'])): row for row in evaluation_rows} check('unique_candidates', len(candidate_map) == len(candidate_rows)) check('unique_evaluations', len(evaluation_map) == len(evaluation_rows)) seen_candidates, seen_evaluations, outcomes = set(), set(), [] attempts, deferred, iterations, operation_seconds = 0, 0, 0, 0. accepted_trial, physical_trials = -1, [] for row in trial_rows: i, alpha = int(row['trial']), float(row['alpha']) label, suffix = str(i) + ':', '_a' + str(i) check(label + 'alpha', alpha == math.ldexp(1., -i)) flags = {key: flag(row, key, label) for key in ('raw_prepared', 'gravity_gate', 'usable', 'before_accepted', 'attempted', 'aux_gate', 'aux_reported_gate', 'aux_verification_applied', 'rhs_saved', 'correction_saved', 'verified_vectors_saved', 'selected_prepared', 'selected_accepted', 'selected_original')} status = int(row['status']); check(label + 'status', 0 <= status < len(STATUS)) name = STATUS[status] raw = get('raw' + suffix); prediction = get('prediction' + suffix) raw_f = get('raw_residual' + suffix, flags['raw_prepared']) check(label + 'raw_state', near_vector(raw, [a + alpha * b for a, b in zip(x, p)], norm(x) + alpha * pn)) check(label + 'prediction', near_vector(prediction, [a + alpha * b for a, b in zip(f, j)], fn + alpha * jn)) check(label + 'preparation_status', (int(row['raw_reason']) == -1 and int(row['raw_stage']) == -1) if flags['raw_prepared'] else (int(row['raw_reason']) >= 0 and int(row['raw_stage']) >= 0)) check(label + 'usable_requires_preparation', not flags['usable'] or (flags['raw_prepared'] and flags['gravity_gate'])) if not gravity_path: check(label + 'raw_path', flags['gravity_gate'] and flags['usable'] == flags['raw_prepared']) before = get('before' + suffix, flags['usable']) before_f = get('before_residual' + suffix, flags['usable']) gravity_correction = get('gravity_correction' + suffix, gravity_path and flags['gravity_gate']) if flags['usable'] and not gravity_path: check(label + 'raw_baseline', before == raw and before_f == raw_f) if gravity_correction is not None: check(label + 'gravity_support', len(gravity_correction) == size and bool(gravity_values) and bool(gravity_rows) and all(value == 0. for index, value in enumerate(gravity_correction) if index not in gravity_values)) check(label + 'gravity_displacement', near_vector(before, [a + b for a, b in zip(raw, gravity_correction)], norm(raw) + norm(gravity_correction))) target = norm([before_f[index] - prediction[index] for index in gravity_rows]) threshold = float(row['gravity_target_threshold']); disagreement = float(row['gravity_affine_disagreement']) check(label + 'gravity_target', close(float(row['gravity_target_defect']), target, fn + alpha * jn)) check(label + 'gravity_gate', math.isfinite(threshold) and threshold >= 0. and target <= threshold + 512 * EPS * fn and math.isfinite(disagreement) and 0. <= disagreement <= threshold) selected = get('selected' + suffix, flags['selected_prepared']) selected_f = get('selected_residual' + suffix, flags['selected_prepared']) check(label + 'selected_prepared', flags['selected_prepared'] == flags['usable']) cutoff = (1. - summary['sufficient_decrease'] * alpha) * m0 for stage, value in (('before', before_f), ('selected', selected_f)): if value is not None: check(label + stage + '_norm', close(float(row[stage + '_norm']), norm(value), norm(value))) check(label + stage + '_merit', close(float(row[stage + '_merit']), merit(value), merit(value))) else: check(label + stage + '_unavailable', math.isnan(float(row[stage + '_norm'])) and math.isnan(float(row[stage + '_merit']))) before_accepted = flags['usable'] and merit(before_f) <= cutoff check(label + 'before_armijo', flags['before_accepted'] == before_accepted) eligible, predicted_decrease, excess = eligibility(m0, merit(prediction), merit(before_f), summary['maximum_excess_ratio']) \ if flags['usable'] else (False, math.nan, math.nan) for key, expected in (('predicted_decrease', predicted_decrease), ('excess_ratio', excess)): if math.isfinite(float(row[key])): check(label + key, close(float(row[key]), expected, abs(expected))) should_attempt = flags['usable'] and mode != 'none' and not before_accepted and attempts < summary['attempt_budget'] and \ (mode != 'near' or eligible) check(label + 'attempt_decision', flags['attempted'] == should_attempt) if mode == 'near' and flags['usable'] and not before_accepted and attempts < summary['attempt_budget']: check(label + 'gate_recorded', same_number(float(row['predicted_decrease']), predicted_decrease, m0) and same_number(float(row['excess_ratio']), excess, abs(excess))) if not should_attempt: expected = 'not_attempted' if not flags['usable'] or mode == 'none' else ( 'original_accepted' if before_accepted else ('budget_exhausted' if attempts >= summary['attempt_budget'] else 'prediction_deferred')) check(label + 'skip_status', name == expected) deferred += expected == 'prediction_deferred' if expected == 'prediction_deferred': check(label + 'deferred_gate_recorded', same_number(float(row['predicted_decrease']), predicted_decrease, m0) and same_number(float(row['excess_ratio']), excess, abs(excess))) attempts += should_attempt check(label + 'shared_budget', int(row['attempts_total']) == attempts <= summary['attempt_budget']) rhs = get('rhs' + suffix, flags['rhs_saved']); correction = get('correction' + suffix, flags['correction_saved']) action = get('action' + suffix, flags['verified_vectors_saved']); saved_defect = get('defect' + suffix, flags['verified_vectors_saved']) check(label + 'save_flags', should_attempt or not any(flags[key] for key in ('rhs_saved', 'correction_saved', 'verified_vectors_saved'))) check(label + 'finite_verification_saved', not (flags['aux_verification_applied'] and math.isfinite(float(row['aux_defect']))) or flags['verified_vectors_saved']) core = int(row['aux_core_status']) if should_attempt: iterations += int(row['aux_iterations']) check(label + 'iteration_limit', 0 <= int(row['aux_iterations']) <= summary['maximum_iterations']) check(label + 'reported_status', flags['aux_reported_gate'] == (int(row['aux_status']) == 0)) cap = summary['maximum_correction_ratio'] * alpha * pn check(label + 'unscaled_cap', same_number(float(row['correction_cap']), cap, cap)) if rhs is not None: check(label + 'rhs', near_vector(rhs, minus(prediction, before_f), norm(prediction) + norm(before_f))) if math.isfinite(float(row['aux_threshold'])): check(label + 'rhs_norm', close(float(row['aux_rhs_norm']), norm(rhs), norm(rhs))) check(label + 'threshold', close(float(row['aux_threshold']), max(summary['absolute_tolerance'], summary['relative_tolerance'] * norm(rhs)), float(row['aux_threshold']))) if flags['verified_vectors_saved']: check(label + 'verified_availability', flags['aux_verification_applied'] and rhs is not None and correction is not None) delta = minus(action, rhs); dn, cn = norm(delta), norm(correction) check(label + 'saved_defect', near_vector(saved_defect, delta, norm(action) + norm(rhs))) check(label + 'true_defect', same_number(float(row['aux_defect']), dn, norm(rhs))) check(label + 'true_gate', flags['aux_gate'] == (math.isfinite(dn) and dn <= float(row['aux_threshold']))) check(label + 'correction_norm', same_number(float(row['correction_norm']), cn, cn)) expected_core = 3 if not math.isfinite(cn) else (1 if not flags['aux_reported_gate'] or not flags['aux_gate'] else (2 if cn > cap else 0)) check(label + 'core_decision', core == expected_core) if core == 0: check(label + 'admissible_requires_gates', flags['aux_reported_gate'] and flags['aux_gate'] and flags['verified_vectors_saved'] and float(row['correction_norm']) <= cap) else: check(label + 'failed_core_status', name == {1: 'linear_failure', 2: 'norm_limit', 3: 'non_finite'}.get(core)) check(label + 'failed_core_no_preparation', int(row['preparations']) == 0 and int(row['restorations']) == 0) else: check(label + 'no_aux_certificate', core == -1 and int(row['aux_status']) == -1 and not flags['aux_gate'] and not flags['aux_reported_gate'] and not flags['aux_verification_applied'] and int(row['aux_iterations']) == 0) check(label + 'no_aux_preparations', int(row['preparations']) == 0 and int(row['restorations']) == 0) count = int(row['candidate_count']) check(label + 'candidate_budget', 0 <= count <= maximum_steps and (should_attempt and core == 0 or count == 0)) crows = [candidate_map[(i, k)] for k in range(count)] seen_candidates.update((i, k) for k in range(count)) erows = [value for key, value in evaluation_map.items() if key[0] == i] check(label + 'evaluation_order', [int(value['evaluation']) for value in erows] == list(range(len(erows)))) observed, mechanisms = [], [] for erow in erows: e = int(erow['evaluation']); seen_evaluations.add((i, e)); tag = suffix + '_e' + str(e) ex, ef = get('evaluated_state' + tag), get('evaluated_residual' + tag) beta = float(erow['beta']) check(label + f'e{e}:requires_correction', should_attempt and core == 0 and correction is not None) scale = norm(before) + norm(correction) check(label + f'e{e}:displacement', near_vector(ex, [b + beta * c for b, c in zip(before, correction)], scale)) allowed_beta = [0.] + [reduction ** k for k in range(count)] check(label + f'e{e}:known_beta_or_restore', any(near_vector(ex, [b + value * c for b, c in zip(before, correction)], scale) for value in allowed_beta)) check(label + f'e{e}:norm', same_number(float(erow['residual_norm']), norm(ef), norm(ef))) check(label + f'e{e}:merit', same_number(float(erow['merit']), merit(ef), merit(ef))) check(label + f'e{e}:armijo', flag(erow, 'accepted', label) == (merit(ef) <= cutoff)) observed.append((beta, ex, ef)) mechanisms.append({'evaluation': e, 'beta': beta, **correction_mechanism(prediction, before_f, ef, action, rhs, beta)}) evaluated = [] first_accepted = None for k, crow in enumerate(crows): beta = reduction ** k check(label + f'c{k}:beta', float(crow['beta']) == beta) cstatus = int(crow['status']); check(label + f'c{k}:status', cstatus in (0, 1, 2)) if cstatus == 0: matches = [(ex, ef) for observed_beta, ex, ef in observed if near_vector(ex, [b + beta * c for b, c in zip(before, correction)], norm(before) + norm(correction))] check(label + f'c{k}:fresh_evaluation', bool(matches)) ex, ef = matches[0] cm = merit(ef); evaluated.append((beta, ex, ef, cm)) check(label + f'c{k}:merit', close(float(crow['merit']), cm, cm)) accepted = cm <= cutoff check(label + f'c{k}:armijo', flag(crow, 'accepted', label) == accepted) if accepted and first_accepted is None: first_accepted = k else: check(label + f'c{k}:rejected', not flag(crow, 'accepted', label)) check(label + f'c{k}:time', math.isfinite(float(crow['preparation_seconds'])) and float(crow['preparation_seconds']) >= 0.) if should_attempt and core == 0: check(label + 'beta_stop_rule', count == (first_accepted + 1 if first_accepted is not None else maximum_steps)) selected_beta = float(row['selected_beta']) if flags['usable']: better_candidates = [item for item in evaluated if item[3] < merit(before_f)] best = min(better_candidates, key=lambda item: item[3]) if better_candidates else None if name == 'kept': fresh = [ef for beta, ex, ef in observed if ex == selected] check(label + 'kept_best', best is not None and selected_beta == best[0] and selected == best[1]) check(label + 'selected_fresh_residual', bool(fresh) and near_vector(selected_f, fresh[-1], norm(fresh[-1])) and merit(selected_f) < merit(before_f)) else: check(label + 'fallback_original', math.isnan(selected_beta) and selected == before and near_vector(selected_f, before_f, norm(before_f))) check(label + 'discarded_best_requires_failed_replay', best is None or (name in ('inadmissible', 'non_finite', 'not_improved') and int(row['restorations']) > 0)) check(label + 'selected_original', flags['selected_original'] == (selected == before)) else: check(label + 'unavailable_selection', math.isnan(selected_beta) and not flags['selected_original']) accepted_now = flags['usable'] and merit(selected_f) <= cutoff check(label + 'selected_armijo', flags['selected_accepted'] == accepted_now) if accepted_now: check(label + 'first_accept_stops', i == len(trial_rows) - 1 and accepted_trial == -1) accepted_trial = i pv = get('selected_physical' + suffix, prefix='coupled_trial_') check(label + 'physical_normalization', near_vector(selected, [v * a for v, a in zip(pv, sf)], norm(selected))) physical_trials.append({'sample': str(i), 'selected_prepared': '1', 'selected_accepted': '1', 'selected_norm': row['selected_norm']}) for key in ('raw_seconds', 'gravity_seconds', 'correction_seconds', 'solve_seconds', 'refresh_seconds', 'verification_seconds', 'preparation_seconds'): check(label + key, math.isfinite(float(row[key])) and float(row[key]) >= 0.) operation_seconds += sum(float(row[key]) for key in ('raw_seconds', 'gravity_seconds', 'correction_seconds')) check(label + 'correction_time_scope', float(row['correction_seconds']) + 1.e-6 >= sum(float(row[key]) for key in ('solve_seconds', 'refresh_seconds', 'verification_seconds', 'preparation_seconds'))) outcomes.append({'trial': i, 'alpha': alpha, 'status': name, 'attempted': should_attempt, 'attempts_total': attempts, 'before_accepted': before_accepted, 'selected_accepted': accepted_now, 'selected_beta': selected_beta, 'before_norm': float(row['before_norm']), 'selected_norm': float(row['selected_norm']), 'predicted_decrease': predicted_decrease, 'excess_ratio': excess, 'candidate_mechanisms': mechanisms}) check('candidate_coverage', seen_candidates == set(candidate_map)) check('evaluation_coverage', seen_evaluations == set(evaluation_map)) check('accepted_trial', summary['accepted_trial'] == accepted_trial) if accepted_trial < 0: check('exhausted_trial_grid', len(trial_rows) == maximum_trials) else: last = trial_rows[-1] for key, value in {'accepted_alpha': float(last['alpha']), 'accepted_norm': float(last['selected_norm']), 'accepted_beta': float(last['selected_beta'])}.items(): check(key, same_number(summary[key], value, value)) for key, value in {'attempts': attempts, 'aux_iterations': iterations, 'deferred_attempts': deferred}.items(): check(key, summary[key] == value) check('operation_time_total', close(summary['search_operation_seconds'], operation_seconds, operation_seconds)) check('wall_time_scope', math.isfinite(summary['search_wall_seconds']) and summary['search_wall_seconds'] + 1.e-6 >= operation_seconds) physical_observations = inspect_physical(folder, physical_trials, metadata, check) check('no_unexplained_vectors', {path.name for pattern in (P + '*.txt', 'coupled_trial_*.txt') for path in folder.glob(pattern)} == {name for name in required if name.endswith('.txt') and name != 'metadata.txt'}) passed = all(item['passed'] for item in checks) return sanitize({'passed': passed, 'observations_valid': passed, 'required_artifacts': sorted(required), 'checks': checks, 'summary': summary, 'metadata': metadata, 'trials': outcomes if passed else [], 'physical_observations': physical_observations if passed else [], 'scope': 'Sequential frozen search with one shared auxiliary-solve budget. Saved-vector arithmetic, not independent FEM or Newton convergence.'}) def inspect_campaign(campaign): campaign = Path(campaign).resolve(); checks, results = [], {} def check(name, value): checks.append({'check': name, 'passed': bool(value)}) try: provenance = json.loads((campaign / 'provenance.json').read_text()) cases = provenance['cases'] check('executable_hash', digest(campaign / 'plateau_diagnostic') == provenance['binary_sha256']) check('mesh_hash', digest(Path(provenance['mesh'])) == provenance['mesh_sha256']) check('case_manifest', json.loads((campaign / 'cases.json').read_text()) == cases) check('unique_cases', bool(cases) and len(cases) == len({case['id'] for case in cases})) for case in cases: name, arguments = case['id'], case['arguments']; folder = campaign / name record = json.loads((campaign / (name + '_run.json')).read_text()) check(name + ':completed', record.get('exit_code') == 0 and bool(record.get('finished_utc')) and not record.get('timed_out', False)) check(name + ':command', record.get('id') == name and record.get('command') == [str(campaign / 'plateau_diagnostic'), *arguments, '--mesh', provenance['mesh'], '--output', str(folder)]) expected_inputs = {} for option, saved in (('--probe-state', 'base_physical'), ('--probe-direction', 'direction')): path = Path(arguments[arguments.index(option) + 1]) path = path if path.is_absolute() else Path(provenance['source']) / path expected_inputs[str(path.resolve())] = saved check(name + ':required_input_hashes', expected_inputs.keys() <= record.get('input_state_hashes', {}).keys()) for path, value in record.get('input_state_hashes', {}).items(): check(name + ':input:' + path, digest(Path(path)) == value) for filename, value in record.get('output_hashes', {}).items(): check(name + ':output:' + filename, digest(folder / filename) == value) for path, saved in expected_inputs.items(): source, target = vector(Path(path)), vector(folder / (P + saved + '.txt')) check(name + ':input_binding:' + saved, source == target if saved == 'direction' else near_vector(source, target, norm(source))) result = inspect_case(folder); results[name] = result check(name + ':required_output_hashes', set(result['required_artifacts']) <= record.get('output_hashes', {}).keys()) check(name + ':probe_scope', '--damped-coupled-probe' in arguments) metadata = result.get('metadata', {}) for option, key in {'--index': 'n', '--angular-momentum': 'J', '--linear-tol': 'linear_tol', '--coupled-tol': 'coupled_tolerance', '--coupled-iterations': 'coupled_iterations', '--coupled-restart': 'coupled_restart', '--coupled-max-ratio': 'coupled_maximum_ratio', '--coupled-beta-steps': 'coupled_beta_steps', '--coupled-beta-reduction': 'coupled_beta_reduction', '--coupled-excess-ratio': 'coupled_excess_ratio', '--coupled-attempts': 'coupled_attempts_per_iteration', '--gravity-tol': 'gravity_tolerance', '--gravity-iterations': 'gravity_iterations', '--gravity-restart': 'gravity_restart'}.items(): if option in arguments: check(name + ':requested:' + key, key in metadata and float(metadata[key]) == float(arguments[arguments.index(option) + 1])) for option, key in (('--coupled-search', 'coupled_search'), ('--strategy', 'strategy')): if option in arguments: check(name + ':requested:' + key, metadata.get(key) == arguments[arguments.index(option) + 1]) if '--no-central' in arguments: check(name + ':central_constraint', metadata.get('central_phase') == '0') passed = all(item['passed'] for item in checks) and all(result['passed'] for result in results.values()) if not all(item['passed'] for item in checks): for result in results.values(): result.update(passed=False, observations_valid=False, trials=[], physical_observations=[]) return {'passed': passed, 'source_commit': provenance['commit'], 'checker_sha256': digest(Path(__file__)), 'checker_helpers_sha256': {name: digest(Path(__file__).with_name(name)) for name in ('check_coupled_trial.py', 'check_nonlinear_step.py')}, 'provenance_sha256': digest(campaign / 'provenance.json'), 'provenance_checks': checks, 'cases': results} except (OSError, ValueError, KeyError, TypeError, IndexError) as error: for result in results.values(): result.update(passed=False, observations_valid=False, trials=[], physical_observations=[]) return {'passed': False, 'provenance_checks': checks + [{'check': 'complete_readable_campaign', 'passed': False, 'error': str(error)}], 'cases': results} def main(): parser = argparse.ArgumentParser(description=__doc__) parser.add_argument('campaign', type=Path); parser.add_argument('--output', required=True, type=Path) args = parser.parse_args() if args.output.resolve() == args.campaign.resolve() or args.campaign.resolve() in args.output.resolve().parents: parser.error('Write the audit outside the immutable campaign.') result = inspect_campaign(args.campaign) args.output.write_text(json.dumps(result, indent=2, allow_nan=False) + '\n') print('PASS' if result['passed'] else 'FAIL', args.output) return 0 if result['passed'] else 1 if __name__ == '__main__': raise SystemExit(main())