# Harmonic Balance Analysis (HB)

`HB` (`anhb.h`/`anhb.cpp`) and its computational core `HBCore`
(`corehb.h`/`corehb.cpp`, with the collocation-point and transform-matrix
setup in `corehbcoloc.cpp`/`corehbxform.cpp`) find the periodic steady state
of a circuit in the frequency domain by multi-tone harmonic balance: the
unknowns are Fourier coefficients (phasors) at a fixed set of frequencies,
and a single large Newton-Raphson solve (`HBNRSolver`, `corehbnr.h`/
`corehbnr.cpp`) finds the coefficients that make the circuit's residual
vanish at every frequency simultaneously. This is an alternative to the
time-domain shooting method of [PSS Analysis](anpss.md) - HB handles
multi-tone (non-commensurate or widely separated) excitation more directly,
at the cost of a much larger, denser NR system. The formulation follows
Kundert, White, Sangiovanni-Vincentelli, *Steady-State Methods for
Simulating Analog and Microwave Circuits* (Springer, 1990), chapters 1-2,
cited directly in `corehb.cpp`/`corehbcoloc.cpp`/`corehbxform.cpp`.

## Class Structure

`HB` owns two block-sparse matrices and one solution repository, passed by
reference into `HBCore`:

- `jacColoc` (`KluBlockSparseRealMatrix`, large per-block bucket) - one
  dense `nt x 2` block per circuit sparsity-map entry, holding the
  time-domain resistive and reactive Jacobian values at each of the `nt`
  collocation timepoints.
- `jac` (`KluBlockSparseRealMatrix`, small bucket) - the actual HB Jacobian,
  one dense `nt x nt` block per sparsity-map entry, in frequency domain.
  Referred to as `bsjac` inside `HBCore`/`HBNRSolver`.
- `solution` (`VectorRepository<double>`) - the NR unknown vector, packed
  per unknown as `[A0, A1r, A1i, A2r, A2i, ..., A(m-1)r, A(m-1)i]` (DC
  magnitude, then real/imaginary parts of each of the `m-1` nonzero-frequency
  phasors) - `2m-1` real components per unknown, matching the collocation
  block width.

`HBCore` in turn owns:

- `spurs_` (`Spurs`, `spurs.h`/`spurs.cpp`) - the frequency grid: the
  fundamental frequencies, the truncated spectrum (`spectrum()`, DC plus
  positive mixing products), and the integer weight vectors describing each
  spectral component as a combination of fundamentals (`spurWeights()`).
  Built by `buildGrid()` from `freq`/`nharm`/`immax`/`truncate`.
- `timepoints` - the `nt = 2m-1` collocation timepoints (`m` = spectrum
  size), selected by `buildColocation()`.
- `APFT`/`IAPFT`/`OmegaGamma`/`GammaInvColumnMajor` (`DenseMatrix<double>`) -
  the almost-periodic Fourier transform and its inverse, and two derived
  matrices used directly in Jacobian/residual assembly (see Algorithm
  below). Built by `buildAPFT()`.
- `nrSolver` (`HBNRSolver`, derived from the generic `NRSolver`) - drives
  the Newton-Raphson iteration over the full `n*nt`-sized system, evaluating
  the circuit at each collocation point and assembling the frequency-domain
  residual/Jacobian each iteration.
- `solutionFD` (`Vector<Complex>`) - the converged solution repacked as one
  complex phasor per unknown per frequency (`n * m` entries), produced by
  `HBNRSolver::postRun()` and consumed by output and by dependent analyses
  such as [Harmonic Balance AC Analysis](anhbac.md).

## Data Structures

- **`HBParameters`** (`corehb.h`) - frequency grid, truncation, sampling,
  and output parameters; see Parameters table below.
