"""Synthetic setup trials, clamp statics, pad reactions, and press-fit screening.

Run in the project folder. No physical measurements are represented.
"""
import json,math
import numpy as np
import matplotlib;matplotlib.use('Agg')
import matplotlib.pyplot as plt
from completion_util import ROOT,OUT,save_json,csv_file,check,table,finish

def clamp(c,torque,nut_factor,friction):
 if min(torque,nut_factor,friction,c['screw_diameter_mm'])<=0:raise ValueError('Positive torque, nut factor, friction and diameter required')
 preload=torque*1000/(nut_factor*c['screw_diameter_mm'])
 toe=preload*(c['heel_y_mm']-c['screw_y_mm'])/(c['heel_y_mm']-c['toe_y_mm'])
 z=c['toe_width_mm']*c['toe_thickness_mm']**2/6
 stress=toe*(c['screw_y_mm']-c['toe_y_mm'])/z
 demand=c['normal_lateral_force_N']+c['normal_drill_torque_Nm']*1000/c['friction_lever_arm_mm']
 return {'torque_Nm':torque,'nut_factor':nut_factor,'preload_N':preload,'toe_force_each_N':toe,'clamp_bending_MPa':stress,'friction_capacity_N':2*friction*toe,'normal_force_margin':2*friction*toe/(c['design_factor']*demand),'pullup_margin':2*toe/(c['design_factor']*c['maximum_pullup_N'])}

def pad_reactions(toe,thrust,x,y):
 # Positive reactions balance two toe forces at (0,+/-39), plus drill thrust.
 A=np.array([[1.,1.,1.],[-45.,45.,0.],[-27.,-27.,28.]])
 return np.linalg.solve(A,[2*toe+thrust,thrust*x,thrust*y])

def press_fit(c,interference,temperature):
 a=5.;inner=2.1;b=c['equivalent_host_outer_radius_mm']
 thermal=10*(c['aluminum_expansion_per_K']-c['steel_expansion_per_K'])*(temperature-20)
 fit=interference-thermal
 compliance=2*a*(((b*b+a*a)/(b*b-a*a)+c['aluminum_poisson'])/c['aluminum_E_MPa']+((a*a+inner*inner)/(a*a-inner*inner)-c['steel_poisson'])/c['steel_E_MPa'])
 pressure=max(0.,fit/compliance)
 retention=c['bushing_interface_friction']*pressure*math.pi*2*a*c['bushing_engagement_mm']
 return {'temperature_C':temperature,'initial_interference_mm':interference,'effective_interference_mm':fit,'contact_pressure_MPa':pressure,'axial_retention_N':retention,'retention_target_N':c['design_factor']*c['maximum_pullup_N'],'meets_retention_target':retention>=c['design_factor']*c['maximum_pullup_N']}

def disk(rng,r):
 a=rng.uniform(0,2*math.pi);return r*math.sqrt(rng.random())*np.array([math.cos(a),math.sin(a)])

