# AC Small-Signal Stability Analysis

`ACStb` (`anacstb.h`/`anacstb.cpp`) measures loop-gain/stability figures
(forward, reverse, and total open-loop gain, plus the two-port admittance
parameters of the device under test) by breaking a feedback loop at a probe
and driving it with two synthetic excitations over a frequency sweep. It is
one of the three `SmallSignal<CoreClass, DataMixin>` instantiations (see
[Small-Signal Analysis Template](ansmsig.md)); its computational core is
`ACStbCore` (`coreacstb.h`/`coreacstb.cpp`).

## Class and Data Structures

`ACStb` is the typedef `SmallSignal<ACStbCore, ACStbData>`. `ACStbData`
supplies the members every `SmallSignal` instantiation needs plus one
specific to stability analysis:

- `acMatrix` (`KluComplexMatrix`) - the complex small-signal system matrix.
- `acSolution` (`Vector<Complex>`) - RHS/solution vector for one excitation.
- `resultsVector` (`Vector<Complex>`) - the seven computed result scalars at
  the current frequency, indexed by `ACStbCore::StbResult`
  (`Wf`, `Wr`, `W`, `y11`, `y12`, `y21`, `y22`).

`ACStb`'s constructor, `resolveSave()`, and `dump()` are template
specializations of `SmallSignal<ACStbCore, ACStbData>` (declared in
`ansmsig.h`, defined here), following the pattern shared by every
`SmallSignal` instantiation.

## Core Algorithm

As with the other small-signal cores, `ACStbCore` first uses
`OperatingPointCore` (`opCore_`) to solve for the DC operating point `x0`
(`f(x0) = 0`), then linearizes: it evaluates the resistive Jacobian `Jr` and
reactive Jacobian `Jc` at `x0` and, for each frequency, solves

```
(Jr + omega*Jc) X = U
```

with `omega = 2*pi*frequency`. `Jr` is copied into the real part of
`acMatrix` once, outside the frequency loop, since it does not change with
frequency; only the imaginary part is reloaded and the matrix refactored per
frequency point.

**Probe placement.** The `probe` parameter names a voltage source instance
that is assumed to be inserted in series into the feedback loop, with its
positive terminal on the driven-under-test (DUT) input side and its negative
terminal on the DUT output side. `probe`'s `$mfactor` is interpreted as
`$mfactor` parallel instances of the probe (its response - branch current -
is scaled by `responseScalingFactor()` accordingly). All voltages are
measured with respect to `localgnd` (global ground by default).

**Two probe excitations per frequency.** For each frequency, `ACStbCore`
solves the linearized system twice:

1. Unity current injected at the probe's positive node (from `localgnd`),
   with the probe itself carrying no forced voltage. The resulting probe
   current and DUT input voltage give parameters `A` (current response,
   `I2I`) and `C` (voltage response, `U1I`).
2. Unity voltage forced across the probe. The resulting probe current and
   DUT input voltage give parameters `B` (`I2U`) and `D` (`U1U`).

From `A`, `B`, `C`, `D` the two-port admittance parameters of the DUT
(assuming side 1 is the input side) are

```
y11 = (1 + AD - BC - A - D)/C     y12 = (BC - AD + D)/C
y21 = (BC - AD + A)/C             y22 = (AD - BC)/C
```

and the forward/reverse/total open-loop gain follow as

```
Wf = y21/(y11+y22)     Wr = y12/(y11+y22)     W = (y21+y12)/(y11+y22)
```

which `coreacstb.h`'s comment rewrites directly in terms of `A`..`D` (using
`ADBC = A*D - B*C`, `den = 1 + 2*ADBC - A - D`):

```
Wf = (BC-AD+A)/den    Wr = (BC-AD+D)/den    W = (2*(BC-AD)+A+D)/den
```

Applying the Nyquist criterion to `W` determines the DUT's stability under
feedback (that criterion itself is applied by the user/post-processing, not
by `ACStbCore`).

