Skip to content

transient ¤

Transient solvers to be used with Diffrax.

Classes:

Name Description
BDF2FactorizedTransientSolver

BDF2 upgrade of :class:FactorizedTransientSolver (frozen-Jacobian Newton).

BDF2RefactoringTransientSolver

BDF2 upgrade of :class:RefactoringTransientSolver (KLU refactor per iteration).

BDF2VectorizedTransientSolver

BDF2 upgrade of :class:VectorizedTransientSolver.

FactorizedTransientSolver

Transient solver using a Modified Newton (frozen-Jacobian) scheme.

RefactoringTransientSolver

Transient solver with full Newton (quadratic) convergence using klujax.refactor.

SDIRK3FactorizedTransientSolver

3rd-order A-stable SDIRK3 solver with frozen-Jacobian Newton across all stages.

SDIRK3RefactoringTransientSolver

3rd-order A-stable SDIRK3 solver with KLU refactor at each Newton iteration.

SDIRK3VectorizedTransientSolver

3rd-order A-stable SDIRK3 solver using full Newton-Raphson at each stage.

VectorizedTransientSolver

Transient solver that works strictly on FLAT (Real) vectors.

Functions:

Name Description
free_numeric

Dispatch free to the correct backend based on handle type.

setup_transient

Configures and returns a function for executing transient analysis.

BDF2FactorizedTransientSolver ¤

Bases: FactorizedTransientSolver

BDF2 upgrade of :class:FactorizedTransientSolver (frozen-Jacobian Newton).

Factors J_eff once at the predictor state and reuses it across all Newton iterations, trading quadratic convergence for cheaper per-iteration cost. The BDF2 Jacobian scaling α₀/h is used when factoring.

BDF2RefactoringTransientSolver ¤

Bases: RefactoringTransientSolver

BDF2 upgrade of :class:RefactoringTransientSolver (KLU refactor per iteration).

Full quadratic Newton convergence via klujax.refactor at each iteration, combined with BDF2 time discretisation for 2nd-order accuracy.

BDF2VectorizedTransientSolver ¤

Bases: VectorizedTransientSolver

BDF2 upgrade of :class:VectorizedTransientSolver.

Implements variable-step BDF2 via the companion method. On the first step Backward Euler is used automatically; from step 2 onward BDF2 is activated. The Jacobian scaling changes from 1/h (BE) to α₀/h (BDF2) where α₀ = (1 + 2ω)/(1 + ω) and ω = h_n/h_{n-1}.

solver_state is a 3-tuple (y_nm1, h_nm1, q_nm1). h_nm1 is initialised to +inf so that ω = 0 on the first step, making the BDF2 formula reduce to Backward Euler via IEEE 754 arithmetic (no branching). q_nm1 caches Q(y_{n-1}) to avoid recomputing it each step.

FactorizedTransientSolver ¤

Bases: VectorizedTransientSolver

Transient solver using a Modified Newton (frozen-Jacobian) scheme.

At each timestep the system Jacobian is assembled and factored once at a predicted state, then reused across all Newton iterations. Compared to a full Newton-Raphson solver this trades quadratic convergence for a much cheaper per-iteration cost — one triangular solve instead of a full factorisation — making it efficient for circuits where the Jacobian varies slowly between steps.

Convergence is linear rather than quadratic, so newton_max_steps is set higher than a standard Newton solver would require. Adaptive damping min(1, 0.5 / max|δy|) is applied at each iteration to stabilise convergence in stiff or strongly nonlinear regions.

Both real and complex assembly paths are supported; the complex path concatenates real and imaginary parts into a single real-valued vector, allowing purely real linear algebra kernels to be reused for frequency-domain-style analyses.

Requires a :class:~circulax.solvers.linear.KLUSplitFactorSolver as the linear_solver — use analyze_circuit(..., backend="klu_split_factor").

RefactoringTransientSolver ¤

Bases: FactorizedTransientSolver

Transient solver with full Newton (quadratic) convergence using klujax.refactor.

At each timestep the Jacobian is factored once at the predicted state to allocate the numeric handle. Each Newton iteration then calls klujax.refactor — which reuses the existing memory and fill-reducing permutation but recomputes L/U values for the current iterate J(y_k) — followed by a triangular solve. This gives full quadratic Newton convergence at a fraction of the cost of re-factoring from scratch each iteration.

