
# Native FEMM 5 embedded MicroPython; all runtime inputs are relative.
import os,json,math,sys
import numpy as np
assert sys.implementation.name=='micropython'
with open('pm-data.json') as f:data=json.load(f)
with open('complete-series-recipe.json') as f:recipe=json.load(f)
out={'passed':False,'checks':[],'fields':[],'implementation':str(sys.implementation)}
import hashlib
def hash_file(name):
 h=hashlib.sha256()
 with open(name,'rb') as f:
  while True:
   b=f.read(65536)
   if not b:break
   h.update(b)
 return ''.join('%02x'%b for b in h.digest())
try:
 with open('full-rebuild-result.json') as f:out=json.load(f)
except OSError:pass
assert not out.get('error'),'Existing failed checkpoint must be reviewed before resuming'
recipe_hash=hash_file('complete-series-recipe.json')
if out['fields']:assert out.get('recipe_sha256')==recipe_hash,'Checkpoint belongs to another recipe or driver'
for name,expected in recipe['inputHashes'].items():assert hash_file(name)==expected,'Changed immutable input '+name
out['recipe_sha256']=recipe_hash
out['passed']=False
completed={r['id'] for r in out['fields']}
remaining=[r for r in recipe['controls'] if r['id'] not in completed]
batch=remaining[:40]

def check(label,a,e,t):
 ok=abs(a-e)<=t;out['checks'].append({'label':label,'actual':a,'expected':e,'tolerance':t,'passed':ok})
 if not ok:raise ValueError(label)
def serial(v):
 if isinstance(v,complex):return [v.real,v.imag]
 if isinstance(v,(list,tuple)):return [serial(x) for x in v]
 if isinstance(v,dict):return {k:serial(x) for k,x in v.items()}
 return v
def close():
 for n in ('mo_close','mi_close'):
  try:globals()[n]()
  except Exception:pass
try:
 try:os.mkdir('pm-full-output')
 except OSError:pass
 if data['lesson']=='II07.2' and not out['fields']:
  for machine,series in data['angular'].items():
   for N in (128,256):
    rows=series[::len(series)//N];check(machine+' count',len(rows),N,0)
    for name in data['phaseNames']:
     values=[complex(*r['circuits'][name][2]).real if isinstance(r['circuits'][name][2],list) else r['circuits'][name][2] for r in rows]
     F=np.fft.fft(np.array(values))
     for k in (1,3,5):
      measured=2*abs(complex(F[k]))/N;expected=data['spectra'][machine][str(N)][name][str(k)]
      check(machine+' FFT '+name+' '+str(N)+' '+str(k),measured,expected,max(1e-10,abs(expected)*1e-7))
  values=[.08*math.sin(2*math.pi*i/256)+.004*math.sin(10*math.pi*i/256) for i in range(256)]
  F=np.fft.fft(np.array(values));check('analytic FFT fundamental',2*abs(complex(F[1]))/256,.08,1e-12);check('analytic FFT fifth',2*abs(complex(F[5]))/256,.004,1e-12)
  check('Q12/10pole cogging period',360/60,6.,1e-12);check('64point step',6/64,.09375,1e-12)
 if data['lesson']=='II07.3' and not out['fields']:
  for c in data['mtpa']['curves']:
   for sweep in c['fine_angular_checks']:
    mean=sum(r['torque_Nm'] for r in sweep['rows'])/len(sweep['rows'])
    check('MTPA mean '+str(c['I_A'])+' '+str(sweep['beta_from_physical_d_deg']),mean,sweep['average_torque_Nm'],1e-12)
    for r in sweep['rows']:
     currents=[r['requested_currents_A'][n] for n in data['phaseNames']]
     check('balanced phase currents',sum(currents),0.,1e-12)
     check('current circle',2/3*sum(v*v for v in currents),c['I_A']**2,1e-10)
 for case in batch:
  close();opendocument(case['model']);mi_saveas('pm-full-output/'+case['id']+'.fem')
  if case['theta_deg']:mi_selectgroup(2);mi_moverotate(0,0,case['theta_deg']);mi_clearselected()
  for name,value in case['currents'].items():mi_modifycircprop(name,1,value)
  mi_analyze(1);mi_loadsolution();row={'id':case['id'],'circuits':{}}
  for name,expected in case['circuits'].items():
   actual=list(mo_getcircuitproperties(name));row['circuits'][name]=actual
   for i in (0,1,2):
    e=complex(*expected[i]) if isinstance(expected[i],list) else complex(expected[i]);check(case['id']+' '+name+' '+str(i),abs(complex(actual[i])-e),0.,max(1e-8,abs(e)*.001))
  mo_clearblock();mo_groupselectblock(2);T=mo_blockintegral(22);mo_clearblock();row['torque_Nm']=T
  check(case['id']+' WST torque',T,case['torque_Nm'],max(1e-6,abs(case['torque_Nm'])*.002));out['fields'].append(row)
  with open('full-rebuild-result.json','w') as f:json.dump(serial(out),f)
 out['pendingCases']=len(recipe['controls'])-len(out['fields'])
 out['complete']=out['pendingCases']==0
 out['passed']=out['complete']
 print('FULL_SERIES_CHECKPOINT',len(out['fields']),len(recipe['controls']),'repeat this command with a fresh FEMM process until complete')
except Exception as error:out['error']=str(error)
finally:
 close()
 with open('full-rebuild-result.json','w') as f:json.dump(serial(out),f)
assert not out.get('error'),out.get('error')
