experiment: record pressure positivity and shape curvature evidence

This commit is contained in:
2026-09-17 08:55:55 -04:00
parent 4280c532e3
commit 7a07caeee3
7 changed files with 1628 additions and 0 deletions

View File

@@ -0,0 +1,475 @@
#!/usr/bin/env python3
"""Read completed/partial pressure diagnostics; never launch a solver or modify inputs."""
import argparse
import csv
import json
import math
from pathlib import Path
ROOT = Path(__file__).resolve().parent
ITERATIONS = (2, 3, 4, 7, 11)
def rows(path):
if not path.exists():
return []
with path.open() as stream:
return [r for r in csv.DictReader(stream) if None not in r and all(v is not None for v in r.values())]
def metrics(path):
return {r['metric']: float(r['value']) for r in rows(path)}
def metadata(path):
if not path.exists():
return {}
return dict(line.split('=', 1) for line in path.read_text().splitlines() if '=' in line)
def numeric(row):
result = {}
for key, value in row.items():
try:
result[key] = float(value)
except ValueError:
result[key] = value
return result
def negative_h_rejection(row):
# Enums at source9e1b65d: thermodynamic_domain=2, pressure_force=6,
# outside_domain=5. The prepared pressure operator emits this for h<0.
return all(float(row.get(k, -999)) == v for k, v in
[('admissible', 0), ('reason', 2), ('stage', 6), ('eos_code', 5)])
def sanitize(value):
if isinstance(value, float) and not math.isfinite(value):
return None
if isinstance(value, dict):
return {k: sanitize(v) for k, v in value.items()}
if isinstance(value, list):
return [sanitize(v) for v in value]
return value
def case(iteration):
folder = ROOT / 'focused' / f'n1_J0p10_iteration_{iteration}'
capture = metadata(ROOT / 'captures/n1_J0p10_capture' / f'capture_{iteration}/capture.txt')
bound = metrics(folder / 'frozen_enthalpy_bound.csv')
inputs = metrics(folder / 'frozen_input_checks.csv')
replay = rows(folder / 'replayed_candidate.csv')
trial_rows = rows(folder / 'pressure_obstruction_trials.csv')
limits = [numeric(r) for r in rows(folder / 'pressure_obstruction_limits.csv')]
summary = metrics(folder / 'pressure_obstruction_summary.csv')
live = next((r for r in rows(ROOT / 'captures/n1_J0p10_capture/newton.csv')
if int(r['iteration']) == iteration), None)
result = {'iteration': iteration, 'folder': str(folder), 'capture': capture,
'bounds': bound, 'input_checks': inputs, 'limits': limits,
'exact_candidate': numeric(replay[0]) if replay else None,
'exact_candidate_confirms_pressure_negative_h': negative_h_rejection(replay[0]) if replay else None,
'restoration_completed': 'final_restoration_residual_difference' in inputs,
'pressure_trials_completed': len(trial_rows),
'pressure_trials_expected': summary.get('candidate_trial_count'),
'trial_checks': [], 'live_newton_step': numeric(live) if live else None}
result['pressure_grid_complete'] = (summary.get('candidate_trial_count') is not None and
len(trial_rows) == summary['candidate_trial_count'])
if capture:
result['direction_roundoff_relative_estimate'] = (
float(capture['direction_roundoff_scale_estimate']) / float(capture['direction_norm']))
if bound:
result['closure_to_pressure_bound_ratio'] = bound['closure_alpha_h'] / bound['pressure_alpha_h']
result['exact_first_trial_over_pressure_bound'] = float(capture['first_trial_alpha']) / bound['pressure_alpha_h']
if live:
result['accepted_alpha_over_union_bound'] = float(live['alpha']) / bound['alpha_thermodynamic']
result['tightest_sampled_family'] = ('closure' if bound['closure_alpha_h'] < bound['pressure_alpha_h'] else
'pressure' if bound['pressure_alpha_h'] < bound['closure_alpha_h'] else 'tie')
midpoint = .5 * (bound['closure_alpha_h'] + bound['pressure_alpha_h'])
result['between_bounds_midpoint'] = {
'alpha': midpoint,
'closure_limiting_h_prediction': bound['limiting_h'] + midpoint * bound['limiting_dh'],
'pressure_limiting_h_prediction': bound['pressure_limiting_h'] + midpoint * bound['pressure_limiting_dh'],
}
for row in trial_rows:
r = numeric(row)
errors = [abs(r[f'{family}_limiting_h_evaluated'] - r[f'{family}_limiting_h_prediction'])
for family in ('pressure', 'closure')]
sign_checks = [((r[f'{family}_limiting_h_evaluated'] < 0) ==
(r[f'{family}_limiting_h_prediction'] < 0)) for family in ('pressure', 'closure')]
ratio = r['alpha_over_union_bound']
r['maximum_limiting_h_evaluation_prediction_absolute_difference'] = max(errors)
r['both_limiting_point_signs_match_prediction'] = all(sign_checks)
r['confirms_pressure_negative_h'] = negative_h_rejection(row)
# Descriptive expectation check for these fixed cases, not a general
# assertion that h>=0 guarantees preparation or merit acceptance.
expected = (r['direction'] == 'shape_only' or ratio < 1)
r['admissibility_matches_enthalpy_only_prediction'] = bool(r['admissible']) == expected
result['trial_checks'].append(r)
if trial_rows:
result['all_limiting_sign_predictions_match'] = all(r['both_limiting_point_signs_match_prediction']
for r in result['trial_checks'])
result['all_admissibility_matches_enthalpy_only_prediction'] = all(
r['admissibility_matches_enthalpy_only_prediction'] for r in result['trial_checks'])
result['mapping_rejections'] = sum(r['admissible'] == 0 and r['reason'] in (0, 1)
for r in result['trial_checks'])
result['maximum_limiting_h_absolute_disagreement'] = max(
r['maximum_limiting_h_evaluation_prediction_absolute_difference'] for r in result['trial_checks'])
shape_rows = [r for r in result['trial_checks'] if r['direction'] == 'shape_only']
h_crossing_rows = [r for r in result['trial_checks']
if r['direction'] == 'enthalpy_only' and r['alpha_over_union_bound'] > 1]
result['shape_only_all_prepared'] = all(r['admissible'] == 1 for r in shape_rows) if len(shape_rows) == 4 else None
result['enthalpy_only_crossing_failures_all_pressure_negative_h'] = (
all(r['confirms_pressure_negative_h'] for r in h_crossing_rows) if len(h_crossing_rows) == 2 else None)
return result
def fmt(value):
return 'pending' if value is None else f'{value:.9g}'
def vector_entries(path, indices):
"""Read only selected zero-based entries; reject an incompatible saved vector."""
if not path.exists():
return None
selected = set(indices)
found = {}
with path.open() as stream:
size = int(next(stream).strip())
if not selected or min(selected) < 0 or max(selected) >= size:
raise ValueError(f'Carrier indices incompatible with {path}')
count = 0
for index, line in enumerate(stream):
value = float(line)
count += 1
if index in selected:
found[index] = value
if count != size or len(found) != len(selected):
raise ValueError(f'Incomplete vector {path}')
return [found[index] for index in indices]
def extrema(values):
return {'count': len(values), 'minimum': min(values), 'maximum': max(values),
'maximum_absolute': max(abs(x) for x in values),
'l2_norm': math.sqrt(math.fsum(x*x for x in values)),
'negative_count': sum(x < 0 for x in values),
'nonzero_count': sum(x != 0 for x in values)}
def supplemental(cases):
"""Find the two supplemental case IDs without imposing an output-parent name."""
result = {'cases': [], 'offline_surface_checks': []}
case_ids = [('n1_J0p10_onset_between_bounds', 2), ('n1_J0p10_late_surface_trace', 11)]
reference_rows = None
for name, iteration in case_ids:
candidates = [path.parent for path in ROOT.glob(f'**/{name}/surface_trace_summary.csv')]
if len(candidates) > 1:
raise ValueError(f'Ambiguous duplicate supplemental case: {name}')
if not candidates:
result['cases'].append({'id': name, 'iteration': iteration, 'available': False})
continue
folder = candidates[0]
carrier_rows = [numeric(r) for r in rows(folder / 'surface_trace_rows.csv')]
summary = metrics(folder / 'surface_trace_summary.csv')
if 'pressure_alpha_h' not in summary:
result['cases'].append({'id': name, 'iteration': iteration, 'available': False})
continue
contribution_rows = [numeric(r) for r in rows(folder / 'surface_trace_limits.csv')]
node_rows = [numeric(r) for r in rows(folder / 'surface_trace_nodes.csv')]
replay_rows = rows(folder / 'replayed_candidate.csv')
source_case = next(c for c in cases if c['iteration'] == iteration)
details = {'id': name, 'iteration': iteration, 'available': True, 'folder': str(folder),
'summary': summary, 'contributions': contribution_rows,
'replayed_candidate': numeric(replay_rows[0]) if replay_rows else None,
'carrier_rows_complete': len(carrier_rows) == summary.get('surface_row_count'),
'nodal_checks': [],
'restoration': metrics(folder / 'frozen_input_checks.csv')}
provenance = folder.parent / 'provenance.json'
if provenance.exists():
p = json.loads(provenance.read_text())
details['provenance'] = {key: p.get(key) for key in ('commit', 'branch', 'mesh_sha256')}
if carrier_rows and details['carrier_rows_complete']:
indices = [int(r['root_index']) for r in carrier_rows]
if len(set(indices)) != len(indices):
raise ValueError(f'Duplicate surface indices in {folder}')
if reference_rows is not None and indices != reference_rows:
raise ValueError('Supplemental surface carrier maps differ')
reference_rows = indices
for family in ('closure', 'pressure'):
nodal = [r for r in node_rows if r['family'] == family]
if not nodal:
continue
if len(nodal) != nodal[0]['node_count']:
continue # A live CSV may not yet have all nodal rows.
union = nodal[0]['alpha_union']
factors = [('base', 0), ('near_below_union', .99*union),
('near_above_union', 1.01*union), ('unit_step', 1)]
if source_case['capture']:
factors.append(('captured_first_alpha', float(source_case['capture']['first_trial_alpha'])))
if source_case['live_newton_step']:
factors.append(('accepted_alpha', source_case['live_newton_step']['alpha']))
scale = max(abs(r[k]) for r in nodal for k in ('h', 'dh'))
tolerance = 64 * math.ulp(1.0) * scale
limit = next((r for r in contribution_rows if r['family'] == family), None)
if limit is None:
continue
for label, alpha in factors:
values = [r['h'] + alpha*r['dh'] for r in nodal]
stats = extrema(values)
stats.update({'family': family, 'element': nodal[0]['element'], 'fe_order': nodal[0]['fe_order'],
'label': label, 'alpha': alpha, 'roundoff_scale_estimate': tolerance,
'negative_beyond_roundoff_count': sum(v < -tolerance for v in values),
'limiting_point_h': limit['full_h'] + alpha*limit['full_dh']})
details['nodal_checks'].append(stats)
if name.endswith('between_bounds') and details['replayed_candidate']:
r = details['replayed_candidate']
details['replay_confirms_closure_outside_domain'] = all(r.get(k) == v for k, v in
[('admissible', 0), ('reason', 2), ('stage', 3), ('eos_code', 5)])
details['candidate_derivation'] = json.loads((ROOT / 'supplemental_inputs/between_bounds_derivation.json').read_text())
result['cases'].append(details)
if reference_rows is not None:
result['surface_root_indices'] = reference_rows
for c in cases:
folder = ROOT / 'captures/n1_J0p10_capture' / f"capture_{c['iteration']}"
base = vector_entries(folder / 'physical_base.txt', reference_rows)
candidate = vector_entries(folder / 'physical_candidate.txt', reference_rows)
if base is None or candidate is None:
continue
alpha = float(c['capture']['first_trial_alpha'])
dh = [(v-b)/alpha for b, v in zip(base, candidate)]
result['offline_surface_checks'].append({'iteration': c['iteration'], 'first_trial_alpha': alpha,
'base_h': extrema(base), 'exact_candidate_h': extrema(candidate),
'reconstructed_direction_dh': extrema(dh),
'base_and_exact_candidate_entire_surface_identically_zero': all(v == 0 for v in base + candidate)})
return result
def main():
global ROOT
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument('campaign_root', nargs='?', type=Path, default=ROOT,
help='Campaign artifact directory (default: directory containing this script).')
ROOT = parser.parse_args().campaign_root.expanduser().resolve()
if not ROOT.is_dir():
parser.error(f'Campaign directory does not exist: {ROOT}')
cases = [case(i) for i in ITERATIONS]
transitions = []
for first, second in zip(cases, cases[1:]):
if second['iteration'] != first['iteration'] + 1 or not first['bounds'] or not second['bounds']:
continue
alpha = first['live_newton_step']['alpha']
for family, prefix in [('closure', ''), ('pressure', 'pressure_')]:
b, c = first['bounds'], second['bounds']
same = all(b[prefix + key] == c[prefix + key] for key in ['limiting_element', 'limiting_point'])
predicted = b[prefix + 'limiting_h'] + alpha * b[prefix + 'limiting_dh']
observed = c[prefix + 'limiting_h']
transitions.append({'from_iteration': first['iteration'], 'to_iteration': second['iteration'],
'family': family, 'same_limiting_point': same, 'accepted_alpha': alpha,
'predicted_next_h': predicted, 'observed_next_h': observed,
'absolute_disagreement': abs(predicted - observed) if same else None})
supplements = supplemental(cases)
provenance_file = ROOT / 'focused/provenance.json'
source_revision = json.loads(provenance_file.read_text()).get('commit', '9e1b65d') if provenance_file.exists() else '9e1b65d'
report = {'schema_version': 2, 'source_revision': source_revision,
'interpretation': 'Exact candidate replay is authoritative; direction probes include reconstruction roundoff.',
'cases': cases, 'consecutive_captured_step_checks': transitions, 'supplemental': supplements}
polynomial_path = ROOT / 'pressure_polynomial_checks.json'
polynomial = json.loads(polynomial_path.read_text()) if polynomial_path.exists() else None
if polynomial:
report['offline_polynomial_artifacts'] = ['pressure_polynomial_checks.json', 'pressure_polynomial_analysis.md']
(ROOT / 'pressure_checks.json').write_text(json.dumps(sanitize(report), indent=2, allow_nan=False) + '\n')
lines = ['# Pressure obstruction analysis', '',
'Read-only analysis of the frozen n=1, J=.1 direction captures. This script launches no solver and does not modify case data. '
f'The original focused source is {source_revision}; iteration numbers are zero-based. '
'Supplemental source revisions are recorded per case in pressure_checks.json. Partial cases are explicitly pending.', '',
'| Iteration | Exact first alpha | Pressure bound | Closure bound | Limiting pressure h | dh | Element / point | Exact replay |',
'|---:|---:|---:|---:|---:|---:|---|---|']
for c in cases:
b = c['bounds']
loc = f"{int(b['pressure_limiting_element'])} / {int(b['pressure_limiting_point'])}" if b else 'pending'
replay = ('pressure negative h' if c['exact_candidate_confirms_pressure_negative_h'] else
('other; inspect JSON' if c['exact_candidate'] else 'pending'))
alpha = float(c['capture']['first_trial_alpha']) if c['capture'] else None
lines.append(f"| {c['iteration']} | {fmt(alpha)} | {fmt(b.get('pressure_alpha_h'))} | "
f"{fmt(b.get('closure_alpha_h'))} | {fmt(b.get('pressure_limiting_h'))} | "
f"{fmt(b.get('pressure_limiting_dh'))} | {loc} | {replay} |")
if all(c['exact_candidate_confirms_pressure_negative_h'] for c in cases):
lines += ['', '**All five exact saved first candidates reproduce finite negative enthalpy in pressure preparation.** '
'These failures are thermodynamic-domain rejections, not geometry rejections. The saved candidates are '
'replayed directly, so this conclusion does not depend on reconstructing the Newton direction.']
populated = [c for c in cases if c['bounds']]
if populated:
lines += ['', 'The family attaining the smaller sampled bound is ' + ', '.join(
f"iteration {c['iteration']}: {c['tightest_sampled_family']}" for c in populated) + '.', '',
'| Iteration | Live accepted alpha | Accepted alpha / union bound |', '|---:|---:|---:|']
for c in populated:
lines.append(f"| {c['iteration']} | {fmt(c['live_newton_step']['alpha'])} | {fmt(c['accepted_alpha_over_union_bound'])} |")
if cases[-1]['bounds'] and cases[0]['bounds']:
first, last = cases[0]['bounds'], cases[-1]['bounds']
lines += ['', f"From iteration 2 to 11 the pressure bound shrinks by "
f"{first['pressure_alpha_h']/last['pressure_alpha_h']:.6g} times. The limiting pressure sample remains "
f"element {int(last['pressure_limiting_element'])}, point {int(last['pressure_limiting_point'])}; "
'its h approaches zero while dh remains negative. '
f"At iteration 11 the closure bound is {last['closure_alpha_h']/last['pressure_alpha_h']:.6g} times larger. "
'The mechanism of the step-length collapse is established along these directions; why the Newton '
'directions continue to decrease this h remains unresolved.']
if transitions:
maximum = max(r['absolute_disagreement'] for r in transitions if r['absolute_disagreement'] is not None)
lines += ['', f"For consecutive captured states at the same limiting point, h_next = h + alpha_accepted*dh "
f"agrees with the independently scanned next base to at most {maximum:.3g} absolute. "
'The pointwise approach to the enthalpy boundary is therefore directly tied to the accepted steps.']
lines += ['', '## Crossing controls', '',
'| Iteration | Candidate rows / expected | Shape-only all prepared | h-only crossing rejects negative h | Geometry rejections | Max h prediction/evaluation difference | Restored |',
'|---:|---:|---|---|---:|---:|---|']
for c in cases:
if c['iteration'] not in (2, 11):
continue
lines.append(f"| {c['iteration']} | {c['pressure_trials_completed']} / {fmt(c['pressure_trials_expected'])} | "
f"{c.get('shape_only_all_prepared') if c.get('shape_only_all_prepared') is not None else 'pending'} | "
f"{c.get('enthalpy_only_crossing_failures_all_pressure_negative_h') if c.get('enthalpy_only_crossing_failures_all_pressure_negative_h') is not None else 'pending'} | "
f"{c.get('mapping_rejections', 'pending')} | {fmt(c.get('maximum_limiting_h_absolute_disagreement'))} | "
f"{c['restoration_completed']} |")
onset = cases[0]
if onset['bounds']:
b = onset['bounds']
midpoint = onset['between_bounds_midpoint']
midpoint_case = next(c for c in supplements['cases'] if c['iteration'] == 2)
measured_midpoint = midpoint_case.get('replay_confirms_closure_outside_domain')
midpoint_outcome = ('The supplemental candidate reproduces thermodynamic-domain rejection at the closure stage '
'(reason=2, stage=3, EOS code=5), confirming pressure preparation passes at this alpha before closure fails.'
if measured_midpoint is True else
'The supplemental replay has a different outcome; inspect pressure_checks.json.'
if measured_midpoint is False else
'Its production outcome is pending; the supplemental replay can separate closure from pressure.')
lines += ['', '## Onset interpretation', '',
f"The onset has a tighter **{onset['tightest_sampled_family']}** bound: closure {b['closure_alpha_h']:.12g}, "
f"pressure {b['pressure_alpha_h']:.12g}. The original alpha=1 trial crosses both. "
'Pressure is prepared before closure, so the recorded pressure-stage rejection identifies the first failing stage at that alpha; '
'it does not establish the pressure quadrature as the tightest restriction.', '',
f"A midpoint alpha={midpoint['alpha']:.12g} predicts closure h="
f"{midpoint['closure_limiting_h_prediction']:.9g} and pressure h="
f"{midpoint['pressure_limiting_h_prediction']:.9g} at the respective limiting points. "
'This midpoint is outside the original probe grid. ' + midpoint_outcome, '']
populated_limits = [c for c in cases if c['limits']]
if populated_limits:
lines += ['', '## Limiting-point locations', '',
'| Iteration | Family | Surface-adjacent element | Reference radius | Physical base (x, y, z) |',
'|---:|---|---|---:|---|']
for c in populated_limits:
for r in c['limits']:
xyz = ', '.join(fmt(r[f'physical_{d}']) for d in 'xyz')
lines.append(f"| {c['iteration']} | {r['family']} | {int(r['element'])}; "
f"{bool(r['element_touches_stellar_surface'])} | {fmt(r['reference_radius'])} | ({xyz}) |")
lines += ['', '## Surface trace is an enforced equation', '',
'The runtime isobaric condition is not absent merely because the field registry has no essential constraint. '
'The root constructs an enthalpy boundary map on StellarSurface (prepared_stellar_equilibrium.cpp:407), '
'replaces surface hydrostatic residual rows (lines 809–810), and replaces their Jacobian rows (904–908). '
'The compiled constraint is h−h(P_surface), with identity h Jacobian for fixed pressure '
'(surface/compiled.cppm:47–59). At P_surface=0 the polytropic EOS gives h=0. '
'The radial seed explicitly sets these boundary coefficients to zero '
'(seed/stellar_equilibrium_projection.cppm:203–208).', '',
'This is algebraic enforcement, not a per-trial coefficient projection. Source inspection alone does not '
'establish exact zero in every captured iterate under an inexact linear solve. Even a zero polynomial '
'boundary trace does not guarantee nonnegative enthalpy within the adjacent high-order element. '
'The observed near-surface wall therefore does not establish a missing boundary condition or interpolation '
'as the cause of the Newton direction.']
trace_cases = [c for c in supplements['cases'] if c['available']]
if trace_cases:
lines += ['', '## Supplemental surface carrier and nodal checks', '',
'The actual surface carrier indices come from GetPressureSurfaceRows(), then select entries in each '
'saved physical state. Boundary-only fields retain those coefficients and zero every other enthalpy '
'coefficient; evaluating this field at a limiting interior point gives the boundary contribution to '
'that polynomial value, not a separate trace sample on the stellar surface.', '',
'| Iteration | Carrier rows | Base max abs h | Direction max abs dh | Unit-step max abs h |',
'|---:|---:|---:|---:|---:|']
for c in trace_cases:
m = c['summary']
lines.append(f"| {c['iteration']} | {fmt(m.get('surface_row_count'))} | {fmt(m.get('base_h_maximum_absolute'))} | "
f"{fmt(m.get('direction_dh_maximum_absolute'))} | {fmt(m.get('h_plus_dh_maximum_absolute'))} |")
offline = supplements['offline_surface_checks']
if len(offline) == len(ITERATIONS) and all(c['base_and_exact_candidate_entire_surface_identically_zero'] for c in offline):
lines += ['', '**All actual surface enthalpy coefficients are exactly zero in all five saved bases and all '
'five exact first candidates.** Their reconstructed boundary directions are therefore also zero. '
'Boundary-row leakage is ruled out for these captured directions, independently of the inexact '
'linear-solve possibility from source inspection.']
else:
lines += ['', 'Offline surface checks (including nonzero and negative counts) are recorded per capture in '
'pressure_checks.json; inspect them before inferring exact trace enforcement in captured states.']
lines += ['', '| Iteration | Family | Full h | Boundary contribution h | Full dh | Boundary contribution dh |',
'|---:|---|---:|---:|---:|---:|']
for c in trace_cases:
for r in c['contributions']:
lines.append(f"| {c['iteration']} | {r['family']} | {fmt(r['full_h'])} | {fmt(r['boundary_h'])} | "
f"{fmt(r['full_dh'])} | {fmt(r['boundary_dh'])} |")
lines += ['', 'Full physical fields are scattered through the production field adapter, then evaluated at FE '
'GetNodes() coordinates. These values do not assume that raw signed local DOF indices are nodal values. '
'The same element is intentionally exported separately for each limiting rule family.', '',
'| Iteration | Family | Step | Nodal minimum h | Negative nodes | Limiting interior-point h |',
'|---:|---|---|---:|---:|---:|']
for c in trace_cases:
for r in c['nodal_checks']:
if r['label'] in ('base', 'near_above_union', 'unit_step'):
lines.append(f"| {c['iteration']} | {r['family']} | {r['label']} | {fmt(r['minimum'])} | "
f"{r['negative_count']} | {fmt(r['limiting_point_h'])} |")
undershoots = [(c['iteration'], r['family']) for c in trace_cases for r in c['nodal_checks']
if r['label'] == 'near_above_union' and r['minimum'] >= 0 and r['limiting_point_h'] < 0]
if undershoots:
lines += ['', 'At 1.01 times the union bound, nonnegative values at every FE node coexist with negative '
'enthalpy at the limiting interior point in ' + ', '.join(f'iteration {i} {family}' for i, family in undershoots) +
'. This demonstrates an interior polynomial undershoot in these trial directions. It does not '
'establish an interpolation implementation error or explain why Newton selects this direction.']
lines += ['', 'Nodal nonnegativity is not a certificate of positivity between nodes. The JSON includes strict '
'negative counts and a separate 64-epsilon times nodal-value-scale estimate, which is descriptive '
'rather than a certified rounding bound. Equality exactly at the union bound is deliberately not '
'used to decide a crossing. The production quadrature union bound also does not certify positivity '
'at unsampled points; these results do not establish the minimum enthalpy over an entire element.']
if polynomial:
lines += ['', '## Offline interior polynomial check', '',
'A separate independently validated tensor-Q3 reconstruction samples 495,264 strictly interior '
'reference points in each exported limiting element, including logarithmic distances to its zero '
'face. It reconstructs all 64 nodes exactly and both production limit points to roundoff.']
for c in polynomial['cases']:
minimum = c['sampled_extrema']['base']['minimum']
if minimum < -c['roundoff_scale_estimate']:
point = ', '.join(fmt(x) for x in c['sampled_extrema']['base']['minimum_reference_point'])
lines += ['', f"**The saved iteration {c['iteration']} base already has negative enthalpy between "
f"production quadrature points:** sampled h={minimum:.9g} at reference ({point}). "
'This base-state finding is independent of Newton-direction reconstruction. The surface '
'coefficients and all 64 FE node values remain nonnegative; production quadrature positivity '
'therefore misses a real polynomial undershoot in this accepted state.']
else:
lines += ['', f"Iteration {c['iteration']} has no negative base samples on this grid; its sampled "
f"directional bound {c['directional_bound']['sampled_admissible_step_bound']:.12g} is tighter "
f"than the production bound {c['union_production_bound']:.12g}. Absence of negative samples "
'does not certify positivity everywhere.']
lines += ['', 'See pressure_polynomial_analysis.md and pressure_polynomial_checks.json for interpolation '
'validation, minima, locations and limitations. No solver or preparation rule was changed.']
lines += ['', '## Scope and interpretation', '',
'A replay with reason=2, stage=6, EOS code=5 is the production pressure operator’s finite h<0 branch. '
'It resolves the old CSV ambiguity with inverted geometry. Missing determinant/F data on rejection are not measured nonfinite residuals.', '',
'The bound scans the exact production pressure/closure quadrature points using pointwise polynomial contractions. '
'The independently re-evaluated h values in the crossing table concern the two frozen limiting points, not a full search for each trial’s most negative point. '
'Optimized tensor contractions can differ at roundoff; .99/1.01 factors deliberately avoid equality at the crossing.', '',
'Element-to-surface adjacency, when available in pressure_obstruction_limits.csv, is an exact topology flag. '
'It is not distance to the surface. Physical/reference Cartesian coordinates and element-local integration coordinates are distinct.', '',
'At iteration 11 the supplied direction is reconstructed from a first-trial difference at alpha=1.9073486328125e-6; '
'its relative subtraction-roundoff scale estimate is approximately 4.88e-7, not a certified error bound. '
'The exact saved candidate is replayed without that reconstruction and is authoritative for the observed rejection.', '',
'Preparation success in a frozen probe does not establish line-search merit acceptance, physical equilibrium, or eventual Newton convergence. '
'No requested stopping test, enthalpy admissibility rule, or solver was changed. '
'The evolving bound is directional: a falling h/dh bound does not by itself identify why the nonlinear correction keeps pointing outward from the admissible set.', '',
'Machine-readable evidence and all individual trial checks: pressure_checks.json. '
'Reproduce this analysis with `python3 analyze_pressure.py /path/to/campaign_artifacts`; '
'the optional campaign path defaults to the script directory.']
(ROOT / 'pressure_analysis.md').write_text('\n'.join(lines) + '\n')
print(json.dumps({'cases_with_exact_replay': sum(c['exact_candidate'] is not None for c in cases),
'cases_fully_restored': sum(c['restoration_completed'] for c in cases),
'supplemental_available': sum(c['available'] for c in supplements['cases']),
'supplemental_fully_restored': sum('final_restoration_residual_difference' in c.get('restoration', {})
for c in supplements['cases']),
'candidate_rows': {c['iteration']: c['pressure_trials_completed'] for c in cases}}, sort_keys=True))
if __name__ == '__main__':
main()