Convergence is quadratic so newton_max_steps is set to 20, matching :class:VectorizedTransientSolver. Adaptive damping min(1, 0.5 / max|δy|) is applied at each iteration to stabilise convergence in stiff or strongly nonlinear regions.

Requires :class:~circulax.solvers.linear.KLUSplitQuadratic as the linear_solver — use analyze_circuit(..., backend="klu_split").

SDIRK3FactorizedTransientSolver ¤

Bases: FactorizedTransientSolver

3rd-order A-stable SDIRK3 solver with frozen-Jacobian Newton across all stages.

Factors J_eff = dF/dy + (1/(γh))·dQ/dy once at the predictor state, then reuses it for all Newton iterations in all three SDIRK stages. This is the recommended backend for large sparse circuits — the single factorisation is shared across all stages because SDIRK's constant diagonal γ gives the same effective Jacobian at every stage.

SDIRK3RefactoringTransientSolver ¤

Bases: RefactoringTransientSolver

3rd-order A-stable SDIRK3 solver with KLU refactor at each Newton iteration.

Provides full quadratic Newton convergence via klujax.refactor within each stage, combined with SDIRK3 time discretisation for 3rd-order accuracy.

SDIRK3VectorizedTransientSolver ¤

Bases: VectorizedTransientSolver

3rd-order A-stable SDIRK3 solver using full Newton-Raphson at each stage.

Uses Alexander's L-stable 3-stage SDIRK tableau with the companion method. Each timestep performs 3 sequential Newton solves (one per stage) with the Jacobian reassembled at every iteration. The same solver_state 2-tuple (y_prev, dt_prev) as Backward Euler is used — SDIRK3 is a one-step method.

VectorizedTransientSolver ¤

Bases: AbstractSolver

Transient solver that works strictly on FLAT (Real) vectors.

Delegates complexity handling to the 'linear_solver' strategy.

free_numeric ¤

free_numeric(handle, dependency=None)

Dispatch free to the correct backend based on handle type.

klujax.KLUHandleManager exposes .close(), which calls the backend's FFI free function. Duck-typing is used so handles are freed correctly without isinstance checks against concrete backend types.

Source code in circulax/solvers/transient.py
def free_numeric(handle, dependency=None):  # noqa: ANN001, ANN201, ARG001
    """Dispatch free to the correct backend based on handle type.

    ``klujax.KLUHandleManager`` exposes ``.close()``, which calls the backend's FFI free
    function.  Duck-typing is used so handles are freed correctly without ``isinstance``
    checks against concrete backend types.
    """
    if hasattr(handle, "close"):
        handle.close()
        return
    if _klujax_free_numeric is not None:
        _klujax_free_numeric(handle)

setup_transient ¤

setup_transient(
    groups: list,
    linear_strategy: CircuitLinearSolver,
    transient_solver: AbstractSolver = None,
) -> Callable[..., Solution]

Configures and returns a function for executing transient analysis.

This function acts as a factory, preparing a transient solver that is pre-configured with the circuit's linear strategy. It returns a callable that executes the time-domain simulation using diffrax.diffeqsolve.

Parameters:

Name Type Description Default
groups list

A list of component groups that define the circuit.

required
linear_strategy CircuitLinearSolver

The configured linear solver strategy, typically obtained from analyze_circuit.

required
transient_solver optional

The transient solver class to use. If None, BDF2VectorizedTransientSolver will be used.

None

Returns:

Type Description
Callable[..., Solution]

Callable[..., Any]: A function that executes the transient analysis.

Callable[..., Solution]

This returned function accepts the following arguments:

t0 (float): The start time of the simulation. t1 (float): The end time of the simulation. dt0 (float): The initial time step for the solver. y0 (ArrayLike): The initial state vector of the system. saveat (diffrax.SaveAt, optional): Specifies time points at which to save the solution. Defaults to None. max_steps (int, optional): The maximum number of steps the solver can take. Defaults to 100000. throw (bool, optional): If True, the solver will raise an error on failure. Defaults to False. term (diffrax.AbstractTerm, optional): The term defining the ODE. Defaults to a zero-value ODETerm. stepsize_controller (diffrax.AbstractStepSizeController, optional): The step size controller. Defaults to ConstantStepSize(). **kwargs: Additional keyword arguments to pass directly to diffrax.diffeqsolve.

