"""Regenerate summaries and figures from archived synthetic trial metrics.
No registration rerun: trials.csv is the immutable input to this export.
"""
import csv,json,hashlib,itertools,statistics
from pathlib import Path
import matplotlib
matplotlib.use('Agg')
import matplotlib.pyplot as plt
import numpy as np
P=Path(__file__).resolve().parent
with (P/'trials.csv').open() as f: rows=[{k:float(v) for k,v in r.items()} for r in csv.DictReader(f)]
assert len(rows)==135
keys=('noise_sd_mm','random_point_dropout_fraction','scale_bias_fraction')
expected=set(itertools.product([.05,.2,.5],[.1,.3,.5],[0,.005,.02]))
assert {tuple(r[k] for k in keys) for r in rows}==expected
passed=lambda r:r['bidirectional_p95_distance_mm']<=1 and r['symmetric_mean_surface_distance_mm']<=.5 and r['surface_coverage_within_tolerance_fraction']>=.7
cells=[]
for key in sorted(expected):
 rr=[r for r in rows if tuple(r[k] for k in keys)==key];assert len(rr)==5
 assert {int(r['trial']) for r in rr}==set(range(1,6))
 c=dict(zip(keys,key));c.update(trials=5,median_p95_mm=statistics.median(r['bidirectional_p95_distance_mm'] for r in rr),median_symmetric_mean_mm=statistics.median(r['symmetric_mean_surface_distance_mm'] for r in rr),median_sample_coverage=statistics.median(r['surface_coverage_within_tolerance_fraction'] for r in rr),trials_passing_gates=sum(map(passed,rr)))
 cells.append(c)
assert sum(map(passed,rows))==89
assert all(r['phantom_surface_points']==4728 and r['surface_coverage_tolerance_mm']==.75 and 0<=r['surface_coverage_within_tolerance_fraction']<=1 for r in rows)
with (P/'cells.csv').open('w',newline='') as f:
 w=csv.DictWriter(f,fieldnames=cells[0]);w.writeheader();w.writerows(cells)
plt.rcParams.update({'font.family':'DejaVu Sans','font.size':10,'axes.spines.top':False,'axes.spines.right':False,'svg.fonttype':'none','pdf.fonttype':42})
fig,axes=plt.subplots(2,3,figsize=(12,7.6),sharex=True,sharey='row')
colors=['#007b91','#7843a4','#c05527'];noise=[.05,.2,.5];biases=[0,.005,.02]
for col,deletion in enumerate([.1,.3,.5]):
 for ni,n in enumerate(noise):
  cc=[c for c in cells if c[keys[0]]==n and c[keys[1]]==deletion]
  for row,(metric,med,scale) in enumerate([('bidirectional_p95_distance_mm','median_p95_mm',1),('surface_coverage_within_tolerance_fraction','median_sample_coverage',100)]):
   ax=axes[row,col]
   for bi,b in enumerate(biases):
    vals=[r[metric]*scale for r in rows if tuple(r[k] for k in keys)==(n,deletion,b)]
    ax.scatter(bi+(ni-1)*.14+np.linspace(-.035,.035,5),vals,s=15,color=colors[ni],alpha=.5)
   ax.plot(np.arange(3)+(ni-1)*.14,[c[med]*scale for c in cc],color=colors[ni],lw=1.7,marker='_',markersize=10,label=f'{n:g} mm noise SD')
 axes[0,col].set_title(f'{deletion:.0%} random sample deletion')
 axes[0,col].axhline(1,color='#777',ls='--',lw=1)
 axes[1,col].axhline(70,color='#777',ls='--',lw=1)
 axes[1,col].set(xticks=range(3),xticklabels=['0','0.5','2'],xlabel='Applied scale bias (%)')
 for ax in axes[:,col]:ax.grid(alpha=.17);ax.set_xlim(-.35,2.35)
axes[0,0].set_ylabel('Pooled bidirectional P95 distance (mm)');axes[1,0].set_ylabel('Reference samples within 0.75 mm (%)')
axes[0,0].legend(fontsize=8,loc='upper left');axes[1,0].set_ylim(55,103)
fig.suptitle('Synthetic scan alignment: noise, missing samples and size bias',fontsize=16,y=.99)
fig.text(.5,.045,'Dots: five synthetic trials per cell. Lines: medians. Dashed: two of three illustrative gates; mean-distance gate is not shown.',ha='center',fontsize=9)
fig.text(.5,.018,'4,728 weighted reference samples, 1,184 unique coordinates. Random deletion is not physical occlusion. No scanner or printer tested.',ha='center',fontsize=9)
fig.tight_layout(rect=[0,.075,1,.965])
for ext in ['png','svg','pdf']:fig.savefig(P/f'scan-sensitivity.{ext}',dpi=190)
summary={'title':'Synthetic point-cloud alignment sensitivity','status':'Re-export of archived synthetic trial metrics; no physical measurement or new registration run','seed_of_original_calculation':1378,'rows':{'trials':135,'cells':27},'reference':{'samples':4728,'unique_coordinates':1184,'voxel_grid_mm':2,'shape':'asymmetric stepped bracket'},'gates':{'pooled_p95_mm_max':1,'symmetric_mean_mm_max':.5,'reference_sample_fraction_min':.7,'sample_distance_tolerance_mm':.75},'passes':89,'passes_by_noise':[{'noise_sd_mm':n,'passes':sum(passed(r) for r in rows if r[keys[0]]==n),'trials':45} for n in noise],'definitions':{'coverage':'Fraction of reference sample records with a nearest aligned sample within 0.75 mm; repeated coordinates retain multiplicity. Not surface-area coverage.','symmetric_mean':'Arithmetic mean of the two directed nearest-neighbour means, giving directions equal weight.','pooled_p95':'95th percentile of concatenated directed distances, giving each sample equal weight; direction weights change with sample count. Original calculation uses NumPy default linear quantile.','registration':'Centroid initialization, rigid point-to-point ICP, closest 90% correspondences retained, maximum 60 iterations. Known reference is available.'},'limitations':['Uniform random sample deletion does not represent optical occlusion or visibility. Duplicate reference positions can survive deletion through other records.','Five trials per condition do not establish a real-world reliability rate; sample points are not independent experiments.','No surface mesh, solid reconstruction, slicing, printing, material chemistry, roughness, shrinkage or internal-defect model.','Thresholds are chosen teaching gates, not an industry standard or an accuracy guarantee.','Archived metrics alone cannot reproduce point clouds or independently re-evaluate nearest-neighbour distances. This generator reproduces summaries and plots only.'],'checks':{'factorial_complete':True,'five_trials_per_cell':True,'gate_count_recomputed':True,'sample_bounds_checked':True},'input_sha256':hashlib.sha256((P/'trials.csv').read_bytes()).hexdigest(),'sources':[{'title':'NIST orientation and uncertainty in 3D imaging','url':'https://www.nist.gov/publications/method-estimate-orientation-and-uncertainty-objects-measured-using-3d-imaging-systems'},{'title':'NIST Additive Manufacturing Test Artifact','url':'https://www.nist.gov/el/intelligent-systems-division-73500/production-systems-group/nist-additive-manufacturing-test'}]}
(P/'study.json').write_text(json.dumps(summary,indent=2)+'\n')
print(json.dumps({'rows':summary['rows'],'passes':89,'input_sha256':summary['input_sha256']}))