View File

@@ -0,0 +1,234 @@
#!/usr/bin/env python3
"""Read-only shape CSV audit. No solver/library dependency; outputs live elsewhere than raw case files."""
from __future__ import annotations
import argparse, csv, hashlib, json, math
from pathlib import Path
from collections import defaultdict
CASES = ('n1_J0p10_iteration_2', 'n1p5_J0_iteration_1', 'n3_J0_seed_R160_curvature')
def rows(path):
with path.open(newline='') as stream:
reader = csv.DictReader(stream)
result = list(reader)
for r in result:
if None in r or any(v is None for v in r.values()):
raise ValueError(f'Malformed/incomplete CSV row in {path}: {r}')
return result
def num(r, key): return float(r[key])
def flag(r, key): return r[key] == '1'
def norm(values): return math.sqrt(math.fsum(v*v for v in values))
def ratio(a,b): return a/b if math.isfinite(a) and math.isfinite(b) and b > 0 else math.nan
def clean(obj):
if isinstance(obj, float) and not math.isfinite(obj): return None
if isinstance(obj, dict): return {str(k):clean(v) for k,v in obj.items()}
if isinstance(obj, (tuple,list)): return [clean(v) for v in obj]
return obj
def signed_aggregate(rs):
out = {}
for kind in ('full','shape','fields','mixed'):
out[kind+'_remainder_norm'] = norm(num(r,kind+'_remainder_norm') for r in rs)
out['linear_dot_'+kind] = math.fsum(num(r,'linear_dot_'+kind) for r in rs)
for name in ('linear_prediction_norm','full_trial_norm','heuristic_roundoff_scale'):
out[name] = norm(num(r,name) for r in rs)
for name in ('merit_identity_error','projection_decomposition_error'):
out[name] = math.fsum(num(r,name) for r in rs)
for a,b in (('full','shape'),('full','fields'),('full','mixed'),('shape','fields'),('shape','mixed'),('fields','mixed')):
dot = 0.
for r in rs:
na,nb = num(r,a+'_remainder_norm'),num(r,b+'_remainder_norm')
if na == 0 or nb == 0: continue
dot += na*nb*num(r,a+'_'+b+'_cosine')
out[a+'_'+b+'_cosine'] = ratio(dot, out[a+'_remainder_norm']*out[b+'_remainder_norm'])
out['linear_curvature_cross_term'] = 2*out['linear_dot_full']
out['curvature_squared_term'] = out['full_remainder_norm']**2
out['merit_curvature_total'] = out['linear_curvature_cross_term']+out['curvature_squared_term']
return out
def analyze_case(case):
summary_path = case/'shape_curvature_summary.csv'
if not summary_path.exists(): return {'status':'pending'}
summary = {r['metric']:num(r,'value') for r in rows(summary_path)}
required = ('preparations','accepted_normalized_state_change','accepted_residual_change','accepted_physical_state_change')
if not all(k in summary for k in required): return {'status':'incomplete', 'summary':summary}
names = ('preparations','restorations','directions','actions','bounds','remainders','mixed')
data = {name:rows(case/f'shape_curvature_{name}.csv') for name in names}
result = {'status':'complete','summary':summary,'sha256':{p.name:hashlib.sha256(p.read_bytes()).hexdigest() for p in case.glob('shape_curvature_*.csv')}}
input_checks=case/'frozen_input_checks.csv'
if input_checks.exists(): result['input_checks']={r['metric']:num(r,'value') for r in rows(input_checks)}
replay=case/'replayed_candidate.csv'
if replay.exists(): result['replayed_candidate']=rows(replay)
checks = result['checks'] = {}
checks['accepted_vectors_unchanged'] = all(summary[k] == 0 for k in required[1:])
checks['direction_split_exact'] = summary['direction_split_error_norm'] == 0
checks['action_split_within_recorded_heuristic'] = summary['action_linearity_within_heuristic'] == 1
checks['preparation_count_matches_events'] = summary['preparations'] == len(data['preparations'])
checks['all_preparations_admissible'] = all(flag(r,'admissible') for r in data['preparations'])
result['rejections'] = [r for r in data['preparations'] if not flag(r,'admissible')]
checks['all_state_increments_resolved'] = all(flag(r,'state_increment_resolved') for r in data['remainders'])
checks['all_alpha_squared_valid'] = all(flag(r,'alpha_squared_valid') for r in data['remainders'])
result['max_restoration_difference'] = max((num(r,'restoration_difference_norm') for r in data['restorations']),default=0)
result['actions'] = data['actions']
result['directions'] = data['directions']
drift = defaultdict(float)
for r in data['restorations']: drift[r['block']] = max(drift[r['block']],num(r,'restoration_difference_norm'))
for r in data['remainders']:
r['effective_floor'] = max(num(r,'heuristic_roundoff_scale'),8*drift[r['block']])
r['effective_floor_ratio'] = ratio(num(r,'remainder_norm'),r['effective_floor'])
r['robustly_above_floor'] = flag(r,'admissible') and flag(r,'state_increment_resolved') and flag(r,'alpha_squared_valid') and r['effective_floor_ratio'] >= 10
gravity = lambda r: r['block'] in ('gravity_gradient_relation', 'poisson_balance')
affine = [r for r in data['remainders'] if r['direction']=='fields' and gravity(r)]
result['fixed_shape_gravity'] = {
'blocks':sorted({r['block'] for r in affine}),
'max_remainder':max((num(r,'remainder_norm') for r in affine),default=math.nan),
'max_effective_floor_ratio':max((r['effective_floor_ratio'] for r in affine),default=math.nan),
'all_below_recorded_or_restoration_floor': bool(affine) and all(r['effective_floor_ratio'] <= 1 for r in affine),
'rows':affine,
}
by_direction_block = defaultdict(list)
for r in data['remainders']: by_direction_block[r['direction'],r['block']].append(r)
slopes = []
for (direction,block),rs in by_direction_block.items():
rs.sort(key=lambda r:num(r,'alpha'),reverse=True)
for high,low in zip(rs,rs[1:]):
if not high['robustly_above_floor'] or not low['robustly_above_floor']: continue
slope = math.log(num(high,'remainder_norm')/num(low,'remainder_norm'))/math.log(num(high,'alpha')/num(low,'alpha'))
slopes.append({'direction':direction,'block':block,'alpha_high':num(high,'alpha'),'alpha_low':num(low,'alpha'),'slope':slope,'high_floor_ratio':high['effective_floor_ratio'],'low_floor_ratio':low['effective_floor_ratio']})
result['resolved_slopes'] = slopes
by_alpha = defaultdict(list)
mixed_by_block = defaultdict(list)
block_identity_ratios, block_projection_ratios = [], []
for r in data['mixed']:
by_alpha[num(r,'alpha')].append(r)
mixed_by_block[r['block']].append(r)
if not flag(r,'all_trials_admissible'): continue
den = num(r,'full_trial_norm')**2 + num(r,'linear_prediction_norm')**2 + 2*abs(num(r,'linear_dot_full')) + num(r,'full_remainder_norm')**2
block_identity_ratios.append(ratio(abs(num(r,'merit_identity_error')),den))
pden = math.fsum(abs(num(r,'linear_dot_'+k)) for k in ('full','shape','fields','mixed'))
if pden > 0: block_projection_ratios.append(ratio(abs(num(r,'projection_decomposition_error')),pden))
for block,rs in mixed_by_block.items():
rs.sort(key=lambda r:num(r,'alpha'),reverse=True)
for high,low in zip(rs,rs[1:]):
floor_high=max(num(high,'heuristic_roundoff_scale'),24*drift[block])
floor_low=max(num(low,'heuristic_roundoff_scale'),24*drift[block])
def robust(r,floor):
return all(flag(r,key) for key in ('all_trials_admissible','all_state_increments_resolved','alpha_squared_valid')) and num(r,'mixed_remainder_norm') >= 10*floor
if not robust(high,floor_high) or not robust(low,floor_low): continue
slopes.append({'direction':'mixed','block':block,'alpha_high':num(high,'alpha'),'alpha_low':num(low,'alpha'),'slope':math.log(num(high,'mixed_remainder_norm')/num(low,'mixed_remainder_norm'))/math.log(num(high,'alpha')/num(low,'alpha')),'high_floor_ratio':ratio(num(high,'mixed_remainder_norm'),floor_high),'low_floor_ratio':ratio(num(low,'mixed_remainder_norm'),floor_low)})
aggregates = []
identity_ratios,projection_ratios = [],[]
for alpha,rs in sorted(by_alpha.items(),reverse=True):
item = {'alpha':alpha,'all_admissible':all(flag(r,'all_trials_admissible') for r in rs),'all_resolved':all(flag(r,'all_state_increments_resolved') for r in rs)}
if item['all_admissible']:
item.update(signed_aggregate(rs))
item['base_residual_norm'] = summary['base_residual_norm']
item['trial_over_base'] = ratio(item['full_trial_norm'],summary['base_residual_norm'])
item['linear_over_base'] = ratio(item['linear_prediction_norm'],summary['base_residual_norm'])
item['full_remainder_over_alpha_squared'] = ratio(item['full_remainder_norm'],alpha**2)
full_rows = [r for r in data['remainders'] if r['direction']=='full' and num(r,'alpha') == alpha]
item['full_effective_floor_norm'] = norm(r['effective_floor'] for r in full_rows)
item['full_effective_floor_ratio'] = ratio(item['full_remainder_norm'],item['full_effective_floor_norm'])
item['gravity'] = signed_aggregate([r for r in rs if gravity(r)])
item['gravity_full_remainder_squared_fraction'] = ratio(item['gravity']['full_remainder_norm']**2,item['full_remainder_norm']**2)
item['blocks'] = rs
den = item['full_trial_norm']**2+item['linear_prediction_norm']**2+2*abs(item['linear_dot_full'])+item['full_remainder_norm']**2
item['merit_identity_relative_error'] = ratio(abs(item['merit_identity_error']),den)
pden = math.fsum(abs(item['linear_dot_'+k]) for k in ('full','shape','fields','mixed'))
item['projection_decomposition_relative_error'] = ratio(abs(item['projection_decomposition_error']),pden)
identity_ratios.append(item['merit_identity_relative_error'])
projection_ratios.append(item['projection_decomposition_relative_error'])
aggregates.append(item)
result['alphas'] = aggregates
result['max_merit_identity_relative_error'] = max(identity_ratios,default=math.nan)
result['max_projection_decomposition_relative_error'] = max(projection_ratios,default=math.nan)
result['max_block_merit_identity_relative_error'] = max(block_identity_ratios,default=math.nan)
result['max_block_projection_decomposition_relative_error'] = max(block_projection_ratios,default=math.nan)
result['remainder_rows'] = data['remainders']
return result
def make_markdown(results,provenance):
lines = ['# Shape-curvature analysis','',f"Focused executable source `{provenance.get('commit','unknown')}`; binary SHA-256 `{provenance.get('binary_sha256','unknown')}`.",'',
'This analysis reads the saved full, shape-only, and fields-only residual probes. Norms and projections use the existing normalization. The mixed remainder is a vector subtraction before its norm. No solver was run by this analysis.','',
'Source expectation: gravity has the form Fg=M(q)g+BᵀPhi and Fp=B g−S(q)rho. With q fixed, it is affine in all remaining fields; shape dependence and products of shape changes with field changes can supply second-order remainders. See [residual assembly](/Users/tboudreaux/Programming/4DSTAR/MeanField/libmeanfield/impl/operators/gravity_field.cpp:415) and [Jacobian shape/source actions](/Users/tboudreaux/Programming/4DSTAR/MeanField/libmeanfield/impl/operators/gravity_field_jacobian.cpp:242).','',
'Arithmetic checks use the reported preparation history and the final restoration drift. Slopes are reported only when both adjacent residual remainders exceed the larger of the recorded floor and eight times final restoration drift by at least 10×, and both increments are resolved. These floors are heuristic, not certified assembly-error bounds.','']
lines += ['## Common-cap comparison','',
'The signed columns below divide squared-merit contributions by each case’s own ‖F0‖². Their sum equals (‖Ftrial‖²−‖L‖²)/‖F0‖². They are evaluated at each direction’s common admissible cap; these are generally different alpha values.','',
'| case | alpha | ‖Ftrial‖/‖F0‖ | 2<L,Rs>/F0² | 2<L,Rf>/F0² | 2<L,Rm>/F0² | ‖Rfull‖²/F0² | total curvature/F0² |',
'|---|---:|---:|---:|---:|---:|---:|---:|']
for name,result in results.items():
if result['status'] != 'complete':
lines += [f"| {name} | {result['status']} | | | | | | |"]
continue
if not result['alphas'] or not result['alphas'][0]['all_admissible']:
lines += [f"| {name} | no complete common-cap triple | | | | | | |"]
continue
a=result['alphas'][0]; den=result['summary']['base_residual_norm']**2
lines += [f"| {name} | {a['alpha']:.7g} | {a['trial_over_base']:.8g} | {2*a['linear_dot_shape']/den:.7g} | {2*a['linear_dot_fields']/den:.7g} | {2*a['linear_dot_mixed']/den:.7g} | {a['curvature_squared_term']/den:.7g} | {a['merit_curvature_total']/den:.7g} |"]
lines += ['']
if all(r['status']=='complete' for r in results.values()):
lines += ['Across these three saved directions, every one of the shape helper’s 66 preparations is admissible and every accepted-vector change and restoration drift is exactly zero. The coupled gravity Taylor remainders have resolved slopes near two, while fields-only gravity remainders stay at approximately 2.5e-14–4.0e-14. This supports geometry–field curvature along the tested directions; no first-derivative inconsistency is visible in the resolved regime. The numerical floor is heuristic: the n1.5 and n3 fields-only maxima exceed it by factors 1.04 and 1.39, which is recorded explicitly.','',
'The practical mechanisms differ. The rotating n1 direction is blocked at its original full step by pressure admissibility; its sampled safe interval decreases the residual. The n1.5 full step is admissible and is strongly rejected by curvature in the residual metric. The n3 saved direction encounters both an enthalpy restriction and a curvature penalty at the largest tested safe step, but smaller sampled steps improve the residual. Its originating linear solve also missed the 3% gate.','']
lines += ['## First-order gravity cancellation','',
'These are cancellations between the two parts of each selected correction. They are not condition-number or symmetry measurements.','',
'| case | block | norm(Jshape) | norm(Jfields) | norm(Jfull) | shape/fields cosine |',
'|---|---|---:|---:|---:|---:|']
for name,result in results.items():
if result['status'] != 'complete': continue
for r in result['actions']:
if r['block'] not in ('gravity_gradient_relation','poisson_balance'): continue
lines += [f"| {name} | {r['block']} | {num(r,'shape_action_norm'):.8g} | {num(r,'fields_action_norm'):.8g} | {num(r,'full_action_norm'):.8g} | {num(r,'shape_fields_cosine'):.12g} |"]
lines += ['']
for name,result in results.items():
lines += ['## '+name,'']
if result['status'] != 'complete':
lines += ['Status: '+result['status']+'.','']; continue
s=result['summary']; checks=result['checks']; aff=result['fixed_shape_gravity']
if name=='n1_J0p10_iteration_2':
lines += ['**Interpretation:** the sampled admissible interval improves the residual, although mixed curvature makes it worse than the linear prediction. The original full-step pressure rejection is a separate admissibility event. The largest-step mixed signed projection is adverse, while the shape-only signed projection is favorable. The last alpha is limited by residual arithmetic and does not diagnose loss of second-order consistency.','']
elif name=='n1p5_J0_iteration_1':
lines += ['**Interpretation:** this is an admissible, curvature-driven merit rejection. At alpha=1 the linear prediction almost removes the residual, while the actual residual grows by over 12×. Alpha=.1 remains merit-worse; alpha=.01 improves it. Gravity shape and mixed remainders dominate despite partial cancellation. Their squared remainder term dominates the full-step merit cost; at alpha=.1 both shape and mixed signed projections are adverse. The fields-only gravity maximum exceeds the chosen heuristic floor by only 4.2%, remaining at approximately 3e-14 absolute; that minor heuristic exceedance is not evidence of nonlinear fixed-shape gravity.','']
elif name=='n3_J0_seed_R160_curvature':
lines += ['**Interpretation:** the largest common safe step is admissible but merit-worse; the next decade improves the residual. The mixed remainder is the largest component and is strongly aligned with the full remainder. Its signed projection onto the linear prediction is nevertheless favorable at the largest alpha, as is the fields-only projection: the adverse total curvature cost comes from the squared remainder norm overwhelming those favorable cross terms. At smaller resolved alphas the net curvature slightly improves the linear prediction. The saved R160 direction retains true linear defect 0.0402590213 and had failed the 0.03 gate; this probe is not a live Newton convergence result. Fields-only gravity stays below 4e-14 absolute, up to 1.393× the heuristic floor.','']
lines += [f"Frozen ‖F‖={s['base_residual_norm']:.9g}; ‖p‖={s['full_direction_norm']:.9g}; common alpha cap={s['common_alpha_cap']:.9g}. {int(s['preparations'])} preparations. Accepted-vector changes are all zero: **{checks['accepted_vectors_unchanged']}**. Direction split exact: **{checks['direction_split_exact']}**. Jacobian split error {s['action_split_error_norm']:.3e}, recorded scale {s['action_linearity_heuristic_scale']:.3e}; maximum per-block restoration drift {result['max_restoration_difference']:.3e}.",'',
f"All preparations admissible: **{checks['all_preparations_admissible']}**; all increments resolved: **{checks['all_state_increments_resolved']}**. Fields-only gravity maximum remainder {aff['max_remainder']:.3e}, maximum ratio to the effective heuristic floor {aff['max_effective_floor_ratio']:.3e}. Every sampled fields-only gravity remainder lies below that floor: **{aff['all_below_recorded_or_restoration_floor']}**.",'',
f"Maximum aggregate merit-identity relative error {result['max_merit_identity_relative_error']:.3e}; projection-decomposition relative error {result['max_projection_decomposition_relative_error']:.3e}.",'',
'| alpha | ‖Ftrial‖/‖F0‖ | ‖L‖/‖F0‖ | ‖Rfull‖/alpha² | full R / floor | ‖Rs‖ | ‖Rf‖ | ‖Rm‖ | gravity share of ‖Rfull‖² |','|---:|---:|---:|---:|---:|---:|---:|---:|---:|']
for a in result['alphas']:
if not a['all_admissible']: lines += [f"| {a['alpha']:.6g} | inadmissible | | | | | | | |"]
else: lines += [f"| {a['alpha']:.6g} | {a['trial_over_base']:.8g} | {a['linear_over_base']:.8g} | {a['full_remainder_over_alpha_squared']:.8g} | {a['full_effective_floor_ratio']:.3g} | {a['shape_remainder_norm']:.4e} | {a['fields_remainder_norm']:.4e} | {a['mixed_remainder_norm']:.4e} | {a['gravity_full_remainder_squared_fraction']:.6%} |"]
lines += ['', '| alpha | 2<L,Rs> | 2<L,Rf> | 2<L,Rm> | ‖Rfull‖² | curvature contribution to squared merit |','|---:|---:|---:|---:|---:|---:|']
for a in result['alphas']:
if a['all_admissible']: lines += [f"| {a['alpha']:.6g} | {2*a['linear_dot_shape']:.7e} | {2*a['linear_dot_fields']:.7e} | {2*a['linear_dot_mixed']:.7e} | {a['curvature_squared_term']:.7e} | {a['merit_curvature_total']:.7e} |"]
lines += ['', 'Positive signed terms increase the squared residual relative to the linear prediction; negative terms decrease it. The quadratic remainder-norm term contains component cross terms, so the signed projections alone are not a unique additive attribution of the entire curvature cost.','',
'| direction | residual block | resolved intervals | slope range | smallest resolved alpha |','|---|---|---:|---:|---:|']
grouped_slopes=defaultdict(list)
for p in result['resolved_slopes']: grouped_slopes[p['direction'],p['block']].append(p)
for (direction,block),ps in grouped_slopes.items():
lines += [f"| {direction} | {block} | {len(ps)} | {min(p['slope'] for p in ps):.6f}–{max(p['slope'] for p in ps):.6f} | {min(p['alpha_low'] for p in ps):.3g} |"]
lines += ['']
lines += ['## Discriminating next experiment','',
'Where the full Jacobian action is small because shape and field actions cancel, suppressing the shape component alone would destroy that first-order balance. A targeted hypothesis is that the current straight Newton trial loses the geometry–gravity balance at second order. Test it by holding each saved trial’s shape, density, and other material/scalar coordinates fixed, solving only its linear gravity subsystem to a documented true defect, and comparing the completed trial against the original one. Record all residual blocks, the new hydrostatic/force response, interior diagnostics and virial; lowering the gravity residual alone is insufficient. This is a proposed diagnostic, not a demonstrated solver improvement or a recommendation to promote a new policy.','',
'The completed comparison should preserve the original direction and trial as a control, disclose solve cost, and separate the pressure-admissibility obstruction from residual-metric rejection. A linear one-way preconditioner’s previous failure does not test this nonlinear conditional completion. These probes do not justify clipping enthalpy or deleting shape coordinates.','',
'Before promoting such a change, localize the important remainder and signed-merit contributions by material and exterior element assembly, preserving their shared-DOF cross terms. The existing aggregate gravity norms cannot tell whether an apparently large improvement benefits the interior or mainly removes exterior error. Interior consistency and virial ≤1e-5 remain the promotion criteria.','',
'## Interpretation limits','',
'A resolved second-order remainder, first-order decay of its difference-quotient error, and arithmetic-level fields-only gravity remainder support geometric/field curvature along these particular directions. They do not prove every Jacobian column correct. Jacobian additivity tests linearity and restoration, not agreement with the nonlinear derivative by itself.','',
'These residual-block aggregates do not separate interior from exterior elements. They neither locate a physical error nor identify a symmetry or near-null mode. A large correction or curvature may coexist with a weak normalized direction, but this split is not a condition-number measurement. No new model was accepted and these probes do not establish a virial milestone.','']
return '\n'.join(lines)
def main():
ap=argparse.ArgumentParser(description=__doc__)
ap.add_argument('--campaign',type=Path,default=Path(__file__).resolve().parent,
help='Campaign root containing focused/; writes shape_checks.json and shape_analysis.md here. Defaults to the script directory.')
args=ap.parse_args()
campaign=args.campaign; focused=campaign/'focused'
provenance=json.loads((focused/'provenance.json').read_text())
results={case:analyze_case(focused/case) for case in CASES}
out={'schema_version':1,'source_commit':provenance.get('commit'),'binary_sha256':provenance.get('binary_sha256'),'cases':results}
(campaign/'shape_checks.json').write_text(json.dumps(clean(out),indent=2,allow_nan=False)+'\n')
(campaign/'shape_analysis.md').write_text(make_markdown(results,provenance))
for name,r in results.items(): print(name,r['status'])
if __name__=='__main__':main()

