# OSDI Instances

`OsdiInstance` (`osdiinstance.h`/`osdiinstance.cpp`) is the [`Instance`](devbase.md) implementation for an OSDI-backed device - the counterpart to [`BuiltinInstance<...>`](devbuiltin.md), non-templated like [`OsdiModel`](osdimodel.md) for the same reason: an OSDI device's instance data layout isn't known until its shared library is loaded.

## The opaque instance blob

`core_` is allocated in the constructor with `alignedAlloc(sizeof(::max_align_t), model->device()->descriptor()->instance_size)` (see [Platform Specifics](libplatform.md)) - `instance_size` is a value the compiled Verilog-A module itself declares via its OSDI descriptor, so the simulator allocates exactly the block layout the module expects without knowing its internal structure, aligned generously enough (`max_align_t`) for any type OSDI might place inside it. Allocation failure throws `std::bad_alloc` rather than proceeding to `memset()` a null pointer. The block is zeroed after allocation and released with `alignedFree()` in the destructor - but only after `OsdiDevice::freeValues()` has released any string/vector parameter storage the compiled module allocated *inside* that block, since freeing the block itself would otherwise leak (or worse, orphan) those nested allocations.

Structural per-instance data the descriptor expects the simulator to fill in - the collapsed-node pattern, the terminal/internal-to-OSDI node mapping, resistive/reactive Jacobian entry pointers, and the state index table - are all reached the same way as [`OsdiDevice`](osdidevice.md)'s parameter access: `getDataPtr<T>(core_, offset)`, with the `offset` read from the descriptor (`collapsed_offset`, `node_mapping_offset`, `jacobian_ptr_resist_offset`, each `jacobian_entries[i].react_ptr_off`, `state_idx_off`). There is no simulator-defined C++ type for any of this - it is all raw pointer arithmetic into a block whose true layout only the compiled module and the OSDI ABI specification agree on.

## Dangling terminal policy: the opposite of builtin devices

Unlike [`BuiltinInstance`](devbuiltin.md), which requires every terminal to be explicitly connected, `OsdiInstance`'s documented policy (in the header) is to connect any dangling (unconnected) terminal to an automatically created internal node instead of treating it as an error - matching how Verilog-A modules commonly tolerate optional terminals. `buildHierarchy()` does the actual work, walking terminals from `connectedTerminalCount` up to `staticNodeCount()` and creating an internal node (tagged `FlowNode` or `PotentialNode` per `descr->nodes[i].is_flow`) for each one, except that for a behavioral source (`parsedInstance_.behavioralData()` set) it skips the range of controlling-current probe terminals - those are bound separately, to another instance's branch flow node, by `populateStructuresCore()` (below) rather than getting a fresh internal node here. `deleteHierarchy()` is the inverse: it just calls `unbindInternalNodes()`.

## Parameters and output variables

`parameterCount()`, `parameterIndex()`, `parameterName()`, `parameterType()`, `getParameter()`, `setParameter()`, and `parameterGiven()` all forward to `model()->device()`'s instance-parameter tables ([`OsdiDevice`](osdidevice.md)), translating a `ParameterIndex` to an OSDI parameter id and reading/writing through the descriptor's `access()` callback. `setParameter()` sets `Instance::Flags::ParamsChanged` when the write actually changes the value, feeding the same dirty-flag convention [`devbase.md`](devbase.md) documents. `principalParameterIndex()` returns index 1 (with index 0 reserved for the implicit `$mfactor` multiplicity parameter every OSDI instance carries) as the *principal* parameter, if the device has more than one instance parameter - the parameter a bare positional value after the instance name (`rmy 1 2 1k`) is assigned to.

The output-variable accessors (`outvarCount()`, `outvarIndex()`, `outvarName()`, `outvarType()`, `getOutvar()`) similarly forward to `model()->device()`'s outvar tables. `outvarOutputSource(ndx)` additionally resolves a live `int*`/`double*` pointer straight into the instance's `core_` block via `OsdiDevice::parameterPtr<T>()`, for output paths that want to read an outvar's value directly without going through `getParameter()`/`Value` on every step.

## Source excitation and response

`sourceExcitation()`/`sourceResponse()` implement `Instance`'s source API ([`devbase.md`](devbase.md)) only when `model()->device()->isSource()`; otherwise both return `(0, 0)` (ground). For a voltage source, the excitation is the flow (branch) unknown at node index 2 (positive sign), and the response is the same unknown with a negative sign (`0` minus the flow node); for a current source, excitation is the positive/negative terminal unknowns (node indices 0 and 1) and response is the voltage between them - the sign conventions `devbase.md`'s Source API section describes in general terms, applied here using the fixed terminal-index layout OSDI current/voltage sources are compiled with.

## Setup and unknown-count change detection

