253 lines
19 KiB
Python
253 lines
19 KiB
Python
#!/usr/bin/env python3
|
|
"""Synthetic vector maps catch false Taylor-order and successful-model claims."""
|
|
import csv
|
|
import json
|
|
import math
|
|
from pathlib import Path
|
|
import tempfile
|
|
import unittest
|
|
|
|
import check_nonlinear_step as c
|
|
|
|
|
|
def csv_file(path, data):
|
|
with path.open('w',newline='') as stream:
|
|
writer=csv.DictWriter(stream,fieldnames=list(data[0]));writer.writeheader();writer.writerows(data)
|
|
|
|
|
|
def vec_file(path, data):
|
|
path.write_text(str(len(data))+'\n'+'\n'.join(repr(v) for v in data)+'\n')
|
|
|
|
|
|
def measure(f,j,t,a):
|
|
linear=[x+a*y for x,y in zip(f,j)];r=[x-y for x,y in zip(t,linear)]
|
|
fn,jn,ln,tn,rn=[math.sqrt(math.fsum(x*x for x in v)) for v in (f,j,linear,t,r)]
|
|
projection=math.fsum(x*y for x,y in zip(linear,r))
|
|
return {'base_norm':fn,'action_norm':jn,'linear_prediction_norm':ln,'residual_norm':tn,
|
|
'remainder_norm':rn,'remainder_over_alpha':rn/a,'remainder_over_alpha_squared':rn/a**2,
|
|
'linear_dot_remainder':projection,'norm_identity_error':tn**2-ln**2-2*projection-rn**2},linear,r
|
|
|
|
|
|
class NonlinearStepCheckerTests(unittest.TestCase):
|
|
def setUp(self):
|
|
self.temp=tempfile.TemporaryDirectory();self.addCleanup(self.temp.cleanup)
|
|
self.campaign=Path(self.temp.name).resolve();self.folder=self.campaign/'synthetic';self.folder.mkdir()
|
|
self.make_case()
|
|
|
|
def make_case(self,eta=.01,order=2):
|
|
folder=self.folder
|
|
f=[.3,.4,.2,.1,.05];x=[2.,3.,4.,5.,6.]
|
|
p=[-(1-eta)*v for v in f];dirs={'full':p,'shape':[0,0,p[2],0,0],'fields':[p[0],p[1],0,p[3],p[4]]}
|
|
layout=[{'kind':'value','block':b,'offset':i,'size':1} for i,b in enumerate(['gravity_gradient','gravity_potential','surface_deformation','density','specific_enthalpy'])]
|
|
layout += [{'kind':'residual','block':b,'offset':i,'size':1} for i,b in enumerate(['gravity_gradient_relation','poisson_balance','material','surface','hydrostatic'])]
|
|
csv_file(folder/(c.P+'layout.csv'),layout)
|
|
blocks={'all':(0,5),**{r['block']:(r['offset'],r['size']) for r in layout if r['kind']=='residual'}}
|
|
for name,v in {'physical_base':[v/2 for v in x],'normalized_base':x,'residual':f,'rhs':[-v for v in f],
|
|
'state_factors':[2.]*5,'residual_factors':[3.]*5,'restored_residual':f}.items():vec_file(folder/(c.P+name+'.txt'),v)
|
|
for d,v in dirs.items():
|
|
for kind in ('direction','action','restored_action'):vec_file(folder/(c.P+kind+'_'+d+'.txt'),v)
|
|
rhs=c.norm(f);true=c.norm([a+b for a,b in zip(f,p)]);passed=true<=.03*rhs
|
|
linear={'status':0 if passed else 1,'converged':int(passed),'iterations':3,'rhs_norm':rhs,'reported_true_norm':true,
|
|
'verified_true_norm':true,'verified_relative_true':true/rhs,'threshold':.03*rhs,'verified_gate':int(passed),
|
|
'operator_applications':4,'inverse_applications':3,'seconds':1.}
|
|
csv_file(folder/(c.P+'linear.csv'),[linear])
|
|
paths,brs,mixed,inner,preparations=[],[],[],[],[]
|
|
def prep(kind,direction,a,n):
|
|
preparations.append({'sequence':len(preparations)+1,'kind':kind,'direction':direction,'alpha':a,'prepared':1,
|
|
'rejection_reason':-1,'rejection_stage':-1,'seconds':.01,'residual_norm':n})
|
|
prep('initial_restore','base',0,rhs)
|
|
alphas=[4.**(-i) for i in range(8)]
|
|
for sample,a in enumerate(alphas):
|
|
samples={}
|
|
for d,j in dirs.items():
|
|
predicted=[v+a*q for v,q in zip(f,j)]
|
|
vec_file(folder/(c.P+'prediction_'+d+'_a'+str(sample)+'.txt'),predicted)
|
|
# The map has shape/field interaction. Its first two equations
|
|
# can be corrected exactly without changing retained state.
|
|
nonlinear=[j[2]**2+j[0]*j[2],j[2]*j[3],2*j[3]**2+j[2]*j[3],j[2]**2,.2*j[4]**2]
|
|
raw=[v+a**order*b for v,b in zip(predicted,nonlinear)]
|
|
correction=[-a**order*nonlinear[i] if i<2 else 0. for i in range(5)]
|
|
vec_file(folder/(c.P+'correction_curvature_'+d+'_a'+str(sample)+'.txt'),correction)
|
|
inner_rhs=c.norm([a**order*v for v in nonlinear[:2]])
|
|
inner.append({'sample':sample,'alpha':a,'direction':d,'status':0,'converged':1,'iterations':1,'rhs_norm':inner_rhs,
|
|
'true_norm':0,'relative_true':0,'threshold':max(1e-14,1e-10*inner_rhs),'operator_applications':2,'inverse_applications':1,
|
|
'seconds':.02,'compact_operator':1,'compact_inverse':1})
|
|
for path in c.PATHS:
|
|
delta=correction if path=='curvature' else [0.]*5
|
|
trial=raw[:] if path=='raw' else predicted[:2]+raw[2:]
|
|
state=[v+a*q+dc for v,q,dc in zip(x,j,delta)]
|
|
intended=[a*q+dc for q,dc in zip(j,delta)];actual=[v-u for v,u in zip(state,x)]
|
|
addition=c.norm([v-u for v,u in zip(actual,intended)])
|
|
values,pred,remainder=measure(f,j,trial,a)
|
|
floor=128*c.EPS*(1+values['base_norm']+a*values['action_norm']+values['residual_norm'])
|
|
record={'sample':sample,'alpha':a,'direction':d,'path':path,'prepared':1,'inner_converged':1,'prepared_gate':1,'usable':1,
|
|
**values,'merit':.5*c.norm(trial)**2,'slope':sum(v*q for v,q in zip(f,j)),
|
|
'relative_derivative_error':values['remainder_over_alpha']/c.norm(j),
|
|
'expected_step_norm':c.norm(intended),'actual_step_norm':c.norm(actual),'addition_error_norm':addition,'increment_resolved':1,
|
|
'gravity_correction_norm':c.norm(correction) if path=='curvature' else math.nan,
|
|
'gravity_target_defect':0 if path=='curvature' else math.nan,'gravity_affine_disagreement':0 if path=='curvature' else math.nan,
|
|
'gravity_arithmetic_allowance':1e-12 if path=='curvature' else math.nan,
|
|
'gravity_target_threshold':1e-12 if path=='curvature' else math.nan,'seconds':.04 if path=='curvature' else .01,
|
|
'heuristic_roundoff_scale':floor}
|
|
paths.append(record);prep(path,d,a,c.norm(trial));samples[(d,path)]=(trial,pred,remainder)
|
|
for kind,v in [('state',state),('residual',trial)]:vec_file(folder/(c.P+kind+'_'+path+'_'+d+'_a'+str(sample)+'.txt'),v)
|
|
for block,(start,size) in blocks.items():
|
|
values,_,_=measure(f[start:start+size],j[start:start+size],trial[start:start+size],a)
|
|
brs.append({'sample':sample,'alpha':a,'direction':d,'path':path,'block':block,'offset':start,'size':size,**values,
|
|
'heuristic_roundoff_scale':128*c.EPS*(1+values['base_norm']+a*values['action_norm']+values['residual_norm'])})
|
|
for path in c.PATHS:
|
|
for block,(start,size) in blocks.items():
|
|
remainders={d:samples[(d,path)][2][start:start+size] for d in c.DIRECTIONS}
|
|
remainders['mixed']=[u-v-w for u,v,w in zip(remainders['full'],remainders['shape'],remainders['fields'])]
|
|
pred=samples[('full',path)][1][start:start+size];trial=samples[('full',path)][0][start:start+size]
|
|
projections={d:sum(a*b for a,b in zip(pred,v)) for d,v in remainders.items()}
|
|
mixed.append({'sample':sample,'alpha':a,'path':path,'block':block,'offset':start,'size':size,'usable':1,
|
|
**{d+'_remainder_norm':c.norm(v) for d,v in remainders.items()},**{'linear_dot_'+d:v for d,v in projections.items()},
|
|
'projection_identity_error':projections['full']-projections['shape']-projections['fields']-projections['mixed'],
|
|
'norm_identity_error':c.norm(trial)**2-c.norm(pred)**2-2*(projections['shape']+projections['fields']+projections['mixed'])-c.norm(remainders['full'])**2})
|
|
prep('final_restore','base',0,rhs)
|
|
restorations=[{'sequence':seq,'kind':kind,'block':block,'offset':start,'size':size,'base_norm':c.norm(f[start:start+size]),'difference_norm':0}
|
|
for seq,kind in [(1,'initial_restore'),(len(preparations),'final_restore')] for block,(start,size) in blocks.items()]
|
|
summary={'schema_version':1,'machine_epsilon':c.EPS,'linear_relative_tolerance':.03,'linear_absolute_tolerance':0,
|
|
'linear_maximum_iterations':10,'linear_restart_length':10,'gravity_relative_tolerance':1e-10,'gravity_absolute_tolerance':1e-14,
|
|
'gravity_maximum_iterations':100,'gravity_restart_length':10,'linear_verified_gate':int(passed),'linear_solve_seconds':1.,
|
|
'maximum_original_restoration_error':0,'maximum_restored_action_difference':0,'accepted_state_change':0,'accepted_residual_change':0,
|
|
'accepted_physical_change':0,'accepted_buffers_bitwise_identical':1,'normalization_factors_bitwise_identical':1,
|
|
'accepted_commits':0,'polynomial_direction_repairs':0,'linear_gate_failure_skips_trials':0,
|
|
'roundoff_scale_is_subtraction_heuristic':1,'path_seconds_include_shared_raw_preparation':1,
|
|
'preparations':len(preparations),'preparation_seconds':.01*len(preparations),'prepared_paths':len(paths),
|
|
'gravity_solves':len(inner),'gravity_iterations':len(inner),'gravity_failures':0,'gravity_solve_seconds':.02*len(inner),'total_seconds':3.}
|
|
summary.update({'alpha_'+str(i):a for i,a in enumerate(alphas)})
|
|
for name,data in [('paths',paths),('blocks',brs),('mixed',mixed),('inner',inner),('preparations',preparations),('restorations',restorations),
|
|
('summary',[{'metric':k,'value':v} for k,v in summary.items()])]:csv_file(folder/(c.P+name+'.csv'),data)
|
|
(folder/'metadata.txt').write_text('linear_tol=0.03\nmax_linear=10\nrestart=10\nnonlinear_step_probe=1\n')
|
|
snapshot=self.campaign/'snapshot.txt';vec_file(snapshot,[v/2 for v in x])
|
|
binary=self.campaign/'plateau_diagnostic';binary.write_bytes(b'not an executable')
|
|
mesh=self.campaign/'mesh.smesh';mesh.write_text('synthetic')
|
|
cases=[{'id':'synthetic','arguments':['--nonlinear-step-probe','--probe-state',str(snapshot)]}]
|
|
provenance={'commit':'synthetic','source':str(self.campaign),'mesh':str(mesh),'mesh_sha256':c.digest(mesh),
|
|
'binary_sha256':c.digest(binary),'cases':cases}
|
|
(self.campaign/'provenance.json').write_text(json.dumps(provenance));(self.campaign/'cases.json').write_text(json.dumps(cases))
|
|
record={'id':'synthetic','finished_utc':'2026-09-18T00:00:00Z','exit_code':0,
|
|
'command':[str(binary),*cases[0]['arguments'],'--mesh',str(mesh),'--output',str(folder)],
|
|
'input_state_hashes':{str(snapshot):c.digest(snapshot)},'output_hashes':{p.name:c.digest(p) for p in folder.iterdir()}}
|
|
(self.campaign/'synthetic_run.json').write_text(json.dumps(record))
|
|
|
|
def assert_valid(self,result):
|
|
self.assertTrue(result['passed'],[r for r in result.get('checks',result.get('provenance_checks',[])) if not r['passed']])
|
|
|
|
def alter(self,name,change):
|
|
path=self.folder/(c.P+name+'.csv');data=c.rows(path);change(data);csv_file(path,data)
|
|
|
|
def test_complete_vector_identities_and_provenance(self):
|
|
self.assert_valid(c.inspect_campaign(self.campaign))
|
|
|
|
def test_failed_raw_linear_gate_preserves_taylor_evidence_without_newton_claim(self):
|
|
self.make_case(eta=.1)
|
|
result=c.inspect_case(self.folder);self.assert_valid(result)
|
|
self.assertFalse(result['direction_quality']['passed'])
|
|
self.assertTrue(any(r['resolved_intervals'] for r in result['vector_order_observations']))
|
|
self.assertTrue(all(not r['eligible_for_newton_forcing_comparison'] for r in result['vector_order_observations']))
|
|
|
|
def test_noninteger_superlinear_order_is_not_a_failure(self):
|
|
self.make_case(order=1.5);result=c.inspect_case(self.folder);self.assert_valid(result)
|
|
entry=next(r for r in result['vector_order_observations'] if r['path']=='raw' and r['direction']=='full' and r['block']=='all')
|
|
self.assertTrue(entry['resolved_intervals'])
|
|
self.assertTrue(all(abs(r['vector_remainder_order']-1.5)<1e-5 for r in entry['resolved_intervals']))
|
|
|
|
def test_first_order_vector_error_is_not_misreported_as_quadratic(self):
|
|
self.make_case(order=1);result=c.inspect_case(self.folder);self.assert_valid(result)
|
|
entry=next(r for r in result['vector_order_observations'] if r['path']=='raw' and r['direction']=='full' and r['block']=='all')
|
|
self.assertTrue(all(abs(r['vector_remainder_order']-1)<1e-5 for r in entry['resolved_intervals']))
|
|
|
|
def test_corrupt_saved_residual_cannot_match_scalar_csv(self):
|
|
path=self.folder/(c.P+'residual_raw_full_a0.txt');values=c.vector(path);values[2]+=.001;vec_file(path,values)
|
|
self.assertFalse(c.inspect_case(self.folder)['passed'])
|
|
|
|
def test_mixed_projection_sign_is_checked(self):
|
|
self.alter('mixed',lambda data:data[0].update(linear_dot_mixed=.2))
|
|
self.assertFalse(c.inspect_case(self.folder)['passed'])
|
|
|
|
def test_missing_invariant_is_not_success(self):
|
|
self.alter('summary',lambda data:data.__setitem__(slice(None),[r for r in data if r['metric']!='accepted_buffers_bitwise_identical']))
|
|
self.assertFalse(c.inspect_case(self.folder)['passed'])
|
|
|
|
def test_changed_retained_coordinate_is_detected(self):
|
|
path=self.folder/(c.P+'state_curvature_full_a0.txt');v=c.vector(path);v[4]+=.1;vec_file(path,v)
|
|
self.assertFalse(c.inspect_case(self.folder)['passed'])
|
|
|
|
def test_unhashed_saved_vector_cannot_support_campaign_result(self):
|
|
p=self.campaign/'synthetic_run.json';record=json.loads(p.read_text());del record['output_hashes'][c.P+'action_full.txt'];p.write_text(json.dumps(record))
|
|
result=c.inspect_campaign(self.campaign);self.assertFalse(result['passed'])
|
|
self.assertFalse(result['cases']['synthetic']['observations_valid'])
|
|
|
|
def test_incomplete_producer_is_not_a_completed_result(self):
|
|
p=self.campaign/'synthetic_run.json';record=json.loads(p.read_text());del record['finished_utc'];p.write_text(json.dumps(record))
|
|
self.assertFalse(c.inspect_campaign(self.campaign)['passed'])
|
|
|
|
def unavailable_trial(self, raw_failure):
|
|
affected={'raw','curvature'} if raw_failure else {'curvature'}
|
|
path=self.folder/(c.P+'paths.csv');data=c.rows(path)
|
|
for row in data:
|
|
if row['sample']=='0' and row['direction']=='full' and row['path'] in affected:
|
|
row.update(prepared=0,usable=0)
|
|
for key in ('residual_norm','merit','remainder_norm','remainder_over_alpha','remainder_over_alpha_squared','relative_derivative_error','linear_dot_remainder','norm_identity_error','heuristic_roundoff_scale','gravity_correction_norm','gravity_target_defect','gravity_affine_disagreement','gravity_arithmetic_allowance','gravity_target_threshold'):
|
|
row[key]=math.nan
|
|
if row['path']=='curvature':row.update(inner_converged=0,prepared_gate=0,increment_resolved=0,expected_step_norm=math.nan,actual_step_norm=math.nan,addition_error_norm=math.nan)
|
|
csv_file(path,data)
|
|
for name in ('blocks','mixed'):
|
|
path=self.folder/(c.P+name+'.csv');data=c.rows(path)
|
|
for row in data:
|
|
if row['sample']=='0' and row['path'] in affected and (name=='mixed' or row['direction']=='full'):
|
|
if name=='mixed':
|
|
row['usable']=0
|
|
for key in row:
|
|
if key.endswith('_norm') or key.startswith('linear_dot_') or key.endswith('_error'):row[key]=math.nan
|
|
else:
|
|
for key in ('residual_norm','remainder_norm','remainder_over_alpha','remainder_over_alpha_squared','linear_dot_remainder','norm_identity_error','heuristic_roundoff_scale'):row[key]=math.nan
|
|
csv_file(path,data)
|
|
for path_kind in affected:
|
|
for kind in ('state','residual'):(self.folder/(c.P+kind+'_'+path_kind+'_full_a0.txt')).unlink()
|
|
(self.folder/(c.P+'correction_curvature_full_a0.txt')).unlink()
|
|
path=self.folder/(c.P+'inner.csv');data=c.rows(path)
|
|
if raw_failure:data=[r for r in data if not(r['sample']=='0' and r['direction']=='full')]
|
|
else:
|
|
row=next(r for r in data if r['sample']=='0' and r['direction']=='full')
|
|
row.update(status=1,converged=0,iterations=100,true_norm=2*float(row['threshold']),relative_true=2*float(row['threshold'])/float(row['rhs_norm']))
|
|
csv_file(path,data);inner=data
|
|
path=self.folder/(c.P+'preparations.csv');data=c.rows(path)
|
|
data=[r for r in data if not(r['kind']=='curvature' and r['direction']=='full' and float(r['alpha'])==1)]
|
|
for i,row in enumerate(data):
|
|
row['sequence']=i+1
|
|
if raw_failure and row['kind']=='raw' and row['direction']=='full' and float(row['alpha'])==1:row.update(prepared=0,rejection_reason=2,rejection_stage=6,residual_norm=math.nan)
|
|
csv_file(path,data)
|
|
count=len(data)
|
|
self.alter('restorations',lambda records:[r.update(sequence=count) for r in records if r['kind']=='final_restore'])
|
|
updates={'prepared_paths':48-len(affected),'preparations':count,'preparation_seconds':.01*count,
|
|
'gravity_solves':len(inner),'gravity_iterations':sum(int(r['iterations']) for r in inner),
|
|
'gravity_failures':0 if raw_failure else 1,'gravity_solve_seconds':.02*len(inner)}
|
|
self.alter('summary',lambda records:[r.update(value=updates[r['metric']]) for r in records if r['metric'] in updates])
|
|
|
|
def test_preparation_failure_is_honest_unavailable_observation(self):
|
|
self.unavailable_trial(True);result=c.inspect_case(self.folder);self.assert_valid(result)
|
|
self.assertEqual(len(result['preparation_failures']),1)
|
|
self.assertTrue(any(r['status']=='raw_preparation_failed' for r in result['paths']))
|
|
|
|
def test_failed_inner_solve_does_not_invent_corrected_trial(self):
|
|
self.unavailable_trial(False);result=c.inspect_case(self.folder);self.assert_valid(result)
|
|
self.assertTrue(any(r['status']=='inner_solve_failed' for r in result['paths']))
|
|
|
|
def test_failed_prepared_gate_retains_finite_vectors_but_not_usable_path(self):
|
|
self.alter('paths',lambda data:[r.update(prepared_gate=0,usable=0,gravity_affine_disagreement=2e-12) for r in data if r['sample']=='0' and r['direction']=='full' and r['path']=='curvature'])
|
|
self.alter('mixed',lambda data:[r.update(usable=0) for r in data if r['sample']=='0' and r['path']=='curvature'])
|
|
result=c.inspect_case(self.folder);self.assert_valid(result)
|
|
self.assertTrue(any(r['status']=='prepared_gravity_gate_failed' for r in result['paths']))
|
|
|
|
def test_below_floor_order_is_unresolved(self):
|
|
observations=[{'path':'raw','direction':'full','block':'all','alpha':a,'usable':True,'increment_resolved':True,
|
|
'remainder_norm':a*a*1e-15,'remainder_over_alpha':a*1e-15,'floor':1e-14} for a in (1,.25)]
|
|
self.assertFalse(c.vector_orders(observations,True)[0]['resolved_intervals'])
|
|
|
|
|
|
if __name__=='__main__':unittest.main(verbosity=2)
|