# Builtin Limit Functions

`limitfunctions.h`/`limitfunctions.cpp` implement the classic SPICE per-iteration voltage/temperature damping heuristics - ported from ngspice - that back the OSDI limiting callbacks documented in [OSDI Callbacks](osdicallback.md). These are the plain numerical routines; `osdicallback.cpp` only adapts them to the `(init, check, ...)` calling convention OSDI's `$limit()` expects.

`DEVpnjlim()` limits the per-iteration change of a PN junction voltage - the workhorse of diode/BJT/MOSFET junction convergence. Its header comment documents a specific fix (attributed to A. Buermen, based on ngspice) to the classic algorithm's handling of `vnew > vcrit` with a small `|vnew-vold|`, where the original computation could limit in the wrong direction; `DEVpnjlim_old()` is kept alongside it as the unmodified original, apparently for comparison/fallback rather than being wired into the callback table itself (only `DEVpnjlim` is, per [OSDI Callbacks](osdicallback.md)'s `limitFunctionTable`).

`DEVlimvds()` and `DEVfetlim()` limit MOSFET drain-source and gate-referenced voltages respectively, each using a region-dependent step-size cap (different bounds depending on whether the device is deep in a given operating region or crossing a boundary) rather than a single fixed limit - a per-iteration change is allowed to be larger where the device's characteristic is flatter and must be capped tighter near a region boundary where it's steep.

`DEVlimitlog()` is different in kind: rather than a voltage, it logarithmically damps a proposed *temperature* change (`deltemp`) once it exceeds `LIM_TOL`, so a self-heating device's temperature doesn't overshoot wildly during Newton iteration. It also guards against a `NaN` input (typically the symptom of a misconfigured thermal resistance) by resetting to `0` and printing a one-time warning instead of propagating the `NaN` into the rest of the solve.

## Global voltage clamp

A module-level `voltagelimit_option` (set from the simulator's own `voltagelimit` option, elsewhere) is applied by every voltage-limiting function (`DEVlimvds`, `DEVpnjlim`, `DEVfetlim`) via a shared `voltagelimit()` helper, on top of whichever function-specific limiting already ran: below `1e100` it's a soft `tanh`-based clamp (`voltagelimit_option * tanh(value/voltagelimit_option)`), asymptotically bounding the value without a hard discontinuity; a value of `1e100` or above disables the soft clamp and switches to a sharp cutoff at `+-1e100`, purely to keep values finite. `DEVpnjlim` additionally checks whether this global clamp actually moved the value by more than 1% and, if so, sets `*check` to force another Newton iteration - a small value near zero (`|vnew| <= 1e-3`) skips this check entirely, since `tanh`-based limiting changes such a small value by an amount below floating-point-meaningful precision anyway (documented as "below 0.3ppm").
