# SmallSignal Analysis Template

`ansmsig.h` defines `SmallSignal<CoreClass, DataMixin>`, a class template that implements the
common two-core structure shared by every analysis that first needs an operating point and then
runs a linear(ized) analysis around it: DC-incremental, DC-transfer-function, AC, AC-transfer-function,
AC-S-parameter, AC-stability, and noise analysis. Rather than writing (and keeping in sync) seven
near-identical `Analysis` subclasses, each of these is a template instantiation
`typedef SmallSignal<XCore, XData> X;` with the analysis-specific behavior supplied by two template
parameters and a handful of explicit specializations. `SmallSignal` has no `.cpp` file - like other
header-only templates in the codebase, its member function bodies are defined inline in the header.

## The Template Mechanism

```cpp
template<typename CoreClass, typename DataMixin> class SmallSignal : public Analysis, public DataMixin {
    ...
    OperatingPointCore opCore;
    CoreClass smsigCore;
};
```

`SmallSignal` always owns exactly two cores: a fixed `OperatingPointCore opCore`
(see [Operating Point Analysis](anop.md)) that establishes the bias point, and a `CoreClass smsigCore`
that is the analysis-specific small-signal core (`ACCore`, `NoiseCore`, `DCIncrementalCore`, ...).
It inherits `DataMixin` as a base class (not a member) purely so `DataMixin`'s protected data
members are visible to `smsigCore`'s constructor call in the same member-initializer list as
`opCore` and `SmallSignal`'s own members - `DataMixin` contributes no behavior, only storage
(inheriting rather than composing avoids naming an object just to hold fields that are otherwise
unused by `SmallSignal` itself).

A concrete analysis is produced by:

1. **A `DataMixin` class** - a plain class with `protected:` data members, one per piece of shared
   linear-algebra workspace the small-signal core needs but does not want to own itself (typically
   because the same storage doubles as output-source state), e.g. `AcData` holds `acMatrix`
   (`KluComplexMatrix`) and `acSolution` (`Vector<Complex>`) for AC analysis; `DCIncrementalData`
   holds just a real `incrementalSolution` vector; `NoiseData` additionally holds the noise
   contribution map and result accumulators. `DataMixin` has no required interface - `CoreClass`'s
   constructor decides what it needs and takes references to the corresponding `DataMixin` fields.
2. **A `CoreClass`** - a concrete `AnalysisCore` subclass (see [Analysis Core Base Class](core.md))
   such as `ACCore`. It must define a nested `Parameters` typedef (used by `SmallSignal::Parameters`
   and the `IStruct<Parameters> params` member) and a constructor `SmallSignal` can call with
   references to the analysis (`*this`, as `OutputDescriptorResolver&`), its own parameter struct,
   the shared `opCore`, the circuit, `CommonData`, the shared Jacobian/solution/state storage, and
   whatever `DataMixin` fields it needs. For example, `ACCore`'s constructor takes
   `(*this, params.core(), opCore, circuit, commons, jac, solution, states, acMatrix, acSolution)`.
   Beyond the constructor, `CoreClass` just needs to implement the ordinary `AnalysisCore` pipeline
   (`preMapping()`, `populateStructures()`, `rebuild()`, `resolveOutputDescriptors()`,
   `initializeOutputs()`/`finalizeOutputs()`/`deleteOutputs()`, `coroutine()`, `formatError()`) -
   `SmallSignal` calls these the same way for every instantiation.
3. **A `Parameters` struct**, nested inside `CoreClass` or referenced by its `Parameters` typedef,
   with an `opParams` (or similarly named) field of type `OperatingPointParameters` nested inside
   it - this is how an instantiation exposes (a subset of) the operating-point core's own
   parameters (nodesets, `writeop`, ...) as parameters of the combined analysis, per the project's
   general rule that an analysis can expose a dependency core's parameters (see
   [Analysis Core Base Class](core.md)).
