Integrator Architecture and Design
The numerical solution of differential equations is a cornerstone of QuTiP’s time-evolution solvers. Because different quantum systems present varying behaviours, no single numerical algorithm is optimal for all scenarios.
The Integrator framework provides a unified, backend-agnostic interface
that wraps external or custom Ordinary Differential Equation (ODE) libraries. This design
allows solvers to select routines from multiple distinct libraries through a single,
interchangeable class.
Motivation
While Python packages like SciPy offer a robust collection of integration routines, they present several architectural limitations when applied directly to quantum dynamics:
Inconsistent API Interfaces: Across SciPy, interfaces vary drastically (e.g., the older
scipy.integrate.odevs. the modernscipy.integrate.solve_ivp), and some legacy algorithms do not natively support complex numbers without manual splitting into real and imaginary parts.Hardware Limitations: Standard SciPy integration routines are strictly bound to CPU execution, preventing seamless acceleration via GPUs or distributed architectures.
Specialized Quantum Algorithms: Certain high-performance routines tailored for quantum mechanics —such as Krylov subspace methods or custom unitary propagators— are too domain-specific to be included in general-purpose mathematical libraries.
The Integrator class resolves these challenges by abstracting numerical backends
into an interchangeable component, ensuring that switching from a SciPy Fortran solver to
a GPU-accelerated backend requires only changing a configuration string.
Core Design
The abstract base class Integrator serves as the common template for all
numerical integration backends.
Deterministic Time-Evolution
For standard solvers, the integrator acts sequentially. Given an initial time $t_0$ and an initial state vector or density matrix, it propagates the state forward to requested target times. This behaviour is governed by four core methods:
set_state(t, state): Configures the initial conditions of the integrator, seeding it with the starting time and the state array.get_state: Returns the current internal numerical state of the integration loop as a tuple of(t, state).integrate(t): Steps the numerical solver explicitly to the target timetusing the internal state configuration.run(tlist): A generator method that yields successive(t, state)tuples at each timestamp specified in the user-provided arraytlist.
Note
The output state returned by get_state, integrate, and run
is consistently packed as a tuple: (t, state).
Note
All inputs and outputs for states are handled as qutip.core.data.Data objects.
However, an integrator is permitted to change the underlying representation format
internally during evolution.
For example, SciPy-based integrators utilize NumPy arrays internally; they will automatically convert an incoming initial state from its initial storage type to a dense format during setup, and will subsequently return only dense states.
Monte Carlo Trajectories
To support Quantum Monte Carlo simulations (mcsolve), integrators must provide
hook interfaces capable of handling non-continuous, conditional integration steps:
mcstep(t): Governs evolution inside stochastic loops. Unlike standard integration,mcstepmust safely handle non-increasing time targets or backtrack queries (inside the last step range) to assist the parent solver in isolating the exact physical moment a quantum collapse event occurs.
For historical reasons, QuTiP delegates the root-finding search for a collapse to the
high-level MCSolver loop rather than relying on native ODE “event” triggers.
This design has the advantage of allowing QuTiP to wrap external ODE packages
that lack native event-detection features, guaranteeing consistent behavior across all
ODE providers.
Configuration and State Resetting
Numerical routines depend on parameters (such as tolerance limits
atol/rtol, step limits nsteps, etc.) that must be tuned to achieve
optimal efficiency. These parameters are configurable by the developer or end
user via the options attribute passed down to the integrator instance.
The Integrator.options attribute operates as a live dictionary containing
the configuration state.
Because used libraries maintain internal state for their ODE solvers,
modifying values within the options dictionary does not always take effect immediately.
Activating the newly assigned options requires an explicit call to reset.
This tears down the active backend instance and restarts it with the new options
while preserving the active state and time.
Derivative Format
While ordinary ODE interfaces strictly expect functions to represent the right-hand side (RHS),
quantum solvers in QuTiP can profit from alternative formats.
Every integrator exposes an rhs_format class attribute string that dictates
the type of RHS object required during initialization:
“callable”: The integrator expects a function matching the signature
rhs(t: float, state: Data) -> Datareturning the derivative of the state at time t. This is the default for most ODE methods. For specific Cython-backed ODE implementations, if the passed function is the method ofQobjEvo.matmul_data, it will bypass standard Python overhead and hook directly into the Cython bindings ofQobjEvo.matmulusing aRHSobject.“matrix”: The integrator accepts a complex matrix formatted as a
qutip.core.data.Dataobject, solving the linear matrix differential equation $frac{dX}{dt} = text{RHS} times X$.“solver”: The integrator takes the parent
Solverinstance that instantiated it and builds the RHS internally. This is reserved for specialized integration methods that deliberately blend the underlying quantum physics with the numerical method.
Class Attributes Reference
Every subclass inheriting from Integrator must define the following class-level attributes:
- integrator_options
A class-level dictionary defining the comprehensive set of configuration keys supported by the numerical backend along with their default values. Upon instantiation, this is copied to an instance-level
optionsdictionary.
- rhs_format
A string identifier defining the expected structural format of the Right-Hand Side (RHS) derivative function (e.g.,
"callable","matrix", or"solver").
- name
A descriptive, human-readable string representation of the algorithm (e.g.,
"SciPy zvode Adams solver") used primarily for populating metadata inside the finalResultcontainer.
Creating New Integrators
To implement a custom numerical backend, create a subclass derived from Integrator.
At a minimum, your class must override integrate, set_state, get_state,
and either __init__ or _prepare:
Let’s build a simple linear step integrator that updates the current state by adding dt * derivative(t, state). This integrator won’t provide very accurate results, but it will nicely illustrate what’s required.
from qutip.solver.integrator import Integrator
class LinearStepODE(Integrator):
rhs_format = "callable"
name = "Simple Linear Step"
method = "linear"
# An integrator may also define `integrator_options` here. If the attribute is not set,
# a default empty dict is assumed, indicating that their are not special options for
# this integrator.
def _prepare(self):
# __init__ automatically prepares the local options dict,
# stores the rhs in self.derivative, and initializes self._is_set to False.
# Apply any specialized options here!
# This will be called from __init__ and reset.
pass
def set_state(self, t, state):
self.t = t
self.state = state
self._is_set = True
def integrate(self, t):
# Basic Euler step example
self.state = self.state + (t - self.t) * self.derivative(self.t, self.state)
self.t = t
def get_state(self):
return self.t, self.state
The base Integrator class automatically provides default implementations for
the run reset and mcstep methods
(though the fallback version of mcstep can be very inefficient).
To expose your custom integrator to a specific physical solver, register it using the solver class’s target method:
from qutip.solver import SESolver
SESolver.add_integrator(LinearStepODE, "linear")
Once added, the method is immediately accessible via the high-level functional
wrappers by configuring the options map (e.g., options={"method": "linear"}).
Registering the class with the parent Solver base class makes it globally available to all solvers except stochastic solver.
The RHS class
To optimize performance, certain Cython-backed integrators reuse memory when computing derivatives by supporting an in-place functional signature:
rhs(t: float, state: Data, out: Data) -> Data
In this paradigm, the operation should be interpreted as returning the computed
derivative added directly into the existing out object. The function is
permitted to reuse, modify, or overwrite the out container as needed.
This pattern eliminates the considerable overhead of frequent memory allocations
and deallocations inside tight numerical integration loops. The returned object
does not need to the same as the out input if it’s immutable or otherwise not reusable.
These specialized integrators are marked as rhs_format="callable".
Internally, they rely on the RHS helper class.
This class acts as an interface layer that safely wraps an ordinary,
out-of-place python derivative function and transforms it into one that behaves
correctly when subjected to in-place calls.
Within the QuTiP’s solver, physical derivatives are usually
computed via QobjEvo.matmul_data. The integrators automatically detect
that method and bind directly to the low-level Cython implementation with
in-place support.
If you are providing a custom Python callable, you must explicitly wrap your
function inside an RHS instance to leverage in-place performance optimizations:
from qutip.solver.integrator._rhs import RHS
def custom_diff(t, state, out):
# Perform in-place operations directly on the 'out' data object
out += ...
return out
derivative = RHS(custom_diff, inplace=True)