# `.option bypass` and analog hold accuracy (SHA / ADC)

**Summary.** A sample-and-hold (SHA) accuracy problem on a 0.18 µm foundry ADC was
traced to `.option bypass=1`, **not** to the compiled-C OSDI device models. With
bypass off (the ngspice default) the held differential output is flat to a few
µV. With bypass on it develops a bounded ~1 mV chaotic meander during hold —
**and this happens identically with native ngspice devices and with the OSDI
compiled-C devices.** Reducing the timestep makes it worse. The recommendation
for precision analog is to leave `bypass` at its default (off).

## Test

* DUT: a sample-and-hold in a mixed-signal ADC; the metric is the SHA
  differential output `v(outn) − v(outp)`.
* Metric: within a single **hold** plateau (window **5.05932–32.3695 ns**), the
  ideal output is a flat DC level. We report the RMS wobble (`std`) and
  peak-to-peak (`ptp`) of the held differential. Lower = better; a perfect
  hold is 0.
* Every run uses the **same netlist and the same options**
  (`trtol=7 reltol=2e-3 vntol=1u`, KLU, `tstop=35 ns`). Only two things vary:
  the device implementation (**native** ngspice `R`/`C` vs the **OSDI**
  compiled-C `R`/`C`) and **`.option bypass`**.

## Results

| device impl | `bypass` | hold RMS wobble | hold ptp | verdict |
|:-----------:|:--------:|----------------:|---------:|:-------:|
| native      |   **0**  |      **3.6 µV** |  19.9 µV | ✅ clean |
| OSDI        |   **0**  |      **3.6 µV** |  19.0 µV | ✅ clean |
| native      |   **1**  |        345.8 µV | 1372.6 µV | ❌ wobble |
| OSDI        |   **1**  |        262.5 µV | 1316.4 µV | ❌ wobble |
| OSDI        | 1 + `tmax=42p` |   533.4 µV | 2437.4 µV | ❌ worse |

The 2×2 is unambiguous:

* **`bypass=0` → clean for both** implementations (3.6 µV, identical).
* **`bypass=1` → wobble for both** implementations (native is if anything
  *worse*, 346 vs 262 µV).
* The variable that sets accuracy is **`bypass`**, not the device algorithm.

## Why `bypass=1` breaks the hold

`bypass` skips re-evaluating a device when its terminal voltage changed by less
than the bypass tolerance *between Newton iterations*, reusing the previous
operating point. On an ultra-high-gain SHA hold loop, freezing devices at stale
operating points lets a small error circulate instead of being corrected — a
bounded, self-adjusting ~1 mV meander (not a drift; it returns toward zero).
This is a property of the bypass approximation and affects native and OSDI
devices alike.

**Smaller timesteps make it worse, not better.** Capping the max step at 42 ps
(the average) *raised* the wobble to 533 µV, because smaller steps mean smaller
inter-iteration ΔV → *more* devices qualify to bypass → more frozen operating
points. Step size is not the lever.

## Cost / benefit

`bypass=1` is not even a meaningful speedup here. The transient step is limited
by local-truncation-error control at ~42 ps regardless of bypass, so the
timepoint count barely moves:

| run | CPU (transient) | accepted tp | avg Δt | `.raw` size |
|-----|----------------:|------------:|-------:|------------:|
| OSDI `bypass=0`          | 483.7 s |  831 | 42.1 ps | 0.80 MB |
| OSDI `bypass=1`          | 464.6 s |  834 | 42.0 ps | 0.80 MB |
| OSDI `bypass=1 tmax=42p` | 486.8 s | 1066 | 32.8 ps | 1.03 MB |

Within the OSDI implementation, `bypass=1` saves ~4 % CPU while costing ~1 mV of
SHA accuracy; the `tmax` cap is both slower *and* less accurate and produces a
larger output file.

## Recommendation

* Leave `.option bypass` at its **default (off)** for precision analog
  (SHA/ADC/reference) simulations. `bypass=0` is the ngspice default
  (`spicelib/devices/cktinit.c`: `CKTbypass = 0`); the artifact only appears
  when a deck explicitly sets `bypass=1`.
* The OSDI compiled-C `R`/`C` devices reproduce the native result to a few µV at
  `bypass=0`; there is no OSDI-specific accuracy penalty and no
  charge-conservation defect.

## Accuracy guidance for precision analog (SHA / ADC / reference)

When output accuracy matters more than runtime — sample-and-hold, ADC
comparators, bandgap/reference nodes, switched-capacitor — configure for
accuracy, not speed. The knobs, with ngspice defaults from
`spicelib/devices/cktinit.c`:

| option | default | speed-tuned | accuracy-first | what it controls |
|--------|--------:|------------:|---------------:|------------------|
| `bypass` | `0` | `1` | **`0`** | operating-point reuse — the accuracy killer here; **leave off** |
| `trtol`  | `7` | `7` | **`1`** | local-truncation-error timestep acceptance (integration accuracy) |
| `reltol` | `1e-3` | `2e-3` | **`1e-4 … 1e-5`** | relative convergence + LTE |
| `chgtol` | `1e-14` | `1e-14` | **`1e-15 … 1e-16`** | **charge** truncation tolerance — governs cap-hold droop directly |
| `vntol`  | `1e-6` | `1e-6` | **`1e-9 … 1e-12`** | absolute voltage convergence floor |
| `abstol` | `1e-12` | `1e-12` | tighten to signal | absolute current convergence floor |

Notes:

* **`bypass=0` first.** It is a global switch (no per-device override): `0`
  means every device — resistor and capacitor, native and OSDI — is fully
  re-evaluated every Newton iteration. The resistor-may/capacitor-never bypass
  asymmetry only exists *when bypass is on*; at `bypass=0` it is moot.
* **`trtol` and `chgtol` are the hold-accuracy levers.** A hold node's voltage
  is the charge stored on its capacitors; `trtol` sets how tightly the
  integrator controls per-step error, and `chgtol` sets the charge-error floor
  that feeds the timestep control. Tightening them shrinks hold droop/wobble at
  the cost of more timepoints (slower) — the accuracy/speed trade you choose.
* **Specify RMS *and* peak.** On the example SHA at `bypass=0` (otherwise
  speed-tuned tolerances) the held differential was **3.6 µV RMS but 19 µV
  peak-to-peak**. An RMS/σ target is met with margin; a worst-excursion (peak)
  target below ~10 µV needs the tighter tolerances above.

This accuracy is *reachable at all* only because the OSDI (or native)
charge-conserving capacitor replaced the behavioral differentiator: with the old
`C=f(v)` → auxiliary-differentiator expansion the same netlist could not even
complete ("timestep too small"), so no tolerance setting would have helped. A
simulator that aborts on a foundry capacitor is not a slow simulator — it is a
non-functional one.

## Raw data

`s_native35_byp0.raw`, `s_osdi35_byp0.raw`, `s_native35.raw`, `s_osdi35.raw`,
`s_osdi35_byp1_tmax42.raw` (in `netlist/`). Longer runs corroborating the same
picture: a native-C validation run (bypass off, 200 ns, clean) and an OSDI run
(bypass on, 100 ns, meander).