4. **Three explicit specializations**, written in the analysis's own `an<suffix>.cpp`, since the
   primary template only declares (does not usefully define) them:
   - The **constructor**, which constructs `opCore` and `smsigCore` in its initializer list. Member
     declaration order matters here: `jac`, `solution`, and `states` are declared before `opCore`,
     and `opCore` before `smsigCore`, specifically so that the references these cores bind to (to
     the shared Jacobian, solution history, states, and to `opCore` itself) are bound to
     already-constructed objects - C++ initializes bases and members in declaration order
     regardless of initializer-list order, so getting this ordering wrong is a real correctness bug,
     not just a style issue (see the header's own comment above the member declarations).
   - **`resolveSave()`**, which interprets `save` directives specific to this analysis, typically
     handling its own directives first and falling back to `resolveOpSave()` (see below) for any
     directive that names an operating-point quantity.
   - **`dump()`**, which prints both cores' state for the `dump` control-block command.
5. **A `typedef SmallSignal<CoreClass, DataMixin> X;`** at the bottom of `an<suffix>.h`, which is
   the actual type registered with `Analysis::registerFactory()` and instantiated by
   `Analysis::create()` - `SmallSignal<CoreClass, DataMixin>::create()` is the generic factory
   function used for every instantiation.

## Shared Lifecycle Methods

`SmallSignal` implements every pure-virtual hook `Analysis` requires (see [Analysis Base
Class](an.md)) by calling both cores in a fixed order - `opCore` first, then `smsigCore` - so a
concrete instantiation never has to repeat this fan-out logic:

| Method | Behavior |
|---|---|
| `clearOutputDescriptors()` | Clears both cores' descriptor lists. |
| `addCommonOutputDescriptor()` | Adds one descriptor (e.g. a sweep variable) to both cores; false if either core already had it. |
| `addCoreOutputDescriptors()` | Adds each core's own intrinsic descriptors (time/frequency-like); fails immediately if `opCore` fails. |
| `addDefaultOutputDescriptors()` | Invoked on both cores unconditionally (even if `opCore` fails), so each core independently falls back to its default save set when it received no save directives. |
| `resolveOutputDescriptors(strict, s)` | Resolves both cores' descriptors into output sources; in strict mode, a failure on either core aborts immediately - `opCore` is checked first. |
| `preMapping()`/`populateStructures()` | Delegate to both cores and combine results (`ok` is an AND, `needMapping` an OR) - by default only `opCore` needs new sparsity/state entries, but the template does not assume this. |
| `rebuildCores()` | Rebuilds the shared `jac` (`KluRealMatrix`) against the circuit's current sparsity map first, since both cores reference it, then calls `opCore.rebuild()` and `smsigCore.rebuild()` in order, aborting immediately on either failure. |
| `initializeOutputs()`/`finalizeOutputs()`/`deleteOutputs()` | Open/close/remove each core's own output file, named `prefixedName_+".op"` for the operating-point core and plain `prefixedName_` for the small-signal core - so an AC analysis named `ac1` produces `ac1.op` (the bias point) alongside `ac1` (the sweep results). `finalizeOutputs()`/`deleteOutputs()` run on both cores regardless of the first core's failure, so a failure on one does not leak the other's file handle. |
| `analysisCore()` | Returns `smsigCore` (used by `Analysis` for progress-bar installation when there is no sweep). |
| `coreCoroutine(continuePrevious)` | Returns `smsigCore.coroutine(continuePrevious)` - the small-signal core's own coroutine is what `Analysis::coroutine()` drives once per sweep point; `opCore`'s solve is not itself coroutine-driven at this level; it is instead something `smsigCore`'s implementation invokes internally before doing its own small-signal work (see [Operating Point Analysis](anop.md) for how a core-to-core dependency like this normally runs `opCore`'s solve to convergence first). |
| `formatCoreError()` | Delegates to `smsigCore.formatError()`. |
| `analysisStateStorageSize()`, `allocateAnalysisStateStorage()`, `deallocateAnalysisStateStorage()`, `storeState()`, `restoreState()`, `makeStateIncoherent()` | All delegate only to `opCore` - only the operating-point solution needs to be preserved/restored across sweep points for continuation; the small-signal core is recomputed from scratch at each sweep point once the operating point is available. |

