# Harmonic Balance AC Analysis (HBAC)

`HBAC` (`anhbac.h`/`anhbac.cpp`) and its computational core `HBACCore`
(`corehbac.h`/`corehbac.cpp`) perform (quasi)periodic small-signal analysis:
a linear frequency sweep of a small excitation superimposed on an already
converged [Harmonic Balance](anhb.md) solution, producing the small-signal
response at every "spur" (mixing product between the input sweep frequency
and the large-signal HB spectrum). This is the harmonic-balance counterpart
of ordinary AC analysis - where AC analysis linearizes around a single DC
operating point, HBAC linearizes around a full periodic (or quasi-periodic,
multi-tone) large-signal HB solution, so a single input frequency `f`
couples into many output frequencies `f + f_n` (one per large-signal spur
`f_n`), not just `f` itself. `HBACCore` owns an `HBCore` internally and
drives it exactly as [Harmonic Balance Analysis](anhb.md) describes; see
that document for the large-signal solve and `corehb.h`'s own comment
("See corehb.h on how to specify nodesets") for how HBAC's `hbParams`
sub-parameters map onto it.

## Class Structure

`HBAC` owns the storage for both the large-signal HB core and its own
small-signal core, in construction order (`hbCore` before `hbacCore`, since
`hbacCore` binds a reference to `hbCore`):

- `jacColoc`/`jac`/`solution` - the same triple `HB`/`HBCore` use on their
  own (see [Harmonic Balance Analysis](anhb.md)), owned here instead so
  `HBAC` can drive the embedded `hbCore` (an `HBCore` instance, `solve`
  parameter controllable via `hbsolve`).
- `jacSpec` (`KluBlockSparseComplexMatrix`) - the large-signal Jacobian
  transformed to frequency domain, `nf x 2` complex blocks (resistive,
  reactive) per sparsity-map entry, where `nf` is the (pruned) small-signal
  spectrum size. Filled by `hbCore.getFrequencyDomainJacobians()`.
- `hbSolution` (`VectorRepository<Complex>`) - declared but not itself
  populated by `HBACCore` (the converged large-signal spectrum lives in
  `hbCore`'s own `solutionFD`, reached through `hbCore_`); present for
  layout/output-source symmetry with `HBCore`.
- `acMatrix` (`KluBlockSparseComplexMatrix`) - the small-signal conversion
  matrix `H(omega)`, one dense complex `nf x nf` block per sparsity-map
  entry, solved once per swept frequency.
- `acSolution` (`Vector<Complex>`) - the small-signal solution vector, `nf`
  complex components (one per small-signal spur) per unknown, plus a
  one-`nf`-wide ground bucket.

`HBACCore` additionally owns its own `Spurs spurs_` - a **pruned copy** of
`hbCore_.spurs()` (see `rebuild()` below), independent of the large-signal
core's own spectrum, and `omega`, the per-spur small-signal angular
frequency vector recomputed at every swept frequency.

## Data Structures

- **`HBACParameters`** (`corehbac.h`) - embeds a full `HBParameters
  hbParams` (the large-signal HB core's own parameters, mostly exposed
  under aliased names, see Parameters table) plus the small-signal sweep
  and spur-selection parameters.
- **`HBACCore::Excitation`** - one small-signal source: the driving
  `Instance*`, the spur indices it excites (into `spurs_.smsigFreq()`), and
  the corresponding complex excitation magnitudes (`mag*exp(j*phase)`),
  collected once per run by `collectExcitations()` from every source
  instance's `spur()` accessor (magnitude/phase pairs the device itself
  specifies, e.g. a port's per-tone small-signal stimulus).
- **`HBACCore::HBACError`** - the core's own failure enum (sweeper setup,
  singular/non-finite matrix, bad frequency conversion, mismatched
  magnitude/phase vector lengths, an unresolvable output spur, ...), on top
  of the errors `hbCore_` itself can report (`HBACError::HBError`).