Source code in circulax/solvers/transient.py
def setup_transient(
    groups: list, linear_strategy: CircuitLinearSolver, transient_solver: AbstractSolver = None
) -> Callable[..., diffrax.Solution]:
    """Configures and returns a function for executing transient analysis.

    This function acts as a factory, preparing a transient solver that is
    pre-configured with the circuit's linear strategy. It returns a callable
    that executes the time-domain simulation using `diffrax.diffeqsolve`.

    Args:
        groups (list): A list of component groups that define the circuit.
        linear_strategy (CircuitLinearSolver): The configured linear solver
            strategy, typically obtained from `analyze_circuit`.
        transient_solver (optional): The transient solver class to use.
            If None, `BDF2VectorizedTransientSolver` will be used.

    Returns:
        Callable[..., Any]: A function that executes the transient analysis.
        This returned function accepts the following arguments:

            t0 (float): The start time of the simulation.
            t1 (float): The end time of the simulation.
            dt0 (float): The initial time step for the solver.
            y0 (ArrayLike): The initial state vector of the system.
            saveat (diffrax.SaveAt, optional): Specifies time points at which
                to save the solution. Defaults to None.
            max_steps (int, optional): The maximum number of steps the solver
                can take. Defaults to 100000.
            throw (bool, optional): If True, the solver will raise an error on
                failure. Defaults to False.
            term (diffrax.AbstractTerm, optional): The term defining the ODE.
                Defaults to a zero-value ODETerm.
            stepsize_controller (diffrax.AbstractStepSizeController, optional):
                The step size controller. Defaults to `ConstantStepSize()`.
            **kwargs: Additional keyword arguments to pass directly to
                `diffrax.diffeqsolve`.

    """
    fdomain_names = [g.name for g in groups.values() if getattr(g, "is_fdomain", False)]
    if fdomain_names:
        msg = (
            "Frequency-domain components cannot be used in transient simulation "
            "(time-domain convolution is not supported). "
            f"Offending groups: {fdomain_names}. "
            "Use setup_harmonic_balance() instead."
        )
        raise RuntimeError(msg)

    if transient_solver is None:
        # Pick the best BDF2 variant the linear solver supports.
        if hasattr(linear_strategy, "refactor_jacobian"):
            transient_solver = BDF2RefactoringTransientSolver
        elif hasattr(linear_strategy, "factor_jacobian"):
            transient_solver = BDF2FactorizedTransientSolver
        else:
            transient_solver = BDF2VectorizedTransientSolver

    import inspect
    tsolver = transient_solver(linear_solver=linear_strategy) if inspect.isclass(transient_solver) else transient_solver

    sys_size = linear_strategy.sys_size // 2 if linear_strategy.is_complex else linear_strategy.sys_size

    def _execute_transient(
        *,
        t0: float,
        t1: float,
        dt0: float,
        y0: ArrayLike,
        saveat: diffrax.SaveAt = None,
        max_steps: int = 100000,
        throw: bool = False,
        **kwargs: Any,
    ) -> diffrax.Solution:
        """Executes the transient simulation for the pre-configured circuit."""
        term = kwargs.pop("term", diffrax.ODETerm(lambda t, y, args: jnp.zeros_like(y)))
        solver = kwargs.pop("solver", tsolver)
        args = kwargs.pop("args", (groups, sys_size))
        stepsize_controller = kwargs.pop("stepsize_controller", ConstantStepSize())
        checkpoints = kwargs.pop("checkpoints", None)

        sol = circuit_diffeqsolve(
            terms=term,
            solver=solver,
            t0=t0,
            t1=t1,
            dt0=dt0,
            y0=y0,
            args=args,
            saveat=saveat,
            max_steps=max_steps,
            throw=throw,
            stepsize_controller=stepsize_controller,
            checkpoints=checkpoints,
        )

        return sol

    return _execute_transient