# Operating Point Analysis

`anop.h`/`anop.cpp` define `OperatingPoint`, the `Analysis` (see [Analysis Base Class](an.md))
that finds the circuit's DC operating point - the solution of the static (`d/dt = 0`) resistive
system `f(x) = 0`. `coreop.h`/`coreop.cpp` define `OperatingPointCore`, the `AnalysisCore` (see
[Analysis Core Base Class](core.md)) that does the actual work; `coreopnr.h`/`coreopnr.cpp` define
`OpNRSolver`, the Newton-Raphson solver `OperatingPointCore` drives, and `PreprocessedUserForces`,
the nodeset-preprocessing helper. `OperatingPointCore` is also the fixed first core of every
[`SmallSignal`](ansmsig.md) instantiation (AC, noise, DC-incremental, ...) - it is the core other
analyses' documentation points to for how nodesets work.

## Class Structure

`OperatingPoint` is a plain, single-core `Analysis` subclass: it owns `params`
(`IStruct<OperatingPointParameters>`), the Jacobian/solution/state storage (`jac`, `solution`,
`states`), and one `OperatingPointCore core`. As in every `Analysis` subclass, member declaration
order matters - `jac`/`solution`/`states` are declared, and therefore constructed, before `core`,
because `core`'s constructor binds references to them. Every pure-virtual hook `Analysis` requires
(`clearOutputDescriptors()`, `rebuildCores()`, `coreCoroutine()`, the state-storage hooks, ...) is a
thin one-line forward to the matching `core` method - the same shape `SmallSignal` uses for its
`opCore`, just without a second small-signal core alongside it (see [SmallSignal Analysis
Template](ansmsig.md)).

`OperatingPointCore` in turn owns an `OpNRSolver nrSolver` (constructed with 2 force slots, see
below) plus the preprocessed-nodeset state (`preprocessedNodeset`, a `PreprocessedUserForces`) and
a pointer to a `CoreStateStorage` slot (`continueState`) used for sweep/homotopy continuation.

## Core Algorithm

`OperatingPointCore::coroutine()` runs, per invocation:

1. Unless `op_skipinitial` is set, try a **plain NR solve** (`runSolver(continuePrevious)`) from
   the standard initial point (all-zero, or nodeset-forced, or a continued state - see below).
2. If that did not converge (and did not request an early exit), try each homotopy algorithm named
   in the `op_homotopy` simulator option, in list order, stopping at the first one that converges
   or requests early exit:

   | Name | Class | Basis |
   |---|---|---|
   | `gdev` | `GminStepping(..., gdev=true)` | Ngspice `dynamic_gmin()`/`new_gmin()`, conductance added in parallel with nonlinear devices |
   | `gshunt` | `GminStepping(..., gdev=false)` | Same algorithm, conductance added node-to-ground instead |
   | `spice3Gmin` | `Spice3GminStepping` | Ngspice `spice3_gmin()` |
   | `src` | `SourceStepping` | Ngspice `gillespie_src()`; can fall back to an alternative homotopy list (`op_srchomotopy`) if plain source stepping fails |
   | `spice3Src` | `Spice3SourceStepping` | Ngspice `spice3_src()` |

   Each is a `Homotopy`-derived class (`circuit`, the owning `AnalysisCore&`) whose `run()` returns
   `(converged, abort)` and reports its step count via `stepCount()`; `OperatingPointCore` sums
   these into `homotopySteps` for its error message.
3. If nothing converged and no early exit was requested, this is an error (`InitialOp` if the
   plain solve was the only thing tried and failed, `Homotopy` if homotopy also failed,
   `NoAlgorithm` if `op_skipinitial` was set and `op_homotopy` is empty, so nothing was tried at
   all).
4. On convergence, one output point is written (`outfile->addPoint()`, if `write` is enabled); the
   core coroutine yields `CoreState::Finished`, otherwise `CoreState::Aborted` - operating point
   analysis has no `Stopped`/continuation-within-a-run concept, unlike transient analysis; the only
   two outcomes are converge or abort.

## The NR Solver: `OpNRSolver`

`OpNRSolver` (`coreopnr.h`) is an `NRSolver` (see `nrsolver.h`) specialization for the static
system `f(x) = 0`. It overrides the standard NR-loop hooks:

- **`buildSystem()`** evaluates and loads the circuit's resistive residual and Jacobian
  (`circuit.evalAndLoad()`), adds a `gshunt` shunt-to-ground conductance if `commons.gshunt>0`
  (used by gshunt-based homotopy), then loads any active forces (nodesets, see below) via
  `loadForces()`. Nodeset forces are automatically disabled once iteration `op_nsiter+1` is
  reached (`setNodesetAndIcFlags()`), so they only bias the first few iterations toward a target
  and do not prevent the solver from finding the true, unforced solution.