- **`Spurs::smsigFreq()`/`mixingStencil()`** (`spurs.h`, see
  [Harmonic Balance Analysis](anhb.md)'s Data Structures) - the two-sided
  small-signal spectrum and the index stencil that maps an `(output, input)`
  spur pair to the large-signal Jacobian's spectral component; `HBACCore`
  builds these once per `rebuild()` via `spurs_.prune()`/`buildMixingMap()`.

## Algorithm / Control Flow

### `rebuild()` - pruned spectrum and matrix sizing

1. Copies `hbCore_.spurs()` into `spurs_` (a private copy the small-signal
   analysis can prune independently of the large-signal solve).
2. Resolves `maxharm` (scalar or per-fundamental vector) and `maxfreq`, and
   calls `spurs_.prune()` to drop small-signal spectral components beyond
   those limits - trimming the (much larger) two-sided small-signal
   spectrum down to what the sweep actually needs, independent of how many
   harmonics HB itself solved for.
3. `spurs_.buildMixingMap()` builds the index stencil `fillDenseBlock()`
   uses to locate, for each `(output spur, input spur)` pair, which
   large-signal Jacobian spectral component mixes them.
4. Resolves `params.outspur` into a list of small-signal-spectrum indices
   (`spurIndices`) - either every spur (empty list, the default), a single
   spur (scalar frequency or integer tone-weight vector), or an explicit
   list mixing both forms (`Spurs::smsigFreqIndex()` resolves either form).
   The set of output spurs is fixed on first `rebuild()` and checked for
   consistency (same signatures) on any later `rebuild()` (e.g. across a
   sweep or continuation run) - the analysis errors out rather than
   silently changing what is reported partway through a run.
5. Builds human-readable suffixes for each output spur (`constructSuffixes()`,
   comma-joined tone weights) used to disambiguate the per-spur output
   variable names (see Recognized Save Directives below).
6. Resizes `jacSpec` (`nf x 2` blocks) and `acMatrix` (`nf x nf` blocks) to
   the pruned small-signal spectrum size `nf = spurs_.mixingStencil().nRows()`.

### `HBACCore::coroutine()` - one run

1. **`collectExcitations()`** scans every source-capable instance in the
   circuit, reading its declared spur excitations (`inst->spur()`, a
   `(spurs, magnitudes, phases)` triple), converts magnitude/phase to
   complex excitation values, and resolves each spur specification against
   `spurs_.smsigFreqIndex()`.
2. Depending on `hbParams.solve` (exposed as `hbsolve`): either
   `hbCore_.run(continuePrevious)` performs a full HB solve, or - if the
   caller already has a converged solution stored under `hbParams.nodeset`
   and only wants to linearize around it - `hbCore_.evaluateAtNodeset()`
   evaluates the circuit at that stored solution without solving (see
   [Harmonic Balance Analysis](anhb.md)'s `evaluateAtNodeset()`). Either way
   this leaves `hbCore_`'s time-domain collocation Jacobian populated at the
   converged/given large-signal operating point.
3. **`hbCore_.getFrequencyDomainJacobians(jacSpec, spurs_)`** transforms
   that time-domain Jacobian into `jacSpec`'s frequency-domain blocks,
   already indexed against HBAC's own pruned `spurs_` spectrum (see
   [Harmonic Balance Analysis](anhb.md) for how the one-sided HB result is
   expanded into the two-sided small-signal spectrum via conjugation).
4. A `ScalarSweep` ([Sweep Support](answeep.md)) is set up from `from`/`to`/
   `step`/`mode`/`points`/`values`, and the frequency loop runs once per
   swept point:
   - `computeOmega(f)` fills `omega[n] = 2*pi*(f + f_n)` for every
     small-signal spur `f_n` in `spurs_.smsigFreq()` - the actual angular
     frequency each row of the linear system is evaluated at.
   - **`fillMatrix()`**: for each circuit sparsity-map block, `fillDenseBlock()`
     fills the `(output spur n, input spur m)` entry as `h_nm = G[k] +
     j*omega_n*C[k]`, where `k = mixingStencil().at(n,m)` selects the
     large-signal Jacobian spectral component that couples input spur `m`
     into output spur `n` (or leaves the entry at zero if the stencil has
     no entry there, i.e. that mixing product does not exist at the current
     truncation), and `omega_n = omega[n]` is that output row's own angular
     frequency, not the swept input frequency alone - this is the harmonic
     conversion matrix `H(omega)` of harmonic-balance small-signal theory.
   - The right-hand side is built from `excitations`: for each excitation
     source, `sourceExcitation()` gives its positive/negative excitation
     unknowns, `scaledUnityExcitation()` gives its per-`$mfactor`-scaled
     unity current/voltage, and the source's own complex magnitude at each
     of its declared spurs is added/subtracted at the corresponding rows of
     `acSolution` (used as the RHS buffer before the solve).
   - The system `acMatrix * acSolution = RHS` is factored (refactored when
     possible, falling back to full factorization on failure) and solved;
     an optional reciprocal-condition-number check (`rcondcheck`) flags a
     near-singular conversion matrix.
   - One output point is written per swept frequency if output is enabled.
5. Yields `Finished` if the whole sweep completes, `Aborted` on any failure
   partway through (matrix errors, non-finite entries, singular matrix,
   sweep evaluation errors) - `errorFreq` records where the sweep was when
   it aborted, for `formatError()`'s message.

Unlike [HB](anhb.md)'s own NR loop, HBAC's per-frequency solve is a single
linear solve, not an iterative Newton loop - the "small-signal" assumption
means the conversion matrix does not need re-linearizing within a sweep
point, only re-evaluated (via `omega`) as the sweep frequency changes; the
underlying Jacobian coefficients `G[k]`/`C[k]` are the same for every swept
frequency, computed once in step 3.

## Parameters

| Parameter | Type | Default | Meaning |
|---|---|---|---|
| `from` | Real | 0 | Start frequency for `step` and `dec`/`oct`/`lin` sweep modes. |
| `to` | Real | 0 | Stop frequency for `step` and `dec`/`oct`/`lin` sweep modes. |
| `step` | Real | 0 | Step size for linear-step sweep. |
| `mode` | Id | - | Sweep mode for `dec`/`oct`/`lin` sweeps (see [Sweep Support](answeep.md)). |
| `points` | Int | 0 | Number of points for `dec`/`oct`/`lin` sweep modes. |
| `values` | Value | 0 | Explicit vector of frequency values (values-list sweep). |
| `outspur` | Value | `{}` (all spurs) | Spurs at which the small-signal response is observed: a scalar frequency, an integer tone-weight vector, or a list mixing both. Empty list means all spurs in the (pruned) small-signal spectrum. Cannot change between `rebuild()` calls within one run. |
| `maxharm` | Value | -1 | Maximum absolute tone weight for small-signal spectrum pruning (scalar or per-fundamental vector). Negative keeps every tone HB computed. |
| `maxfreq` | Real | -1 | Maximum absolute frequency for small-signal spectrum pruning. Negative keeps every frequency. |
| `write` | Int (bool) | 1 | Write HBAC's own small-signal output datasets. |
| `writehb` (-> `hbParams.write`) | Int (bool) | 0 | Write the embedded large-signal HB solve's own output dataset (to `<name>.hb`), independent of HBAC's own `write`. |
| `freq` (-> `hbParams.freq`) | RealVector | `{}` | Large-signal HB fundamental frequencies. See [Harmonic Balance Analysis](anhb.md). |
| `nharm` (-> `hbParams.nharm`) | Int/IntVector | 4 | Large-signal HB harmonic counts. See [Harmonic Balance Analysis](anhb.md). |
| `immax` (-> `hbParams.immax`) | Int | 0 | Large-signal HB max intermodulation order. See [Harmonic Balance Analysis](anhb.md). |
| `truncate` (-> `hbParams.truncate`) | Id | `hybrid` | Large-signal HB truncation scheme. See [Harmonic Balance Analysis](anhb.md). |
| `samplefac` (-> `hbParams.samplefac`) | Real | 5 | Large-signal HB collocation oversampling factor. See [Harmonic Balance Analysis](anhb.md). |
| `tstart` (-> `hbParams.tstart`) | Real | 0 | Large-signal HB collocation reference time. See [Harmonic Balance Analysis](anhb.md). |
| `nper` (-> `hbParams.nper`) | Real | 1 | Large-signal HB collocation period span. See [Harmonic Balance Analysis](anhb.md). |
| `sample` (-> `hbParams.sample`) | Id | `uniform` | Large-signal HB collocation sampling mode. See [Harmonic Balance Analysis](anhb.md). |
| `shift` (-> `hbParams.shift`) | Real | 0.2 | Large-signal HB collocation sample shift. See [Harmonic Balance Analysis](anhb.md). |
| `nodeset` (-> `hbParams.nodeset`) | String | `""` | Stored solution to seed (or, with `hbsolve=0`, directly evaluate at) the large-signal HB solve. |
| `store` (-> `hbParams.store`) | String | `""` | Stored-solution slot to write the converged large-signal HB solution into. |
| `hbsolve` (-> `hbParams.solve`) | Int (bool) | 1 | If true, solve the embedded HB problem normally each run. If false, only evaluate at the `nodeset` solution's existing large-signal spectrum/grid (linearize around an externally supplied operating point instead of re-solving it). |

## Recognized Save Directives

`HBAC::resolveSave()` first tries its own small-signal directives; anything
else is forwarded to `resolveHbSave()`, which dispatches to the embedded
`hbCore`'s directives (prefixed `hb`) for saving the large-signal operating
point alongside the small-signal sweep results.

| Directive | Arguments | Descriptor added | Handled by |
|---|---|---|---|
| `default` | none | All circuit unknowns' small-signal response (`addAllUnknowns()`) | `hbacCore` |
| `full` | none | All non-ground nodes' small-signal response (`addAllNodes()`) | `hbacCore` |
| `dv` | node (or node pair) | Node small-signal voltage (`addNode()`) | `hbacCore` |
| `di` | `<inst>:flow(br)` | Instance small-signal branch current (`addFlow()`) | `hbacCore` |
| `hbdefault` | none | All circuit unknowns, large-signal HB values (`addAllUnknowns()`) | `hbCore` |
| `hbfull` | none | All non-ground nodes, large-signal HB values (`addAllNodes()`) | `hbCore` |
| `v` | node (or node pair) | Node voltage phasor, large-signal HB (`addNode()`) | `hbCore` |
| `i` | `<inst>:flow(br)` | Instance branch current phasor, large-signal HB (`addFlow()`) | `hbCore` |
| `p` | `<inst>.<outvar>` | Instance output variable, large-signal HB (`addInstanceOutvar()`) | `hbCore` |

If no `save` directive produces a descriptor, `addDefaultOutputDescriptors()`
falls back to `addAllUnknowns()` on both `hbCore` and `hbacCore`.
`HBACCore::addCoreOutputDescriptors()` always additionally adds a
`frequency` descriptor (the swept small-signal frequency axis).
Every `hbacCore`-resolved solution-component output is expanded into one
named vector per selected output spur (`spurIndices`), each name suffixed
`;<w1>,<w2>,...` with that spur's comma-joined tone weights
(`constructSuffixes()`), so a single `dv`/`default`/`full` save produces
multiple output columns per unknown, one per requested spur.

## Related Documentation

- [Harmonic Balance Analysis](anhb.md) - the large-signal solve HBAC
  linearizes around, and the source of `evaluateAtNodeset()`/
  `getFrequencyDomainJacobians()`.
- [Analysis Base Class](an.md) - the `Analysis` base `HBAC` derives from.
- [Analysis Core Base Class](core.md) - the `AnalysisCore` base `HBACCore`
  derives from, and the shared save-directive helpers.
- [Sweep Support](answeep.md) - `ScalarSweep`, driving HBAC's own frequency
  sweep.
- [PSS Analysis](anpss.md) - the time-domain shooting method for periodic
  steady state; HBAC's small-signal role has no direct PSS analogue in this
  codebase (PSS does not currently offer its own small-signal extension).
