systems.base.core.DiscreteSystemBase

systems.base.core.DiscreteSystemBase()

Abstract base class for all discrete-time dynamical systems.

All discrete-time systems satisfy: x[k+1] = f(x[k], u[k], k)

Subclasses must implement: 1. dt (property): Sampling period 2. step(x, u, k): Single time step update 3. simulate(x0, u_sequence, n_steps): Multi-step simulation 4. linearize(x_eq, u_eq): Compute linearization

Additional concrete methods provided: - rollout(): Closed-loop simulation with state-feedback policy

Examples

>>> class MyDiscreteSystem(DiscreteSystemBase):
...     def __init__(self, dt=0.1):
...         self._dt = dt
...         self.nx = 2
...         self.nu = 1
...
...     @property
...     def dt(self):
...         return self._dt
...
...     def step(self, x, u=None, k=0):
...         u = u if u is not None else np.zeros(self.nu)
...         return 0.9 * x + 0.1 * u
...
...     def simulate(self, x0, u_sequence, n_steps):
...         # Implement multi-step simulation
...         ...
...
...     def linearize(self, x_eq, u_eq):
...         Ad = 0.9 * np.eye(self.nx)
...         Bd = 0.1 * np.eye(self.nx, self.nu)
...         return (Ad, Bd)

Attributes