- **`checkResidual()`**/**`checkDelta()`** implement the standard SPICE-style relative/absolute
  convergence tests (`reltol`/`abstol`, with `relref`/`relrefsol`/`relrefres` selecting whether the
  tolerance reference is the current point, the historic per-unknown maximum, or the historic
  per-nature-global maximum - see [Namespace and Types](common.md) for `CommonData`'s tolerance
  fields), tracking the worst-offending node (`maxResidualNode`/`maxDeltaNode`) for diagnostics.
- **`postConvergenceCheck()`** rotates the states vector (`states.rotate()`) after every iteration,
  including the last, so device state history is ready for the next call regardless of whether
  this one converged.

`OpNRSolver` is constructed with `forcesSize=2` by `OperatingPointCore` (a third slot is added when
`OpNRSolver` is reused inside transient analysis for its initial-condition solve - see the comment
block at the end of `coreop.cpp`): slot 0 is reserved for sweep/homotopy continuation, slot 1 for
user-specified nodesets. Only slot 1 can carry branch (delta) forces derived from user input; slot
0's forces come from a previously converged solution and are always plain per-unknown forces.

## The Nodeset Mechanism

A nodeset biases the first few NR iterations toward a guessed solution, to help convergence on
circuits with multiple DC operating points (e.g. flip-flops, latches, bistable comparators) or
otherwise difficult starting conditions. It is controlled by the `nodeset` parameter of
`OperatingPointParameters`, which is also referenced from every core built on `OperatingPointCore`
(AC, noise, DC-incremental, transient's initial point, ...) - this section is the canonical
description other analyses' docs point back to.

`nodeset` is a `Value` that must be either:

- **A string** - the name of a previously stored solution (see `store` below, and
  `Circuit::storedSolution()`); at `rebuild()` time, if a solution with that name exists and its
  type tag matches (`solutionTag`, `"op"`), its per-unknown values become the nodeset via
  `nrSolver.setForces(1, *solPtr, strictforce)`. A missing name produces only a warning (no
  nodesets applied), unless `strictforce` requests a hard error.
- **A list (`ValueVec`)** of nodeset entries, each either:
  - `"<node>", value` - a single-node nodeset: node `<node>`'s voltage (or current, for a flow
    unknown) is forced toward `value`.
  - `"<node1>", "<node2>", value` - a differential nodeset: the difference `<node1> - <node2>` is
    forced toward `value`. This is a genuine delta force (unless one of the two nodes is ground, in
    which case it degenerates into a single-node force), and it requires off-diagonal Jacobian
    entries between the two nodes' unknowns - `PreprocessedUserForces::set()` (called from
    `preMapping()`) checks whether those entries already exist in the sparsity map and reports that
    a mapping pass is needed if not; `OperatingPointCore::populateStructures()` adds them via
    `circuit.createJacobianEntry()` when so.

  List-form nodesets are parsed once into a `PreprocessedUserForces` (node pointers resolved,
  entries validated) and applied every `rebuild()` via
  `nrSolver.setForces(1, preprocessedNodeset, false, strictforce)`.

Applying a nodeset does not rewrite the residual equations - it superimposes an extra forcing term
on the diagonal (and, for a delta force, the two off-diagonal) Jacobian entries, scaled by the
matrix row norm and the `nr_nsforce` option, that pulls the corresponding unknown(s) toward the
nodeset value. Because the forcing strength is finite (not a hard constraint), the solver can still
converge somewhere else if the true solution disagrees with the nodeset once forcing is turned off
after `op_nsiter` iterations. Conflicting forces on the same node, or a delta force whose value
disagrees with node forces already applied to both of its nodes, are reported as errors
(`OpNRSolverError::ConflictNode`/`ConflictDelta`).

A nodeset is unrelated to an initial-condition (`ic`) force, which is a hard, permanently-enabled
constraint used by transient analysis's `uic` mode - nodesets only bias early iterations and never
prevent convergence to the correct answer.

## Continuation Across Sweep Points

`OperatingPointCore::storeState()`/`restoreState()` implement the `AnalysisCore` state-storage
interface (see [Analysis Core Base Class](core.md)) by writing/reading a `CoreStateStorage` slot
tagged `solutionTag`: `storeState()` copies the current `solution`/`states` vectors into the slot
(optionally with unknown names, `storeDetails`, needed to remap onto a different topology later);
`restoreState()` just remembers the slot as `continueState` if it is `valid` (actual use happens
inside `runSolver()`).

`runSolver(continuePrevious)` picks one of three starting strategies:

1. **Ordinary continue mode** - `continueState` is valid, coherent with the current topology, and
   its vector lengths match the circuit's current unknown/state counts: the stored solution and
   states are copied directly into `solution`/`states`, no forces are applied, and (if
   `commons.allowContinueStateBypass` and `nr_contbypass` both allow it) the first NR iteration may
   be bypassed entirely, since the circuit state provably has not changed since that solution was
   computed.
2. **Forced continue mode** - `continueState` is valid but `coherent` is false (a core rebuild
   happened since it was stored, so the stored vector lengths may no longer match): its per-unknown
   values are applied as forces in slot 0 instead of copied directly, so the solver starts from
   whatever the current topology's default initial point is but is pulled toward the old solution
   during early iterations - this is the same forcing mechanism nodesets use, just sourced from a
   prior solution instead of user input.
3. **No stored state** - `continuePrevious` was requested but no valid state exists (e.g. a sweep's
   very first point): fall back to whatever is currently in `solution`/`states` with no forces, or,
   if `continuePrevious` is false, a fresh start with slot 1 (user nodesets) enabled if
   `enableNodesets()` allows it.

`continueState` is consumed (reset to `nullptr`) the first time `runSolver()` uses it, so a stale
pointer is never reused across an unrelated later call. `makeStateIncoherent()` (called by
`Analysis::coroutine()` whenever a core rebuild happens - see [Analysis Base Class](an.md)) is what
flips a slot from case 1 into case 2 without discarding its contents.

## Parameters

`OperatingPointParameters` (`coreop.h`):

| Parameter | Type | Default | Description |
|---|---|---|---|
| `nodeset` | Value (String or list) | `""` | Either the name of a stored solution to use as a nodeset, or a list of single-node/differential nodeset entries. See the Nodeset Mechanism section above. |
| `store` | String | `""` | Name under which the converged solution is saved to the circuit's solution repository (`Circuit::newStoredSolution()`), tagged `solutionTag` (`"op"`), for later use as a `nodeset` string value or by other analyses. Nothing is stored if empty. |
| `write` | Int | `1` | Whether to write the operating point results to an output file. Suppressed entirely if 0, or if the simulator is running in no-output mode. |

Every `SmallSignal` instantiation exposes these same three parameters through a nested
`opParams`/`OperatingPointParameters` field in its own `Parameters` struct (see [SmallSignal
Analysis Template](ansmsig.md)), so, for instance, an AC analysis accepts `nodeset` for its
implicit bias-point solve exactly as a standalone `op` analysis does.

## Save Directives

Resolved by `OperatingPoint::resolveSave()` (and reused by every `SmallSignal` instantiation via
`SmallSignal::resolveOpSave()`, see [SmallSignal Analysis Template](ansmsig.md)):

| Directive | Core helper | Meaning |
|---|---|---|
| `default` | `addAllUnknowns()` | All circuit unknowns (representative nodes). |
| `full` | `addAllNodes()` | All non-ground nodes. |
| `v(<node>)` | `addNode()` | One node's operating-point voltage/value. |
| `i(<inst>:flow(...))` | `addFlow()` | An instance's branch current. |
| `p(<inst>.<outvar>)` | `addInstanceOutvar()` | One instance output variable. |

If no `save` directive produces any output descriptor, `addDefaultOutputDescriptors()` falls back
to `default` (all unknowns) - unless output is entirely suppressed (`write=0` or no-output mode).

## Output

`initializeOutputs()` opens one raw output file, `<prefixedName>.raw`, titled "Operating Point",
containing a single output point (the converged solution) - a `Padded` `OutputRawfile` (see
[Rawfile Output](outrawfile.md)) is used since operating point is not a multi-point analysis. When
`OperatingPoint` is the toplevel analysis, the file is named `<analysisName>.raw`; when
`OperatingPointCore` is the `opCore` of a `SmallSignal` instantiation, it is named
`<analysisName>.op.raw` (the small-signal core writes its own, separately-named, file).

## Related Documentation

- [Analysis Base Class](an.md) - `Analysis`, the base `OperatingPoint` derives from.
- [Analysis Core Base Class](core.md) - `AnalysisCore`, the base `OperatingPointCore` derives from, including `CoreStateStorage` and the save-directive helper functions used above.
- [SmallSignal Analysis Template](ansmsig.md) - the template that reuses `OperatingPointCore` as every small-signal analysis's bias-point core.
- [Sweep Support](answeep.md) - `ParameterSweeper`, whose `continuation` setting controls whether `restoreState()`-based continuation is attempted at all.
- [Namespace and Types](common.md) - `CommonData`, tolerance fields, and `UnknownIndex`.
- [Abstract Output Support](output.md) / [Rawfile Output](outrawfile.md) - the output-file machinery `initializeOutputs()` uses.
</content>