def main():
 c=json.loads((ROOT/'completion-inputs.json').read_text());g=json.loads((ROOT/'inputs.json').read_text());t=g['tolerance'];rng=np.random.default_rng(c['seed']);checks=[]
 loads=[clamp(c,T,K,c['minimum_clamp_friction']) for T in c['torque_Nm'] for K in c['nut_factor']]
 csv_file('clamp-envelope.csv',loads)
 stress=max(x['clamp_bending_MPa'] for x in loads);friction=min(x['normal_force_margin'] for x in loads);pull=min(x['pullup_margin'] for x in loads)
 checks += [check('clamp-normal','Declared normal drilling side load with factor 2','minimum capacity/demand >= 1',round(friction,5),friction>=1,'Rigid lever statics, F=T/(Kd), conservative toe friction'),check('clamp-stress','4 mm steel toe bending with factor 1.5',f"<= {c['clamp_yield_MPa']/c['stress_factor']:.4f} MPa",round(stress,5),stress<=c['clamp_yield_MPa']/c['stress_factor'],'Rectangular elastic section, M/Z; stress concentration is excluded'),check('clamp-pullup','Both clamps resist declared pullup with factor 2','margin >= 1',round(pull,4),pull>=1,'Vertical force balance')]
 toe=min(x['toe_force_each_N'] for x in loads);pads=[]
 for v in g['variants']:
  for h,(x,y) in enumerate(v['holes'],1):
   reactions=pad_reactions(toe,c['maximum_downthrust_N'],x,y)
   pads.append({'variant':v['name'],'hole':h,'x_mm':x,'y_mm':y,**{f'pad_{i+1}_N':float(r) for i,r in enumerate(reactions)},'minimum_reaction_N':float(min(reactions)),'maximum_pad_pressure_MPa':float(max(reactions)/(math.pi*6**2)),'force_residual_N':float(abs(sum(reactions)-2*toe-c['maximum_downthrust_N']))})
 csv_file('pad-reactions.csv',pads)
 checks.append(check('pad-contact','Three supports remain compressed at every drilling point','all pad reactions > 0 N',round(min(x['minimum_reaction_N'] for x in pads),4),all(x['minimum_reaction_N']>0 for x in pads),'Rigid workpiece, force and X/Y moment equilibrium'))
 checks.append(check('pad-equilibrium','Reaction forces balance toe load and thrust','residual < 1e-9 N',max(x['force_residual_N'] for x in pads),max(x['force_residual_N'] for x in pads)<1e-9,'Independent linear equilibrium solve'))
 trials=[];budgets=[]
 radial=(t['datum_hole_max']-t['pin_min'])/2
 for v in g['variants']:
  fixed={i:disk(rng,t['locator_position_radial'])+disk(rng,t['bushing_position_radial']) for i in range(len(v['holes']))}
  for op in range(c['operators']):
   for cycle in range(c['cycles_per_operator']):
    trans=disk(rng,radial);rotation=rng.uniform(-2*radial/g['datum_pitch'],2*radial/g['datum_pitch']);seat=disk(rng,t['insert_seating_radial'])
    for i,(x,y) in enumerate(v['holes']):
     err=trans+rotation*np.array([-y,x+40])+seat+fixed[i]+disk(rng,t['drill_bushing_radial_clearance'])+disk(rng,t['drill_walk_allowance'])
     trials.append({'variant':v['name'],'operator':op+1,'cycle':cycle+1,'hole':i+1,'error_x_mm':float(err[0]),'error_y_mm':float(err[1]),'radial_error_mm':float(np.linalg.norm(err)),'synthetic':True})
  r=max(math.hypot(x+40,y) for x,y in v['holes']);nominal=radial+2*radial/g['datum_pitch']*r+sum(t[k] for k in ['locator_position_radial','insert_seating_radial','bushing_position_radial','drill_bushing_radial_clearance','drill_walk_allowance']);worn=nominal+c['worn_bushing_additional_radial_mm']
  budgets.append({'variant':v['name'],'normal_worst_radial_mm':nominal,'worn_worst_radial_mm':worn,'target_mm':t['allowed_radial_error'],'remaining_wear_radial_mm':t['allowed_radial_error']-nominal,'normal_accepted':nominal<=t['allowed_radial_error'],'worn_accepted':worn<=t['allowed_radial_error']})
 csv_file('synthetic-reseating-trials.csv',trials);csv_file('wear-acceptance.csv',budgets)
 checks += [check('reseating-count','Three operators, ten reseats, three variants, three holes','270 recorded hole locations',len(trials),len(trials)==270,'Seeded bounded draws within existing tolerance inputs; no assumed production distribution'),check('reseating-envelope','Synthetic hole errors stay within the conservative envelope','max <= 0.20 mm',max(x['radial_error_mm'] for x in trials),max(x['radial_error_mm'] for x in trials)<=.2,'Vector addition and first-order rotation at each hole'),check('wear-rejection','An extra 0.025 mm radial bushing clearance exceeds allowance','reject every variant',[x['worn_accepted'] for x in budgets],not any(x['worn_accepted'] for x in budgets),'Deterministic worst-case stack, independent of sampled trial tails')]
 fit=[press_fit(c,d,T) for d in c['bushing_interference_mm'] for T in c['temperature_C']];csv_file('bushing-retention.csv',fit)
 hot=next(r for r in fit if r['initial_interference_mm']==.002 and r['temperature_C']==60)
 checks.append(check('hot-fit-rejection','Minimum press fit is lost at 60 C','effective interference < 0; zero retention',hot['effective_interference_mm'],hot['effective_interference_mm']<0 and hot['axial_retention_N']==0,'Differential thermal expansion plus elastic thick-cylinder compliance'))
 minfit20=next(r for r in fit if r['initial_interference_mm']==.002 and r['temperature_C']==20)
 checks.append(check('retention-screen','Bare minimum-interference press fit does not satisfy pullup factor 2','reject minimum-fit retention at 20 C',round(minfit20['axial_retention_N'],3),not minfit20['meets_retention_target'],'Friction-only axial holding force; no credit for an unspecified shoulder'))
 bad=clamp(c,.7,.25,.05)
 checks.append(check('loose-clamp-rejection','Loose oily clamps cannot meet side-force criterion','margin < 1',bad['normal_force_margin'],bad['normal_force_margin']<1,'Reduced torque and friction fault injection'))
 
 # Positive flange and bolted-cap retention added to the CAD after the bare-fit screen failed.
 r=c['retainer'];design_pull=c['design_factor']*c['maximum_pullup_N']
 cap_stress=(design_pull*r['span_mm']/4)/((r['width_mm']-r['drill_clearance_diameter_mm'])*r['thickness_mm']**2/6)
 screw_load=design_pull/2;proof_capacity=r['screw_tensile_area_mm2']*r['screw_proof_MPa']
 thread_capacity=math.pi*r['thread_pitch_diameter_mm']*r['thread_engagement_mm']/2*r['aluminum_thread_shear_allowable_MPa']
 preload_max=max(r['seating_torque_Nm'])*1000/(min(r['nut_factor'])*3)
 flange_pressure=design_pull/(math.pi*(r['flange_diameter_mm']**2-r['stem_diameter_mm']**2)/4)
 retainer={'design_pullup_N':design_pull,'cap_bending_MPa':cap_stress,'cap_allowable_MPa':c['clamp_yield_MPa']/c['stress_factor'],'screw_load_N':screw_load,'preload_max_N':preload_max,'screw_proof_capacity_N':proof_capacity,'thread_shear_capacity_N':thread_capacity,'flange_bearing_MPa':flange_pressure,'flange_allowable_MPa':r['flange_bearing_allowable_MPa']}
 save_json('retainer-loadpath.json',retainer)
 checks.append(check('retainer-cap','Selected retention cap carries factor-two pullup','bending <= yield/1.5',cap_stress,cap_stress<=c['clamp_yield_MPa']/c['stress_factor'],'Central load on 21 mm span; net width 14-4.5 mm, thickness 3 mm'))
 checks.append(check('retainer-fasteners','Two M3 screws and tapped insert carry pullup plus maximum seating preload','load+preload below screw proof and thread screen',{'combined_N':screw_load+preload_max,'proof_N':proof_capacity,'thread_N':thread_capacity},screw_load+preload_max<min(proof_capacity,thread_capacity),'Conservative full external-load addition; 5 mm thread engagement; 50 MPa declared thread shear allowable'))
 checks.append(check('retainer-flange','Flange bearing is adequate under factor-two pullup/downward screen','pressure <= 60 MPa',flange_pressure,flange_pressure<=r['flange_bearing_allowable_MPa'],'Annular projected area between14 mm flange and10 mm stem'))

 fig,ax=plt.subplots(figsize=(9,4));colors=['#d68d70','#a6bea0','#9bbcc5']
 for v,col in zip(g['variants'],colors):
  q=[r['radial_error_mm'] for r in trials if r['variant']==v['name']];ax.plot(q,marker='.',ms=2,lw=.8,label=v['name'],color=col)
 ax.axhline(.2,color='black',ls='--',label='0.20 mm allowance');ax.set(xlabel='Synthetic hole observation within variant',ylabel='Radial error (mm)',title='Bounded synthetic reseating trial');ax.legend();fig.tight_layout();fig.savefig(OUT/'reseating.png',dpi=140);plt.close(fig)
 lim=['The loads, friction coefficients, nut factors, elastic properties and process errors are declared scenarios. No measured drill forces, torque calibration or process capability is implied.','Clamp screening treats the toe as a rectangular elastic beam. Local fillets, screw-hole stress, threads, plate bending and impact require a more detailed model before hardware release.','The press-fit host is approximated as a concentric annulus of 10 mm outer radius. Nearby insert edges and holes make the real stiffness non-axisymmetric. This is a retention screening calculation, not a certified press-fit load.','The original bare press-fit alternative fails the retention screen and is rejected. The supplied revised CAD uses flanged bushes and two-screw retention caps; numerical retention checks do not establish physical assembly durability.','Synthetic operators share a bounded error model. The sample does not establish Cpk, operator performance or a real repeatability distribution.']
 sections=[('Selected operating envelope','<p>The revised reference fixture uses flanged hardened-steel bushes with 30 x 14 x 3 mm steel caps, two M3 x 10 screws per cap, and the supplied toe clamps. The calculated envelope is 40 N lateral force plus 0.6 N m drill torque, 150 N pullup, 350 N downward thrust, 2.8-3.2 N m M6 toe-clamp torque, friction at least 0.12, and fixture temperature 10-35 C. The 0.20 mm radial allowance applies to the stated unworn locating inputs. Caps use 0.22-0.28 N m seating torque and 5 mm thread engagement. Every selected-load acceptance check passes; the original bare fit and worn/loose cases are retained as rejected alternatives.</p>'+table([retainer])),('Decision','<p>The locating stack fits the 0.20 mm radial allowance in its unworn condition. The assumed 2.8-3.2 N m clamp torque range meets the stated side-load, pullup and elastic toe screens. Worn guides and loose/oily clamps are rejected. The bare press-fit alternative failed the axial-retention screen. The supplied CAD now includes flanged bushes and two-screw caps, with a checked retention load path independent of the interference fit.</p>'),('Declared load and material envelope',table([{'parameter':k,'value':v} for k,v in c.items() if k!='basis'])),('Clamp and support calculation','<p>Screw preload F = T/(K d). Toe force follows lever equilibrium about the heel: Ftoe = F (54-50)/(54-39). Both toe forces resist upward pull; conservative lateral demand is side force plus drilling torque divided by a 50 mm friction arm. Toe stress is M/Z with Z = b h squared / 6. The three pad reactions solve force and two moment equations for every drilled point.</p>'+table(loads)+table(pads)),('Synthetic multi-operator trial','<p>Seed 42017 generates fixed locator/bushing errors per variant and bounded reseating, angular, guide-clearance and drill-walk errors. Radial terms are uniformly sampled in disks; rotation is uniform within its bound. These are assumed input distributions. Fixed errors remain fixed over the trial. The deterministic worst-case stack governs acceptance.</p><img src="reseating.png" alt="Synthetic radial errors for three plate variants"/>'+table(budgets)),('Bushing retention and thermal fit','<p>Effective diametral interference equals cold interference minus D (alpha aluminum minus alpha steel) (T-20). Elastic thick-cylinder radial compliances of the insert annulus and hollow steel bush are added. Pressure is nonnegative; axial friction resistance is mu p pi D L. No shoulder or adhesive is assumed. The worst minimum fit loses contact at 60 C; at 20 C it also fails the factor-two pullup requirement.</p>'+table(fit)),('Files','<p>CSV files include every synthetic hole observation, clamp corner case, pad reaction, wear budget and press-fit condition. Expected rejected cases remain in the acceptance matrix so a passing script cannot hide an unsuitable design condition.</p>')]
 finish('Mounting fixture commissioning study',checks,sections,lim,['complete_validation.py','completion_util.py','completion-inputs.json','inputs.json','model.py'],{'synthetic_observations':len(trials),'minimum_clamp_margin':friction,'minimum_retention_case_N':minfit20['axial_retention_N'],'positive_bushing_capture_in_CAD':True,'retainer_design_pull_N':design_pull,'retainer_cap_stress_MPa':cap_stress})
if __name__=='__main__':main()
