systems.base.numerical_integration.IntegratorBase

systems.base.numerical_integration.IntegratorBase(
    system,
    dt=None,
    step_mode=StepMode.FIXED,
    backend='numpy',
    **options,
)

Abstract base class for numerical integrators.

Provides a unified interface for integrating continuous-time dynamical systems with multiple backends and both fixed/adaptive step sizes.

All integrators must implement: - step(): Single integration step - integrate(): Multi-step integration over interval - name: Integrator name for display

Subclasses handle backend-specific implementations for NumPy, PyTorch, JAX.

Result Types

All integrators return IntegrationResult TypedDict with: - t: Time points (T,) - x: State trajectory (T, nx) - success: Integration succeeded - message: Status message - nfev: Number of function evaluations - nsteps: Number of steps taken - integration_time: Computation time - solver: Integrator name

Examples

>>> # Create integrator
>>> integrator = RK4Integrator(system, dt=0.01, backend='numpy')
>>>
>>> # Single step
>>> x_next = integrator.step(x, u)
>>>
>>> # Multi-step integration
>>> result = integrator.integrate(
...     x0=np.array([1.0, 0.0]),
...     u_func=lambda t, x: np.zeros(1),
...     t_span=(0.0, 10.0)
... )
>>> t, x_traj = result["t"], result["x"]
>>> print(f"Integration {'succeeded' if result['success'] else 'failed'}")
>>> print(f"Steps: {result['nsteps']}, Function evals: {result['nfev']}")

Attributes

Name Description
name Get integrator name for display and logging.

Methods

Name Description
get_stats Get integration statistics.
integrate Integrate over time interval with control policy.
reset_stats Reset integration statistics to zero.
step Take one integration step: x(t) → x(t + dt).

get_stats

systems.base.numerical_integration.IntegratorBase.get_stats()

Get integration statistics.

Returns

Name Type Description
dict Statistics with keys: - ‘total_steps’: Total integration steps taken - ‘total_fev’: Total function evaluations - ‘total_time’: Total integration time - ‘avg_fev_per_step’: Average function evaluations per step

Examples

>>> result = integrator.integrate(x0, u_func, (0, 10))
>>> stats = integrator.get_stats()
>>> print(f"Steps: {stats['total_steps']}")
>>> print(f"Function evals: {stats['total_fev']}")
>>> print(f"Evals/step: {stats['avg_fev_per_step']:.1f}")

integrate

systems.base.numerical_integration.IntegratorBase.integrate(
    x0,
    u_func,
    t_span,
    t_eval=None,
    dense_output=False,
)

Integrate over time interval with control policy.

API Level: This is a low-level integration method that directly interfaces with numerical ODE/SDE solvers. For typical use cases, prefer the high-level simulate() method which provides a cleaner interface.

Control Function Convention: This method uses the scipy/ODE solver convention where control functions have signature (t, x) → u, with time as the FIRST argument. This differs from the high-level simulate() API which uses (x, t) → u with state as the primary argument. The difference is intentional:

  • Low-level integrate(): Uses (t, x) for direct solver compatibility
  • High-level simulate(): Uses (x, t) for intuitive control-theoretic API

If you’re implementing controllers for simulate(), use (x, t) order. If calling integrate() directly, use (t, x) order as shown in the examples below.

Parameters

Name Type Description Default
x0 ArrayLike Initial state (nx,) required
u_func Callable[[float, ArrayLike], ArrayLike] Control policy with low-level convention: (t, x) → u - t: float - current time (FIRST argument, scipy convention) - x: ArrayLike - current state (SECOND argument) - Returns: ArrayLike - control input u Can be: - Constant control: lambda t, x: u_const - State feedback: lambda t, x: -K @ x - Time-varying: lambda t, x: u(t) - Autonomous: lambda t, x: None required
t_span Tuple[float, float] Integration interval (t_start, t_end) required
t_eval Optional[ArrayLike] Specific times at which to store solution If None: - FIXED mode: Uses t = t_start + k*dt for k=0,1,2,… - ADAPTIVE mode: Uses solver’s internal time points None
dense_output bool If True, return dense interpolated solution (adaptive only) False

Returns

