"""Executed thermal commissioning and failure cases using the supplied FE matrices."""
import copy,json,math
import numpy as np
import matplotlib;matplotlib.use('Agg')
import matplotlib.pyplot as plt
from scipy.sparse.linalg import factorized
from solver import load,model
from completion_util import ROOT,OUT,csv_file,check,table,finish

def control(error,integral,control,dt):
 if not math.isfinite(error):return 0.,integral
 raw=control['Kp_W_K']*error+integral;power=float(np.clip(raw,0,control['maximum_power_W']))
 if 0<raw<control['maximum_power_W'] or raw>=control['maximum_power_W'] and error<0 or raw<=0 and error>0:integral+=control['Ki_W_Ks']*error*dt
 return power,integral

def simulate(base,c,case,dt=None):
 dt=c['time_step_s'] if dt is None else dt
 if dt<=0 or c['horizon_s']<dt:raise ValueError('Positive timestep inside horizon required')
 g=copy.deepcopy(base);g['ambient_C']=case.get('ambient_C',g['ambient_C']);g['bottom_U_W_m2K']=case.get('bottom_U',g['bottom_U_W_m2K'])
 m=model(g,'even',.005,case.get('poor_contact',False),case.get('top_h',g['top_convection_W_m2K']));M=m['M'];K=m['K'];step=factorized((M/dt+K).tocsc());theta=np.zeros(m['n']+3);ones=np.ones(len(theta));integ=0.;energy_in=0.;energy_out=0.;cutoff_time=None;requested_trip=None;fault_latched=False;frozen=None;ready_since=None;first_ready=None;rows=[]
 for j in range(round(c['horizon_s']/dt)+1):
  now=j*dt;surface=theta[m['working']]+g['ambient_C'];heater=theta[-3:]+g['ambient_C'];actual_sensor=theta[m['sensor']]+g['ambient_C'];measured=actual_sensor
  active=now>=c['fault_time_s'];fault=case['fault']
  if active and fault=='sensor_frozen':
   if frozen is None:frozen=actual_sensor
   measured=frozen
  if active and fault=='sensor_bias':measured+=c['low_sensor_bias_C']
  if active and fault=='sensor_open':measured=float('nan');fault_latched=True
  if case['protected'] and requested_trip is None and (max(heater)>=c['heater_cutoff_C'] or max(surface)>=c['surface_cutoff_C']):requested_trip=now
  if requested_trip is not None and cutoff_time is None and now>=requested_trip+c['cutoff_delay_s']:cutoff_time=now
  power,integ=control(g['target_C']-measured,integ,g['controller'],dt)
  if active and fault=='relay_stuck':power=g['controller']['maximum_power_W']
  if cutoff_time is not None or fault_latched:power=0.
  factors=np.ones(3)
  if active and fault=='heater_open':factors[0]=0.
  applied=power/3*factors
  in_band=bool(min(surface)>=g['target_C']-g['band_C'] and max(surface)<=g['target_C']+g['band_C'])
  if in_band and not fault_latched and cutoff_time is None:
   if ready_since is None:ready_since=now
  else:ready_since=None
  ready=ready_since is not None and now-ready_since>=c['readiness_hold_s']
  if ready and first_ready is None:first_ready=now
  rows.append({'time_s':now,'surface_min_C':float(min(surface)),'surface_max_C':float(max(surface)),'heater_max_C':float(max(heater)),'actual_center_C':float(actual_sensor),'controller_sensor_C':None if not math.isfinite(measured) else float(measured),'applied_power_W':float(sum(applied)),'cutoff_latched':cutoff_time is not None,'sensor_fault_latched':fault_latched,'ready':ready})
  if j==round(c['horizon_s']/dt):break
  f=np.zeros(len(theta));f[-3:]=applied;new=step(M@theta/dt+f);energy_in+=sum(applied)*dt;energy_out+=float(ones@(K@new))*dt;theta=new
 stored=float(ones@(M@theta));err=abs(energy_in-energy_out-stored)/max(energy_in,1)
 result={'case':case['name'],'fault':fault,'protected':case['protected'],'first_ready_s':first_ready,'final_ready':rows[-1]['ready'],'trip_request_s':requested_trip,'cutoff_s':cutoff_time,'sensor_fault_latched':fault_latched,'peak_surface_C':max(r['surface_max_C'] for r in rows),'peak_heater_C':max(r['heater_max_C'] for r in rows),'final_surface_min_C':rows[-1]['surface_min_C'],'final_surface_max_C':rows[-1]['surface_max_C'],'energy_input_J':float(energy_in),'energy_loss_J':energy_out,'stored_J':stored,'relative_energy_error':float(err)}
 return rows,result