View File

@@ -0,0 +1,143 @@
"""Audit the pressure/shape campaign's completed outputs without running solvers.
The sole argument is the campaign directory containing captures/ and focused/.
The script reads existing results and writes focused_audit_checks.json there.
Unfinished cases are reported as pending; this is an observation audit, not a
stellar convergence or physical acceptance gate.
"""
import argparse
import csv
import hashlib
import json
import math
from pathlib import Path
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("campaign_root", type=Path)
ROOT = parser.parse_args().campaign_root.resolve()
FOCUSED = ROOT / "focused"
def rows(path):
with path.open() as source:
result = list(csv.DictReader(source))
if any(None in r or any(v is None for v in r.values()) for r in result):
raise ValueError(f"Malformed CSV row: {path}")
return result
def metrics(path):
data = rows(path)
if len({r["metric"] for r in data}) != len(data):
raise ValueError(f"Duplicate metric: {path}")
return {r["metric"]: float(r["value"]) for r in data}
def sha(path):
return hashlib.sha256(path.read_bytes()).hexdigest()
def clean(value):
if isinstance(value, dict):
return {k: clean(v) for k, v in value.items()}
if isinstance(value, list):
return [clean(v) for v in value]
if isinstance(value, float) and not math.isfinite(value):
return None
return value
provenance = json.loads((FOCUSED / "provenance.json").read_text())
result = {
"source_commit": provenance["commit"],
"binary_hash_matches": sha(FOCUSED / "plateau_diagnostic") == provenance["binary_sha256"],
"completed": [],
"pending": [],
}
for case in json.loads((FOCUSED / "cases.json").read_text()):
case_id = case["id"]
manifest = FOCUSED / f"{case_id}_run.json"
if not manifest.exists():
result["pending"].append(case_id)
continue
run = json.loads(manifest.read_text())
if "exit_code" not in run:
result["pending"].append(case_id)
continue
folder = FOCUSED / case_id
data = {
"id": case_id,
"exit_code": run["exit_code"],
"seconds": run["seconds"],
"input_hash_count": len(run["input_state_hashes"]),
"input_hash_mismatches": [str(p) for p, h in run["input_state_hashes"].items() if sha(Path(p)) != h],
"output_hash_count": len(run["output_hashes"]),
"output_hash_mismatches": [p for p, h in run["output_hashes"].items() if sha(folder / p) != h],
"unhashed_data_files": sorted(str(p.relative_to(folder)) for p in folder.rglob("*") if p.is_file() and p.suffix in (".txt", ".csv", ".json") and str(p.relative_to(folder)) not in run["output_hashes"]),
"csv_rows": {p.name: len(rows(p)) for p in sorted(folder.glob("*.csv"))},
"frozen": metrics(folder / "frozen_input_checks.csv"),
}
args = case["arguments"]
direction = Path(args[args.index("--probe-direction") + 1])
physical = Path(args[args.index("--probe-state") + 1])
data["direction_copy_byte_identical"] = direction.read_bytes() == (folder / "probe_normalized_correction.txt").read_bytes()
data["physical_base_copy_byte_identical"] = physical.read_bytes() == (folder / "frozen_physical_base.txt").read_bytes()
if "iteration_" in case_id:
iteration = int(case_id.rsplit("_", 1)[1])
capture_root = ROOT / "captures" / ("n1p5_J0_capture" if case_id.startswith("n1p5") else "n1_J0p10_capture")
capture_manifest = json.loads((capture_root.parent / f"{capture_root.name}_run.json").read_text())
data["auxiliary_capture_input_hashes_match"] = all(sha(physical.parent / name) == capture_manifest["output_hashes"][str((physical.parent / name).relative_to(capture_root))] for name in ("normalized_base.txt", "normalized_residual.txt"))
old = next(r for r in rows(capture_root / "newton.csv") if int(r["iteration"]) == iteration)
data["historical_linear_defect_relative"] = float(old["linear_true"])
trial = next(r for r in rows(capture_root / "trials.csv") if int(r["iteration"]) == iteration and r["trial"] == "0")
data["original_first_trial"] = trial
replay = rows(folder / "replayed_candidate.csv")[0]
data["candidate_replay"] = replay
if trial["disposition"] == "4":
data["replayed_candidate_residual_exact"] = replay["admissible"] == "1" and replay["residual_norm"] == trial["F"]
data["replayed_candidate_residual_difference"] = float(replay["residual_norm"]) - float(trial["F"])
data["replayed_candidate_minimum_jacobian_exact"] = replay["minimum_jacobian"] == trial["min_jacobian"]
else:
previous = physical.parent
data["historical_linear_defect_relative"] = float(next(r for r in rows(previous / "frozen_linear.csv") if r["restart"] == "160")["true_relative"])
data["linear_defect_relative_difference"] = data["frozen"]["linear_defect_relative"] - data["historical_linear_defect_relative"]
layout = rows(folder / "layout.csv")
residual_blocks = {r["block"] for r in layout if r["kind"] == "residual"}
value_blocks = {r["block"] for r in layout if r["kind"] == "value"}
nr = sum(r["kind"] == "residual" for r in layout)
nv = sum(r["kind"] == "value" for r in layout)
if "--shape-curvature" in args:
data["shape_summary"] = metrics(folder / "shape_curvature_summary.csv")
expected = {"shape_curvature_directions.csv": 3 * nv, "shape_curvature_actions.csv": nr, "shape_curvature_preparations.csv": 22, "shape_curvature_restorations.csv": 4 * nr, "shape_curvature_remainders.csv": 18 * nr, "shape_curvature_mixed.csv": 6 * nr}
data["shape_row_count_mismatches"] = {f: [data["csv_rows"][f], count] for f, count in expected.items() if data["csv_rows"][f] != count}
restorations = rows(folder / "shape_curvature_restorations.csv")
data["shape_max_restoration_difference"] = max(float(r["restoration_difference_norm"]) for r in restorations)
mixed = rows(folder / "shape_curvature_mixed.csv")
alphas = {r["alpha"] for r in mixed}
remainders = rows(folder / "shape_curvature_remainders.csv")
data["shape_six_alphas"] = len(alphas) == 6
data["shape_mixed_keys_complete"] = {(r["alpha"], r["block"]) for r in mixed} == {(a, b) for a in alphas for b in residual_blocks}
data["shape_remainder_keys_complete"] = {(r["alpha"], r["direction"], r["block"]) for r in remainders} == {(a, d, b) for a in alphas for d in ("full", "shape", "fields") for b in residual_blocks}
data["shape_direction_keys_complete"] = {(r["direction"], r["value_block"]) for r in rows(folder / "shape_curvature_directions.csv")} == {(d, b) for d in ("full", "shape", "fields") for b in value_blocks}
data["shape_max_absolute_merit_identity_error"] = max(abs(float(r["merit_identity_error"])) for r in mixed)
data["shape_max_absolute_projection_decomposition_error"] = max(abs(float(r["projection_decomposition_error"])) for r in mixed)
data["shape_all_trials_admissible"] = all(r["all_trials_admissible"] == "1" for r in mixed)
data["shape_all_increments_resolved"] = all(r["all_state_increments_resolved"] == "1" for r in mixed)
data["shape_all_alpha_squared_valid"] = all(r["alpha_squared_valid"] == "1" for r in mixed)
if "--pressure-probe" in args:
data["pressure_summary"] = metrics(folder / "pressure_obstruction_summary.csv")
pressure = rows(folder / "pressure_obstruction_trials.csv")
blocks = rows(folder / "pressure_obstruction_blocks.csv")
data["pressure_admissible_trials"] = sum(r["admissible"] == "1" for r in pressure)
data["pressure_expected_block_rows"] = nr * data["pressure_admissible_trials"]
data["pressure_block_rows_complete"] = len(blocks) == data["pressure_expected_block_rows"]
data["pressure_trial_keys_unique"] = len({(r["direction"], r["alpha"]) for r in pressure}) == len(pressure)
data["pressure_block_keys_complete"] = {(r["direction"], r["alpha"], r["block"]) for r in blocks} == {(r["direction"], r["alpha"], b) for r in pressure if r["admissible"] == "1" for b in residual_blocks}
data["pressure_limiting_h_prediction_max_absolute_error"] = {
family: max(abs(float(r[f"{family}_limiting_h_prediction"]) - float(r[f"{family}_limiting_h_evaluated"])) for r in pressure)
for family in ("closure", "pressure")
}
result["completed"].append(data)
(ROOT / "focused_audit_checks.json").write_text(json.dumps(clean(result), indent=2) + "\n")
print(json.dumps({"completed": [r["id"] for r in result["completed"]], "pending": result["pending"]}, indent=2))

