# Transient Analysis

`Tran` (`antran.h`/`antran.cpp`) computes the large-signal time-domain
response of the circuit by integrating the DAE system forward from an
initial condition, adapting timestep and integration order at every point
from a local-truncation-error (LTE) estimate. Unlike the three small-signal
analyses ([AC S-Parameters](anacsp.md), [AC Stability](anacstb.md),
[Noise](annoise.md)), `Tran` does **not** instantiate the
`SmallSignal<CoreClass, DataMixin>` template (see
[Small-Signal Analysis Template](ansmsig.md)) - it derives from `Analysis`
directly and owns its own Newton-Raphson core (`TranNRSolver`) and
integration-coefficient helper (`IntegratorCoeffs`), separate from the
generic `OpNRSolver`-based cores. Its computational core is `TranCore`
(`coretran.h`/`coretran.cpp`), built on `TranNRSolver`
(`coretrannr.h`/`coretrannr.cpp`) and `IntegratorCoeffs`
(`coretrancoef.h`/`coretrancoef.cpp`).

## Class and Data Structures

`Tran : public Analysis` owns, like every `Analysis`, a `KluRealMatrix jac`
and solution/state history repositories, plus an `OperatingPointCore opCore`
(used only for the initial condition, see below) and a `TranCore tranCore`.
It reimplements the same hook methods `SmallSignal` implements generically
(`resolveSave()`, `addCoreOutputDescriptors()`, `preMapping()`,
`populateStructures()`, `rebuildCores()`, state-storage forwarding, ...) by
hand, forwarding to `opCore`/`tranCore` in essentially the same pattern -
see [Analysis Base Class](an.md) for the hook sequence every `Analysis`
subclass participates in.

`TranCore` owns:

- `nrSolver` (`TranNRSolver`) - the Newton-Raphson solver for one timestep.
- `integCoeffs` (`IntegratorCoeffs`) - the corrector's LMS coefficients,
  shared by reference with `nrSolver` (constructed before it, since
  `nrSolver`'s init list binds a reference to it).
- `predictorCoeffs` (`IntegratorCoeffs`) - a second, independently-ordered
  `IntegratorCoeffs` configured for `Method::PolynomialExtrapolation`, used
  only to extrapolate a starting guess for Newton, never to correct.
- `pastTimesteps` (`CircularBuffer<double>`) and `breakPoints`
  (`CircularBuffer<double>`, 3 slots) - step-size and discontinuity
  history driving the timestep-cutting logic described below.
- `predictedSolution`, `scaledLte` - per-unknown predictor output and scaled
  local-truncation-error, recomputed every step.
- `preprocessedIc` (`PreprocessedUserForces`) - the parsed `ic` parameter,
  turned into NR-solver forces (see Initial Conditions below).
- `whiteBlock`/`flickerBlock` (`TimeDomainNoiseBlock<mt19937_64>`) - the
  transient-noise sample generators, present only when `noisefmax != 0`.

`TranNRSolver : public OpNRSolver` (`coreopnr.h`, see the operating-point
core docs) differs from the plain OP solver in a handful of ways specific to
transient stepping:

- It always loads the transient Jacobian (`loadTransientJacobian = true`)
  rather than a separate resistive/reactive pair, and carries a pointer to
  the shared `IntegratorCoeffs` (`evalSetup_.integCoeffs`) so device
  evaluation can compute `dq/dx` scaled by the leading coefficient.
- It owns three force slots: 0 (continuation nodesets), 1 (user `nodeset`),
  2 (initial-condition forces).
- `enableNoise()`/`disableNoise()`, `advanceNoise()`/`revertNoise()`,
  `collectNoiseScaling()`, and `buildNoiseResidual()` implement transient
  noise: `buildSystem()` (override) adds the current noise sample,
  contributed by every instance's noise-source excitation nodes and scaled
  by `sqrt(|PSD|)*noisescale`, into the resistive-residual RHS after the
  base `OpNRSolver::buildSystem()` has run; `checkResidual()` (override)
  additionally folds the noise residual into the convergence tolerance
  reference so noise does not itself trip false non-convergence.

`IntegratorCoeffs` (`coretrancoef.h`) is a self-contained numerical helper,
independent of the circuit: given a method (`AdamsMoulton`, `BDF`,
`AdamsBashforth`, `PolynomialExtrapolation`), an order, and a step-size
history, it solves a small (confluent-Vandermonde) linear system for the
method's dimensionless coefficients and exposes them in the derivative form
the Newton loop needs (see Core Algorithm). The full derivation - order
conditions, sensitivities to the current step size, and the closed-form
polynomial predictor - is worked out in
[docs/theory/numint.md](../theory/numint.md), with an explicit
correspondence table to this class's members; this document only summarizes
the result `coretran.cpp` consumes.

## Core Algorithm

The circuit is assembled as a DAE, exactly as in the small-signal analyses'
header comments:

```
f(x(t)) + d/dt q(x(t)) = 0
```

with `f` the resistive residual and `q` the reactive residual. `coretran.cpp`
opens with the governing formula the Newton loop actually uses at each
timepoint (its own comment, reproduced verbatim):

```
qdot(t_{k+1}) = 1/(h_k*b_{-1}) * q(t_{k+1})
              - sum_i a_i/(h_k*b_{-1}) * q_{k-i}
              - sum_i b_i/b_{-1}       * qdot_{k-i}
```

i.e. a linear multistep (LMS) formula expressing the reactive residual's
derivative at the new timepoint as the unknown, implicit contribution of
`q(x_{k+1})` (multiplier `1/(h_k*b_{-1})`, the *leading coefficient*) plus a
history term built from already-known past values of `q` and `qdot`. The
reactive Jacobian contribution to the linear system's coefficient matrix is
then

```
di/dx |_{t_{k+1}} = 1/(h_k*b_{-1}) * dq/dx |_{t_{k+1}}
```

i.e. `J = G + alpha*C` with `alpha = 1/(h_k*b_{-1})` the leading coefficient
(`load_jacobian_react()`/`load_jacobian_tran()` in the device evaluation
path apply this scaling). `a_i`, `b_i`, `b_{-1}` are the method's
dimensionless coefficients, recomputed by `IntegratorCoeffs::compute()` from
the past step-size history (`pastTimesteps`) every time the step size or
step-size history changes - in practice, every step. Backward Euler
(`a_0=1`, `b_{-1}=1`) and trapezoidal (`a_0=1`, `b_{-1}=b_0=1/2`) are the
order-1/order-2 special cases with universal, step-independent coefficients;
BDF/Gear and Adams-Moulton of higher order pull in additional past `q`/`qdot`
samples per the current `order`. See
[docs/theory/numint.md](../theory/numint.md) for the full order-condition
derivation and the `Method::BDF`/`Method::AdamsMoulton` correspondence.

**Integration method.** The method and its maximum order are simulator
options (`tran_method` - `am`, `bdf`/`gear`, `euler`, `trap`, `bdf2`/`gear2`
- and `tran_maxord`), not `TranParameters` fields; `euler` and `trap` pin
`maxOrder` to 1/2 regardless of `tran_maxord`.

**Predictor-corrector loop.** Every accepted or attempted timepoint follows
the same sequence:

1. `predictorCoeffs`/`integCoeffs` are recomputed for the current `order`
   and step size `hk` (`compute()`, then `scalePredictor()`/
   `scaleDifferentiator()` to get the derivative-form coefficients).
2. If enough history exists, `predictorCoeffs.predict()` extrapolates a
   starting guess (pure polynomial extrapolation through past solution
   values, no derivative term - see the predictor section of
   [numint.md](../theory/numint.md)); otherwise the previous accepted
   solution is reused as the starting guess.
3. `nrSolver.run()` solves the nonlinear system at `t_{k+1} = tSolve` with
   that starting guess.
4. If `options.tran_trapltefilter` is set and the corrector is trapezoidal
   (order 2 Adams-Moulton), a ringing-suppression filter
   (`filteredSolution`) is applied to the accepted-looking solution before
   it is used for LTE/predictor purposes, without touching the actual
   `solution` history (a heuristic against trapezoidal's known oscillatory
   response to fast transients).

**Step-size and order control.** If the solve failed, the point is rejected
outright, the step is shrunk by `tran_ft`, and order drops to 1. Otherwise
the LTE is estimated from the corrector/predictor difference:

```
factor  = integCoeffs.errorCoeff() / (integCoeffs.errorCoeff() - predictorCoeffs.errorCoeff())
lte     = factor * (corrector_solution - predicted_solution)
ratio   = |lte| / (tol * tran_lteratio)                      # or scaled by (order+1)! if tran_spicelte
hkFactor = ratio^(-1/(order+1))                               # or ^(-1/order) if tran_spicelte
```

per unknown, using a per-unknown tolerance `tol = max(|tolref*reltol|,
abstol)` whose reference value (`tolref`) is chosen by the `relreflte`/
`relref` simulator options (global/local/point-local combinations - the
`(2.9)`/`(4.57)`/`(2.10)` comments in `coretran.cpp` cross-reference "A
Designer's Guide to SPICE and Spectre"). The worst-case (largest) ratio
across all unknowns determines the proposed next step `hkNew = min(2*hk, hk
* hkFactor)`; if the current step turned out to have been too large relative
to `hkNew` (`hk/hkNew > tran_redofactor`), the point is rejected and
retried at a smaller step instead. Accepted points that clear this check
also get their order increased by one (up to `tran_maxord`); a point
accepted exactly at a breakpoint, or any rejected point, resets order to 1
(the LMS coefficients are not valid across a discontinuity in `x(t)` or its
derivatives).

**Breakpoints.** `breakPoints` tracks two bracketing entries (last-accepted,
next-known); a step is transparently cut or halved so that `tSolve` never
overshoots the next breakpoint and lands close to, but never uncomfortably
near, one. Breakpoints are reported by device evaluation
(`esInit.nextBreakPoint`/`nrSolver.evalSetup().nextBreakPoint`, e.g. from
piecewise sources) and are also seeded from `stop` and `start`. `maxstep`,
the transient-noise sampling limit, `tran_ffmax`-derived frequency limiting,
and `tran_rmax`/`tran_minpts`-derived caps all further bound the maximal
step `hmax` independent of the breakpoint mechanism.

**Initial conditions (`icmode`).** Two modes, selected by `params.icmode`:

- `op` (default) - `TranCore` runs a full operating-point solve
  (`opCore_.run()`) to obtain `x(0)`. If `ic` supplies values, they are
  applied as hard NR-solver forces (`opCore_.solver().forces(2)`, "IC
  forces") during that solve rather than as a soft starting nodeset - the
  solve is constrained to reproduce them exactly, and user `nodeset`s are
  disabled for that run when any IC force is present.
- `uic` (`icmode="uic"`, SPICE3-compatible) - no operating-point solve at
  all; `ic` values are collected directly into `TranCore`'s own NR-solver
  force slot 2 (`uicMode=true`, checked for internal conflicts but not
  treated as delta/equality forces), and the initial solution is read
  straight out of those forces (zero wherever no IC was given).

`ic` accepts either a string (the name of a previously stored solution,
loaded via `circuit.storedSolution()`) or a list of explicit per-node
values, in the same format as operating-point nodesets - see
[Operating Point Analysis](anop.md).

**Transient noise (`noisefmax != 0`).** Two noise-sampling modes
(`noisemode`): `zoh` (default) approximates flicker noise with a ladder of
octave-spaced telegraph-process rows (`TimeDomainZohFlickerNoise`) sampled
at a fixed rate `2*noisefmax*oversample`; `sde` instead sums Lorentzian
(Ornstein-Uhlenbeck-style) processes (`TimeDomainSdeFlickerNoise`) and,
unlike ZOH, also enables `computeNoiseContribution` so the noise's own
linearized solution perturbation can be excluded from the LTE reference via
`nrSolver.computeNoiseSolutionContribution()`. Either way the timestep is
capped at `noiseStepLimit = 1/(2*noisefmax*oversample)`, and the generators
are advanced/reverted in lockstep with accepted/rejected timepoints.

## Parameters

Fields of `TranParameters` (`coretran.h`):

| Parameter | Type | Description |
|---|---|---|
| `step` | Real | Initial timestep; 0 uses an internally derived default. |
| `stop` | Real | Time up to which the circuit is simulated. |
| `start` | Real | Time at which recorded output begins (must be before `stop`, unless `stop` is 0). |
| `maxstep` | Real | Maximal timestep (optional). |
| `icmode` | Id | `op` (default, operating-point solve with IC forcing) or `uic` (SPICE3-style, no operating-point solve). |
| `ic` | Value | Initial conditions: a string naming a stored-solution slot, or a list of node/value pairs (same format as nodesets). |
| `noiseseed` | Int | Seed for the transient-noise random generator. |
| `noisescale` | Real | Scaling factor applied to every noise sample. |
| `noisefmax` | Real | Maximum frequency for transient noise; 0 disables transient noise. |
| `noisefmin` | Real | Minimum frequency for transient noise; defaults to `noisefmax/1e3` when 0. |
| `noisemode` | Id | `zoh` (default) or `sde` transient-noise sampling mode. |
| `oversample` | Int | Noise oversampling factor (default 6); timestep is capped at `1/(2*oversample*noisefmax)`. |
| `store` | String | Name of the stored-solution slot the final transient solution vector is written to. |
| `write` | Int | Write results to a file (default on). |
| `nodeset` (`opParams.nodeset`) | Value | Nodesets for the `icmode="op"` initial operating-point solve; see [Operating Point Analysis](anop.md). |

Unlike the small-signal analyses, `Tran` does **not** expose `opParams.write`
or `opParams.store`: `Tran::clearOutputDescriptors()` unconditionally forces
`opParams.write = 0`, so the initial operating-point solve never produces
its own output file, and `TranParameters::store` (transient solution) is an
unrelated, separately-named parameter from any operating-point store.

## Save Directives

`Tran::resolveSave()` recognizes exactly the node/branch/instance-output
save directives, resolved directly against `tranCore` (there is no separate
operating-point save layer here, so the names are `default`/`full` rather
than the `opdefault`/`opfull` used by the `SmallSignal`-based analyses):

| Directive | Resolves to |
|---|---|
| `default` | All circuit unknowns (representative nodes) - `tranCore.addAllUnknowns()`. |
| `full` | All non-ground nodes - `tranCore.addAllNodes()`. |
| `v(<node>)` or `v(<node1>,<node2>)` | Node (or node-pair difference) voltage - `tranCore.addNode()`. |
| `i(<inst>:flow(<br>))` | Instance branch current - `tranCore.addFlow()`. |
| `p(<inst>.<outvar>)` | Instance output variable - `tranCore.addInstanceOutvar()`. |

If no `save` directive produces a descriptor,
`TranCore::addDefaultOutputDescriptors()` falls back to `default` (all
circuit unknowns). `time` is always added by
`TranCore::addCoreOutputDescriptors()` whenever `write` is nonzero,
independent of `save`.

## Related Documentation

- [Operating Point Analysis](anop.md) - `OperatingPointCore`, the nodeset
  mechanism used by `icmode="op"`'s initial solve and by `ic`'s node/value
  list format, and `OpNRSolver`, the base `TranNRSolver` extends.
- [Small-Signal Analysis Template](ansmsig.md) - contrast: the
  `SmallSignal<CoreClass, DataMixin>` template `Tran` deliberately does
  *not* use, unlike `ACSP`/`ACStb`/`Noise`.
- [Analysis Base Class](an.md) - the hook-method sequence `Tran`
  reimplements directly instead of inheriting from `SmallSignal`.
- [Analysis Core Base Class](core.md) - `AnalysisCore`, the save-directive
  helpers (`addAllUnknowns()`, `addAllNodes()`, `addNode()`, `addFlow()`,
  `addInstanceOutvar()`) `Tran::resolveSave()` calls on `tranCore`.
- [docs/theory/numint.md](../theory/numint.md) - full derivation of the LMS
  ansatz, order conditions, step-size sensitivities, and the polynomial
  predictor behind `IntegratorCoeffs`.
- [Periodic Steady-State Analysis](anpss.md) - `PssTranCore` (`corepsstran.h`)
  derives from `TranCore` and reuses it verbatim for PSS's stabilization
  transient and per-period shooting integration, overriding the protected
  `onTimestepAccepted()` hook to accumulate the state-transition
  (monodromy) matrix at each accepted step instead of (or in addition to)
  writing regular output.