## `resolveOpSave()`: Shared Operating-Point Save Handling

Because every `SmallSignal` instantiation carries an `opCore`, `SmallSignal` provides
`resolveOpSave()` as a non-virtual helper any instantiation's `resolveSave()` specialization can
call for save directives that name an operating-point quantity rather than something specific to
the small-signal analysis itself. It recognizes:

| Save directive | Descriptor added |
|---|---|
| `opdefault` | All circuit unknowns (`opCore.addAllUnknowns()`). |
| `opfull` | All non-ground nodes (`opCore.addAllNodes()`). |
| `v(...)` | A single named node's operating-point voltage (`opCore.addNode()`). |
| `i(...)` | An instance's branch current at the operating point (`opCore.addFlow()`). |
| `p(...)` | An instance output variable at the operating point (`opCore.addInstanceOutvar()`). |

It returns `(ok, handled)`: `handled=false` means the directive name was not one of the above (the
caller's `resolveSave()` should try its own directives, or report an unsupported-directive error if
`verify` is set), while `ok` reports whether a recognized directive's arguments were valid. Each
instantiation's own `resolveSave()` is expected to try its analysis-specific directive names first
and fall back to `resolveOpSave()` for anything else, since directive names like `dv`/`di` at the
small-signal level otherwise resemble `v`/`i` at the operating-point level closely enough to
invite confusion if the fallback ran first.

## Save Directives

`SmallSignal` itself defines no save directives beyond the operating-point ones handled by
`resolveOpSave()` above - `resolveSave()` is a pure-virtual-like hook (the primary template's body
just `return true`s) that every instantiation must specialize. Each instantiation's own recognized
`save` names (e.g. AC's `default`/`full`/`dv`/`di`) are documented in that analysis's own doc.

## Parameters

`SmallSignal` itself defines no parameter fields of its own; `Parameters` is simply
`CoreClass::Parameters`, and `params` (an `IStruct<Parameters>`) is owned by `SmallSignal` and
passed by reference into both cores' constructors. Every instantiation's `Parameters` struct nests
an `OperatingPointParameters opParams` (or similarly named) field, exposing the operating-point
core's own parameters (see [Operating Point Analysis](anop.md) for that table, including the
nodeset mechanism) as part of the combined analysis's parameter set. Each instantiation documents
its own additional parameters (e.g. AC's `from`/`to`/`step`/`mode`/`points`/`values` frequency
sweep) in its own doc.

## Instantiations

| Analysis | `CoreClass` | `DataMixin` | Doc |
|---|---|---|---|
| DC incremental | `DCIncrementalCore` | `DCIncrementalData` | [DC Incremental Analysis](andcinc.md) |
| DC transfer function | `DCXFCore` | `DCXFData` | [DC Transfer Function Analysis](andcxf.md) |
| AC | `ACCore` | `AcData` | [AC Analysis](anac.md) |
| AC transfer function | `ACXFCore` | `ACXFData` | [AC Transfer Function Analysis](anacxf.md) |
| AC S-parameters | `ACSPCore` | `ACSPData` | [AC S-Parameter Analysis](anacsp.md) |
| AC stability | `ACStbCore` | `ACStbData` | [AC Stability Analysis](anacstb.md) |
| Noise | `NoiseCore` | `NoiseData` | [Noise Analysis](annoise.md) |

## Related Documentation

- [Analysis Base Class](an.md) - `Analysis`, the base `SmallSignal` derives from.
- [Analysis Core Base Class](core.md) - `AnalysisCore`, the base every `CoreClass` derives from.
- [Operating Point Analysis](anop.md) - `OperatingPointCore`, the fixed first core of every instantiation, and the nodeset mechanism its parameters expose.
- [Parameterized Objects](parameterized.md) - `IStruct`, `Parameterized`.
</content>