View File

@@ -0,0 +1,167 @@
"""Audit completed supplemental surface observations without running solvers.
Pass the campaign directory containing captures/, focused/, supplemental/ and
supplemental_inputs/. Writes surface_audit_checks.json in that directory.
This records method invariants and sampled values, not physical acceptance.
"""
import argparse
import csv
import hashlib
import json
import math
from pathlib import Path
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("campaign_root", type=Path)
ROOT = parser.parse_args().campaign_root.resolve()
SUPPLEMENTAL = ROOT / "supplemental"
def sha(path):
return hashlib.sha256(path.read_bytes()).hexdigest()
def rows(path):
with path.open() as source:
data = list(csv.DictReader(source))
if any(None in r or any(v is None for v in r.values()) for r in data):
raise ValueError(f"Malformed CSV row: {path}")
return data
def metrics(path):
data = rows(path)
if len({r["metric"] for r in data}) != len(data):
raise ValueError(f"Duplicate metric: {path}")
return {r["metric"]: float(r["value"]) for r in data}
def vector(path):
tokens = path.read_text().split()
data = list(map(float, tokens[1:]))
if len(data) != int(tokens[0]) or not all(map(math.isfinite, data)):
raise ValueError(f"Malformed/nonfinite vector: {path}")
return data
def clean(value):
if isinstance(value, dict):
return {k: clean(v) for k, v in value.items()}
if isinstance(value, list):
return [clean(v) for v in value]
if isinstance(value, float) and not math.isfinite(value):
return None
return value
provenance = json.loads((SUPPLEMENTAL / "provenance.json").read_text())
derivation = json.loads((ROOT / "supplemental_inputs/between_bounds_derivation.json").read_text())
base = vector(Path(derivation["normalized_base"]))
direction = vector(Path(derivation["direction"]))
candidate = vector(Path(derivation["candidate"]))
if not len(base) == len(direction) == len(candidate):
raise ValueError("Midpoint candidate vector dimensions differ")
alpha = derivation["alpha"]
bounds = metrics(ROOT / "focused/n1_J0p10_iteration_2/frozen_enthalpy_bound.csv")
result = {
"source_commit": provenance["commit"],
"binary_hash_matches": sha(SUPPLEMENTAL / "plateau_diagnostic") == provenance["binary_sha256"],
"midpoint": {
"alpha": alpha,
"vector_size": len(candidate),
"maximum_reconstruction_error": max(abs(b + alpha * d - c) for b, d, c in zip(base, direction, candidate)),
"candidate_hash_matches": sha(Path(derivation["candidate"])) == derivation["candidate_sha256"],
"bounds_match_original_scan": bounds["closure_alpha_h"] == derivation["closure_bound"] and bounds["pressure_alpha_h"] == derivation["pressure_bound"],
"strictly_between_bounds": derivation["closure_bound"] < alpha < derivation["pressure_bound"],
"exact_arithmetic_midpoint": alpha == 0.5 * (derivation["closure_bound"] + derivation["pressure_bound"]),
},
"completed": [],
"pending": [],
}
for case in json.loads((SUPPLEMENTAL / "cases.json").read_text()):
case_id = case["id"]
manifest = SUPPLEMENTAL / f"{case_id}_run.json"
if not manifest.exists():
result["pending"].append(case_id)
continue
run = json.loads(manifest.read_text())
if "exit_code" not in run:
result["pending"].append(case_id)
continue
folder = SUPPLEMENTAL / case_id
args = case["arguments"]
physical_path = Path(args[args.index("--probe-state") + 1])
direction_path = Path(args[args.index("--probe-direction") + 1])
capture_index = int(physical_path.parent.name.split("_")[-1])
original_focused = ROOT / "focused" / f"n1_J0p10_iteration_{capture_index}"
capture_root = physical_path.parent.parent
capture_manifest = json.loads((capture_root.parent / f"{capture_root.name}_run.json").read_text())
summary = metrics(folder / "surface_trace_summary.csv")
carrier = rows(folder / "surface_trace_rows.csv")
limits = rows(folder / "surface_trace_limits.csv")
nodes = rows(folder / "surface_trace_nodes.csv")
physical_base = vector(physical_path)
normalized_direction = vector(direction_path)
capture_metadata = dict(line.split("=", 1) for line in (physical_path.parent / "capture.txt").read_text().splitlines())
replay_alpha = alpha if "between_bounds" in case_id else float(capture_metadata["first_trial_alpha"])
data = {
"id": case_id,
"exit_code": run["exit_code"],
"seconds": run["seconds"],
"input_hash_count": len(run["input_state_hashes"]),
"input_hash_mismatches": [p for p, h in run["input_state_hashes"].items() if sha(Path(p)) != h],
"output_hash_count": len(run["output_hashes"]),
"output_hash_mismatches": [p for p, h in run["output_hashes"].items() if sha(folder / p) != h],
"unhashed_data_files": sorted(str(p.relative_to(folder)) for p in folder.rglob("*") if p.is_file() and p.suffix in (".txt", ".csv", ".json") and str(p.relative_to(folder)) not in run["output_hashes"]),
"auxiliary_capture_input_hashes_match": all(sha(physical_path.parent / name) == capture_manifest["output_hashes"][str((physical_path.parent / name).relative_to(capture_root))] for name in ("normalized_base.txt", "normalized_residual.txt")),
"frozen": metrics(folder / "frozen_input_checks.csv"),
"frozen_residual_byte_identical_to_original_focus": (folder / "frozen_normalized_residual.txt").read_bytes() == (original_focused / "frozen_normalized_residual.txt").read_bytes(),
"enthalpy_bound_byte_identical_to_original_focus": (folder / "frozen_enthalpy_bound.csv").read_bytes() == (original_focused / "frozen_enthalpy_bound.csv").read_bytes(),
"direction_copy_byte_identical": direction_path.read_bytes() == (folder / "probe_normalized_correction.txt").read_bytes(),
"physical_base_copy_byte_identical": physical_path.read_bytes() == (folder / "frozen_physical_base.txt").read_bytes(),
"candidate_replay": rows(folder / "replayed_candidate.csv")[0],
"surface_summary": summary,
"csv_rows": {p.name: len(rows(p)) for p in sorted(folder.glob("*.csv"))},
"carrier_rows_complete": len(carrier) == int(summary["surface_row_count"]) and {int(r["surface_index"]) for r in carrier} == set(range(len(carrier))),
"carrier_reduced_indices_unique": len({r["reduced_h_index"] for r in carrier}) == len(carrier),
"carrier_root_offsets_correct": all(int(r["root_index"]) == int(summary["h_root_offset"]) + int(r["reduced_h_index"]) for r in carrier),
"carrier_base_matches_physical_input": all(float(r["base_h"]) == physical_base[int(r["root_index"])] for r in carrier),
"all_input_carrier_normalized_direction_coefficients_zero": all(normalized_direction[int(r["root_index"])] == 0 for r in carrier),
"carrier_unit_step_arithmetic_error": max(abs(float(r["base_h"]) + float(r["direction_dh"]) - float(r["h_plus_dh"])) for r in carrier),
"carrier_recomputed": {},
"carrier_summary_differences": {},
"limit_families_complete": {r["family"] for r in limits} == {"closure", "pressure"} and len(limits) == 2,
"limit_partition_max_errors": {},
"nodes_by_family": {},
}
for name in ("base_h", "direction_dh", "h_plus_dh"):
values = [float(r[name]) for r in carrier]
data["carrier_recomputed"][name] = {
"minimum": min(values), "maximum": max(values), "maximum_absolute": max(map(abs, values)),
"norm": math.sqrt(math.fsum(v * v for v in values)),
"negative_count": sum(v < 0 for v in values), "nonzero_count": sum(v != 0 for v in values),
}
data["carrier_summary_differences"][name] = {stat: value - summary[f"{name}_{stat}"] for stat, value in data["carrier_recomputed"][name].items()}
for suffix in ("h", "dh", "h_plus_dh"):
data["limit_partition_max_errors"][suffix] = max(abs(float(r[f"full_{suffix}"]) - float(r[f"boundary_{suffix}"]) - float(r[f"interior_{suffix}"])) for r in limits)
for limit in limits:
family = limit["family"]
subset = [r for r in nodes if r["family"] == family]
data["nodes_by_family"][family] = {
"rows": len(subset),
"element_matches_limit": all(r["element"] == limit["element"] for r in subset),
"node_indices_complete": len(subset) > 0 and len(subset) == int(subset[0]["node_count"]) and {int(r["local_node"]) for r in subset} == set(range(len(subset))),
"fe_orders": sorted({int(r["fe_order"]) for r in subset}),
"geometry_types": sorted({int(r["geometry_type"]) for r in subset}),
"base_minimum": min(float(r["h"]) for r in subset),
"union_prediction_minimum": min(float(r["h_plus_alpha_union_dh"]) for r in subset),
"unit_step_minimum": min(float(r["h_plus_dh"]) for r in subset),
"replay_alpha": replay_alpha,
"replay_candidate_nodal_prediction_minimum": min(float(r["h"]) + replay_alpha * float(r["dh"]) for r in subset),
"union_prediction_max_arithmetic_error": max(abs(float(r["h"]) + float(r["alpha_union"]) * float(r["dh"]) - float(r["h_plus_alpha_union_dh"])) for r in subset),
"unit_step_max_arithmetic_error": max(abs(float(r["h"]) + float(r["dh"]) - float(r["h_plus_dh"])) for r in subset),
}
result["completed"].append(data)
(ROOT / "surface_audit_checks.json").write_text(json.dumps(clean(result), indent=2) + "\n")
print(json.dumps({"completed": [r["id"] for r in result["completed"]], "pending": result["pending"]}, indent=2))

