LC tank oscillator -- Maffezzoni et al. 2013 (Fig. 3, Table I)

// IEEE TMTT vol. 61, no. 9, September 2013:
//   "A Unified Method for the Analysis of Phase and Amplitude Noise
//    in Electrical Oscillators" -- Maffezzoni, Pepe, Bonfanti.
//
// Topology: symmetric cross-coupled NMOS LC differential oscillator.
//   Two LC tanks (L1, L2 = 40 nH each from VDD to drain nodes),
//   differential tank cap C = 0.3 pF (vp-vm), parallel loss R = 11 kΩ (vp-vm),
//   drain parasitics C1,C2 = 1 fF (to ground), tail cap C3 = 1 fF.
//   Cross-coupled NMOS pair M1,M2 (W/L=10) + tail source M3 (W/L=33).
//
// NMOS:  Vt0=0.5 V, Kp=120 µA/V², Lambda=0.05 V⁻¹  (Spice level-1)
// Supply: VDD=2.5 V, Vgs(M3)=1.0 V
//
// Half-circuit resonance (differential mode):
//   L=40 nH, C_eff = 2*C + C1 = 600f+1f = 601 fF
//   f0 = 1/(2π√(L·C_eff)) ≈ 1.026 GHz,  T0 ≈ 0.975 ns
//   (matches paper: f0 = 1.0261 GHz, T0 ≈ 0.9746 ns)

ground 0

load "resistor.osdi"
load "capacitor.osdi"
load "inductor.osdi"
load "spice/mos1.osdi"

model r resistor noisy=0
model c capacitor
model l inductor
model v vsource
model i isource

model nfet sp_mos1 (
    type=1
    vto=0.5
    kp=120e-6
    lambda=5e-2
)

// Power supply
v1 (vdd 0) v dc=2.5

// Left LC tank: VDD → L1 → vp
l1 (vdd vp) l l=40n

// Right LC tank: VDD → L2 → vm
l2 (vdd vm) l l=40n

// Parallel tank loss resistance (differential)
r1 (vp vm) r r=11k

// Main differential tank capacitor C = 0.3 pF
c1 (vp vm) c c=300f

// Drain parasitic capacitors C1, C2 = 1 fF (Table I, first case)
c3 (vp 0) c c=1f
c4 (vm 0) c c=1f

// Cross-coupled NMOS pair -- (W/L)1,2 = 10
// T1: drain=vp, gate=vm, source=tail, bulk=0
// T2: drain=vm, gate=vp, source=tail, bulk=0
t1 (vp vm tail 0) nfet w=10e-6 l=1e-6
t2 (vm vp tail 0) nfet w=10e-6 l=1e-6

// Tail current source transistor -- (W/L)3 = 33
// T3: drain=tail, gate=vbias, source=0, bulk=0
t3 (tail vbias 0 0) nfet w=33e-6 l=1e-6

// Tail bypass capacitor C3 = 1 fF (Table I, first case)
c2 (tail 0) c c=1f

// Gate bias for T3: Vgs = 1.0 V
v2 (vbias 0) v dc=1

// Startup kick on vm -- 2 mA pulse at 2 ns, well before shooting window
i1 (0 vm) i type="pulse" val0=0 val1=2m delay=2n rise=0.1n fall=0.1n width=1n

control
  abort never
  options pss_debug=1 tran_debug=0
  options rawfile="binary" strictsave=1
  options reltol=1e-3 abstol=1e-12

  save default

  options pss_tolscale=1e-9

  analysis pss6 pss tper=0.975n tstab=100n writestab=1

  postprocess(PYTHON, "runme.py")
endc

embed "runme.py" <<<FILE
from rawfile import rawread
from runtest import *
import numpy as np
import sys
import matplotlib.pyplot as plt

# Paper: Maffezzoni et al. 2013, Table I / Section VI, first case (C1=C2=C3=1fF)
f0_paper = 1.0261e9   # Hz
T0_expected = 1.0 / f0_paper

tests = []
pss_ok = False

