"""Deterministic lateral-error recursion; step index is not physical time."""
import csv,json,math
from pathlib import Path
import matplotlib
matplotlib.use('Agg')
import matplotlib.pyplot as plt
P=Path(__file__).resolve().parent
GAINS=[0,.4,.8,1.6];DELAYS=[0,2,5]
def run(gain,delay,missing):
 e=[.3];rows=[]
 for k in range(200):
  idx=max(0,k-delay);available=not(missing and 70<=k<=89);raw=-gain*e[idx];u=max(-.05,min(.05,raw)) if available else 0.;w={40:.3,100:-.3,150:.3}.get(k,0.);nxt=e[k]+u+w
  rows.append(dict(gain=gain,delay_steps=delay,missing_observation_window=missing,step=k,error_before_m=e[k],observation_state_index=idx,observation_available=available,observed_error_m=e[idx] if available else None,correction_m=u,disturbance_m=w,error_after_m=nxt));e.append(nxt)
 return rows,dict(gain=gain,delay_steps=delay,missing_observation_window=missing,initial_error_m=e[0],final_error_m=e[-1],max_abs_error_m=max(map(abs,e)),RMS_error_all_201_states_m=math.sqrt(sum(v*v for v in e)/len(e)),total_abs_correction_m=sum(abs(r['correction_m']) for r in rows))
rows=[];summaries=[]
for g in GAINS:
 for d in DELAYS:
  for missing in [False,True]:
   r,s=run(g,d,missing);rows+=r;summaries.append(s)
for name,data in [('steps.csv',rows),('summaries.csv',summaries)]:
 with (P/name).open('w',newline='') as f:w=csv.DictWriter(f,fieldnames=data[0]);w.writeheader();w.writerows(data)
assert len(rows)==4800 and len(summaries)==24
for r in rows:
 assert abs(r['error_after_m']-r['error_before_m']-r['correction_m']-r['disturbance_m'])<1e-14
 assert abs(r['correction_m'])<=.05
 if not r['observation_available']:assert r['correction_m']==0 and r['observed_error_m'] is None
 if r['gain']==0:assert r['correction_m']==0
assert all(math.isclose(s['final_error_m'],.6,abs_tol=1e-14) for s in summaries if s['gain']==0)
# Zero-delay disturbance-free unsaturated recurrence has closed form (1-K)^k*e0.
rr,_=run(.4,0,False)
start=next(i for i,r in enumerate(rr[:40]) if abs(r['error_before_m']*.4)<.05)
closed=max(abs(rr[k]['error_before_m']-rr[start]['error_before_m']*.6**(k-start)) for k in range(start,40));assert closed<1e-14
assert all(r['observation_state_index']==max(0,r['step']-r['delay_steps']) for r in rows)
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,ax=plt.subplots(1,2,figsize=(11,4.8))
for a,delay in zip(ax,[0,5]):
 for g,c in [(0,'#777'),(.4,'#087e91'),(.8,'#a14786'),(1.6,'#7550a0')]:
  rr=[r for r in rows if r['gain']==g and r['delay_steps']==delay and not r['missing_observation_window']];a.plot(range(201),[rr[0]['error_before_m']]+[r['error_after_m'] for r in rr],label=f'Gain {g:g}',color=c)
 for step in [41,101,151]:a.axvline(step,color='#ccc',ls=':',lw=.8)
 a.set(xlabel='Update step (not seconds)',ylabel='Signed lateral error (m)',title=f'Delay {delay} steps, observations available');a.grid(alpha=.2);a.legend(fontsize=8)
fig.suptitle('Feedback strength and stale observations interact',fontsize=14);fig.text(.5,.04,'Correction limited to ±0.05 m per update; assigned disturbances enter after the correction at steps 40, 100 and 150.',ha='center',fontsize=8);fig.text(.5,.012,'Synthetic state recursion, not vehicle dynamics. A smaller simulated error is not a safety or hardware-performance result.',ha='center',fontsize=8);fig.tight_layout(rect=[0,.09,1,.93])
for ext in ['png','svg','pdf']:fig.savefig(P/f'gain-and-delay.{ext}',dpi=190)
fig,ax=plt.subplots(1,2,figsize=(11,4.8))
for missing,c,label in [(False,'#087e91','Observations available'),(True,'#a14786','Missing at steps 70–89')]:
 rr=[r for r in rows if r['gain']==.8 and r['delay_steps']==5 and r['missing_observation_window']==missing];ax[0].plot(range(201),[rr[0]['error_before_m']]+[r['error_after_m'] for r in rr],color=c,label=label);ax[1].step([r['step'] for r in rr],[r['correction_m'] for r in rr],where='post',color=c,label=label)
for a in ax:a.axvspan(70,90,color='#aaa',alpha=.18);a.set_xlabel('Update step (not seconds)');a.grid(alpha=.2)
ax[0].set(ylabel='Signed lateral error (m)',title='A  |  Same model, one observation gap');ax[0].legend(fontsize=8);ax[1].set(ylabel='Correction per update (m)',title='B  |  Missing observation means zero correction')
fig.suptitle('A paused correction is not a stopped vehicle',fontsize=14);fig.text(.5,.035,'Assigned gain 0.8 and delay 5 steps. Gray region: no estimate supplied, correction set to zero; no braking or safe-stop model exists.',ha='center',fontsize=8);fig.tight_layout(rect=[0,.075,1,.93])
for ext in ['png','svg','pdf']:fig.savefig(P/f'missing-observations.{ext}',dpi=190)
meta={'title':'Deterministic delayed lateral-error feedback','status':'New hypothetical recursion; separate from any feature-matching benchmark','scenarios':24,'rows':{'steps':4800,'summaries':24},'parameters':{'gains':GAINS,'delays_steps':DELAYS,'missing_window_enabled':[False,True],'initial_error_m':.3,'correction_limit_m_per_update':.05,'updates':200,'missing_steps_inclusive':[70,89],'disturbances_m':{'40':.3,'100':-.3,'150':.3}},'equation':'e[k+1]=e[k]+clip(-K*e[max(0,k-d)],-.05,.05)+w[k]; correction=0 on missing observation steps','row_convention':'Each row contains e[k], applied correction and disturbance, then e[k+1]. 200 rows encode 201 states per scenario. RMS includes all 201 states.','checks':{'all_pass':True,'recurrence_all_4800_rows':True,'correction_bound':True,'missing_observation_zero_correction':True,'zero_gain_final_error_0_6m':True,'analytic_unsaturated_error_max_m':closed,'observation_index':True},'limits':['Update steps have no assigned duration or forward distance. Correction is an instantaneous change of the scalar lateral state, not velocity, steering or acceleration.','Delay is an integer count of state updates; initial delayed references use e[0].','No observation noise, feature matching, obstacle, collision, actuator dynamics, braking, slip, platform geometry or energy model.','Missing data pauses correction only; it does not implement a physical stop or a safe fallback.','All disturbances and gains are chosen teaching inputs; no robustness or physical safety guarantee follows.','No pass/fail or hardware acceptance threshold is assigned. This is not a rerun of the archived mapped-feature simulation.']}
(P/'study.json').write_text(json.dumps(meta,indent=2)+'\n');(P/'validation.json').write_text(json.dumps(meta['checks'],indent=2)+'\n');print(meta['rows'])
