#!/usr/bin/env python3
from pathlib import Path
import csv,hashlib,json,subprocess
R=Path(__file__).resolve().parent
WLS=[0.8,1.0,1.2,1.4,1.6,1.8]
ASSISTS=[1.8,1.6,1.4,1.2]
VDD=1.8
TEMPLATE="""* SemiAgora SKY130 6T SRAM dynamic write boundary
.lib /opt/pdk/sky130A/libs.tech/ngspice/sky130.lib.spice tt
.param VDDVAL=1.8 LCH=0.15 WPU=0.42 WPD=0.65 WAX=0.42
VDD vdd 0 PWL(0 {VDD} 2.0n {VDD} 2.2n {ASSIST} 5.2n {ASSIST} 5.4n {VDD} 8n {VDD})
VWL wl 0 PWL(0 0 2.0n 0 2.2n {WL} 5.2n {WL} 5.4n 0 8n 0)
VBL bl 0 {VDD}
VBLB blb 0 PWL(0 {VDD} 1.8n {VDD} 2.0n 0 5.4n 0 5.6n {VDD} 8n {VDD})
XPUQ q qb vdd vdd sky130_fd_pr__pfet_01v8 L={LCH} W={WPU}
XPDQ q qb 0 0 sky130_fd_pr__nfet_01v8 L={LCH} W={WPD}
XPUQB qb q vdd vdd sky130_fd_pr__pfet_01v8 L={LCH} W={WPU}
XPDQB qb q 0 0 sky130_fd_pr__nfet_01v8 L={LCH} W={WPD}
XAXQ bl wl q 0 sky130_fd_pr__nfet_01v8 L={LCH} W={WAX}
XAXQB blb wl qb 0 sky130_fd_pr__nfet_01v8 L={LCH} W={WAX}
CBL bl 0 30f
CBLB blb 0 30f
CQ q 0 2f
CQB qb 0 2f
.ic v(q)=0 v(qb)=1.8
.control
set wr_singlescale
set wr_vecnames
tran 2p 8n uic
wrdata {CSV} time v(vdd) v(wl) v(bl) v(blb) v(q) v(qb) i(vdd)
quit
.endc
.end
"""
def read(path):
 rows=[]
 for line in path.read_text(encoding="ascii").splitlines()[1:]:
  if line.strip(): rows.append([float(x) for x in line.split()])
 return rows
def crossing(t,a,b,start):
 for i in range(1,len(t)):
  if t[i]>=start and a[i-1]<=b[i-1] and a[i]>b[i]:
   da=(a[i]-b[i])-(a[i-1]-b[i-1]);f=-(a[i-1]-b[i-1])/da
   return t[i-1]+f*(t[i]-t[i-1])
 return None
