# Periodic Steady-State Analysis (PSS)

`Pss` (`anpss.h`/`anpss.cpp`) and its computational core `PssCore`
(`corepss.h`/`corepss.cpp`) find the periodic steady-state solution of a
circuit by single-shooting Newton-Raphson iteration, for both driven
(known period) and autonomous (unknown period, oscillator) circuits. The
shooting-Newton formulation, the linear multistep (LMS) integration used
for the trajectory, and the monodromy/period-sensitivity math are derived
in [PSS Analysis - Implementation Notes](../theory/pss.md); this document
covers only the C++ implementation - the class layout, the subsidiary
cores, and the control flow that realizes that math.

## Class Structure

`Pss` follows the same "analysis owns shared repositories, wires them into
its cores" pattern as [Transient Analysis](antran.md). It owns one
`KluRealMatrix` (`jac_`), three `VectorRepository<double>` (`opSolution_`,
`solution_`, `states_`), and four cores built on top of them:

- `opCore_` (`OperatingPointCore`, see [Operating Point Analysis](anop.md)) -
  computes the DC operating point used as the starting point for the
  stabilization transient (and, in continuation mode, as the entry point
  for re-stabilizing from a stored, topology-incoherent solution).
- `stabilTran_` (`TranCore`, see [Transient Analysis](antran.md)) - runs an
  ordinary transient of length `tstab` from the operating point so initial
  transients decay before shooting starts. Its endpoint is the initial
  guess `x0` for the Newton loop.
- `pssTran_` (`PssTranCore`, a `TranCore` subclass, `corepsstran.h`/
  `corepsstran.cpp`) - runs exactly one period `T0` of shooting transient
  per outer Newton iteration and, inline with the same forward sweep,
  integrates the linearized (variational) circuit equations to build the
  monodromy matrix `PhiT` and the period-sensitivity vector `PsiT`.
- `pssCore_` (`PssCore`, `corepss.h`/`corepss.cpp`) - the top-level
  `AnalysisCore` that runs the outer Newton loop, drives the other three
  cores, and assembles/solves the augmented `(n+1)x(n+1)` shooting Jacobian.

All four cores share the same Jacobian and repositories, so the operating
point, the stabilization transient, and the shooting transient all operate
on the same sparsity pattern and unknown vector; `Pss::rebuildCores()`
rebuilds the shared `jac_` once and then rebuilds the four cores in a
specific order - `pssTran_` first (otherwise it would clear the IC forces
`stabilTran_` sets up in `opCore_`), then `stabilTran_` (which sets up
those IC forces), then `opCore_` (now that the forces exist), then
`pssCore_` itself. `PssCore` produces no output of its own; the raw output
file(s) are written by `stabilTran_` (if `writestab` is set) and by
`pssTran_` (the converged PSS trajectory) - `Pss`'s output-descriptor
hooks (`addCommonOutputDescriptor()`, `addCoreOutputDescriptors()`,
`resolveOutputDescriptors()`, `resolveSave()`, `addDefaultOutputDescriptors()`,
`initializeOutputs()`, `finalizeOutputs()`, `deleteOutputs()`) all forward
only to `stabilTran_` and `pssTran_`, never to `opCore_` or `pssCore_`.

`opCore_.solver()` and `stabilTran_.solver()` are each resized to 4 NR
forces slots in `PssCore`'s constructor; slot 3 is reserved for the initial
condition supplied in continuation mode (sweep/homotopy), separate from the
slots the cores use for their own ordinary IC/nodeset forces.

## Control Flow

### `Pss::rebuildCores()` and mapping