`setup()` builds an `OsdiSimParas` from the current `SimulatorOptions`/`CommonData` (via `OsdiDevice::populateSimParas()`, on stack-allocated scratch arrays sized by `model()->device()->simParasSizes()`), converts the configured temperature from Celsius to Kelvin, and calls `setupWrapper()`. `setupWrapper()` only invokes the descriptor's `setup_instance()` entry point when `force || checkFlags(Flags::NeedsSetup)` - unlike `BuiltinInstance::setupWrapper()`, which runs its `setupCore()` unconditionally (see [`devbuiltin.md`](devbuiltin.md)). Before calling `setup_instance()`, it saves a copy of the current collapsed-node pattern and clears the live one (since `setup_instance()` only ever writes `true` into positions that should collapse, never clearing stale `true`s itself); afterwards it compares the new pattern against the saved copy to decide whether node collapsing changed, which is how `setupWrapper()`'s returned `unknownsChanged` bool is computed. If the instance has no prior setup history (`Flags::HasSetupHistory` not yet set - i.e. this is the first setup), `unknownsChanged` is unconditionally `true` and no comparison is attempted. `setup_instance()`'s result is interpreted via `OsdiDevice::processInitInfo()`; a hard failure there leaves the collapsed-node pattern restored to its pre-call state and aborts with `(false, false, false)`. OSDI instances never change matrix sparsity on their own (the third tuple element is always `false`) - only node collapsing can shrink the unknown count.

`setStaticTolerances()`/`setStaticTolerancesCore()` register each node's tolerance quantities with `CommonData`, either via the legacy SPICE helpers (`updatePotentialNodeSpiceTolerances()`/`updateFlowNodeSpiceTolerances()`, [`devbase.md`](devbase.md)) when `SimulatorOptions::tolmodeSpice` is active, or via `OsdiDevice::tolerances()` (the natures the module itself declares) otherwise. For a behavioral source, it skips every terminal beyond the two main ones (indices `2..terminalCount()-1`, covering both kinds of controlling-quantity probe described below) - those nodes are owned, and already tolerance-tagged, by the instance/terminal they probe rather than by this one.

## Building structures: Jacobian entries, states, and behavioral sources

