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