- **`Spurs`** (`spurs.h`) - not itself part of this document's scope, but
  central to HB: `build()` constructs the truncated spectrum from
  fundamentals/harmonics/truncation rule; `spectrum()` is the one-sided
  (nonnegative-frequency) grid HB solves on; `smsigFreq()`/`mixingStencil()`
  extend this to the full two-sided small-signal spectrum consumed by
  [Harmonic Balance AC Analysis](anhbac.md).
- **Truncation schemes** (`params.truncate`): `box` keeps all
  `0 <= kj <= Hj` per-fundamental harmonic combinations; `diamond` keeps
  combinations with `sum(abs(kj)) <= immax`; `hybrid` (the default) is
  `Spurs::build()`'s combined box/diamond rule (passed as its `hybrid`
  argument).
- **Sampling modes** (`params.sample`, used by `buildColocation()`):
  `uniform` places `nsam = ceil(samplefac*nt)` evenly spaced candidate
  points over `nper` periods of the lowest nonzero frequency, offset by
  `shift` of one sample spacing; `random` draws `nsam` points uniformly at
  random (fixed seed, reproducible); `mixed` is uniform placement jittered
  by up to `shift` of one sample spacing.
- **`HBCore::HBError`** / **`HBNRSolver::HBNRSolverError`** - failure enums
  for the outer HB driver and the NR solver, respectively.

## Algorithm / Control Flow

### Grid, collocation, and transforms (`rebuild()`)

`HBCore::rebuild()` branches on `params.solve`:

- **`solve=1`** (normal HB solve): `buildGrid()` builds `spurs_` from
  `freq`/`nharm`/`immax`/`truncate`; `buildColocation()` selects
  `timepoints`; `buildAPFT()` builds the transform matrices. The `nt x nt`
  block-sparse HB Jacobian `bsjac` and `solutionFD` are (re)sized.
- **`solve=0`** (evaluate only, used by [HBAC](anhbac.md) to linearize
  around an already-converged HB solution without re-solving): `spurs_` and
  `timepoints` are copied from a stored solution named by `nodeset`
  (`solPtr->spurs()`/`auxRealVector()`) instead of rebuilt, and only
  `buildAPFT()` runs (the transform is still needed to move the linearized
  time-domain Jacobian into frequency domain). `bsjac` is not built in this
  mode, saving the memory a full HB Jacobian would cost.

In both modes `jacColoc` (the collocation-point Jacobian storage) is always
rebuilt, and the circuit is bound so device residual/Jacobian loads at a
collocation point land in `jacColoc`'s 0-based sub-entries `(0,0)`
(resistive) and `(0,1)` (reactive) of each dense block - `circuit.bind()`
with `Component::Real` at both positions, since collocation loading is a
per-timepoint scalar operation, not the block operation `bsjac` uses.
Nodeset forces (`params.nodeset`, if any names a stored solution) are
translated into NR forces slot 1 via `HBNRSolver::setForces()` (see below).

### `buildGrid()`

Validates `freq` (all components nonzero) and `nharm` (scalar or
per-fundamental vector, all `>0`), validates `truncate` against the three
known schemes, and calls `spurs_.build()`. Requires at least 2 spectral
components (DC plus one nonzero frequency) afterward.

### `buildColocation()` (`corehbcoloc.cpp`)

Selects `nt = 2m-1` collocation timepoints (`m = spurs_.spectrum().size()`,
including DC) from an oversampled candidate pool, following Appendix B,
section 2.1 of the Kundert/White/Sangiovanni-Vincentelli reference:

1. Generate `nsam = ceil(samplefac*nt)` candidate timepoints per
   `params.sample` (see Sampling modes above).
2. Build the candidate transform matrix `IAPFT` (`buildTransformMatrix()`,
   one row per candidate, `2m-1` columns: DC plus cos/-sin pairs per
   nonzero frequency, phases referenced to `tstart`).
3. Greedily select the best-conditioned `nt` rows by a Gram-Schmidt-style
   orthogonalization sweep: starting from row 0 (always kept), at each step
   orthogonalize every remaining candidate row against the last kept row and
   keep the one with the largest residual norm, swapping it into place. This
   maximizes the numerical conditioning of the final transform, rather than
   picking rows by any fixed pattern.