Name Description
analysis Access system analysis utilities.
control Access control synthesis utilities.
control_plotter Access control system analysis plotting utilities.
dt Sampling period / time step of the discrete system.
is_continuous Return False (this is NOT a continuous-time system).
is_discrete Return True (this is 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 step k.
phase_plotter Access phase portrait plotting utilities.
plotter Access trajectory plotting utilities.
sampling_frequency Get sampling frequency in Hz.

Methods

Name Description
linearize Compute linearized discrete dynamics around an equilibrium point.
plot Plot simulation result (convenience method).
rollout Rollout system trajectory with optional state-feedback policy.
simulate Simulate system for multiple discrete time steps.
step Compute next state: x[k+1] = f(x[k], u[k], k).

linearize

systems.base.core.DiscreteSystemBase.linearize(x_eq, u_eq=None)

Compute linearized discrete dynamics around an equilibrium point.

For a discrete system x[k+1] = f(x[k], u[k]), compute the linearization: δx[k+1] = Ad·δx[k] + Bd·δu[k]

where: Ad = ∂f/∂x|(x_eq, u_eq) (State Jacobian, nx × nx) Bd = ∂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
DiscreteLinearization Tuple containing Jacobian matrices: - Deterministic systems: (Ad, Bd) - Stochastic systems: (Ad, Bd, Gd) where Gd is diffusion matrix

Notes

The linearization is valid for small deviations from the equilibrium: δx[k] = x[k] - x_eq δu[k] = u[k] - u_eq

For symbolic systems, Jacobians are computed symbolically then evaluated. For data-driven systems, Jacobians may be computed via finite differences.

The equilibrium point should satisfy f(x_eq, u_eq) = x_eq (fixed point).

Stability analysis for discrete systems: - Stable if all |eigenvalues(Ad)| < 1 - Unstable if any |eigenvalue(Ad)| > 1 - Marginal if |eigenvalue(Ad)| = 1

Examples

Linearize at origin:

>>> x_eq = np.zeros(2)
>>> u_eq = np.zeros(1)
>>> Ad, Bd = system.linearize(x_eq, u_eq)
>>> print(f"Ad matrix:\n{Ad}")
>>> print(f"Bd matrix:\n{Bd}")

Check discrete stability:

>>> eigenvalues = np.linalg.eigvals(Ad)
>>> is_stable = np.all(np.abs(eigenvalues) < 1)
>>> print(f"System stable: {is_stable}")

Design discrete LQR controller:

>>> from scipy.linalg import solve_discrete_are
>>> P = solve_discrete_are(Ad, Bd, Q, R)
>>> K = np.linalg.inv(R + Bd.T @ P @ Bd) @ (Bd.T @ P @ Ad)

Relationship to continuous linearization:

>>> # For Euler discretization: Ad ≈ I + dt * A
>>> dt = system.dt
>>> A_approx = (Ad - np.eye(system.nx)) / dt

plot

systems.base.core.DiscreteSystemBase.plot(result, state_names=None, **kwargs)

Plot simulation result (convenience method).

Wrapper around plotter.plot_trajectory() for quick visualization of discrete-time simulation results.

Parameters

Name Type Description Default
result DiscreteSimulationResult Simulation result dictionary with ‘t’ and ‘x’ keys from simulate() or rollout() 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.simulate(x0, u_sequence, n_steps=100)
>>> fig = system.plot(result)
>>> fig.show()
>>>
>>> # With state names and custom title
>>> fig = system.plot(
...     result,
...     state_names=['θ', 'ω'],
...     title='Discrete 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)
>>> results = []
>>> for x0 in initial_conditions:
...     results.append(system.simulate(x0, u_sequence, n_steps=100))
>>> 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 trajectories

See 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.DiscreteSystemBase.rollout(
    x0,
    policy=None,
    n_steps=100,
    **kwargs,
)

Rollout system trajectory with optional state-feedback policy.

This is a higher-level alternative to simulate() that provides a cleaner interface for closed-loop simulation with state-dependent policies.

Parameters

Name Type Description Default
x0 StateVector Initial state (nx,) required
policy Optional[Callable[[StateVector, int], ControlVector]] Control policy u = policy(x, k) If None, uses zero control (open-loop) None
n_steps int Number of simulation steps 100
**kwargs Additional arguments (stored in metadata) {}

Returns

Name Type Description
DiscreteSimulationResult TypedDict (returns as dict) containing trajectory and metadata - states: (n_steps+1, nx) - TIME-MAJOR - controls: (n_steps, nu) - TIME-MAJOR - time_steps: (n_steps+1,) - dt: float - metadata: dict with closed_loop flag

Examples

Open-loop rollout:

>>> result = system.rollout(x0, n_steps=100)

State feedback policy (LQR):

>>> K = np.array([[-1.0, -2.0]])  # LQR gain
>>> def policy(x, k):
...     return -K @ x
>>> result = system.rollout(x0, policy, n_steps=100)

Time-varying policy with reference:

>>> x_ref_trajectory = generate_reference()
>>> def policy(x, k):
...     x_ref = x_ref_trajectory[k]
...     return K @ (x_ref - x)
>>> result = system.rollout(x0, policy, n_steps=100)

MPC policy:

>>> mpc_controller = system.control.mpc(horizon=10, Q=Q, R=R)
>>> def policy(x, k):
...     return mpc_controller.compute_control(x, k)
>>> result = system.rollout(x0, policy, n_steps=100)

simulate

systems.base.core.DiscreteSystemBase.simulate(
    x0,
    u_sequence=None,
    n_steps=100,
    **kwargs,
)

Simulate system for multiple discrete time steps.

Run the discrete dynamics forward in time: x[0] = x0 x[k+1] = f(x[k], u[k], k) for k = 0, 1, …, n_steps-1

Parameters

Name Type Description Default
x0 StateVector Initial state (nx,) required
u_sequence Optional[Union[ControlVector, Sequence, Callable]] Control input sequence, can be: - None: Zero control for all steps - Array (nu,): Constant control u[k] = u for all k - Sequence: Pre-computed sequence u[0], u[1], …, u[n_steps-1] - Callable: Control policy u[k] = u_func(k) None
n_steps int Number of simulation steps (default: 100) 100
**kwargs Additional simulation options (e.g., save_intermediate) {}

Returns

Name Type Description
DiscreteSimulationResult TypedDict (returns as dict) containing: - states: State trajectory (nx, n_steps+1) - includes x[0] - controls: Control sequence (nu, n_steps) if applicable - time_steps: Time step indices [0, 1, …, n_steps] - dt: Sampling period - metadata: Additional info (method, success, etc.)

Notes

The state trajectory includes n_steps+1 points (including x0). The control sequence has n_steps points (one for each transition).

For closed-loop simulation with state-dependent control, you can use rollout() instead, which provides a cleaner interface for state feedback.

Examples

Open-loop with constant control:

>>> x0 = np.array([1.0, 0.0])
>>> u = np.array([0.5])
>>> result = system.simulate(x0, u, n_steps=100)
>>> plt.step(result["time_steps"], result["states"][0, :])

Pre-computed control sequence:

>>> u_seq = [np.array([0.5 * np.sin(k * 0.1)]) for k in range(100)]
>>> result = system.simulate(x0, u_seq, n_steps=100)

Time-indexed control function:

>>> def u_func(k):
...     return np.array([0.5 * np.sin(k * system.dt)])
>>> result = system.simulate(x0, u_func, n_steps=100)

Autonomous system (no control):

>>> result = system.simulate(x0, u_sequence=None, n_steps=100)

step

systems.base.core.DiscreteSystemBase.step(x, u=None, k=0)

Compute next state: x[k+1] = f(x[k], u[k], k).

This is the core state update method. It computes the next state given the current state and control input.

Parameters

Name Type Description Default
x StateVector Current state vector (nx,) or (nx, n_batch) required
u Optional[ControlVector] Control input vector (nu,) or (nu, n_batch) If None, assumes zero control or autonomous dynamics None
k int Current discrete time step (default: 0) Used for time-varying systems 0

Returns

Name Type Description
StateVector Next state x[k+1] with same shape as x

Notes

  • For autonomous systems, k is ignored
  • For time-invariant systems, k is typically ignored
  • For batch evaluation, x and u should have shape (n_dim, n_batch)
  • The returned state should be in the same backend as the input

Examples

Single step update:

>>> x = np.array([1.0, 2.0])
>>> u = np.array([0.5])
>>> x_next = system.step(x, u)

Batch evaluation:

>>> x_batch = np.random.randn(2, 100)  # 100 states
>>> u_batch = np.random.randn(1, 100)  # 100 controls
>>> x_next_batch = system.step(x_batch, u_batch)

Manual simulation loop:

>>> x = x0
>>> for k in range(100):
...     u = controller(x, k)
...     x = system.step(x, u, k)
...     # Log or visualize x