`resolveOutputDescriptors()` reads `wf`/`wr`/`w`/`y(1,1)`/`y(1,2)`/`y(2,1)`/
`y(2,2)` directly out of `resultsVector` (via `OutdGain`/`OutdY` descriptor
types) and `frequency` from the core's own scalar; anything else is
delegated to the owning `ACStb` analysis (which in turn forwards
operating-point saves to `opCore`).

## Parameters

Fields of `ACStbParameters` (`coreacstb.h`), read from the analysis's own
argument list plus the exposed operating-point parameters:

| Parameter | Type | Description |
|---|---|---|
| `probe` | Id | Voltage source instance that breaks the feedback loop. |
| `localgnd` | Id | Local ground node the probe responses are measured against (global ground if empty). |
| `from` | Real | Start frequency for `step` and `dec`/`oct`/`lin` sweeps. |
| `to` | Real | Stop frequency for `step` and `dec`/`oct`/`lin` sweeps. |
| `step` | Real | Step size for a stepped frequency sweep. |
| `mode` | Id | Sweep mode for `dec`/`oct`/`lin` sweeps. |
| `points` | Int | Point (interval) count for `dec`/`oct`/`lin` sweeps. |
| `values` | Value | Explicit vector of frequency values. |
| `write` | Int | Write results to a file (default on). |
| `writeop` (`opParams.write`) | Int | Write the operating-point solution to its own output file (off by default for this analysis). |
| `nodeset` (`opParams.nodeset`) | Value | Nodesets for the initial operating-point solve; see [Operating Point Analysis](anop.md). |
| `store` (`opParams.store`) | String | Name of the stored-solution slot the operating point is written to. |

The `from`/`to`/`step`/`mode`/`points`/`values` fields configure a
`ScalarSweep` exactly as for plain AC analysis; see
[Sweep Support](answeep.md) for the shared stepped/lin/log/value sweep
machinery.

## Save Directives

`ACStb::resolveSave()` recognizes only the operating-point save directives,
delegated verbatim to `opCore` through `resolveOpSave()` (see
[Small-Signal Analysis Template](ansmsig.md) and
[Analysis Core Base Class](core.md)):

| Directive | Resolves to |
|---|---|
| `opdefault` | All circuit unknowns (representative nodes) of the operating point. |
| `opfull` | All non-ground nodes of the operating point. |
| `v(<node>)` or `v(<node1>,<node2>)` | Operating-point node (or node-pair difference) voltage. |
| `i(<inst>:flow(<br>))` | Operating-point branch current of an instance. |
| `p(<inst>.<outvar>)` | Operating-point instance output variable. |

The stability results themselves are not selected through `save` at all: as
long as `write` is nonzero, `ACStbCore::addCoreOutputDescriptors()`
unconditionally adds `frequency`, `wf`, `wr`, `w`, `y(1,1)`, `y(1,2)`,
`y(2,1)`, and `y(2,2)` descriptors, independent of what (if anything) was
saved.

## Related Documentation

- [Small-Signal Analysis Template](ansmsig.md) - the `SmallSignal<CoreClass,
  DataMixin>` template `ACStb` instantiates, and the AC linearization it
  shares with `ACSP` and `Noise`.
- [AC Small-Signal S-Parameter Analysis](anacsp.md) - sibling small-signal
  analysis using the same operating-point-then-linearize approach for a
  multi-port S-parameter sweep instead of a loop-gain probe.
- [Small-Signal Noise Analysis](annoise.md) - sibling small-signal analysis
  computing noise contributions instead of loop-gain figures.
- [Operating Point Analysis](anop.md) - `OperatingPointCore`, nodesets, and
  the DC solve every small-signal analysis starts from.
- [Analysis Core Base Class](core.md) - `AnalysisCore`, the save-directive
  helpers (`addAllUnknowns()`, `addNode()`, `addFlow()`,
  `addInstanceOutvar()`, ...) reused by `resolveOpSave()`.
- [Sweep Support](answeep.md) - `ScalarSweep`, the frequency-sweep
  machinery shared by `from`/`to`/`step`/`mode`/`points`/`values`.