4. Keep the first `nt` timepoints found this way, then sort them by time.

`buildTransformMatrix()` computes each row's cos/-sin phase using an
extended-precision (`fma`-based) reduction of `f*tstart` modulo 1 before
adding the (small) `f*(t-tstart)` term, avoiding cancellation error when
`f*tstart` is large.

### `buildAPFT()` (`corehbxform.cpp`)

Builds the forward/inverse almost-periodic Fourier transform pair and the
two matrices `buildSystem()` uses directly:

- `IAPFT` = `buildTransformMatrix()` output (frequency-to-time, `nt` rows,
  `2m-1` columns).
- `APFT` = `IAPFT`'s inverse (time-to-frequency), obtained by dense
  `factorAndInvert()` (LAPACK, column-major).
- `OmegaGamma` = the frequency-domain differentiation operator applied
  after `APFT`: multiplying a phasor's `(cos, -sin)` pair by `omega =
  2*pi*f` and rotating them into each other reproduces `d/dt` in the
  amplitude-vector convention (DC row is all zero, since `d(DC)/dt = 0`).
- `GammaInvColumnMajor` = `IAPFT` stored column-major, for better cache
  behavior in the per-block multiply `buildSystem()` performs every
  iteration.

### `HBNRSolver::buildSystem()` - one NR iteration (`corehbnr.cpp`)

Each iteration:

1. **`evaluate()`**: transform the current frequency-domain unknowns to
   time domain at every collocation point (`GammaInv.multiply()` per
   unknown), then loop over the `nt` collocation timepoints, evaluating the
   circuit once per timepoint (`circuit.evalAndLoad()` with
   `tranAnalysis=true`, no limiting - HB does not support device current/
   voltage limiting) to fill `jacColoc`'s per-block `(resistive, reactive)`
   columns and the time-domain resistive/reactive residual vectors.
2. **Per-block frequency-domain assembly**: for each circuit sparsity-map
   block, with `Gij`/`Cij` the diagonal `nt x nt` matrices of that block's
   resistive/reactive Jacobian values at the `nt` collocation points, the HB
   Jacobian block is
   `Gamma*Gij*GammaInv + OmegaGamma*Cij*GammaInv` (`corehbnr.cpp`'s own
   derivation comment) - computed as `blockTmp.scaledColumns(Gamma, gCol)`
   (scale `Gamma`'s columns by the resistive values at each collocation
   point, writing into `blockTmp`), `blockTmp.addScaledColumns(OmegaGamma,
   cCol)` (add the reactive contribution scaled through the differentiation
   operator), then a single dense multiply by `GammaInvColumnMajor`.
3. **Residual**: `Gamma*f + OmegaGamma*q` per unknown, where `f`/`q` are the
   time-domain resistive/reactive residual vectors from step 1.
4. **Forces**: if any NR force slot is enabled, `loadForces()` folds forced
   unknown values into the diagonal (row-norm-scaled penalty, same
   forces-as-soft-constraint scheme used by other analyses' `NRSolver`
   subclasses) - and, at iteration `hb_nsiter+1`, both the continuation
   slot (0) and the user-nodeset slot (1) are disabled, so early iterations
   are guided toward the nodeset/continuation state but later iterations
   solve the true HB system unconstrained.

`checkResidual()` is a no-op (always "passed") because HB's residual
vector is a frequency-domain quantity with no meaningful maximum-
contribution reference the way a time-domain residual has; convergence is
judged entirely by `checkDelta()`, a per-unknown, per-frequency delta check
against `max(|tolRef|*reltol, abstol)`, where `tolRef` is the largest
magnitude (DC or any phasor) that unknown reaches across all frequencies.

### `HBCore::coroutine()` - solve driver

1. Sizes `solution` to `n*nt`.
2. Unless `hb_skipinitial` is set, attempts a plain NR solve
   (`runSolver()`) from the standard initial point (or a continuation/
   nodeset state, see below).
3. If that fails and `hb_homotopy` names one or more homotopy algorithms
   (currently only `src`, source stepping - `SourceStepping`,
   `hmtpsrc.h`), tries each in turn until one converges or requests an
   early exit.
4. On convergence, if output is enabled, iterates over `spurs_.spectrum()`
   and, for each frequency, copies that frequency's phasor for every
   unknown into `outputPhasors` and calls `outfile->addPoint()` - so the
   raw file has one output "point" per HB frequency, each carrying every
   unknown's phasor at that frequency (mirroring an AC-analysis-style
   per-frequency dataset, but with a `Complex` value type flag on the
   output file).
5. Yields `Finished` if converged, `Aborted` otherwise - HB has no partial/
   "stopped" outcome.

### `runSolver()` - continuation and nodesets

Mirrors the standard `AnalysisCore::runSolver()` continuation contract:

- **Continuing, valid + coherent stored state** (same spectrum): the stored
  complex spectrum is unpacked directly into `solution.vector()`'s packed
  real layout, forces are disabled, and the solve starts from that exact
  point (`runInContinueMode=true`).
- **Continuing, valid but incoherent** (topology or spectrum changed): the
  stored solution is applied as forces (slot 0) instead of an initial
  vector, so the solve is guided toward it without assuming a matching
  layout.
- **Continuing, no valid state**: falls through to whatever is already in
  `solution`.
- **Not continuing**: nodeset forces (slot 1, from `params.nodeset`) are
  enabled, continuation forces (slot 0) are disabled.

`HBNRSolver::setForces()` (used both for the nodeset slot and, indirectly,
for continuation) translates a stored `AnnotatedSolution`'s complex spectrum
into the packed real force vector, matching frequencies between the stored
solution's own spectrum and the solver's current one by ascending-frequency
merge (`xlat[]`) - frequencies present in the solver's spectrum but absent
from the stored solution are simply not forced.

### `evaluateAtNodeset()` and `getFrequencyDomainJacobians()`

Used by dependent analyses (in particular [HBAC](anhbac.md)) to obtain
linearized time- and frequency-domain Jacobians at an already-converged HB
solution, without paying for a full NR solve or for building the dense
`bsjac` Jacobian:

- **`evaluateAtNodeset()`**: copies the nodeset (forces slot 1) into
  `solution`, disables all forces, `rebuild()`s the NR solver sized for
  evaluation only, then calls `nrSolver.evaluate(true)` - a single pass
  through `HBNRSolver::evaluate()` that fills `jacColoc` with the
  time-domain resistive/reactive Jacobian at every collocation point,
  without ever assembling or solving the frequency-domain system.
- **`getFrequencyDomainJacobians(jacSpec, prunedSpurs)`**: for each circuit
  sparsity-map block, applies `APFT` to that block's collocation-point
  resistive/reactive columns to obtain the block's one-sided (DC + positive
  frequency) frequency-domain Jacobian spectrum, halves all nonzero-
  frequency components (the collocation transform naturally produces a
  two-sided-equivalent magnitude that must be split evenly, matching HB's
  one-sided convention), and then, for each frequency the *caller's*
  (possibly differently pruned) small-signal spectrum `prunedSpurs` needs,
  either copies the matching positive-frequency value directly or its
  complex conjugate for a negative-frequency counterpart - reusing HB's own
  one-sided results to populate a two-sided small-signal Jacobian without
  recomputation.

## Parameters

| Parameter | Type | Default | Meaning |
|---|---|---|---|
| `freq` | RealVector | `{}` | Fundamental frequencies `f1, f2, ..., fd` (Hz), one per tone. Every component must be nonzero. |
| `nharm` | Int or IntVector | 4 | Number of harmonics per fundamental (`H1, ..., Hd`) for box/diamond/hybrid truncation. Scalar applies to all fundamentals; a vector must match `freq` in length. |
| `immax` | Int | 0 | Maximum intermodulation order for diamond/hybrid truncation (`sum(abs(kj)) <= immax`). If `<=0`, defaults to the largest `nharm` component. |
| `truncate` | Id | `hybrid` | Truncation scheme: `box` (`0<=kj<=Hj`), `diamond` (`sum(abs(kj))<=immax`), or `hybrid` (combined box+diamond rule). |
| `samplefac` | Real | 5 | Oversampling factor (`>=1`) for the collocation-point candidate pool: `nsam = ceil(samplefac * nt)`, `nt = 2m-1`. |
| `tstart` | Real | 0 | Reference time for collocation-point phase computation and sample placement. |
| `nper` | Real | 1 | Number of periods of the lowest nonzero frequency across which candidate collocation points are drawn. |
| `sample` | Id | `uniform` | Collocation candidate sampling mode: `uniform`, `random`, or `mixed`. |
| `shift` | Real | 0.2 | Sample offset (as a fraction of one sample spacing) for `uniform`/`mixed` sampling. |
| `store` | String | `""` | If non-empty, name of a stored-solution slot to write the converged complex spectrum, spurs, and collocation timepoints into (tagged `HBCore::solutionTag`). |
| `nodeset` | String | `""` | Name of a stored solution supplying initial nodeset forces (`solve=1`) or, when `solve=0`, the spectrum/collocation grid and solution to evaluate at instead of solving. |
| `solve` | Int (bool) | 1 | If true, solve the HB problem normally. If false, only evaluate the circuit at the `nodeset` solution's existing grid (used internally by [HBAC](anhbac.md); not exposed to the user). |
| `write` | Int (bool) | 1 | Write output datasets. |

The simulator options `hb_debug`, `hb_itl`, `hb_itlcont`, `hb_skipinitial`,
`hb_homotopy`, and `hb_nsiter` (`options.h`, not analysis parameters)
control, respectively, verbose HB debug output, the NR iteration limit for
a fresh solve and for a continuation solve, whether the initial plain-NR
attempt is skipped in favor of homotopy directly, the ordered list of
homotopy algorithms to try on failure, and the iteration count after which
nodeset/continuation forces are dropped mid-solve.

## Recognized Save Directives

`HB::resolveSave()` dispatches directly to `HBCore`'s inherited
`AnalysisCore` save helpers (see [Analysis Core Base Class](core.md)):

| Directive | Arguments | Descriptor added |
|---|---|---|
| `default` | none | All circuit unknowns (`addAllUnknowns()`) |
| `full` | none | All non-ground nodes (`addAllNodes()`) |
| `v` | node (or node pair) | Node voltage phasor (`addNode()`) |
| `i` | `<inst>:flow(br)` | Instance branch current phasor (`addFlow()`) |

If no `save` directive produces a descriptor, `addDefaultOutputDescriptors()`
falls back to `addAllUnknowns()`. `HBCore::addCoreOutputDescriptors()`
additionally always adds a `frequency` descriptor (the per-point frequency
axis), unconditionally of `save`. Every other save-directive shape (`p`,
`tf`, `zin`, `yin`, noise contributions, ...) is rejected when `verify` is
set - HB does not support instance output-variable saves or small-signal
transfer-function saves (`p` is handled by other transient/AC-family
analyses, not HB).

## Related Documentation

- [Analysis Base Class](an.md) - the `Analysis` base `HB` derives from.
- [Analysis Core Base Class](core.md) - the `AnalysisCore` base `HBCore`
  derives from, and the shared save-directive helpers.
- [Harmonic Balance AC Analysis](anhbac.md) - quasi-periodic small-signal
  analysis built directly on a converged `HBCore` solution, via
  `evaluateAtNodeset()`/`getFrequencyDomainJacobians()`.
- [Periodic Steady-State Analysis (PSS)](anpss.md) - the time-domain
  shooting alternative to harmonic balance for finding periodic steady
  state.
