systems.base.core.ContinuousSystemBase
systems.base.core.ContinuousSystemBase()Abstract base class for all continuous-time dynamical systems.
This class defines the fundamental interface that all continuous-time systems must implement. All continuous-time systems satisfy: dx/dt = f(x, u, t)
Subclasses must implement: 1. call(x, u, t): Evaluate dynamics at a point 2. integrate(x0, u, t_span): Low-level numerical integration with solver diagnostics 3. linearize(x_eq, u_eq): Compute linearization
Additional concrete methods provided: - simulate(): High-level simulation with regular time grid (wraps integrate())
Examples
>>> class MyODESystem(ContinuousSystemBase):
... def __call__(self, x, u=None, t=0.0):
... return -x + (u if u is not None else 0.0)
...
... def integrate(self, x0, u, t_span, method="RK45", **kwargs):
... # Use scipy.integrate.solve_ivp or similar
... result = solve_ivp(...)
... return {
... "t": result.t,
... "y": result.y,
... "success": result.success,
... "nfev": result.nfev,
... ...
... }
...
... def linearize(self, x_eq, u_eq):
... A = -np.eye(self.nx)
... B = np.eye(self.nx, self.nu)
... return (A, B)Attributes
| Name | Description |
|---|---|
| analysis | Access system analysis utilities. |
| control | Access control synthesis utilities. |
| control_plotter | Access control system analysis plotting utilities. |
| is_continuous | Return True (this is a continuous-time system). |
| is_discrete | Return False (this is NOT a discrete-time system). |
| is_stochastic | Return True if system has stochastic dynamics. |
| is_time_varying | Return True if system dynamics depend explicitly on time. |
| phase_plotter | Access phase portrait plotting utilities. |
| plotter | Access trajectory plotting utilities. |
Methods
| Name | Description |
|---|---|
| integrate | Low-level numerical integration with ODE solver diagnostics. |
| linearize | Compute linearized dynamics around an equilibrium point. |
| plot | Plot integration result (convenience method). |
| rollout | Rollout system trajectory with optional state-feedback policy. |
| simulate | High-level simulation interface with regular time grid. |
integrate
systems.base.core.ContinuousSystemBase.integrate(
x0,
u=None,
t_span=(0.0, 10.0),
method='RK45',
**integrator_kwargs,
)Low-level numerical integration with ODE solver diagnostics.
Numerically solve the initial value problem: dx/dt = f(x, u, t) x(t0) = x0
API Level: This is a low-level integration method that exposes raw solver output including adaptive time points, convergence information, and performance metrics. For typical use cases, prefer simulate() which provides a cleaner interface with regular time grids and the intuitive (x, t) controller convention.
Control Input Handling: This method accepts flexible control input formats and automatically converts them to the internal (t, x) → u function signature expected by numerical solvers. You can provide: - None for autonomous/zero control - Arrays for constant control - Functions with various signatures (see below)
The conversion to solver convention is handled internally - you don’t need to worry about the (t, x) vs (x, t) distinction at this level.
Parameters
| Name | Type | Description | Default |
|---|---|---|---|
| x0 | StateVector | Initial state (nx,) | required |
| u | Union[ControlVector, Callable[[float], ControlVector], None] | Control input in flexible formats: - None: Zero control or autonomous system - Array (nu,): Constant control u(t) = u_const for all t - Callable u(t): Time-varying control, signature: t → u - Callable u(t, x): State-feedback control (auto-detected) - Callable u(x, t): State-feedback control (auto-detected) The method will automatically detect the function signature and convert to the internal (t, x) → u convention used by solvers. For callables with two parameters, it attempts to detect the order by inspecting parameter names or testing with dummy values. Standard Convention: Functions with two parameters should use (t, x) order to avoid ambiguity. If using (x, t) order, the wrapper will attempt detection but may fail on edge cases - prefer wrapping explicitly: >>> u_func = lambda t, x: my_controller(x, t) | None |
| t_span | tuple[float, float] | Time interval (t_start, t_end) | (0.0, 10.0) |
| method | str | Integration method. Options: | 'RK45' |
| **integrator_kwargs | Additional arguments passed to the ODE solver: - dt : float (required for fixed-step methods) - rtol : float (relative tolerance, default: 1e-6) - atol : float (absolute tolerance, default: 1e-8) - max_steps : int (maximum steps, default: 10000) - t_eval : ArrayLike (specific times to return solution) - dense_output : bool (return interpolant, default: False) - first_step : float (initial step size guess) - max_step : float (maximum step size) | {} |
Returns
| Name | Type | Description |
|---|---|---|
| IntegrationResult | TypedDict containing: - t: Time points (T,) - adaptive, chosen by solver - x or y: State trajectory - scipy returns ‘y’ with shape (nx, T) - other backends return ‘x’ with shape (T, nx) - success: bool - whether integration succeeded - message: str - solver status message - nfev: int - number of function evaluations - nsteps: int - number of steps taken - integration_time: float - computation time (seconds) - solver: str - integrator name used - njev: int - number of Jacobian evaluations (if applicable) - nlu: int - number of LU decompositions (implicit methods) - status: int - termination status code |
Notes
Adaptive Time Points: The time points in the result are chosen adaptively by the solver based on error control, NOT on a regular grid. This means: - Solver takes larger steps when dynamics are smooth - Solver takes smaller steps when dynamics change rapidly - Time points are NOT uniformly spaced
For a regular time grid suitable for plotting or analysis, use simulate() instead, or provide t_eval parameter.
Backend Differences: - scipy: Returns ‘y’ with shape (nx, T) - state-major - torchdiffeq/diffrax: Return ‘x’ with shape (T, nx) - time-major - This method handles both conventions automatically in simulate()
Examples
Basic usage - autonomous system:
>>> x0 = np.array([1.0, 0.0])
>>> result = system.integrate(x0, u=None, t_span=(0, 10))
>>> print(f"Success: {result['success']}")
>>> print(f"Function evaluations: {result['nfev']}")
>>>
>>> # Handle both scipy and other backends
>>> if 'y' in result:
>>> trajectory = result['y'] # scipy: (nx, T)
>>> plt.plot(result['t'], trajectory[0, :])
>>> else:
>>> trajectory = result['x'] # others: (T, nx)
>>> plt.plot(result['t'], trajectory[:, 0])Constant control:
>>> u_const = np.array([0.5])
>>> result = system.integrate(x0, u=u_const, t_span=(0, 10))Time-varying control - single parameter function:
>>> def u_func(t):
... return np.array([np.sin(t)])
>>> result = system.integrate(x0, u=u_func, t_span=(0, 10))State feedback - two parameter function (auto-detected):
>>> def u_func(t, x):
... K = np.array([[1.0, 2.0]])
... return -K @ x
>>> result = system.integrate(x0, u=u_func, t_span=(0, 10))Stiff system with tight tolerances:
>>> result = system.integrate(
... x0,
... u=None,
... t_span=(0, 10),
... method='Radau',
... rtol=1e-8,
... atol=1e-10
... )
>>> print(f"Stiff solver steps: {result['nsteps']}")High-accuracy Julia solver:
>>> result = system.integrate(
... x0,
... u=None,
... t_span=(0, 10),
... method='Vern9', # Julia high-accuracy
... rtol=1e-12,
... atol=1e-14
... )Regular time grid for plotting:
>>> t_eval = np.linspace(0, 10, 1001) # 1001 points
>>> result = system.integrate(
... x0,
... u=None,
... t_span=(0, 10),
... t_eval=t_eval
... )
>>> assert len(result['t']) == 1001Fixed-step integration (RK4):
>>> result = system.integrate(
... x0,
... u=None,
... t_span=(0, 10),
... method='rk4',
... dt=0.01 # Required for fixed-step
... )Check solver performance:
>>> result = system.integrate(x0, u=None, t_span=(0, 10))
>>> if result['nfev'] > 10000:
... print("⚠ Warning: Many function evaluations!")
... print("Consider:")
... print(" - Using stiff solver (Radau, BDF)")
... print(" - Relaxing tolerances")
... print(" - Checking for stiffness")Dense output (interpolation):
>>> result = system.integrate(
... x0,
... u=None,
... t_span=(0, 10),
... dense_output=True
... )
>>> if 'sol' in result:
... # Evaluate at arbitrary times
... t_fine = np.linspace(0, 10, 10000)
... x_fine = result['sol'](t_fine)Comparing backends:
>>> # NumPy (scipy)
>>> result_np = system.integrate(x0, u=None, t_span=(0, 10), method='RK45')
>>>
>>> # Julia (DiffEqPy)
>>> result_jl = system.integrate(x0, u=None, t_span=(0, 10), method='Tsit5')
>>>
>>> # JAX (diffrax)
>>> system.set_default_backend('jax')
>>> result_jax = system.integrate(x0, u=None, t_span=(0, 10), method='tsit5')Error handling:
>>> try:
... result = system.integrate(
... x0,
... u=None,
... t_span=(0, 10),
... method='RK45',
... max_steps=100 # Very low limit
... )
... if not result['success']:
... print(f"Integration failed: {result['message']}")
... except RuntimeError as e:
... print(f"Runtime error: {e}")See Also
simulate : High-level simulation with regular time grid (recommended) IntegratorFactory : Create custom integrators with specific methods linearize : Compute linearized dynamics at equilibrium
linearize
systems.base.core.ContinuousSystemBase.linearize(x_eq, u_eq=None)Compute linearized dynamics around an equilibrium point.
For a continuous-time system dx/dt = f(x, u), compute the linearization: d(δx)/dt = A·δx + B·δu
where: A = ∂f/∂x|(x_eq, u_eq) (State Jacobian, nx × nx) B = ∂f/∂u|(x_eq, u_eq) (Control Jacobian, nx × nu)
Parameters
| Name | Type | Description | Default |
|---|---|---|---|
| x_eq | StateVector | Equilibrium state (nx,) | required |
| u_eq | Optional[ControlVector] | Equilibrium control (nu,) If None, uses zero control | None |
Returns
| Name | Type | Description |
|---|---|---|
| LinearizationResult | Tuple containing Jacobian matrices: - Deterministic systems: (A, B) - Stochastic systems: (A, B, G) where G is diffusion matrix |
Notes
The linearization is valid for small deviations from the equilibrium: δx = x - x_eq δu = u - u_eq
For symbolic systems, Jacobians are computed symbolically then evaluated. For data-driven systems, Jacobians may be computed via finite differences or automatic differentiation.
The equilibrium point should satisfy f(x_eq, u_eq) ≈ 0 (within tolerance).
Examples
Linearize at origin:
>>> x_eq = np.zeros(2)
>>> u_eq = np.zeros(1)
>>> A, B = system.linearize(x_eq, u_eq)
>>> print(f"A matrix:\n{A}")
>>> print(f"B matrix:\n{B}")Check stability (continuous-time):
>>> eigenvalues = np.linalg.eigvals(A)
>>> is_stable = np.all(np.real(eigenvalues) < 0)
>>> print(f"System stable: {is_stable}")Design LQR controller:
>>> from scipy.linalg import solve_continuous_are
>>> P = solve_continuous_are(A, B, Q, R)
>>> K = np.linalg.inv(R) @ B.T @ Pplot
systems.base.core.ContinuousSystemBase.plot(result, state_names=None, **kwargs)Plot integration result (convenience method).
Wrapper around plotter.plot_trajectory() for quick visualization of continuous-time integration results.
Parameters
| Name | Type | Description | Default |
|---|---|---|---|
| result | IntegrationResult | Integration result dictionary with ‘t’ and ‘x’ keys from integrate() or simulate() | required |
| state_names | Optional[list] | Names for state variables (e.g., [‘Position’, ‘Velocity’]) If None, uses generic labels [‘x₁’, ‘x₂’, …] | None |
| **kwargs | Additional arguments passed to plot_trajectory(): - title : str - Plot title - color_scheme : str - Color scheme name - show_legend : bool - Show legend for batched trajectories | {} |
Returns
| Name | Type | Description |
|---|---|---|
| go.Figure | Interactive Plotly figure object |
Examples
>>> # Simple usage
>>> result = system.integrate(x0, u=None, t_span=(0, 10))
>>> fig = system.plot(result)
>>> fig.show()
>>>
>>> # With state names and custom title
>>> fig = system.plot(
... result,
... state_names=['θ', 'ω'],
... title='Pendulum Dynamics'
... )
>>>
>>> # Export to HTML
>>> fig.write_html('simulation.html')
>>>
>>> # Apply publication theme
>>> from controldesymulation.visualization.themes import PlotThemes
>>> fig = system.plot(result)
>>> fig = PlotThemes.apply_theme(fig, theme='publication')
>>> fig.show()
>>>
>>> # Batched trajectories (Monte Carlo for stochastic systems)
>>> results = []
>>> for trial in range(10):
... results.append(system.integrate(x0, u=None, t_span=(0, 10)))
>>> x_batch = np.stack([r['x'] for r in results])
>>> result_batch = {'t': results[0]['t'], 'x': x_batch}
>>> fig = system.plot(result_batch) # Plots all 10 trajectoriesSee Also
plotter.plot_trajectory : Full trajectory plotting method plotter.plot_state_and_control : Plot states and controls together phase_plotter.plot_2d : Phase space visualization control_plotter : Control analysis plots
Notes
This is a convenience wrapper that: - Extracts time and state from result dictionary - Calls plotter.plot_trajectory() with appropriate arguments - Returns Plotly figure for further customization
For more control over plotting, use plotter methods directly.
rollout
systems.base.core.ContinuousSystemBase.rollout(
x0,
policy=None,
t_span=(0.0, 10.0),
dt=0.01,
method='RK45',
**kwargs,
)Rollout system trajectory with optional state-feedback policy.
This is an alias for simulate() that provides API consistency with discrete systems. The name “rollout” is commonly used in reinforcement learning and control theory for executing a policy over time.
Parameters
| Name | Type | Description | Default |
|---|---|---|---|
| x0 | StateVector | Initial state (nx,) | required |
| policy | Optional[Callable[[StateVector, float], ControlVector]] | State-feedback policy u = policy(x, t) STANDARD CONVENTION: State is primary argument, time is secondary If None, uses zero control (open-loop) | None |
| t_span | tuple[float, float] | Simulation time interval (t_start, t_end) | (0.0, 10.0) |
| dt | float | Output time step for regular grid (default: 0.01) | 0.01 |
| method | str | Integration method passed to integrate() | 'RK45' |
| **kwargs | Additional arguments passed to integrate() | {} |
Returns
| Name | Type | Description |
|---|---|---|
| SimulationResult | TypedDict containing: - time: Time points array (T,) with uniform spacing dt - states: State trajectory (T, nx) - TIME-MAJOR ordering - controls: Control trajectory (T, nu) if policy provided - metadata: Additional information with ‘closed_loop’ flag |
Notes
API Consistency: This method provides the same interface as DiscreteSystemBase.rollout(), making it easier to work with both continuous and discrete systems using identical code patterns.
The only difference from simulate() is: - Parameter name: “policy” instead of “controller” (same semantics) - Metadata includes ‘closed_loop’ flag for compatibility - Name emphasizes trajectory generation with state feedback
When to Use: - Use rollout() when emphasizing policy execution (RL/control context) - Use simulate() for general-purpose simulation - Both methods are functionally equivalent for continuous systems
Policy Signature
Policies must have signature (x, t) -> u: - x: StateVector - current state (PRIMARY argument) - t: float - current time (secondary argument) - Returns: ControlVector - control input
Examples
Open-loop rollout:
>>> result = system.rollout(x0, t_span=(0, 10), dt=0.01)
>>> plt.plot(result["time"], result["states"][:, 0])State feedback policy (LQR):
>>> K = np.array([[-1.0, -2.0]]) # LQR gain
>>> def policy(x, t):
... return -K @ x
>>> result = system.rollout(x0, policy, t_span=(0, 10))Time-varying policy with reference:
>>> x_ref_func = lambda t: np.array([np.sin(t), np.cos(t)])
>>> def policy(x, t):
... x_ref = x_ref_func(t)
... K = np.array([[1.0, 0.5]])
... return K @ (x_ref - x)
>>> result = system.rollout(x0, policy, t_span=(0, 10))Nonlinear policy (e.g., neural network):
>>> def neural_policy(x, t):
... # Example: simple nonlinear policy
... hidden = np.tanh(W1 @ x + b1)
... u = W2 @ hidden + b2
... return u
>>> result = system.rollout(x0, neural_policy, t_span=(0, 5))MPC-style receding horizon:
>>> def mpc_policy(x, t):
... # Solve optimization at each step
... u_opt = solve_mpc(x, horizon=10, Q=Q, R=R)
... return u_opt
>>> result = system.rollout(x0, mpc_policy, t_span=(0, 10), dt=0.1)Comparing policies:
>>> policies = {
... "LQR": lqr_policy,
... "MPC": mpc_policy,
... "Neural": neural_policy
... }
>>>
>>> results = {}
>>> for name, policy in policies.items():
... results[name] = system.rollout(x0, policy, t_span=(0, 10))
...
>>> # Plot comparison
>>> for name, result in results.items():
... plt.plot(result["time"], result["states"][:, 0], label=name)
>>> plt.legend()Trajectory optimization context:
>>> # Generate initial trajectory
>>> result = system.rollout(x0, initial_policy, t_span=(0, 5))
>>>
>>> # Extract trajectory for optimization
>>> trajectory = result["states"] # (T, nx)
>>>
>>> # Optimize policy
>>> optimized_policy = optimize_policy(trajectory)
>>>
>>> # Re-rollout with optimized policy
>>> final_result = system.rollout(x0, optimized_policy, t_span=(0, 5))Reinforcement learning context:
>>> # Collect rollout for policy gradient
>>> def stochastic_policy(x, t):
... mu = policy_network(x)
... u = mu + np.random.randn(*mu.shape) * sigma
... return u
>>>
>>> rollouts = []
>>> for episode in range(num_episodes):
... result = system.rollout(x0, stochastic_policy, t_span=(0, 10))
... reward = compute_reward(result["states"], result["controls"])
... rollouts.append((result, reward))Monte Carlo evaluation:
>>> # Evaluate policy robustness
>>> results = []
>>> for _ in range(100):
... x0_perturbed = x0 + np.random.randn(len(x0)) * 0.1
... result = system.rollout(x0_perturbed, policy, t_span=(0, 10))
... results.append(result)
>>>
>>> # Analyze performance distribution
>>> final_errors = [np.linalg.norm(r["states"][-1, :]) for r in results]
>>> print(f"Mean final error: {np.mean(final_errors):.3f}")
>>> print(f"Std final error: {np.std(final_errors):.3f}")Using metadata closed_loop flag:
>>> result = system.rollout(x0, policy, t_span=(0, 10))
>>> if result["metadata"]["closed_loop"]:
... print("Closed-loop rollout with state feedback")
... else:
... print("Open-loop rollout")See Also
simulate : Equivalent method with “controller” parameter name integrate : Low-level integration with solver diagnostics DiscreteSystemBase.rollout : Discrete-time analog
simulate
systems.base.core.ContinuousSystemBase.simulate(
x0,
controller=None,
t_span=(0.0, 10.0),
dt=0.01,
method='RK45',
**kwargs,
)High-level simulation interface with regular time grid.
This method wraps integrate() and post-processes the result to provide a regular time grid and cleaner output. This is the recommended method for most use cases (currently, may be deprecated once DiscretizedSystem is developed).
Parameters
| Name | Type | Description | Default |
|---|---|---|---|
| x0 | StateVector | Initial state (nx,) | required |
| controller | Optional[Callable[[StateVector, float], ControlVector]] | Feedback controller u = controller(x, t) STANDARD CONVENTION: State is primary argument, time is secondary This aligns with discrete systems’ policy(x, k) signature If None, uses zero control (open-loop) | None |
| t_span | tuple[float, float] | Simulation time interval (t_start, t_end) | (0.0, 10.0) |
| dt | float | Output time step for regular grid (default: 0.01) | 0.01 |
| method | str | Integration method passed to integrate() | 'RK45' |
| **kwargs | Additional arguments passed to integrate() | {} |
Returns
| Name | Type | Description |
|---|---|---|
| SimulationResult | TypedDict containing: - time: Time points array (T,) with uniform spacing dt - states: State trajectory (T, nx) - TIME-MAJOR ordering - controls: Control trajectory (T, nu) if controller provided - metadata: Additional information (method, dt, success, nfev) |
Notes
Time-Major Convention: This method returns states in (T, nx) shape, which is the modern standard for time series data: - Compatible with pandas DataFrames - Natural for time-series analysis - Consistent with ML/data science conventions - Matches discrete systems’ output format
Unlike integrate(), this method: - Returns states on a regular time grid (not adaptive) - Supports state-feedback controllers - Hides solver diagnostics (cleaner output) - Is easier to use for plotting and analysis
Controller Signature
Controllers must have signature (x, t) -> u: - x: StateVector - current state (PRIMARY argument) - t: float - current time (secondary argument) - Returns: ControlVector - control input
This matches the discrete systems’ policy(x, k) convention where state is the primary argument. An internal adapter converts to scipy’s (t, x) convention when calling integrate().
Examples
Open-loop simulation:
>>> result = system.simulate(x0, t_span=(0, 5), dt=0.01)
>>> # Time-major indexing
>>> plt.plot(result["time"], result["states"][:, 0]) # First state
>>> plt.plot(result["time"], result["states"][:, 1]) # Second state
>>> plt.xlabel("Time (s)")
>>> plt.ylabel("State")Using with pandas (natural with time-major):
>>> import pandas as pd
>>> result = system.simulate(x0, controller, t_span=(0, 10))
>>> df = pd.DataFrame(
... result["states"],
... index=result["time"],
... columns=[f"x{i}" for i in range(system.nx)]
... )
>>> df.plot()Closed-loop with state feedback:
>>> K = np.array([[-1.0, -2.0]]) # LQR gain
>>> def controller(x, t):
... return -K @ x
>>> result = system.simulate(x0, controller, t_span=(0, 5))
>>>
>>> # Extract trajectory
>>> t = result["time"]
>>> x = result["states"] # (T, nx)
>>> u = result["controls"] # (T, nu)Time-varying reference tracking:
>>> def controller(x, t):
... x_ref = np.array([np.sin(t), np.cos(t)])
... K = np.array([[1.0, 0.5]])
... return K @ (x_ref - x)
>>> result = system.simulate(x0, controller, t_span=(0, 10))Adaptive gain controller:
>>> def controller(x, t):
... K = 1.0 + 0.1 * t # Gain increases with time
... return np.array([-K * x[0]])
>>> result = system.simulate(x0, controller, t_span=(0, 10))Saturated control:
>>> def controller(x, t):
... u_raw = -2.0 * x[0] - 0.5 * x[1]
... return np.array([np.clip(u_raw, -1.0, 1.0)])
>>> result = system.simulate(x0, controller, t_span=(0, 10))Time-only control (uncommon, but supported):
>>> def controller(x, t):
... return np.array([np.sin(2 * np.pi * t)])
>>> result = system.simulate(x0, controller, t_span=(0, 10))Batch plotting multiple states:
>>> result = system.simulate(x0, controller, t_span=(0, 10))
>>> fig, axes = plt.subplots(system.nx, 1, sharex=True)
>>> for i, ax in enumerate(axes):
... ax.plot(result["time"], result["states"][:, i])
... ax.set_ylabel(f"$x_{i}$")
>>> axes[-1].set_xlabel("Time (s)")Phase portrait (using time-major indexing):
>>> result = system.simulate(x0, controller, t_span=(0, 10))
>>> plt.plot(result["states"][:, 0], result["states"][:, 1])
>>> plt.xlabel("$x_0$")
>>> plt.ylabel("$x_1$")See Also
integrate : Low-level integration with solver diagnostics rollout : Alternative name for simulation