Name Type Description
IntegrationResult TypedDict containing: - t: Time points (T,) - x: State trajectory (T, nx) - time-major ordering - success: Whether integration succeeded - message: Status message - nfev: Number of function evaluations - nsteps: Number of steps taken - integration_time: Computation time (seconds) - solver: Integrator name - sol: Dense output object (if dense_output=True)

Raises

Name Type Description
RuntimeError If integration fails (e.g., step size too small, max steps exceeded)

Examples

Low-level integrate() usage (uses (t, x) convention):

Zero control (autonomous):

>>> result = integrator.integrate(
...     x0=np.array([1.0, 0.0]),
...     u_func=lambda t, x: None,  # Autonomous
...     t_span=(0.0, 10.0)
... )
>>> print(f"Final state: {result['x'][-1]}")

Constant control:

>>> result = integrator.integrate(
...     x0=np.array([1.0, 0.0]),
...     u_func=lambda t, x: np.array([0.5]),  # Note: (t, x) order
...     t_span=(0.0, 10.0)
... )

State feedback controller (note time-first order):

>>> K = np.array([[1.0, 2.0]])
>>> result = integrator.integrate(
...     x0=np.array([1.0, 0.0]),
...     u_func=lambda t, x: -K @ x,  # (t, x) order for integrate()
...     t_span=(0.0, 10.0)
... )
>>> print(f"Function evaluations: {result['nfev']}")

Time-varying control:

>>> result = integrator.integrate(
...     x0=np.array([1.0, 0.0]),
...     u_func=lambda t, x: np.array([np.sin(t)]),  # Time-dependent
...     t_span=(0.0, 10.0)
... )

Evaluate at specific times:

>>> t_eval = np.linspace(0, 10, 1001)
>>> result = integrator.integrate(
...     x0=np.array([1.0, 0.0]),
...     u_func=lambda t, x: np.zeros(1),
...     t_span=(0, 10),
...     t_eval=t_eval
... )
>>> assert len(result["t"]) == 1001

High-level simulate() usage (uses (x, t) convention - recommended):

For typical use cases, prefer system.simulate() which uses the more intuitive (x, t) convention:

>>> # Controller with (x, t) order - state is primary
>>> def controller(x, t):  # Note: (x, t) order for simulate()
...     K = np.array([[1.0, 2.0]])
...     return -K @ x
>>>
>>> result = system.simulate(
...     x0=np.array([1.0, 0.0]),
...     controller=controller,  # Uses (x, t) signature
...     t_span=(0.0, 10.0),
...     dt=0.01
... )

Converting between conventions:

If you have a controller designed for simulate() and need to use integrate():

>>> # Controller for simulate() - uses (x, t)
>>> def my_controller(x, t):
...     return -K @ x
>>>
>>> # Wrap for integrate() - convert to (t, x)
>>> result = integrator.integrate(
...     x0=x0,
...     u_func=lambda t, x: my_controller(x, t),  # Swap argument order
...     t_span=(0, 10)
... )

Notes

  • The (t, x) signature matches scipy.integrate.solve_ivp convention
  • This allows direct compatibility with numerical solver libraries
  • The high-level simulate() method handles the conversion automatically
  • Most users should use simulate() instead of calling integrate() directly

See Also

simulate : High-level simulation with (x, t) controller convention (recommended) step : Single integration step

reset_stats

systems.base.numerical_integration.IntegratorBase.reset_stats()

Reset integration statistics to zero.

Examples

>>> integrator.reset_stats()
>>> integrator.get_stats()['total_steps']
0

step

systems.base.numerical_integration.IntegratorBase.step(x, u, dt=None)

Take one integration step: x(t) → x(t + dt).

Parameters

Name Type Description Default
x ArrayLike Current state (nx,) or (batch, nx) required
u ArrayLike Control input (nu,) or (batch, nu) required
dt Optional[float] Step size (uses self.dt if None) None

Returns

Name Type Description
ArrayLike Next state x(t + dt), same shape and type as input

Notes

For fixed-step integrators, dt should match self.dt. For adaptive integrators, dt may be adjusted internally.

Examples

>>> x = np.array([1.0, 0.0])
>>> u = np.array([0.5])
>>> x_next = integrator.step(x, u)
>>> x_next.shape
(2,)