Skip to content

Reference

Solvers

The numerical-method layer. Each solver is a SolverSpec carrying capability flags (SolverCaps: explicit/implicit, adaptive, needs-Jacobian, supported families); method= strings resolve against the registry, an unknown name raises with the available list, and an auto-stiffness heuristic can pick an implicit method from the Jacobian spectrum. Third-party solvers register through the same entry point.

Most users never call these directly — they pass method="rk45" (or similar) to a system's integrate (see Integration & methods). This page documents the registry itself.

Specs & capabilities

SolverSpec dataclass

SolverSpec(
    name: str,
    caps: SolverCaps,
    kernel: str = "",
    description: str = "",
    origin: str = "builtin",
)

Registry metadata for one solver — the Python mirror of a Rust kernel.

PARAMETER DESCRIPTION
name

The unique method= name users select the solver by.

TYPE: str

caps

The solver's capabilities.

TYPE: SolverCaps

kernel

The name of the Rust kernel (tsdyn_solvers registry) that performs the integration. Defaults to :attr:name when omitted.

TYPE: str DEFAULT: ''

description

A one-line human description (shown in listings / docs).

TYPE: str DEFAULT: ""

origin

Where the spec came from — set to "plugin" for entry-point plugins.

TYPE: ('builtin', 'plugin') DEFAULT: "builtin"

SolverCaps dataclass

SolverCaps(
    kind: str,
    adaptive: bool = False,
    needs_jacobian: bool = False,
    supports: frozenset[str] = (
        lambda: frozenset({"ode"})
    )(),
)

A solver's capabilities — the Python mirror of tsdyn_solvers::Caps.

Drives method= resolution and the auto-stiffness layer (stream C-SOLV). The four flags are how the selection layer reasons about a kernel without running it: kind is the auto-stiffness axis (:func:~select.select returns an "implicit" kernel only on a stiff RHS), needs_jacobian drives the with_jacobian=True tape-build decision (:func:~select.build_kwargs), supports gates the family check in :func:~select.resolve, and adaptive is informational metadata exposed through :attr:Resolution.adaptive (the engine, not this layer, acts on it).

PARAMETER DESCRIPTION
kind

Whether the kernel is explicit or implicit. This is the primary auto-stiffness axis: the selection policy hands back an implicit kernel only when the RHS is judged stiff.

TYPE: ('explicit', 'implicit') DEFAULT: "explicit"

adaptive

Whether the kernel does its own embedded error estimate + step adaption (an adaptive embedded pair / variable-order multistep), as opposed to a fixed-step kernel where the caller controls dt.

TYPE: bool DEFAULT: False

needs_jacobian

Whether the kernel requires the analytic Jacobian. Every implicit kernel sets this; an explicit kernel may also (e.g. SDE Milstein reads ∂g/∂u). It is exactly the signal :func:~select.build_kwargs turns into with_jacobian=True so a Jacobian-carrying tape is built.

TYPE: bool DEFAULT: False

supports