`preMapping()`/`populateStructures()` forward to `opCore_`, `stabilTran_`,
and `pssTran_` (in that order for `populateStructures()`) so their sparsity
and state-vector needs (including `stabilTran_`'s IC forces, which
`opCore_`'s nodeset forces share the same Jacobian with) are registered
before the shared Jacobian is built. `PssCore` itself has no sparsity needs
of its own; `PssCore::rebuild()` only resizes its dense scratch objects
(`Fp`, `alpha`, `Jp`, `rowPerm_`, `x0`, `xT`) to `n+1` and forwards the NR
force-scaling option (`nr_force`) to `stabilTran_`'s and `opCore_`'s IC/
nodeset force slots.

### `PssCore::coroutine()` - the outer Newton loop

This is the heart of the analysis. In order:

1. **Parameter check.** `stabstep` must be non-negative, and if both
   `tstab` and `stabstep` are given, `stabstep < tstab`; otherwise abort
   with `StabstepInvalid`.
2. **`runStabilisation(continuePrevious)`** (see below) determines the
   period `period` and leaves the stabilized state in `solution.vector()`.
   A non-positive period aborts with `TperInvalid`.
3. **`x0 = solution.vector()`, `T0 = period`** - the shooting Newton state
   is seeded from the stabilization endpoint.
4. **Initial shoot.** `pssTran_.setShootIC(x0)` and `pssTran_.clearTrajectory()`
   prepare `pssTran_` to start exactly at `x0` (see `setShootIC()`/
   `clearTrajectory()` below), then `runShoot(T0)` integrates one period and
   `xT = solution.vector()` captures the endpoint `x_T^(0)`.
5. **Newton loop**, up to `pss_itl` iterations (simulator option):
   - Require `pssTran_.phiValid()` (at least one accepted step since the
     last `clearTrajectory()`); abort with `SensitivityFailed` otherwise.
   - Residual `Fp = x0 - xT` (periodicity condition, `pss.md` "Periodicity
     condition").
   - Jacobian block `Jp = I - PhiT` from `pssTran_.phiCurrent()`.
   - For autonomous circuits (`driven == 0`): `computePhaseConstraint()`
     builds `alpha` from `pssTran_.firstStepX()`/`firstStepH()`; the
     augmented row/column (`-PsiT` on the right, `alpha^T` on the bottom,
     zero corner) are written into `Jp`, and `Fp[n] = 0` (the phase
     condition's residual is always exactly zero by construction, `pss.md`
     "PSS Newton-Raphson: autonomous circuit"). For driven circuits the
     corner `Jp.at(n,n)` is set to `1` instead, so the augmented row is an
     inert `dT0 = 0` equation and the system stays `(n+1)x(n+1)` without a
     branch in the solve path.
   - **Solve** `Jp * [dx0; dT0] = Fp` via `DenseMatrix::factorAndLuSolve()`
     (dense LU, `pss.md` "Efficient solve"); abort with `SingularJacobian`
     on failure.
   - Update `x0 -= dx0` and (autonomous only) `T0 -= dT0`.
   - **Re-shoot** from the updated `x0`/`T0`: `solution.vector() = x0`,
     `setShootIC(x0)`, `clearTrajectory()`, `runShoot(T0)`, `xT = solution.vector()`.
   - **Convergence check**: per-unknown, `|x0[i]-xT[i]| <= pss_tolscale *
     max(|x0[i]|*reltol, abstol[i])` (same SPICE-style delta check as
     OP/TRAN, scaled by the `pss_tolscale` option, which should be `>= 1`
     since PSS cannot converge tighter than the underlying OP/TRAN NR
     solves it is built from). If every unknown passes, the loop breaks
     with `converged = true`; otherwise `iterIndex` advances to the next
     iteration.
6. **Non-convergence**: if the loop exhausts `pss_itl` iterations without
   converging, abort with `NoConvergence`.
7. **Store converged monodromy** `phiT_ = pssTran_.phiCurrent()`, and if
   `store` names a slot, save `(x0, T0)` as a new stored solution tagged
   `OperatingPointCore::solutionTag`.
8. **Final shoot with output enabled.** The Newton loop runs with
   `shootParams.write = 0` throughout (intermediate shooting iterations
   must not pollute the output file). After convergence, `shootParams.write`
   is restored to `params.write` and one more shoot is run from the
   converged `x0`/`T0` - this is the shoot whose trajectory
   `pssTran_`/`stabilTran_` actually write out. If `adjoint` is set,
   `pssTran_.enableTrajectoryCapture()` is called first so this final shoot
   also records the per-step data (`aData`/`cData`/`gData`/`gammaC`/`gammaG`)
   `integrateAdjointMonodromy()` needs.
9. **Adjoint monodromy.** If `adjoint` is set, `pssTran_.integrateAdjointMonodromy(omegaT_)`
   runs after the final shoot, consuming the just-captured trajectory.
10. `x0_converged_`/`T0_converged_` are set and the coroutine yields
    `CoreState::Finished`.

`PssCore::run()` is a plain synchronous wrapper that drives the coroutine
to completion, returning `false` on `Aborted`. Because the Newton loop
calls `pssTran_.run()` directly (not via a coroutine of its own) once per
shooting iteration, `PssCore`'s own outer coroutine is a thin wrapper
around this synchronous `run()`-driven loop rather than a step-by-step
generator - see `corepss.h`'s header comment.

### `runStabilisation()` - three modes

`prepareStabilisation(period)` sets `stabilParams.step`/`maxstep` (either
the explicit `stabstep` or `period / pss_minpts`), `stop = tstab`,
`start = 0`, then `clampStepToMaxacfreq()` clamps `maxstep` (and `step`, if
larger) to `1/(2*maxacfreq)` - with `maxacfreq` itself floored at `40/period`
so an unreasonably small `maxacfreq` cannot force a step smaller than
`period/40` - whenever `maxacfreq > 0`. The same clamp is applied to the
shoot's own step in `runShoot()`.

`runStabilisation(continuePrevious)` branches into three cases:

- **Not continuing** (ordinary run): the period is taken from `tper`, or
  failing that from the `auxReal()` of a stored solution named by
  `stabilParams.ic` (forwarded from the `ic` save-directive-style
  parameter). A warning is issued if `tstab < 10*tper` (oscillator may not
  have settled). `stabilParams.icmode` is set to `icmode` (defaulting to
  `icmodeOp`) and `stabilTran_.run(false)` performs the stabilization
  transient from scratch.
- **Continuing, with a valid and coherent stored state** (same topology):
  the period comes from `tper` or the stored solution's `auxReal()`; the
  stored solution is installed directly as IC forces (slot 3) on
  `stabilTran_`, and `solution.vector()` is set from those forces - no
  transient re-run is needed, the stored endpoint is reused as-is.
- **Continuing, with a valid but incoherent stored state** (topology
  changed since storage): the stored solution is installed as forces on
  `opCore_` instead, `stabilTran_`'s IC-forces slot is temporarily switched
  to slot 3 with `icmode = icmodeOp`, and a fresh stabilization transient is
  run from that operating point (since the stored state's node/unknown
  mapping may no longer line up directly) before switching the IC-forces
  slot back to slot 2.
- **Continuing, with no valid stored state**: falls through with whatever
  is already in `solution.vector()` and `period = tper` (must still be
  `>0`).

In all cases `PssError::ForcesFailed` (or an immediate abort under
`strictforce`) results if installing forces from a stored solution fails.

### `runShoot()`

Sets `shootParams.stop = T0`, `step = maxstep = T0/pss_minpts`, `start = 0`,
applies the same `clampStepToMaxacfreq()` clamp as the stabilization
transient, then calls `pssTran_.run(false)`. For autonomous circuits
(`!driven`) it additionally calls `pssTran_.computePsiT()` once per shoot,
right after the transient finishes - `PsiT` is a property of only the last
accepted step (see below), so computing it once per shoot rather than at
every accepted step avoids an extra `n`-sized back-solve on every timestep
of the shoot for a value nothing reads until the shoot is over.

### `PssTranCore` - inline sensitivity integration

`PssTranCore` is a `TranCore` subclass that overrides `onTimestepAccepted()`
to advance the monodromy matrix `Phi(t)` in the same forward sweep as the
nominal transient integration, using the discretized linear-response
equation derived in `pss.md` ("Monodromy accumulation" and "Krozer's LR
integration, discretized"):

- `setShootIC(x0)` populates `preprocessedIc` with every circuit unknown
  from `x0` (not just user-visible IC nodes, since `TranCore`'s UIC branch
  zeros `unknownValue_` before applying forces) so `shootParams.icmode` -
  hardcoded to `icmodeUic` in `PssParameters`'s constructor, since the
  shoot must start from an exact vector, never from a nodeset/IC-file spec -
  reproduces `x0` exactly.
- `clearTrajectory()` resets every `phiHist_` slot to the identity
  (`Phi(t0) = I`), zeroes the `C`/`G`/`q`/`qdot` ring buffers, and evaluates
  `C_0`/`q_0` at the starting point with a single `evalAndLoad()` pass
  (`G_0` and `qdot_0` are left at zero - unused before the first real step).
  Must be called before every shoot (initial, each Newton iteration's
  re-shoot, and the final output-enabled shoot).
- **`onTimestepAccepted(tSolve, hk, order)`**, called by `TranCore` at every
  accepted step with `jacobian` holding the already-factored NR iteration
  matrix `Alr_k = G_k + alpha_k*C_k`:
  1. Copies `Alr_k` into `lastAlr_` and refactors it (reusing the symbolic
     factorization from `rebuild()` - only a numeric refactor, no
     resymbolization).
  2. A single `evalAndLoad()` pass evaluates the reactive Jacobian `C_k`
     and residual `q_k` (`storeReactiveState = true`, `icEnabled = false` -
     the same OpenVAF `idt()` fix `coretran.cpp` uses).
  3. `G_k = Alr_k - alpha_k*C_k` is derived from the raw KLU data arrays
     directly, no extra evaluation.
  4. Per-order coefficients `gammaC[p] = alpha_k*a[p]` and
     `gammaG[p] = -b[p]/b1` (zero for BDF, since BDF has no `b`-history) are
     built from `TranCore`'s own `IntegratorCoeffs` for this step.
  5. The RHS `sum_p (gammaC[p]*C_{k-p} + gammaG[p]*G_{k-p}) * Phi_{k-p}` is
     accumulated column-by-column (sparse-matrix x dense-column products,
     via `KluRealMatrix::product()`) directly into `phiHist_`'s future slot.
  6. A single block solve `Alr_k * Phi_k = RHS` (`KluRealMatrix::solveBlock()`,
     one factorization reused for all `n` right-hand-side columns) produces
     the new `Phi_k`; `phiHist_.advance()` promotes it to `phiCurrent()`.
  7. `qdot_{k+1}` is reconstructed via the standard LMS derivative form
     (`IntegratorCoeffs::differentiate()`) and stored in `qDotHist_` -
     needed only by `computePsiT()` after the shoot ends.
  8. If trajectory capture is enabled (`enableTrajectoryCapture()`, used
     only for the final, adjoint-enabled shoot), a `StepRecord` snapshotting
     `A_k`/`C_k`/`G_k`/`gammaC`/`gammaG`/`order` is appended to `trajectory_`.
  On the very first call since `clearTrajectory()`, the state and step size
  are additionally captured as `firstStepX_`/`firstStepH_` - `PssCore` uses
  these to estimate the phase-constraint vector `alpha ~= (x1-x0)/h0`
  (`pss.md` "Choosing alpha").
- **`computePsiT()`** is called once, after the shoot's last accepted step,
  for autonomous circuits only. It takes a mutable copy of the live
  `IntegratorCoeffs` (the original stays valid and unmodified for the same
  step until the next `run()`), calls `computeSensitivities()`/
  `scaleDifferentiatorSensitivities()` to get `d(a)/dh`, `d(b)/dh`,
  `d(alpha)/dh` at the last step's own size (the generic, any-order/
  any-method formulae of `numint.md`, not the closed-form BDF1/trapezoidal
  special cases `pss.md` derives only as illustrations), assembles the RHS
  `d(qdot_N)/dh_{N-1}` from those sensitivities and the `q`/`qdot` history,
  and back-solves against the already-factored `lastAlr_` (`J_N`) to get
  `PsiT = -J_N^{-1} * d(qdot_N)/dh_{N-1}` (`pss.md` "Computing Psi_T").
- **`integrateAdjointMonodromy(Omega)`** walks the captured `trajectory_`
  backward in time, maintaining a short history of `Omega_k` matrices
  (trimmed to the deepest LMS order actually used across the trajectory).
  At each backward step it builds a right-hand side from the transposed
  `C`/`G` products against the already-computed future `Omega` history
  (weighted by each step's own `gammaC`/`gammaG`), then solves
  `A_k^T * Omega_k = RHS` against a freshly refactored transpose-solve of
  that step's `A_k`. `Omega_N = I` seeds the backward recursion (periodicity:
  `A_0`/`C_0` at `k=0` are taken from the last trajectory entry). This
  adjoint pass has no counterpart in `pss.md`; it is available through
  `PssCore::convergedAdjointMonodromy()` only when `adjoint=1`.

## Data Structures

- **`PssParameters`** (`corepss.h`) - the analysis's own parameters, plus
  the three subsidiary cores' parameter structs (`opParams`,
  `stabilParams`, `shootParams`) forwarded by reference into their
  constructors. `shootParams.icmode` is hardcoded to `icmodeUic` in the
  constructor (shooting always starts from an exact state vector); the
  `nodeset` and `ic` parameters are exposed through `opParams`/`stabilParams`
  (see Parameters table).
- **`PssCore::PssError`** / **`PssTranCore::PssTranError`** - the failure
  enums for the outer Newton loop and the inline sensitivity integration,
  respectively, each formatted by the corresponding `formatError()`.
- **`PssTranCore::StepRecord`** - one accepted step's raw `A`/`C`/`G` KLU
  data and `gammaC`/`gammaG` coefficients, retained only when trajectory
  capture is enabled, consumed by `integrateAdjointMonodromy()`.
- Ring buffers (`phiHist_`, `qHist_`, `qDotHist_`, `cHistData_`, `gHistData_`)
  are `CircularBuffer`/`VectorRepository` instances sized once in
  `rebuild()` to `tran_maxord + 2` slots (worst-case LMS order, plus one
  slot for the "current" point, plus one "future" slot `onTimestepAccepted()`
  writes into before promoting) - no per-step (re)allocation.

## Parameters

| Parameter | Type | Default | Meaning |
|---|---|---|---|
| `driven` | Int (bool) | 0 | Non-autonomous (driven) circuit: period `tper` is fixed and only `x0` is solved for. If 0 (autonomous), the period is also a Newton unknown and the phase condition is enforced. |
| `tper` | Real | 0.0 | Initial (driven: fixed) period guess, in seconds. Required to be `>0` (directly, or via a stored solution's `auxReal()`); a non-positive resolved period aborts with `TperInvalid`. |
| `tstab` | Real | 0.0 | Stabilization transient length in seconds, run before shooting starts. |
| `stabstep` | Real | 0.0 | Stabilization transient timestep. If `<=0`, defaults to `period / pss_minpts`. Must be `< tstab` if both are given. |
| `icmode` | Id | `"op"` | IC mode for the stabilization transient (`op` or `uic`, `TranCore::icmodeOp`/`icmodeUic`). Not used in continuation mode with a coherent stored state. |
| `maxacfreq` | Real | 0.0 | If `>0`, caps the stabilization/shoot `maxstep` to `1/(2*maxacfreq)`, so the trajectory resolves AC content up to this frequency; the effective frequency is floored at `40/period` so a very small value cannot force excessively short steps. |
| `store` | String | `""` | If non-empty, name of a stored-solution slot to write the converged `(x0, T0)` into (tagged `OperatingPointCore::solutionTag`, reusable by a later continuation run or another analysis). |
| `adjoint` | Int (bool) | 0 | If set, compute the adjoint monodromy matrix `Omega` after convergence (`convergedAdjointMonodromy()`). Requires an extra trajectory-capturing final shoot. |
| `write` | Int (bool) | 1 | Write output datasets. Forwarded to `shootParams.write` at the points where PSS enables output (see Control Flow); the analysis itself always runs `shootParams.write = 0` during the Newton loop's intermediate shoots. |
| `ic` (-> `stabilParams.ic`) | Value | - | Initial condition for the stabilization transient - either a stored-solution name or explicit node values, per [Transient Analysis](antran.md)'s `ic` parameter. |
| `writestab` (-> `stabilParams.write`) | Int (bool) | 0 | Write the stabilization transient's own trajectory (to `<name>.tran`) alongside the converged PSS output. |
| `nodeset` (-> `opParams.nodeset`) | Value | - | Nodeset forwarded to the DC operating point used to seed the stabilization transient (only relevant on a topology-incoherent continuation restart), per [Operating Point Analysis](anop.md). |

The simulator options `pss_minpts`, `pss_tolscale`, `pss_itl`, and
`pss_debug` (`options.h`, not analysis parameters) additionally control,
respectively, the default step-count-per-period used when `stabstep`/shoot
step is not explicit, the periodicity-convergence tolerance scale factor,
the maximum outer Newton iteration count, and whether PSS emits verbose
per-iteration debug output (`x0`, `xT`, `PhiT`, `PsiT`, `alpha`, residual
ratio) via `Simulator::dbg()`.

## Recognized Save Directives

`Pss::resolveSave()` dispatches every recognized directive to both
`stabilTran_` and `pssTran_` (both `TranCore`-derived, so both use the
shared `AnalysisCore` save helpers documented in [Analysis Core Base
Class](core.md)) - a name must resolve on both cores or the directive is
rejected. `opCore_` and `pssCore_` never produce output.

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

If no `save` directive produces any descriptor, `addDefaultOutputDescriptors()`
falls back to each core's own default set (again via both `stabilTran_` and
`pssTran_`). Every other save-directive shape (`tf`, `zin`, `yin`, noise
contributions, ...) is not supported by PSS and is rejected when `verify`
is set.

## Related Documentation

- [PSS Analysis - Implementation Notes](../theory/pss.md) - the
  shooting-Newton formulation, LMS integration, and monodromy/`PsiT`
  sensitivity math this document builds on.
- [Analysis Base Class](an.md) - the `Analysis` base `Pss` derives from.
- [Analysis Core Base Class](core.md) - the `AnalysisCore` base `PssCore`
  and `PssTranCore` derive from, and the shared save-directive helpers.
- [Operating Point Analysis](anop.md) - `OperatingPointCore`, used by
  `opCore_`.
- [Transient Analysis](antran.md) - `TranCore`, the base class of
  `stabilTran_` and `pssTran_` (`PssTranCore`).
- [Harmonic Balance Analysis](anhb.md) - an alternative periodic
  steady-state method (frequency-domain, not shooting).
