# Why OSDI code generation — compiling foundry device equations natively

*Read this first. It explains **why** the OSDI resistor and capacitor code generators
exist and what problem they solve. For **how** each works, see:*
- *`docs/osdi_res_codegen/` — behavioral resistors → OSDI*
- *`docs/osdi_cap_codegen/` — behavioral capacitors → OSDI*
- *`docs/osdi_bypass_wobble/` — `.option bypass` and analog hold accuracy (leave bypass off for SHA/ADC)*

---

## The problem: foundry passives are arbitrary nonlinear equations

Modern PDKs don't describe their precision passives (poly resistors, MIM caps) with
the simple SPICE `R`/`C` primitives. To capture the real silicon they write the value
as a **behavioral equation** in the netlist — a function of the terminal voltage and
temperature. For example (generic form, foundry coefficients replaced by symbols):

```
* poly resistor (high-field, silicide-free):
Rpure a b 'Rbase*Tfac(temper)*(1 + pvc1*(tanh(pvc2*abs(v(a,b))/Leff + pvc3) - tanh(pvc3)))'

* MIM capacitor (voltage- and temperature-dependent):
cmim top bottom c='Cbase(temper)*(1 + v(top,bottom)*vc1 + v(top,bottom)^2*vc2)'
```

These equations contain physics that **no stock SPICE primitive can express**:

| feature in the foundry equation | stock ngspice `R` / `C` |
|---|---|
| `tanh(|v|)` high-field resistance | not supported |
| `vc1·v + vc2·v²` voltage coefficients | not supported |
| temperature written *into the equation* (`Tfac(temper)`, `Cbase(temper)`) | `R`/`C` have `tc1/tc2`, but only as fixed model params — not an arbitrary `f(temper)` in the value |
| charge-conserving nonlinear capacitance | not supported |

The foundry puts temperature and voltage dependence directly in the value expression
"to simplify their life" — it's one line that captures everything. But that leaves the
simulator with an arbitrary equation it has no native device for.

## The two old escape hatches — and why they hurt

Historically there were only two ways to run these equations, both bad:

1. **Behavioral B-source expansion (the default).** ngspice turns `C='f(v)'` into a
   differentiator sub-circuit (an `E`/`C`/`B` network that forms `i = f(v)·dv/dt`).
   For a nonlinear cap this injects **gigaamp internal currents** and the transient
   dies with *"Timestep too small; initial timepoint."* On a chip with thousands of
   MIM caps the simulation simply won't start.

2. **Rewrite the model (the "hack").** To get past that, the caps were hand-converted
   to a *linear* native device (a linearizing workaround that drops the `v()` term).
   The lib's own comment admits the cost: *"the voltage non-linearity is lost."* So the
   sim ran, but the device physics the foundry specified was thrown away — and every new
   corner or device needed another manual model rewrite.

So the choice was: **accurate but won't simulate**, or **simulates but inaccurate.**

## The OSDI solution: compile the equation into a native device

The OSDI code generator removes that trade-off. At netlist-parse time it:

1. reads the foundry's **original behavioral equation, unchanged**,
2. reuses ngspice's own expression parser and symbolic differentiator to get the value
   **and its analytic Jacobian** (and, for capacitors, the charge integral),
3. **generates C** for a proper OSDI device implementing that exact law,
4. compiles it with the **system `gcc`** and loads it — no OpenVAF, no Verilog-A,
5. and replaces the behavioral instance with that compiled native device.

The result is charge/current-conserving, stable, and bit-exact to the equation —
**with no model edits and no loss of physics.** Devices with identical equation
*structure* share one compiled model (parameters ride per-instance), so a whole chip
collapses to a handful of `.osdi` files.

## What it looks like on a real chip

Running the full mixed-signal chip on the **unmodified foundry lib** (behavioral poly
resistors + behavioral MIM caps), the generator produced exactly **four** native
devices — one per distinct foundry law:

| model | law (numbers omitted) | why OSDI is required |
|---|---|---|
| **R #0** | `I = (vp−vn) / [ geom · Tpoly(T) · (1 + p·(tanh(p·\|v\|+p) − tanh(p))) ]` | tanh high-field **+ temperature baked into the equation** |
| **R #1** | same, field = `\|v/leff\|` (body segment) | tanh high-field + temperature — the poly-R body law |
| **R #2** | `I = (vp−vn) / [ geom · (1 + p·\|v\| + p·v²) · Tpoly(T) ]` | **vc1/vc2** voltage coefficients + temperature |
| **C #0** | charge `Q(V,T) = ∫₀ᵛ c(u,T) du`, temperature-dependent | **vc1/vc2 voltage nonlinearity + temperature** in the MIM cap |

Notes for designers:

- **Resistors:** stock SPICE `R` has *no* voltage coefficients at all, and no
  `tanh(|v|)` high-field term. Two models are the tanh poly-R (end + body segments),
  the third is the `vc1/vc2` polynomial resistor. Standard `R` does support `tc1/tc2`,
  but the foundry wrote the temperature **into the equation** (`Tpoly(T)`), so a native
  `R` still couldn't take it as-is. OSDI just compiles whatever they wrote.
- **Capacitor:** stock `C` supports neither `vc1/vc2` nor temperature; the MIM cap has
  both. The generated device carries the temperature through the charge integral
  automatically, and is charge-conserving (so it is stable where the behavioral
  differentiator was not).

The chip — which previously required the accuracy-losing linear-cap workaround to
run at all — now runs on the **original behavioral equations**, stable, with the voltage
nonlinearity restored. Dropping the workaround is therefore an **accuracy gain**, not
just cleanup.

---

## Simulation-accuracy note: `.option bypass`

The OSDI devices reproduce the native result to a few µV — **provided `.option
bypass` is left at its default (off)**. `bypass=1` skips re-evaluating settled
devices between Newton iterations, and on high-gain sample-and-hold loops that
lets a bounded ~1 mV error meander through the held output. This is a property
of the bypass approximation, **not** of OSDI: native ngspice devices show the
same wobble (in fact slightly worse), and forcing a smaller timestep makes it
worse still. It buys only ~4 % runtime. Leave `bypass` off for precision analog
(SHA/ADC/reference).

Measured on the chip (held SHA differential, ideal = flat):

| device impl | `bypass=0` | `bypass=1` |
|---|---|---|
| native | 3.6 µV ✅ | 345.8 µV ❌ |
| OSDI   | 3.6 µV ✅ | 262.5 µV ❌ |

Full data and analysis: **`docs/osdi_bypass_wobble/`**.

---

## One-line takeaway

The foundry writes device physics as arbitrary equations (`tanh`, `vc1/vc2`,
temperature-in-value). Stock SPICE can't model them, behavioral expansion can't
simulate them, and hand-rewriting the model loses them. **OSDI codegen compiles the
foundry's own equation into a native, charge/current-conserving device — automatically,
with no model changes.**
