diff --git a/experiments/check_coupled_trial.py b/experiments/check_coupled_trial.py new file mode 100644 index 0000000..1194394 --- /dev/null +++ b/experiments/check_coupled_trial.py @@ -0,0 +1,452 @@ +#!/usr/bin/env python3 +"""Audit coupled-trial saved vectors and transactions, not physical convergence. + +A retained correction can still fail Armijo. Each alpha is an independent frozen +trial, not one step in a nonlinear trajectory. Saved J actions are checked by +vector algebra; this checker does not perform another FEM operator application. +""" +import argparse +import json +import math +from pathlib import Path + +from check_nonlinear_step import EPS, close, digest, dot, layout_for, minus, norm, rows, sanitize, vector + +P = 'coupled_trial_' +STATUS = ('not_attempted','original_accepted','budget_exhausted','kept','linear_failure', + 'norm_limit','non_finite','inadmissible','not_improved') +GRAVITY_VALUES = {'gravity_gradient','gravity_potential'} +GRAVITY_ROWS = {'gravity_gradient_relation','poisson_balance'} +BASE = {'metadata.txt'} | {P+n+'.csv' for n in ('summary','layout','preparations','trials','blocks','physical')} | { + P+n+'.txt' for n in ('base_normalized','base_physical','base_residual','state_factors','residual_factors', + 'direction','base_action','base_defect','restored_residual','restored_action')} + + +def near_vector(a,b,scale): + return len(a)==len(b) and norm(minus(a,b)) <= 1024*EPS*max(scale,1.e-28) + + +def inspect_case(folder): + try: + return inspect_case_artifacts(Path(folder)) + except (OSError,ValueError,KeyError,TypeError,IndexError,OverflowError,ZeroDivisionError) as error: + return {'passed':False,'checks':[{'check':'complete_readable_artifacts','passed':False,'error':str(error)}], + 'observations_valid':False,'required_artifacts':sorted(BASE),'trial_outcomes':[]} + + +def inspect_case_artifacts(folder): + checks=[] + def check(name,value): checks.append({'check':name,'passed':bool(value)}) + def flag(row,key,label): + value=float(row[key]);check(label+key+':boolean',value in (0,1));return value==1 + required=set(BASE) + def get(name,needed=True): + path=folder/(P+name+'.txt') + if needed: + required.add(path.name);return vector(path) + check(name+':unavailable_file_absent',not path.exists());return None + summary_rows=rows(folder/(P+'summary.csv')) + summary={r['metric']:float(r['value']) for r in summary_rows} + check('unique_summary',len(summary)==len(summary_rows)) + meta=dict(line.split('=',1) for line in (folder/'metadata.txt').read_text().splitlines() if '=' in line) + for key,metadata_key in {'outer_linear_tolerance':'linear_tol','seed_control':'coupled_seed_control', + 'coupled_maximum_correction_ratio':'coupled_maximum_ratio','coupled_relative_tolerance':'coupled_tolerance', + 'coupled_maximum_iterations':'coupled_iterations','coupled_restart':'coupled_restart', + 'gravity_relative_tolerance':'gravity_tolerance','gravity_maximum_iterations':'gravity_iterations', + 'gravity_restart':'gravity_restart','coupled_attempt_budget_per_independent_alpha':'coupled_attempts_per_iteration'}.items(): + check('summary_metadata:'+key,summary[key]==float(meta[metadata_key])) + check('strategy_scope',meta['strategy'] in ('coupled','curvature-coupled') and + summary['gravity_path']==int(meta['strategy']=='curvature-coupled')) + for key in ('outer_linear_tolerance','sufficient_decrease'): + check('valid_control:'+key,math.isfinite(summary[key]) and 00) + check('valid_gravity_relative_tolerance',math.isfinite(summary['gravity_relative_tolerance']) and summary['gravity_relative_tolerance']>=0) + for key in ('coupled_maximum_iterations','coupled_restart','gravity_maximum_iterations','gravity_restart'): + check('valid_control:'+key,math.isfinite(summary[key]) and summary[key]>0 and summary[key]==int(summary[key])) + check('valid_correction_cap',math.isfinite(summary['coupled_maximum_correction_ratio']) and summary['coupled_maximum_correction_ratio']>0) + check('fixed_driver_controls',summary['sufficient_decrease']==1.e-4 and + summary['coupled_absolute_tolerance']==1.e-14 and summary['gravity_absolute_tolerance']==1.e-14) + for name,value in {'schema_version':1,'independent_frozen_alphas':1,'accepted_commits':0, + 'polynomial_direction_repairs':0,'sampled_enthalpy_policy':1,'coupled_attempt_budget_per_independent_alpha':1, + 'probe_exception':0,'accepted_buffers_bitwise_identical':1,'normalization_factors_bitwise_identical':1, + 'direction_buffers_bitwise_identical':1,'direction_buffers_unchanged_before_restoration':1, + 'fresh_operator_and_inverse_current':1}.items():check(name,summary[name]==value) + for i,name in enumerate(STATUS):check('status_mapping:'+name,summary['soc_status_'+name]==i) + x,physical,f,p,j,sf,rf=[get(n) for n in ('base_normalized','base_physical','base_residual','direction','base_action','state_factors','residual_factors')] + size=len(x) + check('square_vector_dimensions',all(len(v)==size for v in (physical,f,p,j,sf,rf))) + check('positive_normalization',all(v>0 for v in sf+rf)) + check('base_physical_normalization',near_vector(x,[v*a for v,a in zip(physical,sf)],norm(x))) + layout=rows(folder/(P+'layout.csv'));vl=layout_for(layout,'value',size);rl=layout_for(layout,'residual',size) + gi=[i for name,(start,n) in vl.items() if name in GRAVITY_VALUES for i in range(start,start+n)] + gr=[i for name,(start,n) in rl.items() if name in GRAVITY_ROWS for i in range(start,start+n)] + f0,pn,jn=norm(f),norm(p),norm(j) + check('finite_positive_base_residual',math.isfinite(f0) and f0>0) + check('finite_direction_norm',math.isfinite(pn)) + defect=[a+b for a,b in zip(f,j)];dn=norm(defect) + check('base_defect_vector',near_vector(get('base_defect'),defect,f0+jn)) + outer_threshold=summary['outer_linear_tolerance']*f0;outer_gate=dn<=outer_threshold + for key,value in {'base_residual_norm':f0,'base_merit':.5*f0*f0,'direction_norm':pn, + 'base_merit_slope':dot(f,j),'outer_verified_defect_norm':dn,'outer_verified_relative_defect':dn/f0, + 'outer_verified_threshold':outer_threshold}.items():check(key,close(summary[key],value,f0+jn)) + check('outer_gate',summary['outer_verified_gate']==int(outer_gate)) + check('outer_failed_gate_skips_trials',summary['outer_gate_failure_skips_trials']==int(not outer_gate)) + check('restored_residual_vector',near_vector(get('restored_residual'),f,f0)) + check('restored_action_vector',near_vector(get('restored_action'),j,jn)) + restored_f=norm(minus(get('restored_residual'),f));restored_j=norm(minus(get('restored_action'),j)) + check('restoration_envelope',math.isfinite(summary['maximum_base_restoration_error']) and + restored_f<=summary['maximum_base_restoration_error']+1024*EPS*f0 and summary['maximum_base_restoration_error']<=1.e-10*f0) + check('restored_action_error',close(summary['restored_base_action_error'],restored_j,jn) and restored_j<=1.e-10*max(jn,1.e-28)) + for name in ('gravity_path','seed_control'):flag(summary,name,'summary:') + expected_alphas=[1.] if summary['seed_control']==1 else [1/16,1/64,1/256,1/1024] + for i,a in enumerate(expected_alphas):check('alpha_'+str(i),summary['alpha_'+str(i)]==a) + trial_rows=rows(folder/(P+'trials.csv'));block_rows=rows(folder/(P+'blocks.csv')) + check('complete_trial_grid',[int(r['sample']) for r in trial_rows]==(list(range(len(expected_alphas))) if outer_gate else [])) + block_map={(int(r['sample']),r['stage'],r['block']):r for r in block_rows} + check('unique_block_rows',len(block_map)==len(block_rows));seen_blocks=set() + partitions={'all':(0,size),**rl} + outcomes=[] + totals={'attempts':0,'kept':0,'fallbacks':0,'iterations':0,'preparations':0,'restorations':0, + 'solve_seconds':0.,'verification_seconds':0.,'preparation_seconds':0.,'refresh_seconds':0.} + def block_check(sample,a,stage,residual): + for name,(start,n) in partitions.items(): + key=(sample,stage,name);seen_blocks.add(key);r=block_map[key] + check(f'{sample}:{stage}:{name}:layout',int(r['offset'])==start and int(r['size'])==n and float(r['alpha'])==a) + check(f'{sample}:{stage}:{name}:norm',close(float(r['residual_norm']),norm(residual[start:start+n]),norm(residual))) + for row in trial_rows: + i=int(row['sample']);a=float(row['alpha']);label=str(i)+':' + check(label+'alpha',a==expected_alphas[i]);suffix='_a'+str(i) + flags={k:flag(row,k,label) for k in ('gravity_path','raw_prepared','gravity_attempted','gravity_reported_gate', + 'gravity_prepared_gate','before_prepared','before_usable','before_accepted','soc_called','soc_attempted', + 'aux_reported_gate','aux_verified_gate','aux_verification_applied','corrected_metric_observed', + 'corrected_accepted','selected_prepared','selected_accepted','selected_is_original','rhs_saved', + 'correction_saved','verified_vectors_saved')} + check(label+'gravity_policy',int(flags['gravity_path'])==summary['gravity_path']) + prediction=[v+a*w for v,w in zip(f,j)];raw_state=get('raw_state'+suffix) + check(label+'linear_prediction',near_vector(get('prediction'+suffix),prediction,f0+a*jn)) + check(label+'raw_displacement',near_vector(raw_state,[v+a*w for v,w in zip(x,p)],norm(x)+a*pn)) + vectors={} + for stage,available,normkey,meritkey in ( + ('raw',flags['raw_prepared'],'raw_norm','raw_merit'), + ('before',flags['before_prepared'],'before_norm','before_merit'), + ('corrected',flags['corrected_metric_observed'],'corrected_norm','corrected_merit'), + ('selected',flags['selected_prepared'],'selected_norm','selected_merit')): + residual=get(stage+'_residual'+suffix,available) + state=raw_state if stage=='raw' else get(stage+'_state'+suffix,available) + if available: + check(label+stage+':dimensions',len(residual)==len(state)==size) + rn=norm(residual);check(label+stage+':norm',close(float(row[normkey]),rn,rn)) + check(label+stage+':merit',close(float(row[meritkey]),.5*rn*rn,rn*rn)) + vectors[stage]=(state,residual);block_check(i,a,stage,residual) + if stage in ('corrected','selected'): + pv=get(stage+'_physical'+suffix) + check(label+stage+':physical',near_vector(state,[v*b for v,b in zip(pv,sf)],norm(state))) + else: + check(label+stage+':unavailable_metrics',math.isnan(float(row[normkey])) and math.isnan(float(row[meritkey]))) + if stage in ('corrected','selected'):get(stage+'_physical'+suffix,False) + check(label+'raw_rejection', (int(row['raw_rejection_reason'])==-1 and int(row['raw_rejection_stage'])==-1) + if flags['raw_prepared'] else (int(row['raw_rejection_reason'])>=0 and int(row['raw_rejection_stage'])>=0)) + check(label+'gravity_attempt',flags['gravity_attempted']==(flags['gravity_path'] and flags['raw_prepared'])) + if not flags['gravity_path']: + check(label+'vacuous_gravity_gates',flags['gravity_reported_gate'] and flags['gravity_prepared_gate']) + check(label+'raw_baseline_preparation',flags['before_prepared']==flags['raw_prepared']) + get('gravity_correction'+suffix,False) + if flags['before_prepared']: + check(label+'raw_baseline_state',vectors['before'][0]==raw_state) + check(label+'raw_baseline_residual',vectors['before'][1]==vectors['raw'][1]) + else: + save_gravity=flags['gravity_attempted'] and flags['gravity_reported_gate'] + gc=get('gravity_correction'+suffix,save_gravity) + if save_gravity: + check(label+'gravity_partition_present',bool(gi) and bool(gr)) + check(label+'gravity_only_correction',len(gc)==size and all(v==0 for k,v in enumerate(gc) if k not in set(gi))) + if flags['before_prepared']: + check(label+'gravity_displacement',near_vector(vectors['before'][0],[v+w for v,w in zip(raw_state,gc)],norm(raw_state)+norm(gc))) + target=norm([vectors['before'][1][k]-prediction[k] for k in gr]) + check(label+'gravity_affine_target',close(float(row['gravity_target_defect']),target,f0+a*jn)) + if flags['gravity_prepared_gate']: + threshold=float(row['gravity_target_threshold']);disagreement=float(row['gravity_affine_disagreement']) + check(label+'gravity_prepared_necessary_gate',math.isfinite(threshold) and threshold>=0 and target<=threshold+512*EPS*f0 and + math.isfinite(disagreement) and 0<=disagreement<=threshold) + usable=flags['before_prepared'] and flags['gravity_reported_gate'] and flags['gravity_prepared_gate'] + check(label+'before_usable',flags['before_usable']==usable) + armijo=(1-summary['sufficient_decrease']*a)*.5*f0*f0 + before_accept=usable and float(row['before_merit'])<=armijo + check(label+'before_armijo',flags['before_accepted']==before_accept) + check(label+'soc_called',flags['soc_called']==usable) + attempted=usable and not before_accept + check(label+'one_independent_attempt',flags['soc_attempted']==attempted) + status=int(row['soc_status']);check(label+'known_status',0<=statuscap else 0) + check(label+'core_decision',int(row['aux_core_status'])==expected_core) + core=int(row['aux_core_status']) + if core in (0,1,2): + check(label+'finite_core_complete_evidence',flags['rhs_saved'] and flags['correction_saved'] and + flags['aux_verification_applied'] and flags['verified_vectors_saved']) + if core!=0: + check(label+'failed_core_falls_back',name=={1:'linear_failure',2:'norm_limit',3:'non_finite'}.get(core)) + check(label+'failed_core_no_preparation',int(row['soc_preparations'])==0 and int(row['soc_restorations'])==0 and not flags['corrected_metric_observed']) + else: + check(label+'admissible_core_requires_gates',flags['aux_reported_gate'] and flags['aux_verified_gate'] and + flags['verified_vectors_saved'] and float(row['correction_norm'])<=cap) + if flags['corrected_metric_observed']: + check(label+'corrected_requires_admissible_core',core==0) + check(label+'corrected_displacement',near_vector(vectors['corrected'][0], + [v+w for v,w in zip(vectors['before'][0],correction)],norm(vectors['before'][0])+norm(correction))) + better=float(row['corrected_merit'])=0) + parts=sum(float(row['soc_'+k]) for k in ('refresh_seconds','solve_seconds','verification_seconds','preparation_seconds')) + check(label+'soc_time_scope',float(row['soc_seconds'])+1.e-6>=parts) + check(label+'sample_time_scope',float(row['total_sample_seconds'])+1.e-6>=sum(float(row[k]) for k in ('raw_seconds','gravity_seconds','soc_seconds','capture_seconds'))) + for key in ('preparations','restorations','solve_seconds','verification_seconds','preparation_seconds','refresh_seconds'): + totals[key]+=float(row['soc_'+key]) + totals['iterations']+=int(row['aux_iterations']) + outcomes.append({'sample':i,'alpha':a,'transaction_status':name,'before_usable':usable,'before_armijo':before_accept, + 'correction_attempted':attempted,'aux_verified_gate':flags['aux_verified_gate'],'corrected_metric_observed':flags['corrected_metric_observed'], + 'selected_armijo':flags['selected_accepted'],'before_residual_norm':float(row['before_norm']), + 'selected_residual_norm':float(row['selected_norm']),'full_merit_improved':name=='kept', + 'correction_norm':float(row['correction_norm']),'correction_cap':float(row['correction_cap']), + 'auxiliary_iterations':int(row['aux_iterations']),'auxiliary_verified_defect_norm':float(row['aux_verified_true_norm']), + 'corrected_affine_target_defect_norm':norm(minus(vectors['corrected'][1],prediction)) if flags['corrected_metric_observed'] else None, + 'auxiliary_seconds':{k:float(row['soc_'+k]) for k in ('refresh_seconds','solve_seconds','verification_seconds','preparation_seconds')}, + 'residual_norms_by_block':{stage:{block:norm(value[1][start:start+n]) for block,(start,n) in rl.items()} + for stage,value in vectors.items()}}) + check('complete_block_coverage',seen_blocks==set(block_map)) + if outer_gate: + for key,value in totals.items():check('soc_total:'+key,close(summary['soc_'+key],value,value)) + preparations=rows(folder/(P+'preparations.csv')) + check('preparation_sequence',[int(r['sequence']) for r in preparations]==list(range(1,len(preparations)+1))) + check('explicit_preparation_count',summary['explicit_probe_preparations']==len(preparations)) + for r in preparations: + check('preparation_time:'+r['sequence'],math.isfinite(float(r['seconds'])) and float(r['seconds'])>=0) + if r['kind']!='raw':check('base_restoration_prepares:'+r['sequence'],r['prepared']=='1' and close(float(r['residual_norm']),f0,f0)) + raw_preparations=[r for r in preparations if r['kind']=='raw'] + check('raw_preparation_coverage',[int(r['sample']) for r in raw_preparations]==[int(r['sample']) for r in trial_rows]) + for prep,trial in zip(raw_preparations,trial_rows): + label=prep['sample']+':raw_preparation:' + for key in ('prepared','rejection_reason','rejection_stage'): + check(label+key,prep[key]==trial['raw_'+key]) + check(label+'alpha',float(prep['alpha'])==float(trial['alpha'])) + measured=float(prep['residual_norm']);reported=float(trial['raw_norm']) + check(label+'residual',close(measured,reported) if prep['prepared']=='1' else math.isnan(measured) and math.isnan(reported)) + check(label+'time_scope',float(prep['seconds'])<=float(trial['raw_seconds'])+1.e-6) + check('preparation_time_total',close(summary['explicit_probe_preparation_seconds'],sum(float(r['seconds']) for r in preparations))) + check('final_restoration_present',bool(preparations) and preparations[-1]['kind']=='final_base_restore') + check('total_time',math.isfinite(summary['total_seconds']) and summary['total_seconds']>=0) + physical_observations=inspect_physical(folder,trial_rows,meta,check) + check('no_unexplained_vector_artifacts',{p.name for p in folder.glob(P+'*.txt')}== + {name for name in required if name.startswith(P) and name.endswith('.txt')}) + passed=all(r['passed'] for r in checks) + return sanitize({'passed':passed,'checks':checks,'observations_valid':passed,'metadata':meta,'summary':summary, + 'required_artifacts':sorted(required),'outer_linear_gate':outer_gate,'trial_outcomes':outcomes if passed else [], + 'physical_observations':physical_observations if passed else [], + 'scope':'Independent saved-vector arithmetic and transaction/provenance audit. Frozen alpha samples are not a nonlinear trajectory. Physical replay metrics are checked for internal consistency and reported separately; no independent FEM quadrature is performed.', + 'gravity_scope':'Saved vectors independently check the affine gravity target defect. This schema does not independently reconstruct the gravity inner solve or its contribution-based arithmetic allowance.', + 'operator_scope':'Saved SOC J actions verify the reported algebraic defect; this checker does not perform another FEM action.'}) + + +def inspect_physical(folder,trials,metadata,check): + """Check replay binding and scalar identities without certifying the star.""" + records=rows(folder/(P+'physical.csv')) + measured={(int(r['sample']),r['source'],int(r['quadrature']),r['metric']):float(r['value']) for r in records} + check('physical:unique_measurements',len(measured)==len(records)) + used=set() + def get(sample,source,q,key): + index=(sample,source,q,key);used.add(index);return measured[index] + for key in ('accepted_buffers_unchanged_before_restoration','accepted_buffers_unchanged_after_restoration', + 'normalization_factors_unchanged','direction_buffers_unchanged','accepted_operator_and_inverse_current'): + check('physical:'+key,get(-1,'restoration',0,key)==1) + selected=[r for r in trials if r['selected_prepared']=='1'] + check('physical:snapshot_count',get(-1,'replay',0,'selected_snapshots_measured')==len(selected)) + check('physical:sample_coverage',{k[0] for k in measured if k[0]>=0}=={int(r['sample']) for r in selected}) + observations=[] + for trial in selected: + sample=int(trial['sample']);source=P+'selected_physical_a'+str(sample)+'.txt' + saved=vector(folder/source);label='physical:'+str(sample)+':' + check(label+'fresh_preparation',get(sample,source,0,'fresh_preparation_passed')==1) + for q in (14,18): + prefix=label+str(q)+':' + m={key[3]:value for key,value in measured.items() if key[:3]==(sample,source,q)} + used.update(key for key in measured if key[:3]==(sample,source,q)) + check(prefix+'quadrature',m['quadrature_order']==q) + check(prefix+'physics',m['polytropic_index']==float(metadata['n']) and + m['polytropic_constant']==float(metadata['K']) and m['target_mass']==float(metadata['M'])) + for key in ('stellar_samples','valid_stellar_samples','invalid_stellar_samples','negative_density_samples', + 'negative_enthalpy_samples','invalid_density_eos_samples','invalid_enthalpy_eos_samples'): + check(prefix+key,math.isfinite(m[key]) and m[key]>=0 and m[key]==int(m[key])) + check(prefix+'sample_accounting',m['valid_stellar_samples']+m['invalid_stellar_samples']==m['stellar_samples']) + complete=m['invalid_stellar_samples']==0 and m['valid_stellar_samples']>0 and math.isfinite(m['volume']) and m['volume']>0 + check(prefix+'complete_measurement',m['complete_volume_measurement']==int(complete)) + hp=complete and m['invalid_enthalpy_eos_samples']==0 + rp=complete and m['negative_density_samples']==0 and m['invalid_density_eos_samples']==0 + check(prefix+'enthalpy_pressure_definition',m['pressure_from_enthalpy_defined_everywhere']==int(hp)) + check(prefix+'density_pressure_definition',m['pressure_from_density_defined_everywhere']==int(rp)) + check(prefix+'snapshot_roundtrip',0<=m['saved_prepared_l2_difference']<=1024*EPS*(1+norm(saved)) and + 0<=m['saved_prepared_maximum_difference']<=m['saved_prepared_l2_difference']+1024*EPS and + 0<=m['saved_prepared_maximum_scaled_difference']<=1024*EPS) + selected_norm=float(trial['selected_norm']) + # Repreparing a denormalized snapshot introduces roundoff. This + # tolerance checks replay identity, not physical residual accuracy. + check(prefix+'selected_residual_binding',math.isfinite(m['normalized_bordered_residual']) and + abs(m['normalized_bordered_residual']-selected_norm)<=1024*EPS*(1+selected_norm)) + check(prefix+'central_border_finite',math.isfinite(m['central_border']) and math.isfinite(m['normalized_border_action'])) + if metadata['central_phase']=='0': + check(prefix+'unbordered_model',m['central_border']==0 and m['normalized_border_action']==0 and + close(m['normalized_unbordered_residual'],m['normalized_bordered_residual'],selected_norm)) + if complete: + check(prefix+'mass_error',close(m['mass_absolute_error'],abs(m['mass']-m['target_mass']),abs(m['mass'])+m['target_mass'])) + check(prefix+'sampled_signs', + (m['minimum_density']<0)==(m['negative_density_samples']>0) and + (m['minimum_enthalpy']<0)==(m['negative_enthalpy_samples']>0)) + w=m['binding_energy'];wf=m['force_binding_energy'];t=m['kinetic_energy'] + virials={} + for prefix_pressure,defined,pressure_key in (('',rp,'pressure_integral'),('enthalpy_',hp,'enthalpy_pressure_integral')): + pressure=m[pressure_key] + check(prefix+pressure_key+':definition',math.isfinite(pressure) if defined else math.isnan(pressure)) + for force,binding in (('',w),('force_',wf)): + key=prefix_pressure+force+'virial_';signed=m[key+'signed'];error=m[key+'error'] + valid=defined and math.isfinite(w) and w<0 + if valid: + expected=(2*t+binding+3*pressure)/abs(w) + scale=(2*abs(t)+abs(binding)+3*abs(pressure))/abs(w) + check(prefix+key+'identity',close(signed,expected,scale) and close(error,abs(signed),scale)) + else:check(prefix+key+'undefined',math.isnan(signed) and math.isnan(error)) + virials[key+'within_1e5']=(error<=1.e-5) if valid and math.isfinite(error) else None + observations.append({'sample':sample,'quadrature':q,'selected_armijo':trial['selected_accepted']=='1', + 'complete_volume_measurement':complete,'pressure_from_density_defined':rp,'pressure_from_enthalpy_defined':hp, + 'negative_density_samples':m['negative_density_samples'],'minimum_density':m['minimum_density'], + 'negative_enthalpy_samples':m['negative_enthalpy_samples'],'minimum_enthalpy':m['minimum_enthalpy'], + 'global_enthalpy_certificate':'not_measured','virial_status':virials,'reported_metrics':m}) + check('physical:source_and_quadrature_coverage',used==set(measured)) + return observations + + +def suppress_observations(results): + for result in results.values(): + result.update(passed=False,observations_valid=False,trial_outcomes=[],physical_observations=[]) + + +def inspect_campaign(campaign): + campaign=Path(campaign).resolve();checks=[];results={} + def check(name,value):checks.append({'check':name,'passed':bool(value)}) + try: + provenance=json.loads((campaign/'provenance.json').read_text());cases=provenance['cases'] + check('executable_hash',digest(campaign/'plateau_diagnostic')==provenance['binary_sha256']) + check('mesh_hash',digest(Path(provenance['mesh']))==provenance['mesh_sha256']) + check('unique_cases',bool(cases) and len(cases)==len({c['id'] for c in cases})) + check('manifest_snapshot',json.loads((campaign/'cases.json').read_text())==cases) + for case in cases: + name=case['id'];folder=campaign/name;args=case['arguments'];record=json.loads((campaign/(name+'_run.json')).read_text()) + check(name+':completed',bool(record.get('finished_utc')) and record.get('exit_code')==0 and not record.get('timed_out',False)) + check(name+':command',record.get('id')==name and record.get('command')==[str(campaign/'plateau_diagnostic'),*args,'--mesh',provenance['mesh'],'--output',str(folder)]) + expected={} + for option,saved in (('--probe-state','base_physical'),('--probe-direction','direction')): + if option not in args:raise ValueError('A frozen coupled trial must bind '+option) + path=Path(args[args.index(option)+1]);path=path if path.is_absolute() else Path(provenance['source'])/path + expected[str(path.resolve())]=saved + check(name+':required_input_hashes',expected.keys()<=record.get('input_state_hashes',{}).keys()) + for path,value in record.get('input_state_hashes',{}).items():check(name+':input:'+path,digest(Path(path))==value) + for filename,value in record.get('output_hashes',{}).items():check(name+':output:'+filename,digest(folder/filename)==value) + for path,saved in expected.items(): + source=vector(Path(path));target=vector(folder/(P+saved+'.txt')) + check(name+':input_binding:'+saved,source==target if saved=='direction' else near_vector(source,target,norm(source))) + result=inspect_case(folder);results[name]=result;meta=result.get('metadata',{}) + check(name+':required_output_hashes',set(result['required_artifacts'])<=record.get('output_hashes',{}).keys()) + check(name+':expert_probe','--coupled-trial-probe' in args and meta.get('coupled_trial_probe')=='1') + bindings={'--index':'n','--angular-momentum':'J','--linear-tol':'linear_tol','--coupled-tol':'coupled_tolerance', + '--coupled-iterations':'coupled_iterations','--coupled-restart':'coupled_restart','--coupled-max-ratio':'coupled_maximum_ratio', + '--coupled-attempts':'coupled_attempts_per_iteration','--gravity-tol':'gravity_tolerance', + '--gravity-iterations':'gravity_iterations','--gravity-restart':'gravity_restart'} + for option,key in bindings.items(): + if option in args:check(name+':requested_'+key,key in meta and float(meta[key])==float(args[args.index(option)+1])) + if '--strategy' in args:check(name+':strategy',meta.get('strategy')==args[args.index('--strategy')+1]) + if '--no-central' in args:check(name+':no_central',meta.get('central_phase')=='0') + check(name+':independent_alpha_metadata',meta.get('coupled_trial_alphas_are_independent')=='1') + check(name+':sampled_enthalpy_scope',meta.get('coupled_trial_admissibility')=='sampled_enthalpy_without_repair') + check(name+':seed_control',meta.get('coupled_seed_control')==str(int('--coupled-seed-control' in args))) + passed=all(c['passed'] for c in checks) and all(r['passed'] for r in results.values()) + if not all(c['passed'] for c in checks): + suppress_observations(results) + return {'passed':passed,'source_commit':provenance['commit'],'checker_sha256':digest(Path(__file__)), + 'checker_utility_sha256':digest(Path(__file__).with_name('check_nonlinear_step.py')), + 'provenance_sha256':digest(campaign/'provenance.json'),'provenance_checks':checks,'cases':results, + 'scope':'Artifact validity and numerical/physical success are separate conclusions.'} + except (OSError,ValueError,KeyError,TypeError,IndexError) as error: + suppress_observations(results) + return {'passed':False,'provenance_checks':checks+[{'check':'complete_readable_campaign','passed':False,'error':str(error)}],'cases':results} + + +def main(): + parser=argparse.ArgumentParser(description=__doc__) + parser.add_argument('campaign',type=Path);parser.add_argument('--output',type=Path,required=True) + args=parser.parse_args() + if args.output.resolve()==args.campaign.resolve() or args.campaign.resolve() in args.output.resolve().parents: + parser.error('Write the review outside the immutable campaign.') + result=inspect_campaign(args.campaign);args.output.write_text(json.dumps(result,indent=2,allow_nan=False)+'\n') + print('PASS' if result['passed'] else 'FAIL',args.output) + for name,case in result['cases'].items():print(name,'artifacts=',case['passed'],'outer_linear=',case.get('outer_linear_gate')) + return 0 if result['passed'] else 1 + + +if __name__=='__main__':raise SystemExit(main()) diff --git a/experiments/test_coupled_trial_checker.py b/experiments/test_coupled_trial_checker.py new file mode 100644 index 0000000..2f15522 --- /dev/null +++ b/experiments/test_coupled_trial_checker.py @@ -0,0 +1,335 @@ +#!/usr/bin/env python3 +"""Synthetic counterexamples guard against false coupled-correction successes.""" +import csv +import json +import math +from pathlib import Path +import tempfile +import unittest + +import check_coupled_trial as c + + +def csv_file(path,data,fields=None): + with path.open('w',newline='') as stream: + writer=csv.DictWriter(stream,fieldnames=fields or list(data[0]));writer.writeheader();writer.writerows(data) + + +def vec_file(path,values): + path.write_text(str(len(values))+'\n'+'\n'.join(repr(v) for v in values)+'\n') + + +class CoupledTrialCheckerTests(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,mode='kept'): + for path in self.folder.iterdir():path.unlink() + folder=self.folder;nan=math.nan + x=[0.,0.,0.];f=[1.,1.,1.];p=[-1.,-1.,-1.];j=p[:];raw_state=p[:] + factors=[2.]*3;fn=math.sqrt(3.);pn=fn;alpha=1. + raw_prepared=mode!='raw_failure';attempted=mode not in ('original_accepted','raw_failure') + before=[.5,0.,0.] if mode=='original_accepted' else [10.,0.,0.] + rhs=[-v for v in before] + correction=[-5.,0.,0.] if mode=='failed_gate' else [-10.,0.,0.] + action=correction[:];defect=[v-w for v,w in zip(action,rhs)] + cn=c.norm(correction);dn=c.norm(defect) + cap_ratio=1. if mode=='cap' else 10.;cap=cap_ratio*pn + core=1 if mode=='failed_gate' else (2 if mode=='cap' else 0) + corrected_observed=attempted and core==0 + corrected=[12100.,0.,0.] if mode=='worse' else ([2.,0.,0.] if mode=='improved_not_accepted' else [0.,0.,0.]) + corrected_state=[v+w for v,w in zip(raw_state,correction)] + kept=corrected_observed and mode!='worse' + status='kept' if kept else ('not_improved' if mode=='worse' else ('linear_failure' if mode=='failed_gate' else + ('norm_limit' if mode=='cap' else ('original_accepted' if mode=='original_accepted' else 'not_attempted')))) + selected_state=corrected_state if kept else raw_state[:] + selected=corrected if kept else before[:] + aux_preps=1 if kept else (2 if mode=='worse' else 0) + aux_restores=int(mode=='worse') + def save(name,values):vec_file(folder/(c.P+name+'.txt'),values) + for name,values in {'base_normalized':x,'base_physical':x,'base_residual':f,'state_factors':factors, + 'residual_factors':[3.]*3,'direction':p,'base_action':j,'base_defect':[0.]*3,'restored_residual':f, + 'restored_action':j,'prediction_a0':[0.]*3,'raw_state_a0':raw_state}.items():save(name,values) + if raw_prepared: + for name,values in {'raw_residual_a0':before,'before_state_a0':raw_state,'before_residual_a0':before, + 'selected_state_a0':selected_state,'selected_residual_a0':selected, + 'selected_physical_a0':[v/2 for v in selected_state]}.items():save(name,values) + if attempted: + for name,values in {'soc_rhs_a0':rhs,'soc_correction_a0':correction,'soc_action_a0':action,'soc_defect_a0':defect}.items():save(name,values) + if corrected_observed: + for name,values in {'corrected_state_a0':corrected_state,'corrected_residual_a0':corrected, + 'corrected_physical_a0':[v/2 for v in corrected_state]}.items():save(name,values) + layout=[{'kind':kind,'block':name,'offset':i,'size':1} for kind,names in ( + ('value',['density','surface_deformation','specific_enthalpy']), + ('residual',['closure','shape','hydrostatic'])) for i,name in enumerate(names)] + csv_file(folder/(c.P+'layout.csv'),layout) + def merit(v):return .5*c.norm(v)**2 + cutoff=(1-1.e-4)*1.5 + row={'sample':0,'alpha':alpha,'gravity_path':0,'raw_prepared':int(raw_prepared), + 'raw_rejection_reason':-1 if raw_prepared else 2,'raw_rejection_stage':-1 if raw_prepared else 6, + 'raw_norm':c.norm(before) if raw_prepared else nan,'raw_merit':merit(before) if raw_prepared else nan, + 'gravity_attempted':0,'gravity_reported_gate':1,'gravity_prepared_gate':1,'gravity_target_defect':nan, + 'gravity_affine_disagreement':nan,'gravity_target_threshold':nan,'before_prepared':int(raw_prepared), + 'before_usable':int(raw_prepared),'before_norm':c.norm(before) if raw_prepared else nan, + 'before_merit':merit(before) if raw_prepared else nan,'before_accepted':int(raw_prepared and merit(before)<=cutoff), + 'soc_called':int(raw_prepared),'soc_status':c.STATUS.index(status),'soc_attempted':int(attempted), + 'aux_status':0 if attempted else -1,'aux_reported_gate':int(attempted),'aux_iterations':1 if attempted else 0, + 'aux_rhs_norm':c.norm(rhs) if attempted else nan,'aux_reported_true_norm':0. if attempted else nan, + 'aux_verified_true_norm':dn if attempted else nan,'aux_verified_gate':int(attempted and dn<=.03*c.norm(rhs)), + 'aux_threshold':.03*c.norm(rhs) if attempted else nan,'aux_verification_applied':int(attempted), + 'correction_norm':cn if attempted else nan,'correction_cap':cap if attempted else nan, + 'correction_ratio':cn/pn if attempted else nan,'corrected_metric_observed':int(corrected_observed), + 'corrected_norm':c.norm(corrected) if corrected_observed else nan, + 'corrected_merit':merit(corrected) if corrected_observed else nan, + 'corrected_accepted':int(corrected_observed and merit(corrected)<=cutoff),'selected_prepared':int(raw_prepared), + 'selected_norm':c.norm(selected) if raw_prepared else nan,'selected_merit':merit(selected) if raw_prepared else nan, + 'selected_accepted':int(raw_prepared and merit(selected)<=cutoff),'selected_is_original':int(raw_prepared and not kept), + 'fallback_residual_difference':0. if raw_prepared and not kept else nan, + 'raw_seconds':.1,'gravity_seconds':0.,'soc_seconds':.3 if attempted else 0., + 'soc_refresh_seconds':.02 if attempted else 0.,'soc_solve_seconds':.1 if attempted else 0., + 'soc_verification_seconds':.01 if attempted else 0.,'soc_preparation_seconds':.05*aux_preps, + 'soc_preparations':aux_preps,'soc_restorations':aux_restores,'aux_operator_applications':2 if attempted else 0, + 'aux_inverse_applications':1 if attempted else 0,'verified_action_defect_norm':dn if attempted else nan, + 'capture_seconds':.1,'total_sample_seconds':2.,'aux_core_status':core if attempted else -1, + 'rhs_saved':int(attempted),'correction_saved':int(attempted),'verified_vectors_saved':int(attempted)} + csv_file(folder/(c.P+'trials.csv'),[row]) + blocks=[] + for stage,values in [('raw',before),('before',before),('corrected',corrected),('selected',selected)]: + if not raw_prepared or (stage=='corrected' and not corrected_observed):continue + for name,start,size in [('all',0,3),('closure',0,1),('shape',1,1),('hydrostatic',2,1)]: + blocks.append({'sample':0,'alpha':1.,'stage':stage,'block':name,'offset':start,'size':size, + 'residual_norm':c.norm(values[start:start+size])}) + csv_file(folder/(c.P+'blocks.csv'),blocks,['sample','alpha','stage','block','offset','size','residual_norm']) + preparations=[] + for kind,prepared,rn in [('initial_base_restore',True,fn),('capture_base',True,fn),('raw',raw_prepared,c.norm(before)),('final_base_restore',True,fn)]: + preparations.append({'sequence':len(preparations)+1,'kind':kind,'sample':0 if kind=='raw' else -1, + 'alpha':1. if kind=='raw' else 0.,'prepared':int(prepared),'rejection_reason':-1 if prepared else 2, + 'rejection_stage':-1 if prepared else 6,'seconds':.1,'residual_norm':rn if prepared else nan}) + csv_file(folder/(c.P+'preparations.csv'),preparations) + summary={'schema_version':1,'independent_frozen_alphas':1,'accepted_commits':0,'outer_linear_tolerance':.03, + 'sufficient_decrease':1.e-4,'gravity_path':0,'seed_control':1,'coupled_maximum_correction_ratio':cap_ratio, + 'coupled_relative_tolerance':.03,'coupled_absolute_tolerance':1.e-14,'coupled_maximum_iterations':10,'coupled_restart':4, + 'gravity_relative_tolerance':1.e-10,'gravity_absolute_tolerance':1.e-14,'gravity_maximum_iterations':100,'gravity_restart':20, + 'polynomial_direction_repairs':0,'sampled_enthalpy_policy':1,'coupled_attempt_budget_per_independent_alpha':1, + 'alpha_0':1.,'base_residual_norm':fn,'base_merit':1.5,'direction_norm':pn,'base_merit_slope':-3., + 'outer_verified_defect_norm':0.,'outer_verified_relative_defect':0.,'outer_verified_threshold':.03*fn, + 'outer_verified_gate':1,'outer_gate_failure_skips_trials':0,'soc_attempts':int(attempted),'soc_kept':int(kept), + 'soc_fallbacks':int(attempted and not kept),'soc_iterations':int(attempted), + 'soc_solve_seconds':row['soc_solve_seconds'],'soc_verification_seconds':row['soc_verification_seconds'], + 'soc_preparation_seconds':row['soc_preparation_seconds'],'soc_refresh_seconds':row['soc_refresh_seconds'], + 'soc_preparations':aux_preps,'soc_restorations':aux_restores,'probe_exception':0, + 'accepted_buffers_bitwise_identical':1,'normalization_factors_bitwise_identical':1,'direction_buffers_bitwise_identical':1, + 'direction_buffers_unchanged_before_restoration':1,'maximum_base_restoration_error':0.,'restored_base_action_error':0., + 'fresh_operator_and_inverse_current':1,'explicit_probe_preparations':4,'explicit_probe_preparation_seconds':.4, + 'explicit_probe_inverse_refreshes':3,'explicit_probe_inverse_refresh_seconds':.06, + 'explicit_probe_operator_applications':2,'explicit_probe_operator_seconds':.02,'total_seconds':10.} + summary.update({'soc_status_'+name:i for i,name in enumerate(c.STATUS)}) + csv_file(folder/(c.P+'summary.csv'),[{'metric':k,'value':v} for k,v in summary.items()]) + meta={'n':1.5,'K':1.,'M':1.,'J':0,'central_phase':0,'coupled_trial_probe':1,'linear_tol':.03,'coupled_tolerance':.03, + 'coupled_iterations':10,'coupled_restart':4,'coupled_maximum_ratio':cap_ratio,'coupled_attempts_per_iteration':1, + 'coupled_seed_control':1,'gravity_tolerance':1.e-10,'gravity_iterations':100,'gravity_restart':20, + 'strategy':'coupled','coupled_trial_alphas_are_independent':1,'coupled_trial_admissibility':'sampled_enthalpy_without_repair'} + (folder/'metadata.txt').write_text('\n'.join(f'{k}={v}' for k,v in meta.items())+'\n') + physical=[] + def measurement(sample,source,q,key,value): + physical.append({'sample':sample,'source':source,'quadrature':q,'metric':key,'value':value}) + for key in ('accepted_buffers_unchanged_before_restoration','accepted_buffers_unchanged_after_restoration', + 'normalization_factors_unchanged','direction_buffers_unchanged','accepted_operator_and_inverse_current'): + measurement(-1,'restoration',0,key,1) + measurement(-1,'replay',0,'selected_snapshots_measured',int(raw_prepared)) + if raw_prepared: + source=c.P+'selected_physical_a0.txt';measurement(0,source,0,'fresh_preparation_passed',1) + for q in (14,18): + metrics={'quadrature_order':q,'polytropic_index':1.5,'polytropic_constant':1.,'target_mass':1., + 'stellar_samples':10,'valid_stellar_samples':10,'invalid_stellar_samples':0,'negative_density_samples':0, + 'negative_enthalpy_samples':0,'invalid_density_eos_samples':0,'invalid_enthalpy_eos_samples':0, + 'volume':1.,'complete_volume_measurement':1,'pressure_from_enthalpy_defined_everywhere':1, + 'pressure_from_density_defined_everywhere':1,'saved_prepared_l2_difference':0., + 'saved_prepared_maximum_difference':0.,'saved_prepared_maximum_scaled_difference':0., + 'normalized_bordered_residual':c.norm(selected),'normalized_unbordered_residual':c.norm(selected), + 'normalized_border_action':0.,'central_border':0.,'mass':1.,'mass_absolute_error':0., + 'minimum_density':0.,'minimum_enthalpy':0.,'binding_energy':-3.,'force_binding_energy':-3., + 'kinetic_energy':0.,'pressure_integral':1.,'enthalpy_pressure_integral':1.} + for prefix in ('','enthalpy_'): + for force in ('','force_'): + metrics[prefix+force+'virial_signed']=0.;metrics[prefix+force+'virial_error']=0. + for key,value in metrics.items():measurement(0,source,q,key,value) + csv_file(folder/(c.P+'physical.csv'),physical) + state=self.campaign/'input_state.txt';direction=self.campaign/'input_direction.txt';vec_file(state,x);vec_file(direction,p) + binary=self.campaign/'plateau_diagnostic';binary.write_bytes(b'synthetic executable identity') + mesh=self.campaign/'mesh.smesh';mesh.write_text('synthetic mesh identity') + args=['--coupled-trial-probe','--probe-state',str(state),'--probe-direction',str(direction),'--strategy','coupled','--coupled-seed-control'] + cases=[{'id':'synthetic','arguments':args}] + 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),*args,'--mesh',str(mesh),'--output',str(folder)], + 'input_state_hashes':{str(state):c.digest(state),str(direction):c.digest(direction)}, + 'output_hashes':{p.name:c.digest(p) for p in folder.iterdir()}} + (self.campaign/'synthetic_run.json').write_text(json.dumps(record)) + + def alter(self,name,callback): + path=self.folder/(c.P+name+'.csv');data=c.rows(path);callback(data);csv_file(path,data) + + def valid(self,result): + failed=[x for x in result.get('checks',result.get('provenance_checks',[])) if not x['passed']] + failed += [x for case in result.get('cases',{}).values() for x in case['checks'] if not x['passed']] + self.assertTrue(result['passed'],failed) + + def test_exact_mixed_correction_and_provenance(self): + self.valid(c.inspect_campaign(self.campaign)) + + def test_failed_verified_gate_is_valid_evidence_but_not_a_correction_success(self): + self.make_case('failed_gate');r=c.inspect_case(self.folder);self.valid(r) + self.assertFalse(r['trial_outcomes'][0]['aux_verified_gate']);self.assertFalse(r['trial_outcomes'][0]['full_merit_improved']) + + def test_cap_rejection_preserves_original_trial(self): + self.make_case('cap');r=c.inspect_case(self.folder);self.valid(r) + self.assertEqual(r['trial_outcomes'][0]['transaction_status'],'norm_limit') + + def test_worse_corrected_merit_requires_fallback(self): + self.make_case('worse');r=c.inspect_case(self.folder);self.valid(r) + self.assertEqual(r['trial_outcomes'][0]['transaction_status'],'not_improved') + self.assertFalse(r['trial_outcomes'][0]['selected_armijo']) + + def test_kept_correction_does_not_imply_armijo_acceptance(self): + self.make_case('improved_not_accepted');r=c.inspect_case(self.folder);self.valid(r) + self.assertTrue(r['trial_outcomes'][0]['full_merit_improved']);self.assertFalse(r['trial_outcomes'][0]['selected_armijo']) + + def test_already_acceptable_trial_needs_no_auxiliary_solve(self): + self.make_case('original_accepted');r=c.inspect_case(self.folder);self.valid(r) + self.assertFalse(r['trial_outcomes'][0]['correction_attempted']);self.assertTrue(r['trial_outcomes'][0]['selected_armijo']) + + def test_failed_raw_preparation_has_no_false_correction_certificate(self): + self.make_case('raw_failure');r=c.inspect_case(self.folder);self.valid(r) + self.assertFalse(r['trial_outcomes'][0]['before_usable']);self.assertFalse(r['trial_outcomes'][0]['selected_armijo']) + + def test_failed_outer_gate_is_valid_evidence_without_trials(self): + for path in self.folder.glob(c.P+'*_a*.txt'):path.unlink() + for name,values in {'base_action':[-.5]*3,'restored_action':[-.5]*3,'base_defect':[.5]*3}.items(): + vec_file(self.folder/(c.P+name+'.txt'),values) + for name in ('trials','blocks'): + path=self.folder/(c.P+name+'.csv');csv_file(path,[],list(c.rows(path)[0])) + updates={'base_merit_slope':-1.5,'outer_verified_defect_norm':math.sqrt(.75), + 'outer_verified_relative_defect':.5,'outer_verified_gate':0,'outer_gate_failure_skips_trials':1, + 'explicit_probe_preparations':2,'explicit_probe_preparation_seconds':.2} + self.alter('summary',lambda rows:[r.update(value=updates[r['metric']]) for r in rows if r['metric'] in updates]) + def preparations(rows): + rows[:]=[r for r in rows if r['kind'] in ('initial_base_restore','final_base_restore')] + for i,row in enumerate(rows):row['sequence']=i+1 + self.alter('preparations',preparations) + def physical(rows): + rows[:]=[r for r in rows if r['sample']=='-1'] + next(r for r in rows if r['metric']=='selected_snapshots_measured')['value']=0 + self.alter('physical',physical) + result=c.inspect_case(self.folder);self.valid(result) + self.assertFalse(result['outer_linear_gate']);self.assertEqual(result['trial_outcomes'],[]) + + def test_gravity_path_then_full_correction_has_distinct_saved_targets(self): + def save(name,values):vec_file(self.folder/(c.P+name+'_a0.txt'),values) + save('gravity_correction',[2.,0.,0.]);save('before_state',[1.,-1.,-1.]);save('before_residual',[0.,10.,0.]) + for name in ('soc_rhs','soc_correction','soc_action'):save(name,[0.,-10.,0.]) + for name in ('corrected_state','selected_state'):save(name,[1.,-11.,-1.]) + for name in ('corrected_physical','selected_physical'):save(name,[.5,-5.5,-.5]) + self.alter('trials',lambda rows:rows[0].update(gravity_path=1,gravity_attempted=1, + gravity_target_defect=0.,gravity_affine_disagreement=0.,gravity_target_threshold=1.e-12)) + self.alter('summary',lambda rows:next(r for r in rows if r['metric']=='gravity_path').update(value=1)) + meta=self.folder/'metadata.txt';meta.write_text(meta.read_text().replace('strategy=coupled','strategy=curvature-coupled')) + self.alter('layout',lambda rows:[r.update(block={'density':'gravity_potential','closure':'poisson_balance'}.get(r['block'],r['block'])) for r in rows]) + def blocks(rows): + for row in rows: + if row['stage']=='before' and row['block']=='closure':row['residual_norm']=0. + if row['stage']=='before' and row['block']=='shape':row['residual_norm']=10. + if row['block']=='closure':row['block']='poisson_balance' + self.alter('blocks',blocks) + self.valid(c.inspect_case(self.folder)) + self.alter('trials',lambda rows:rows[0].update(gravity_target_defect=1.e-6)) + self.assertFalse(c.inspect_case(self.folder)['passed']) + + def test_false_saved_true_gate_cannot_pass(self): + self.make_case('failed_gate');self.alter('trials',lambda rows:rows[0].update(aux_verified_gate=1)) + self.assertFalse(c.inspect_case(self.folder)['passed']) + + def test_finite_failed_verification_cannot_be_hidden_by_save_flags(self): + self.make_case('failed_gate');self.alter('trials',lambda rows:rows[0].update(verified_vectors_saved=0)) + for name in ('soc_action_a0','soc_defect_a0'):(self.folder/(c.P+name+'.txt')).unlink() + self.assertFalse(c.inspect_case(self.folder)['passed']) + + def test_looser_reported_controls_cannot_override_requested_controls(self): + self.make_case('failed_gate') + self.alter('summary',lambda rows:next(r for r in rows if r['metric']=='coupled_relative_tolerance').update(value=1.)) + self.assertFalse(c.inspect_case(self.folder)['passed']) + + def test_conflicting_raw_preparation_is_not_usable_evidence(self): + self.alter('preparations',lambda rows:next(r for r in rows if r['kind']=='raw').update(prepared=0,rejection_reason=2,rejection_stage=6)) + self.assertFalse(c.inspect_case(self.folder)['passed']) + + def test_physical_replay_is_bound_to_selected_residual(self): + self.alter('physical',lambda rows:next(r for r in rows if r['metric']=='normalized_bordered_residual').update(value=.1)) + self.assertFalse(c.inspect_case(self.folder)['passed']) + + def test_undefined_density_pressure_is_valid_evidence_not_virial_success(self): + def negative_density(rows): + for row in rows: + if row['sample']=='0' and row['quadrature'] in ('14','18'): + key=row['metric'] + if key=='negative_density_samples':row['value']=1 + elif key=='minimum_density':row['value']=-.001 + elif key=='pressure_from_density_defined_everywhere':row['value']=0 + elif key in ('pressure_integral','virial_signed','virial_error','force_virial_signed','force_virial_error'):row['value']=math.nan + self.alter('physical',negative_density);result=c.inspect_case(self.folder);self.valid(result) + self.assertIsNone(result['physical_observations'][0]['virial_status']['virial_within_1e5']) + self.assertTrue(result['physical_observations'][0]['virial_status']['enthalpy_virial_within_1e5']) + self.assertEqual(result['physical_observations'][0]['global_enthalpy_certificate'],'not_measured') + + def test_false_physical_virial_cannot_be_declared(self): + self.alter('physical',lambda rows:next(r for r in rows if r['metric']=='enthalpy_pressure_integral').update(value=2.)) + self.assertFalse(c.inspect_case(self.folder)['passed']) + + def test_missing_physical_restoration_guard_fails(self): + self.alter('physical',lambda rows:rows.__setitem__(slice(None),[r for r in rows if r['metric']!='direction_buffers_unchanged'])) + self.assertFalse(c.inspect_case(self.folder)['passed']) + + def test_corrupted_corrected_displacement_is_detected(self): + path=self.folder/(c.P+'corrected_state_a0.txt');v=c.vector(path);v[1]+=.1;vec_file(path,v) + self.assertFalse(c.inspect_case(self.folder)['passed']) + + def test_rejected_correction_cannot_replace_original_selected_state(self): + self.make_case('worse');path=self.folder/(c.P+'selected_state_a0.txt');vec_file(path,c.vector(self.folder/(c.P+'corrected_state_a0.txt'))) + self.assertFalse(c.inspect_case(self.folder)['passed']) + + def test_missing_restoration_invariant_is_not_success(self): + self.alter('summary',lambda data:data.__setitem__(slice(None),[r for r in data if r['metric']!='direction_buffers_unchanged_before_restoration'])) + self.assertFalse(c.inspect_case(self.folder)['passed']) + + def test_false_armijo_acceptance_is_detected(self): + self.make_case('improved_not_accepted');self.alter('trials',lambda rows:rows[0].update(selected_accepted=1,corrected_accepted=1)) + self.assertFalse(c.inspect_case(self.folder)['passed']) + + def test_unhashed_auxiliary_action_cannot_support_campaign(self): + path=self.campaign/'synthetic_run.json';r=json.loads(path.read_text());del r['output_hashes'][c.P+'soc_action_a0.txt'];path.write_text(json.dumps(r)) + self.assertFalse(c.inspect_campaign(self.campaign)['passed']) + + def test_incomplete_producer_cannot_support_campaign(self): + path=self.campaign/'synthetic_run.json';r=json.loads(path.read_text());del r['finished_utc'];path.write_text(json.dumps(r)) + self.assertFalse(c.inspect_campaign(self.campaign)['passed']) + + def test_later_missing_producer_suppresses_earlier_interpretation(self): + path=self.campaign/'provenance.json';provenance=json.loads(path.read_text()) + provenance['cases'].append({'id':'unfinished','arguments':provenance['cases'][0]['arguments']}) + path.write_text(json.dumps(provenance));(self.campaign/'cases.json').write_text(json.dumps(provenance['cases'])) + result=c.inspect_campaign(self.campaign);self.assertFalse(result['passed']) + self.assertFalse(result['cases']['synthetic']['observations_valid']) + self.assertEqual(result['cases']['synthetic']['trial_outcomes'],[]) + self.assertEqual(result['cases']['synthetic']['physical_observations'],[]) + + def test_rehashed_changed_input_direction_is_not_the_measured_direction(self): + path=self.campaign/'input_direction.txt';vec_file(path,[1.,2.,3.]);rp=self.campaign/'synthetic_run.json' + r=json.loads(rp.read_text());r['input_state_hashes'][str(path)]=c.digest(path);rp.write_text(json.dumps(r)) + self.assertFalse(c.inspect_campaign(self.campaign)['passed']) + + +if __name__=='__main__':unittest.main()