A Verilog-A "behavioral source" (`parsedInstance_.behavioralData()` set - a source expressed as an RPN/Verilog-A expression directly in the netlist rather than compiled from a separate `.va` model) declares two kinds of extra controlling-quantity terminals beyond its two main nodes: potential probes (`bd->node`, ordinary node references, connected like any other terminal at instantiation time and therefore already covered by `connectedTerminalCount`) and current probes (`bd->flow`, references to *another instance's* branch flow unknown, which cannot be resolved by ordinary terminal binding). `populateStructuresCore()` resolves each current probe via `Instance::findControl()` ([`devbase.md`](devbase.md)), binding it as a dangling terminal at index `2+bd->node.size()+k` and transferring the reference count from any prior binding via `incRef()`/`releaseNode()` - this is also why `buildHierarchy()` (above) must skip exactly that index range rather than creating fresh internal nodes there.

Beyond that, `populateStructuresCore()` walks `model()->device()->jacobianEntry(i)` for every Jacobian entry the descriptor declares and calls `circuit.createJacobianEntry()` with `EntryFlags::Resistive`/`Reactive` set according to `JACOBIAN_ENTRY_RESIST`/`JACOBIAN_ENTRY_REACT`. It then allocates state-vector storage: `internalStateCount()` slots for the module's own internal states (recorded into `stateIndexTable()` as local, zero-based offsets), two more slots per nonzero reactive residual (charge and its time-derivative/flow, tracked via `offsStates`), and a separate `CommonData`-managed block (`offsDeviceStates`) sized to hold one entry per declared input plus one per nonzero resistive/reactive residual - the "previous value" history `inputBypassCheckCore()`/`outputBypassCheckCore()` compare against on later steps. It finishes by registering, for every node the descriptor marks as contributing a resistive/reactive residual, a call to `circuit.newResistiveContribution()`/`newReactiveContribution()`.

`bindCore()` fills the OSDI node-mapping array (`nodeMappingArray()`) with each static node's `UnknownIndex`, then walks the Jacobian entries again to resolve each one's row/column `Node*` to an unknown pair and store the resulting matrix-element pointer (`KluMatrixAccess::valuePtr()`) into the resistive/reactive Jacobian pointer slots the descriptor reads from during `load_jacobian_*()`.

## `propagateParameters()`

A one-line hook: if `Flags::ParamsChanged` is set it is cleared and `Flags::NeedsSetup` is set instead, so the next `setup()` pass picks up the change - OSDI instances have no children to propagate parameters to, unlike [`HierarchicalInstance`](hierdevice.md).

## Bypass and convergence checking: `evalCore()`, `inputBypassCheckCore()`, `outputBypassCheckCore()`

`evalCore()` decides, before doing any real evaluation, whether this instance can be *bypassed* (skipped, reusing its last-computed Jacobian/residual contributions) this NR iteration. Bypass is impossible unless the device type is marked `Device::Flags::Bypassable`; beyond that, `evalSetup.requestHighPrecision` forces a full evaluation (clearing `Flags::Bypassed`/`OutputConverged`), `evalSetup.forceBypass` (continuation bypass) forces a bypass unconditionally, and otherwise (`evalSetup.allowBypass`, the ordinary inactive-element-bypass case) the instance is bypassed only if its outputs were already converged (`Flags::OutputConverged`, set by a prior `outputBypassCheckCore()` call) *and* `inputBypassCheckCore()` finds every declared input unchanged (within `reltol`/`nr_bypasstol` of the last-stored input value, comparing against `evalSetup.oldSolution`) since it was last evaluated. When bypassing, only the internal-state slots are copied forward (`newStates[i] = oldStates[i]`) so reactive residual history stays consistent; otherwise the descriptor's `eval()` entry point runs, and its returned flags are decoded into `Flags::LimitingApplied` (limiting was applied to some quantity, so the NR loop cannot yet be considered converged) and into `evalSetup.requests.abort`/`finish`/`stop` (from `$stop`, `$finish`, or a fatal Verilog-A error). Whenever `evalSetup.integCoeffs` or `evalSetup.storeReactiveState` is set, reactive residual contributions are then stored into the state vector for every nonzero reactive residual node, additionally differentiated (via `evalSetup.integCoeffs->differentiate()`) into the following state slot when `integCoeffs` is present; `evalSetup.setBoundStep()` is called if the descriptor requested a bounded next timestep. If `nr_bypass` is enabled, the device is bypassable, and this step's evaluation wasn't itself a forced/inactive-element bypass, `outputBypassCheckCore()` re-checks convergence and updates `Flags::OutputConverged` accordingly, feeding the next iteration's bypass decision above.

`outputBypassCheckCore()` compares each nonzero resistive residual against its previously stored value (within `reltol`/`nr_convtol` of `commons.residual_abstol`), and, only when `evalSetup.checkReactiveConvergece` is set, each nonzero reactive residual likewise (against `commons.residual_idt_abstol`); any node's residual moving more than that means "not converged" (`Flags::LimitingApplied` set on the instance short-circuits this entirely, returning "not converged" immediately - a limited instance's residuals aren't meaningful to compare). Both loops always update the stored residual history regardless of the outcome, and, only when the instance is (still) converged, the routine also captures the current input values (from `evalSetup.oldSolution`) for `inputBypassCheckCore()` to compare against next time. It sets `Flags::HasDeviceHistory` on every normal return so a later call knows the stored history is usable (the early `LimitingApplied` return clears that flag instead, since no history was updated that time).

## Loading: `loadCore()`

`loadCore()` calls the descriptor's `load_jacobian_resist()`/`load_jacobian_react()`/`load_jacobian_tran()` (or their `load_jacobian_with_offset_*()` variants, when `loadSetup.jacobianLoadOffset!=0`, used for the perturbed evaluations complex/AC analyses need) into the Jacobian pointers `bindCore()` wired up, gated on the corresponding `loadSetup.load*Jacobian` flag. It then loads the resistive/reactive residual vectors via `load_residual_resist()`/`load_residual_react()` (skipped when the destination pointer is `nullptr`, per [`elsetup.md`](elsetup.md)'s opt-out convention) and, only if `Flags::LimitingApplied`, the corresponding linearized right-hand-side correction via `load_limit_rhs_resist()`/`load_limit_rhs_react()` - the actual residual under limiting is the loaded residual minus this correction, which is why every place that reads a residual off `core_` directly (`outputBypassCheckCore()`, the maximum-contribution accumulation below) subtracts the limiting offset when `Flags::LimitingApplied` is set. Finally it accumulates, when the corresponding `loadSetup` destination pointers are non-null, the per-unknown maximum resistive residual contribution, the reactive residual derivative (summed into `loadSetup.reactiveResidualDerivative`), and the per-unknown maximum reactive residual contribution/derivative - all read back from the state slots `evalCore()`/`populateStructuresCore()` maintain rather than recomputed.

## Noise

The noise API (`noiseSourceCount()`, `uniqueNoiseSourceCount()`, `noiseSourceName()`, `uniqueNoiseSourceIndex()`, `noiseSourceType()`, `loadNoiseParameters()`, `loadNoise()`, `noiseExcitation()`) mostly forwards straight through `model()->device()` to [`OsdiFile`](osdifile.md)'s noise-source translation tables, with `loadNoiseParameters()`/`loadNoise()` calling the descriptor's `load_noise_params`/`load_noise` entry points directly. `noiseExcitation(ndx)` translates the device-local node pair the descriptor reports for a noise source into equation indices, treating `UINT32_MAX` (the descriptor's ground marker) as unknown `0`.

## `dump()`

Prints the instance's terminal-to-node bindings, any collapsed node pairs currently active (decoded from `collapsedNodesPattern()` against `descriptor()->collapsible`), its parameters (each tagged `(not given)` when `parameterGiven()` reports it was never set from the netlist), and its output variables - the same self-describing report [`HierarchicalModel::dump()`](hierdevice.md) and the builtin devices provide for their own kinds of instances.