The problem families the kernel can integrate; a subset of {"ode", "dde", "sde", "map"}. Mirrors the Rust ProblemKind set, so an explicit ODE kernel is tagged {"ode"} even though the DDE method-of-steps reuses it (that reuse is the selection layer's :func:~select._spec_supports rule, not a caps flag).

TYPE: frozenset[str] DEFAULT: {"ode"}

supports_family

supports_family(family: str) -> bool

Return whether this kernel can integrate problems of family.

Source code in src/tsdynamics/solvers/__init__.py
def supports_family(self, family: str) -> bool:
    """Return whether this kernel can integrate problems of *family*."""
    return family in self.supports

Resolution & selection

resolve

resolve(
    method: str, *, family: str | None = None
) -> Resolution

Resolve a method= string to a concrete, validated kernel.

Normalises the name (:func:normalize), applies the alias table, looks the canonical name up in the registry, and — if family is given — checks the kernel supports it. Built-in and plugin-registered solvers resolve identically.

PARAMETER DESCRIPTION
method

The requested method (e.g. "RK45", "dopri5", "rosenbrock").

TYPE: str

family

The problem family the kernel must support ("ode", "dde", "sde"). When omitted, only existence is checked.

TYPE: str DEFAULT: None

RETURNS DESCRIPTION
Resolution

The canonical name, its spec, and the with_jacobian decision.

RAISES DESCRIPTION
ValueError

If method names no registered kernel (the message lists the available methods, with a stiff-family hint for SciPy/v2 stiff names), or if the kernel does not support family.

Source code in src/tsdynamics/solvers/select.py
def resolve(method: str, *, family: str | None = None) -> Resolution:
    """Resolve a ``method=`` string to a concrete, validated kernel.

    Normalises the name (:func:`normalize`), applies the alias table, looks the
    canonical name up in the registry, and — if *family* is given — checks the
    kernel supports it.  Built-in and plugin-registered solvers resolve
    identically.

    Parameters
    ----------
    method : str
        The requested method (e.g. ``"RK45"``, ``"dopri5"``, ``"rosenbrock"``).
    family : str, optional
        The problem family the kernel must support (``"ode"``, ``"dde"``,
        ``"sde"``).  When omitted, only existence is checked.

    Returns
    -------
    Resolution
        The canonical name, its spec, and the ``with_jacobian`` decision.

    Raises
    ------
    ValueError
        If *method* names no registered kernel (the message lists the available
        methods, with a stiff-family hint for SciPy/v2 stiff names), or if the
        kernel does not support *family*.
    """
    norm = normalize(method)
    canonical = _ALIASES.get(norm, norm)

    specs = all_specs()
    if canonical not in specs:
        raise ValueError(_unknown_method_message(method, norm, family))

    spec = get(canonical)
    if family is not None and not _spec_supports(spec, family):
        raise ValueError(
            f"solver {canonical!r} does not support the {family!r} family "
            f"(it supports {sorted(spec.caps.supports)}); "
            f"available for {family!r}: {available_for(family)}"
        )
    return Resolution(name=canonical, spec=spec, family=family)

default_method

default_method(family: str = 'ode') -> str

Return the zero-config default kernel name for family.

PARAMETER DESCRIPTION
family

"ode", "dde", or "sde".

TYPE: str DEFAULT: "ode"

RETURNS DESCRIPTION
str
RAISES DESCRIPTION
ValueError

If family has no default kernel (e.g. "map" — maps iterate on the engine's own loop and take no method=).

Source code in src/tsdynamics/solvers/select.py
def default_method(family: str = "ode") -> str:
    """Return the zero-config default kernel name for *family*.

    Parameters
    ----------
    family : str, default "ode"
        ``"ode"``, ``"dde"``, or ``"sde"``.

    Returns
    -------
    str

    Raises
    ------
    ValueError
        If *family* has no default kernel (e.g. ``"map"`` — maps iterate on the
        engine's own loop and take no ``method=``).
    """
    try:
        return DEFAULT_METHOD[family]
    except KeyError:
        raise ValueError(
            f"no default solver for family {family!r}; "
            f"families with a method= are {sorted(DEFAULT_METHOD)} "
            f"(maps iterate without a solver kernel)"
        ) from None

available_for

available_for(family: str | None = None) -> list[str]

Return the registered solver names, optionally filtered to family.

PARAMETER DESCRIPTION
family

If given, keep only solvers whose caps support that problem family ("ode", "sde", …). "dde" reuses the explicit ODE stage integrators (method-of-steps), so it returns the explicit ODE kernels.

TYPE: str DEFAULT: None

RETURNS DESCRIPTION
list[str]

Sorted solver names.

Source code in src/tsdynamics/solvers/select.py
def available_for(family: str | None = None) -> list[str]:
    """Return the registered solver names, optionally filtered to *family*.

    Parameters
    ----------
    family : str, optional
        If given, keep only solvers whose caps support that problem family
        (``"ode"``, ``"sde"``, …).  ``"dde"`` reuses the explicit ODE stage
        integrators (method-of-steps), so it returns the explicit ODE kernels.

    Returns
    -------
    list[str]
        Sorted solver names.
    """
    specs = all_specs()
    if family is None:
        return sorted(specs)
    return sorted(name for name, spec in specs.items() if _spec_supports(spec, family))

recommend

recommend(
    system: SystemBase,
    *,
    family: str = "ode",
    ic: Any = None,
    t: float = 0.0,
    ratio_threshold: float = _DEFAULT_RATIO_THRESHOLD,
) -> Resolution

Recommend a solver for system, auto-selecting implicit on a stiff RHS.

Combines the detector (:func:is_stiff) with the policy (:func:select): probes the Jacobian spectrum and resolves to the family's stiff kernel when stiff, the explicit default otherwise. The returned :class:Resolution is always resolvable for family, so this never raises on the selection itself.

PARAMETER DESCRIPTION
system

The system to integrate.

TYPE: SystemBase

family

The problem family. Only "ode" has a selectable stiff alternative (:data:STIFF_METHOD); "dde"/"sde" always recommend their explicit default, so the stiffness probe is skipped for them.

TYPE: str DEFAULT: "ode"

ic

Forwarded to :func:is_stiff.

TYPE: Any DEFAULT: None

t

Forwarded to :func:is_stiff.

TYPE: Any DEFAULT: None

ratio_threshold

Forwarded to :func:is_stiff.

TYPE: Any DEFAULT: None

RETURNS DESCRIPTION
Resolution

The recommended kernel, ready to feed to the engine (and its :attr:~Resolution.build_kwargs).

Source code in src/tsdynamics/solvers/select.py
def recommend(
    system: SystemBase,
    *,
    family: str = "ode",
    ic: Any = None,
    t: float = 0.0,
    ratio_threshold: float = _DEFAULT_RATIO_THRESHOLD,
) -> Resolution:
    """Recommend a solver for *system*, auto-selecting implicit on a stiff RHS.

    Combines the detector (:func:`is_stiff`) with the policy (:func:`select`):
    probes the Jacobian spectrum and resolves to the family's stiff kernel when
    stiff, the explicit default otherwise.  The returned :class:`Resolution` is
    always resolvable for *family*, so this never raises on the selection itself.

    Parameters
    ----------
    system : SystemBase
        The system to integrate.
    family : str, default "ode"
        The problem family.  Only ``"ode"`` has a selectable stiff alternative
        (:data:`STIFF_METHOD`); ``"dde"``/``"sde"`` always recommend their
        explicit default, so the stiffness probe is skipped for them.
    ic, t, ratio_threshold
        Forwarded to :func:`is_stiff`.

    Returns
    -------
    Resolution
        The recommended kernel, ready to feed to the engine (and its
        :attr:`~Resolution.build_kwargs`).
    """
    stiff = family in STIFF_METHOD and is_stiff(system, ic=ic, t=t, ratio_threshold=ratio_threshold)
    return resolve(select(family, stiff=stiff), family=family)

is_stiff

is_stiff(
    system: SystemBase,
    *,
    ic: Any = None,
    t: float = 0.0,
    ratio_threshold: float = _DEFAULT_RATIO_THRESHOLD,
) -> bool

Cheap a-priori stiffness test from the Jacobian spectrum at one point.

Evaluates the analytic Jacobian ∂f/∂u at (ic, t) (via the pure-Python reference evaluator — no compiled engine needed), takes its eigenvalues, and reports stiff when the stiffness ratio — the largest decay rate over the smallest non-negligible decay rate — exceeds ratio_threshold while at least one genuinely fast decaying mode is present.

This is a one-point heuristic meant to seed solver choice, not a guarantee: the engine's runtime detector (rejected-step ratio over a window) is the authoritative signal once integration is underway. Returns False conservatively when the Jacobian cannot be formed or has no decaying mode.

.. note:: Only decay (real-part) stiffness is detected — a wide spread of negative real parts. Oscillatory stiffness (eigenvalues with a large imaginary part but a small real part, e.g. a fast-rotating but barely damped mode that still bounds an explicit step by stability) is not flagged: the ratio is built from real parts only, so such a spectrum reports False here. The engine's runtime rejected-step detector remains authoritative for those cases — pass an explicit stiff method= (e.g. "bdf") if you know the RHS is oscillatorily stiff.

PARAMETER DESCRIPTION
system

An ODE system (anything :func:tsdynamics.engine.run.eval_jac can lower with a Jacobian). Non-ODE families return False.

TYPE: SystemBase

ic

Point to evaluate the Jacobian at. Defaults to the system's resolved IC.

TYPE: array - like DEFAULT: None

t

Time to evaluate at (for non-autonomous systems).

TYPE: float DEFAULT: 0.0

ratio_threshold

Stiffness-ratio boundary above which the RHS is called stiff.

TYPE: float DEFAULT: 1e3

RETURNS DESCRIPTION
bool
Source code in src/tsdynamics/solvers/select.py
def is_stiff(
    system: SystemBase,
    *,
    ic: Any = None,
    t: float = 0.0,
    ratio_threshold: float = _DEFAULT_RATIO_THRESHOLD,
) -> bool:
    """Cheap a-priori stiffness test from the Jacobian spectrum at one point.

    Evaluates the analytic Jacobian ``∂f/∂u`` at ``(ic, t)`` (via the pure-Python
    reference evaluator — no compiled engine needed), takes its eigenvalues, and
    reports stiff when the **stiffness ratio** — the largest decay rate over the
    smallest non-negligible decay rate — exceeds *ratio_threshold* while at least
    one genuinely fast decaying mode is present.

    This is a one-point heuristic meant to seed solver choice, not a guarantee:
    the engine's *runtime* detector (rejected-step ratio over a window) is the
    authoritative signal once integration is underway.  Returns ``False``
    conservatively when the Jacobian cannot be formed or has no decaying mode.

    .. note::
       Only **decay (real-part) stiffness** is detected — a wide spread of
       negative real parts.  **Oscillatory stiffness** (eigenvalues with a large
       imaginary part but a small real part, e.g. a fast-rotating but barely
       damped mode that still bounds an explicit step by stability) is *not*
       flagged: the ratio is built from real parts only, so such a spectrum
       reports ``False`` here.  The engine's runtime rejected-step detector
       remains authoritative for those cases — pass an explicit stiff
       ``method=`` (e.g. ``"bdf"``) if you know the RHS is oscillatorily stiff.

    Parameters
    ----------
    system : SystemBase
        An ODE system (anything :func:`tsdynamics.engine.run.eval_jac` can lower
        with a Jacobian).  Non-ODE families return ``False``.
    ic : array-like, optional
        Point to evaluate the Jacobian at.  Defaults to the system's resolved IC.
    t : float, default 0.0
        Time to evaluate at (for non-autonomous systems).
    ratio_threshold : float, default 1e3
        Stiffness-ratio boundary above which the RHS is called stiff.

    Returns
    -------
    bool
    """
    eigs = _jacobian_eigenvalues(system, ic=ic, t=t)
    if eigs is None:
        return False

    # Decay rates: negative real parts (stable/decaying modes drive stiffness).
    decay = -eigs.real
    fast = float(decay.max()) if decay.size else 0.0
    if fast <= _NEGLIGIBLE_RE:
        # No decaying mode at all → an explicit method is never stability-bound.
        return False

    slow_modes = decay[decay > _NEGLIGIBLE_RE]
    slowest = float(slow_modes.min())
    ratio = fast / slowest
    return ratio >= ratio_threshold

build_kwargs

build_kwargs(
    method: str, *, family: str | None = None
) -> dict[str, bool]

Return the build_problem kwargs implied by method.

{"with_jacobian": True} when the resolved kernel needs the Jacobian (every implicit method, plus Milstein), else {}. The family engine-dispatch seam merges this into its :func:~tsdynamics.engine.problem.build_problem call so the stiff path "just works" — an implicit method= produces a Jacobian-carrying tape and the engine's Jacobian guard (PR #74) is satisfied rather than raising.

PARAMETER DESCRIPTION
method

The requested method= (resolved through :func:resolve).

TYPE: str

family

The problem family, forwarded to :func:resolve for validation.

TYPE: str DEFAULT: None

RETURNS DESCRIPTION
dict[str, bool]
Source code in src/tsdynamics/solvers/select.py
def build_kwargs(method: str, *, family: str | None = None) -> dict[str, bool]:
    """Return the ``build_problem`` kwargs implied by *method*.

    ``{"with_jacobian": True}`` when the resolved kernel needs the Jacobian
    (every implicit method, plus Milstein), else ``{}``.  The family
    engine-dispatch seam merges this into its
    :func:`~tsdynamics.engine.problem.build_problem` call so the stiff path
    "just works" — an implicit ``method=`` produces a Jacobian-carrying tape and
    the engine's Jacobian guard (PR #74) is satisfied rather than raising.

    Parameters
    ----------
    method : str
        The requested ``method=`` (resolved through :func:`resolve`).
    family : str, optional
        The problem family, forwarded to :func:`resolve` for validation.

    Returns
    -------
    dict[str, bool]
    """
    return resolve(method, family=family).build_kwargs

Registration

register

register(
    spec: SolverSpec, *, override: bool = False
) -> None

Register spec under its :attr:~SolverSpec.name.

PARAMETER DESCRIPTION
spec

The solver metadata to register.

TYPE: SolverSpec

override

If False (the default), registering a name that already exists raises :class:ValueError — this catches accidental in-tree clashes the way the system registry does for duplicate class names. Pass True to replace.

TYPE: bool DEFAULT: False

RAISES DESCRIPTION
ValueError

If spec's name is already registered and override is False.

Source code in src/tsdynamics/solvers/__init__.py
def register(spec: SolverSpec, *, override: bool = False) -> None:
    """Register *spec* under its :attr:`~SolverSpec.name`.

    Parameters
    ----------
    spec : SolverSpec
        The solver metadata to register.
    override : bool, default False
        If False (the default), registering a name that already exists raises
        :class:`ValueError` — this catches accidental in-tree clashes the way the
        system registry does for duplicate class names.  Pass True to replace.

    Raises
    ------
    ValueError
        If *spec*'s name is already registered and ``override`` is False.
    """
    if spec.name in _REGISTRY and not override:
        raise ValueError(
            f"a solver named {spec.name!r} is already registered (pass override=True to replace it)"
        )
    _REGISTRY[spec.name] = spec