cases=[];hashes={}
for assist in ASSISTS:
 for wl in WLS:
  stem=f"write-wl{int(wl*10):02d}-va{int(assist*10):02d}";net=R/(stem+".spice");csvp=R/(stem+".csv");log=R/(stem+".log")
  net.write_text(TEMPLATE.format(VDD=VDD,ASSIST=assist,WL=wl,CSV=csvp.name,LCH="{LCH}",WPU="{WPU}",WPD="{WPD}",WAX="{WAX}"),encoding="ascii")
  cp=subprocess.run(["ngspice","-b",net.name],cwd=R,text=True,capture_output=True);log.write_text(cp.stdout+cp.stderr)
  if cp.returncode or not csvp.exists(): raise RuntimeError(stem+" failed")
  rr=read(csvp);t=[x[0] for x in rr];vdd,word,bl,blb,qv,qbv,ivdd=([x[j] for x in rr] for j in range(2,9))
  flip=crossing(t,qv,qbv,2e-9);end=min(range(len(t)),key=lambda i:abs(t[i]-7.5e-9));success=qv[end]>1.35 and qbv[end]<.45
  energy=sum(.5*(max(-ivdd[i-1]*vdd[i-1],0)+max(-ivdd[i]*vdd[i],0))*(t[i]-t[i-1]) for i in range(1,len(t)) if 2e-9<=t[i]<=5.4e-9)
  ids=list(range(0,len(rr),max(1,len(rr)//240)))
  wave=[{"time_ns":round(t[i]*1e9,4),"vdd_v":round(vdd[i],5),"wl_v":round(word[i],5),"bl_v":round(bl[i],5),"blb_v":round(blb[i],5),"q_v":round(qv[i],5),"qb_v":round(qbv[i],5)} for i in ids]
  hashes[net.name]=hashlib.sha256(net.read_bytes()).hexdigest()
  cases.append({"id":stem,"wordline_amplitude_v":wl,"write_vdd_assist_v":assist,"vdd_collapse_mv":round((VDD-assist)*1000),"success":success,"flip_time_ns":round(flip*1e9,5) if flip else None,"write_delay_ps":round((flip-2.1e-9)*1e12,3) if flip else None,"final_q_v":round(qv[end],6),"final_qb_v":round(qbv[end],6),"write_window_energy_fj":round(energy*1e15,5),"raw_points":len(rr),"waveform":wave})
# Boundary: minimum WL among successful cases at each assist.
boundaries=[]
for a in ASSISTS:
 row=[c for c in cases if c["write_vdd_assist_v"]==a];ok=[c["wordline_amplitude_v"] for c in row if c["success"]]
 boundaries.append({"write_vdd_assist_v":a,"minimum_successful_wordline_v":min(ok) if ok else None,"successful_cases":len(ok)})
assert len(cases)==24 and all(c["raw_points"]>=4000 for c in cases)
assert any(c["success"] for c in cases) and any(not c["success"] for c in cases)
for b0,b1 in zip(boundaries,boundaries[1:]):
 if b0["minimum_successful_wordline_v"] is not None and b1["minimum_successful_wordline_v"] is not None: assert b1["minimum_successful_wordline_v"]<=b0["minimum_successful_wordline_v"]
payload={"schema":"semiagora.sky130-sram-write-boundary.v1","experiment_id":"SKY130_SRAM_WRITE_BOUNDARY_001","title":"SKY130 6T SRAM dynamic write-success boundary","metric_contract":{"name":"dynamic write-success boundary","stimulus_axes":["wordline amplitude","cell VDD during 3 ns write pulse"],"initial_state":"Q=0, QB=1","intended_write":"Q=1, QB=0","success_criterion":"at 7.5 ns after supply restoration: Q > 1.35 V and QB < 0.45 V","flip_time_definition":"first Q/QB crossing after write begins; delay referenced to 2.1 ns pulse midpoint"},"conditions":{"nominal_vdd_v":VDD,"temperature_c":27,"corner":"tt","write_window_ns":[2.2,5.2],"bitline_load_ff":30,"internal_node_load_ff":2,"dimensions_um":{"length":.15,"pull_up_width":.42,"pull_down_width":.65,"access_width":.42}},"simulator":{"name":"ngspice","version":"42","analysis":"transient 2 ps / 8 ns, UIC","pdk":"SKY130A"},"verification":{"case_count":len(cases),"all_converged":True,"has_success_and_failure_regions":True,"assist_boundary_monotonic":True,"minimum_raw_points":min(c["raw_points"] for c in cases)},"boundaries":boundaries,"cases":cases,"provenance":{"netlist_sha256":hashes},"limitations":["Pre-layout schematic compact-model simulation at one TT/27 C corner; no mismatch, PVT, extracted parasitics, package, or measured silicon","UIC establishes one deterministic initial polarity; the opposite write direction is not separately characterized","Cell-VDD collapse is ideal and local; assist driver resistance, slew, recovery noise, and half-selected cells are excluded","This is a named dynamic failure boundary, not WSNM, BLVM, WLVM, N-curve, or a universal write-margin number"]}
(R/"sky130-sram-write-boundary-web-v1.json").write_text(json.dumps(payload,indent=2),encoding="utf-8")
with (R/"write-boundary-summary.csv").open("w",newline="",encoding="utf-8") as f:w=csv.DictWriter(f,fieldnames=[k for k in cases[0] if k!="waveform"]);w.writeheader();w.writerows([{k:v for k,v in c.items() if k!="waveform"} for c in cases])
print(json.dumps({"boundaries":boundaries,"successes":sum(c["success"] for c in cases),"failures":sum(not c["success"] for c in cases),"delay_ps":[min(c["write_delay_ps"] for c in cases if c["write_delay_ps"] is not None),max(c["write_delay_ps"] for c in cases if c["write_delay_ps"] is not None)]},indent=2))