def main():
 c=json.loads((ROOT/'completion-inputs.json').read_text());g=load();hist={};summaries=[];checks=[]
 for case in c['cases']:
  rows,r=simulate(g,c,case);hist[case['name']]=rows;summaries.append(r);csv_file(case['name']+'.csv',rows)
  checks.append(check('energy-'+case['name'],'Heat input equals stored plus lost energy','relative error < 1e-8',r['relative_energy_error'],r['relative_energy_error']<1e-8,'Implicit Euler FE balance including actual failed-heater power'))
 csv_file('fault-summary.csv',summaries);by={r['case']:r for r in summaries}
 checks.append(check('normal-readiness','Full modeled working area stays in band for 300 s','ready at end; no cutoff',{'ready':by['normal']['final_ready'],'cutoff':by['normal']['cutoff_s']},by['normal']['final_ready'] and by['normal']['cutoff_s'] is None,'Full surface node criterion, not center sensor alone'))
 for name in ['relay_stuck','sensor_frozen']:
  p=by[name+'_protected'];u=by[name+'_unprotected'];ok=p['cutoff_s'] is not None and p['peak_heater_C']<=c['screen_maximum_heater_C'] and p['peak_surface_C']<=c['screen_maximum_surface_C']
  checks.append(check(name+'-protection','Independent cutoff limits the modeled fault excursion','cutoff operates; heater <=135 C and surface <=110 C',{'cutoff_s':p['cutoff_s'],'heater_C':p['peak_heater_C'],'surface_C':p['peak_surface_C']},ok,'A separate modeled contactor removes power even with the control output stuck'))
  checks.append(check(name+'-unsafe','The same fault without cutoff exceeds the screened temperature','surface >110 C',u['peak_surface_C'],u['peak_surface_C']>c['screen_maximum_surface_C'],'Counterfactual fault run; expected rejected condition'))
 p=by['poor_contact'];checks.append(check('poor-contact-cutoff','Poor contact is caught at the heater even if surface stays cooler','cutoff before 135 C heater',{'cutoff_s':p['cutoff_s'],'heater_C':p['peak_heater_C']},p['cutoff_s'] is not None and p['peak_heater_C']<=135,'Individual heater temperature monitoring in the model'))
 op=hist['sensor_open'];checks.append(check('open-sensor-failoff','An invalid controller sensor latches power off','all power = 0 after fault',max(r['applied_power_W'] for r in op if r['time_s']>=c['fault_time_s']),all(r['applied_power_W']==0 for r in op if r['time_s']>=c['fault_time_s']),'NaN sensor injection with explicit nonfinite guard'))
 checks.append(check('bias-quality-gate','A 5 C low-reading controller cannot falsely satisfy the full-surface criterion','not ready at end',by['sensor_bias_minus5']['final_ready'],not by['sensor_bias_minus5']['final_ready'],'Separate modeled surface grid checks the target band'))
 normal_cases=[r for r in summaries if r['case'] in ['normal','hot_ambient'] or r['case'].startswith('normal_grid_')]
 checks.append(check('selected-envelope','Six ambient/loss corner cases meet the selected operating conditions','all six ready at end with no cutoff', [{'case':r['case'],'ready':r['final_ready'],'cutoff':r['cutoff_s']} for r in normal_cases],len(normal_cases)==6 and all(r['final_ready'] and r['cutoff_s'] is None for r in normal_cases),'22/35 C ambient x 5/10/20 W/m2K top convection; bottom U2, contact1000, 60 W limit'))
 checks.append(check('heater-open-rejection','Loss of one heater cannot release an out-of-band process','not ready',by['heater_open']['final_ready'],not by['heater_open']['final_ready'],'Full-surface readiness interlock under an open heater'))
 checks.append(check('insulation-loss-rejection','Combined high top/bottom losses outside the selected envelope are rejected','not ready at end',by['high_losses']['final_ready'],not by['high_losses']['final_ready'],'Changed bottom U12 and top h20; the original temperature target is retained'))
 # Rerun a protected stuck-output case at half dt to bound cutoff timing discretization.
 case=next(x for x in c['cases'] if x['name']=='relay_stuck_protected');_,fine=simulate(g,c,case,dt=c['time_step_s']/2);coarse=by['relay_stuck_protected'];peak_delta=abs(coarse['peak_heater_C']-fine['peak_heater_C']);trip_delta=abs(coarse['cutoff_s']-fine['cutoff_s'])
 checks.append(check('cutoff-step-refinement','Halving dt preserves protected-fault peak and cutoff time','peak difference < 1 C; trip difference <= 4 s',{'peak_difference_C':peak_delta,'trip_difference_s':trip_delta},peak_delta<1 and trip_delta<=4,'2 s versus 1 s integration and sampled cutoff'))
 (OUT/'cutoff-refinement.json').write_text(json.dumps({'coarse':coarse,'fine':fine},indent=2)+'\n')
 fig,axes=plt.subplots(2,1,figsize=(10,7),sharex=True)
 for name,color,ls in [('normal','#4d7557','-'),('relay_stuck_unprotected','#9b4f38','--'),('relay_stuck_protected','#c28260','-'),('poor_contact','#6f607f','-')]:
  rr=hist[name];x=[r['time_s']/60 for r in rr];axes[0].plot(x,[r['surface_max_C'] for r in rr],label=name,color=color,ls=ls);axes[1].plot(x,[r['heater_max_C'] for r in rr],color=color,ls=ls)
 axes[0].axhspan(88,92,color='#a6bea0',alpha=.25);axes[0].axhline(c['surface_cutoff_C'],color='gray',ls=':');axes[1].axhline(c['heater_cutoff_C'],color='gray',ls=':');axes[0].set(ylabel='Maximum working surface (C)');axes[1].set(xlabel='Time (minutes)',ylabel='Maximum heater (C)');axes[0].legend(fontsize=8);fig.tight_layout();fig.savefig(OUT/'fault-transients.png',dpi=140);plt.close(fig)
 compact=[{k:r[k] for k in ['case','final_ready','cutoff_s','peak_surface_C','peak_heater_C','final_surface_min_C','final_surface_max_C']} for r in summaries]
 limits=['The same 2D thin-plate FE model underlies normal and fault cases. Through-thickness gradients, radiation separated from effective losses, adhesive behavior and actual wiring are outside this model.','Cutoff sensors, a separate contactor, threshold accuracy and a 2 s response are assumed. This is a completed theoretical protection study, not certified safety hardware or firmware.','The independent surface readiness check uses modeled surface temperatures. Real sensor placement, attachment errors, drift and calibration need measured data before a process qualification.','Faults are single modeled failures except the explicit unprotected counterfactual. Common-cause loss of all sensing/power interruption is not protected by a numerical assumption.','No physical heating, insulation fire assessment, electrical safety test, curing trial or measured warm-up result is claimed.']
 sections=[('Selected operating conditions','<p>The selected reference assembly uses all three heaters, nominal 1000 W/(m2 K) contact, effective bottom U = 2 W/(m2 K), a 60 W power ceiling, and independent heater/surface cutoff monitoring. Six runs cover ambient 22/35 C crossed with top convection 5/10/20 W/(m2 K). All six finish with the entire working area in 88-92 C after a 300 s hold and without a protection trip. An open heater, poor contact, biased controller, and the U = 12 high-loss challenge are rejected operating states. Readiness is revoked whenever the full-surface band is lost.</p>'),('Decision','<p>The model reaches the full-surface band under the normal case. Poor heater contact, a stuck control output and a frozen controller sensor are examined with an independent power cutoff. Removing that protection produces excessive temperatures. A low-reading sensor can produce the wrong process temperature without reaching the cutoff; the full-surface readiness criterion rejects that case.</p>'),('Protection and test inputs',table([{'parameter':k,'value':v} for k,v in c.items() if k!='cases'])+'<p>The numerical protection latches when any heater reaches 125 C or the working surface reaches 105 C, then opens an independent power path after 2 s. It remains open for the rest of the run. A sensor-open condition latches the main output off immediately. Readiness requires every modeled working-area node to stay between 88 and 92 C for 300 s.</p>'),('Executed fault cases',table(compact)+'<img src="fault-transients.png" alt="Working surface and heater temperatures in normal and fault cases"/>'),('Equations and implementation','<p>Each step solves M (theta next - theta current) / dt + K theta next = f. The same supplied FE mass and conductance matrices are used. f contains the actual power at each heater, so an open heater contributes zero. A stuck output sets total requested power to 60 W. A separate modeled cutoff sets all actual heater powers to zero, independent of that output. The accumulated energy balance includes the powers that were actually applied.</p>'),('Bench exercise represented by the data','<p>The CSV records provide the complete cold start, fault injection at 600 s, trip request, delayed cutoff, cool-down and readiness state. The poor-contact case starts degraded. Hot-ambient and high-loss cases exercise boundary assumptions. The half-step rerun records cutoff timing and peak-temperature sensitivity. These are synthetic run logs, not filled-in measurement sheets.</p>')]
 finish('Heated platen fault and commissioning study',checks,sections,limits,['complete_validation.py','completion_util.py','completion-inputs.json','inputs.json','solver.py'],{'thermal_and_fault_cases':len(summaries),'selected_operating_cases':len(normal_cases),'energy_checks':len(summaries),'half_step_peak_change_C':peak_delta,'normal_ready_s':by['normal']['first_ready_s']})
if __name__=='__main__':main()