View File

@@ -0,0 +1,218 @@
#!/usr/bin/env python3
"""Offline Q3 interpolation and bounded interior sampling; no solver or input mutation."""
import argparse
import csv
import itertools
import json
import math
from pathlib import Path
def read_rows(path):
with path.open() as stream:
return list(csv.DictReader(stream))
def weights(nodes, x):
return [math.prod((x-other)/(node-other) for other in nodes if other != node) for node in nodes]
def evaluate(axes, coefficients, point):
w = [weights(axis, x) for axis, x in zip(axes, point)]
return math.fsum(coefficients[i][j][k]*w[0][i]*w[1][j]*w[2][k]
for i, j, k in itertools.product(range(4), repeat=3))
def extrema_record():
return {'minimum': math.inf, 'minimum_reference_point': None,
'negative_samples': 0, 'negative_beyond_roundoff_samples': 0}
def main():
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument('campaign_root', nargs='?', type=Path, default=Path(__file__).resolve().parent)
args = parser.parse_args()
root = args.campaign_root.expanduser().resolve()
trajectory = {int(r['iteration']): r for r in read_rows(root/'captures/n1_J0p10_capture/newton.csv')}
case_ids = [('n1_J0p10_onset_between_bounds', 2), ('n1_J0p10_late_surface_trace', 11)]
results = []
for name, iteration in case_ids:
candidates = list(root.glob(f'**/{name}/surface_trace_nodes.csv'))
if len(candidates) != 1:
raise ValueError(f'Expected one nodal export for {name}')
folder = candidates[0].parent
raw = read_rows(candidates[0])
nodal = [r for r in raw if r['family'] == 'pressure']
closure = [r for r in raw if r['family'] == 'closure']
limits = read_rows(folder/'surface_trace_limits.csv')
if len(nodal) != 64 or any(int(r['fe_order']) != 3 or int(r['node_count']) != 64 for r in nodal):
raise ValueError(f'{name} is not the expected 64-node cubic element')
if len({r['element'] for r in nodal + closure}) != 1:
raise ValueError('This bounded follow-up expects one shared limiting element per captured state')
for field in ('local_node', 'ip_x', 'ip_y', 'ip_z', 'h', 'dh'):
if [r[field] for r in nodal] != [r[field] for r in closure]:
raise ValueError(f'Family exports disagree: {name} {field}')
axes = [sorted({float(r['ip_'+axis]) for r in nodal}) for axis in 'xyz']
if any(len(axis) != 4 or axis[0] != 0 or axis[-1] != 1 for axis in axes):
raise ValueError('Expected four distinct interpolation coordinates per unit-cube axis')
coordinates = [tuple(float(r['ip_'+axis]) for axis in 'xyz') for r in nodal]
if set(coordinates) != set(itertools.product(*axes)) or len(set(coordinates)) != 64:
raise ValueError('Export is not a complete unique tensor-product grid')
fields = []
for field in ('h', 'dh'):
values = [[[0.0 for _ in range(4)] for _ in range(4)] for _ in range(4)]
for r, point in zip(nodal, coordinates):
i, j, k = [axis.index(x) for axis, x in zip(axes, point)]
values[i][j][k] = float(r[field])
fields.append(values)
scale = max(abs(float(r[field])) for r in nodal for field in ('h', 'dh'))
tolerance = 128*math.ulp(1.0)*scale
checks = []
node_error = max(abs(evaluate(axes, coefficients, point)-float(r[field]))
for field, coefficients in zip(('h', 'dh'), fields)
for r, point in zip(nodal, coordinates))
if node_error > tolerance:
raise ValueError(f'Node interpolation validation failed: {node_error}')
for limit in limits:
point = tuple(float(limit['ip_'+axis]) for axis in 'xyz')
check = {'family': limit['family'], 'reference_point': point}
for field, coefficients in zip(('h', 'dh'), fields):
value = evaluate(axes, coefficients, point)
expected = float(limit['full_'+field])
check[field] = value
check[field+'_production'] = expected
check[field+'_absolute_disagreement'] = abs(value-expected)
if abs(value-expected) > tolerance:
raise ValueError(f'Production-point interpolation validation failed: {check}')
checks.append(check)
pressure_limit = next(r for r in limits if r['family'] == 'pressure')
pressure_x, pressure_y = [float(pressure_limit['ip_'+axis]) for axis in 'xy']
surface_line = []
for z in (1e-6, .001, .005, float(pressure_limit['ip_z'])):
point = (pressure_x, pressure_y, z)
h, dh = [evaluate(axes, values, point) for values in fields]
surface_line.append({'reference_point': point, 'h': h, 'dh': dh})
zero_faces = []
for dim, axis in enumerate('xyz'):
for side in (0.0, 1.0):
face = [r for r in nodal if float(r['ip_'+axis]) == side]
if len(face) == 16 and all(float(r['h']) == 0 and float(r['dh']) == 0 for r in face):
zero_faces.append((dim, side))
if zero_faces != [(2, 0.0)]:
raise ValueError(f'Expected the exported exact-zero face z=0; found {zero_faces}')
grids = []
for dim, axis in enumerate('xyz'):
values = {i/64 for i in range(1, 64)} | set(axes[dim][1:-1])
values.update(float(r['ip_'+axis]) for r in limits)
if dim == 2:
values.update(10**(-12+i/4) for i in range(45)) # 1e-12 through 1e-1.
grids.append(sorted(values))
union = float(nodal[0]['alpha_union'])
alphas = [('base', 0.0), ('accepted_alpha', float(trajectory[iteration]['alpha'])),
('near_below_union', .99*union), ('near_above_union', 1.01*union)]
if iteration == 11:
alphas.append(('unit_step', 1.0))
extrema = {label: extrema_record() for label, _ in alphas}
directional_bound = {'minimum_nonnegative_base_ratio': math.inf, 'reference_point': None,
'h': None, 'dh': None, 'base_negative_and_direction_decreasing_samples': 0}
wz = [weights(axes[2], z) for z in grids[2]]
# Tensor contractions avoid a 64-term sum at every point. This is an
# independent offline interpolation, validated above before use.
for x in grids[0]:
wx = weights(axes[0], x)
yz = [[[math.fsum(values[i][j][k]*wx[i] for i in range(4)) for k in range(4)]
for j in range(4)] for values in fields]
for y in grids[1]:
wy = weights(axes[1], y)
z_values = [[math.fsum(values[j][k]*wy[j] for j in range(4)) for k in range(4)] for values in yz]
for z, weights_z in zip(grids[2], wz):
h, dh = [math.fsum(values[k]*weights_z[k] for k in range(4)) for values in z_values]
point = (x, y, z)
for label, alpha in alphas:
value = h+alpha*dh
record = extrema[label]
if value < record['minimum']:
record['minimum'] = value
record['minimum_reference_point'] = point
record['negative_samples'] += value < 0
record['negative_beyond_roundoff_samples'] += value < -tolerance
if h < 0 and dh < 0:
directional_bound['base_negative_and_direction_decreasing_samples'] += 1
if h >= 0 and dh < 0 and -h/dh < directional_bound['minimum_nonnegative_base_ratio']:
directional_bound.update(minimum_nonnegative_base_ratio=-h/dh, reference_point=point, h=h, dh=dh)
base_admissible = extrema['base']['negative_samples'] == 0
for label, alpha in alphas:
record = extrema[label]
h, dh = [evaluate(axes, values, record['minimum_reference_point']) for values in fields]
error = abs(h+alpha*dh-record['minimum'])
record['minimum_direct_sum_verification_error'] = error
if error > tolerance:
raise ValueError(f'Tensor contraction/direct-sum disagreement at minimum: {error}')
directional_bound['sampled_base_admissible'] = base_admissible
directional_bound['sampled_admissible_step_bound'] = directional_bound['minimum_nonnegative_base_ratio'] if base_admissible else None
result = {'case': name, 'iteration': iteration, 'element': int(nodal[0]['element']), 'fe_order': 3,
'geometry_type': int(nodal[0]['geometry_type']), 'node_count': 64, 'axes': axes,
'zero_faces': [{'axis': 'z', 'side': 0}], 'node_interpolation_max_error': node_error,
'production_point_checks': checks, 'roundoff_scale_estimate': tolerance,
'pressure_xy_surface_line': surface_line,
'grid': {'axes': grids, 'shape': [len(g) for g in grids], 'sample_count': math.prod(map(len, grids)),
'strictly_interior': True, 'log_surface_distances': [1e-12, 1e-1]},
'union_production_bound': union, 'alphas': dict(alphas), 'sampled_extrema': extrema,
'directional_bound': directional_bound}
results.append(result)
report = {'schema_version': 1, 'method': 'Independent tensor Q3 nodal interpolation and finite interior sampling',
'limitations': ['No global positivity certificate or rigorous polynomial minimum.',
'Reported minima are over the specified finite reference-coordinate grids.',
'Direction coefficients include saved-direction reconstruction roundoff; base h does not.',
'The roundoff scale is descriptive, not a certified interpolation error bound.'], 'cases': results}
(root/'pressure_polynomial_checks.json').write_text(json.dumps(report, indent=2, allow_nan=False)+'\n')
lines = ['# Offline enthalpy polynomial sampling', '',
'Both captured limiting elements have 64 distinct tensor-product nodes and cubic enthalpy order. '
'The interpolation uses only exported physical nodal h and dh; it launches no solver and changes no inputs. '
'The complete z=0 nodal face has exactly zero h and dh in both cases.', '',
'| Iteration | Node reconstruction error | Max production-point discrepancy | Grid points |',
'|---:|---:|---:|---:|']
for c in results:
error = max(r[key] for r in c['production_point_checks'] for key in ('h_absolute_disagreement', 'dh_absolute_disagreement'))
lines.append(f"| {c['iteration']} | {c['node_interpolation_max_error']:.9g} | {error:.9g} | {c['grid']['sample_count']} |")
lines += ['', 'Each grid is strictly inside the reference cube: uniform spacing 1/64, exported interior node '
'coordinates and production-limit coordinates, plus z distances 1e-12 through 1e-1 in quarter decades '
'from the exactly zero face. Minima below are sampled minima, not global polynomial minima.', '',
'| Iteration | State | Alpha | Sampled minimum h | Reference (x,y,z) | Negative samples |',
'|---:|---|---:|---:|---|---:|']
for c in results:
for label, record in c['sampled_extrema'].items():
point = ', '.join(f'{x:.9g}' for x in record['minimum_reference_point'])
lines.append(f"| {c['iteration']} | {label} | {c['alphas'][label]:.9g} | {record['minimum']:.9g} | ({point}) | {record['negative_samples']} |")
for c in results:
bound = c['directional_bound']
if not bound['sampled_base_admissible']:
lines += ['', f"Iteration {c['iteration']} already has negative enthalpy in the saved **base** on this grid. "
'A positivity-preserving step bound from that base is therefore undefined; the JSON separately '
'reports the smallest nonnegative-base ratio only as a diagnostic. This base conclusion does '
'not depend on reconstructing the Newton direction.']
else:
lines += ['', f"Iteration {c['iteration']} has no negative base samples on this grid. The sampled directional "
f"bound is {bound['sampled_admissible_step_bound']:.12g}, versus production-rule bound {c['union_production_bound']:.12g}. "
'This remains a sampled condition, not a guarantee of positivity everywhere.']
lines += ['', 'An independent direct 64-term sum also reproduces every contracted-grid minimum to roundoff. '
'Along the pressure-limit x,y line, the offline base-field values are:', '',
'| Iteration | z reference coordinate | Base h |', '|---:|---:|---:|']
for c in results:
for row in c['pressure_xy_surface_line']:
lines.append(f"| {c['iteration']} | {row['reference_point'][2]:.12g} | {row['h']:.12g} |")
lines += ['', 'The production-point validation establishes consistency with the exported full-field values at '
'both pressure and closure limit points to roundoff. Dense sampling can prove the existence of negative '
'values when their size clearly exceeds numerical disagreement; absence of negative samples cannot '
'prove positivity throughout the element. These data diagnose pointwise enthalpy, not virial balance '
'or the origin of the selected Newton direction.', '',
'Reproduce with `python3 sample_pressure_polynomial.py /path/to/campaign_artifacts`. '
'Machine-readable grids, locations, validation errors and sample counts are in pressure_polynomial_checks.json.']
(root/'pressure_polynomial_analysis.md').write_text('\n'.join(lines)+'\n')
print(json.dumps([{'iteration': c['iteration'], 'base_minimum': c['sampled_extrema']['base']['minimum'],
'sample_count': c['grid']['sample_count'], 'base_admissible': c['directional_bound']['sampled_base_admissible']}
for c in results]))
if __name__ == '__main__':
main()