# --- Stabilisation transient ---
try:
    stab = rawread('pss6.tran.raw').get()
    ts = np.real(stab['time'])
    vs = np.real(stab['vp'])
    last = vs[-200:]
    V_amp = 0.5 * (np.max(last) - np.min(last))
    print(f"Stab: {len(ts)} points, final amplitude V(vp) ~{V_amp:.4f} V")
except FileNotFoundError:
    print("pss6.tran.raw not found")
    ts = vs = None

# --- PSS result ---
try:
    pss = rawread('pss6.raw').get()
    time = np.real(pss['time'])
    vp   = np.real(pss['vp'])
    vm   = np.real(pss['vm'])
    pss_ok = True

    T0_sim = float(time[-1] - time[0])
    f0_sim = 1.0 / T0_sim

    T0_err = abs(T0_sim - T0_expected) / T0_expected
    status1 = T0_err < 0.05
    print(f"T0_expected={T0_expected*1e9:.4f} ns  T0_sim={T0_sim*1e9:.4f} ns  "
          f"rel_err={T0_err:.4e}  {'PASS' if status1 else 'FAIL'}")
    tests.append(status1)

    vp_pp = float(np.max(vp) - np.min(vp))
    status2 = vp_pp > 0.1
    print(f"V(vp) p-p = {vp_pp:.4f} V  {'PASS' if status2 else 'FAIL (no oscillation?)'}")
    tests.append(status2)

    vm_pp = float(np.max(vm) - np.min(vm))
    diff_pp = float(np.max(vp - vm) - np.min(vp - vm))
    print(f"V(vm) p-p = {vm_pp:.4f} V,  V(vp)-V(vm) p-p = {diff_pp:.4f} V")

    match = np.abs(vp[-1]-vp[0])/np.abs(vp).max()
    ok    = match<1e-12
    print("vp endpoint relative delta:", match, "PASS" if ok else "")
    tests.append(ok)

    match = np.abs(vm[-1]-vm[0])/np.abs(vm).max()
    ok    = match<1e-12
    print("vm endpoint relative delta:", match, "PASS" if ok else "")
    tests.append(ok)
    
    print()
    print(f"{'Simulator':<16} {'f0 [MHz]':>14}  {'|Gf| vs paper':>14}")
    Gf = abs(f0_sim - f0_paper) / f0_paper
    print(f"{'Paper':<16} {f0_paper/1e6:>14.4f}  {'---':>14}")
    print(f"{'VACASK':<16} {f0_sim/1e6:>14.4f}  {Gf*100:>13.4f}%")

except FileNotFoundError:
    print("pss6.raw not found -- PSS did not produce output")
    tests.append(False)
except KeyError as e:
    print(f"pss6.raw missing key: {e}")
    tests.append(False)

if isTest():
    sys.exit(not all(tests))

# --- Plots ---
n_panels = (1 if ts is not None else 0) + (1 if pss_ok else 0)
if n_panels == 0:
    n_panels = 1
fig, axes = plt.subplots(n_panels, 1, figsize=(10*0.6, 0.6*4 * n_panels), dpi=100, squeeze=False)
axes = axes.flatten()
ax = 0

if ts is not None:
    axes[ax].set_title('Maffezzoni 2013 LC oscillator -- stabilisation transient')
    axes[ax].set_xlabel('time [ns]')
    axes[ax].set_ylabel('V(vp) [V]')
    axes[ax].plot(ts * 1e9, vs, lw=0.5)
    ax += 1

if pss_ok:
    axes[ax].set_title(f'Maffezzoni 2013 LC oscillator -- PSS limit cycle, f0={f0_sim/1e6:.4f} MHz')
    axes[ax].set_xlabel('time [ns]')
    axes[ax].set_ylabel('V [V]')
    axes[ax].plot(time * 1e9, vp, label='V(vp)', marker='.', markersize=3)
    axes[ax].plot(time * 1e9, vm, label='V(vm)', marker='.', markersize=3)
    axes[ax].legend()
    ax += 1

plt.tight_layout()
plt.show()
>>>FILE
