---
title: "UI Framework Architecture"
subtitle: "User-Facing System Classes and Symbolic System Definition"
author: "Gil Benezer"
date: today
format:
html:
toc: true
toc-depth: 5
code-fold: show
code-tools: true
theme: cosmo
execute:
eval: true
cache: true
warning: false
---
## Overview {#sec-overview}
The **UI (User Interface) Framework** is Layer 3 of the architecture, consisting of **8 core files** organized into a **4-layer hierarchy** that provides the user-facing API for defining and working with dynamical systems. This layer eliminates approximately **1,800 lines of code duplication** while maintaining clean separation of concerns.
::: {.callout-note}
## Framework Components
**Layer 0:** `SymbolicSystemBase` - Time-domain agnostic foundation
**Layer 1:** `ContinuousSystemBase`, `DiscreteSystemBase` - Time-domain interfaces
**Layer 2:** `ContinuousSymbolicSystem`, `DiscreteSymbolicSystem` - Concrete implementations
**Layer 3:** `ContinuousStochasticSystem`, `DiscreteStochasticSystem` - Stochastic extensions
**Special:** `DiscretizedSystem` - Continuous → discrete conversion
:::
::: {.callout-important}
## User Interaction Model
**Most users interact with the UI framework at two levels:**
```python
# Level 1: Use built-in systems (simplest)
from cdesym import Pendulum, CartPole, VanDerPol
system = Pendulum()
result = system.simulate(x0, controller=None, t_span=(0, 10))
# Level 2: Define custom systems (subclass framework)
from cdesym import ContinuousSymbolicSystem
class MySystem(ContinuousSymbolicSystem):
def define_system(self, **params):
# Define symbolic system
self.state_vars = [...]
self._f_sym = sp.Matrix([...])
```
**Users should NOT:**
- Directly instantiate internal utilities (BackendManager, CodeGenerator, etc.)
- Access private attributes (those starting with `_`)
- Override methods other than `define_system()` and `print_equations()`
The framework automatically composes internal components and exposes functionality through clean public methods. This documentation describes the internal architecture for framework developers.
:::
```{python}
#| label: setup-imports
#| echo: false
#| output: false
# Global setup for all code examples
import numpy as np
import torch
import jax.numpy as jnp
import sympy as sp
from typing import Optional, Tuple, Callable
import warnings
warnings.filterwarnings('ignore', category=UserWarning)
# Import systems for examples
from cdesym import Pendulum, ContinuousSymbolicSystem, DiscreteSymbolicSystem
from cdesym import ContinuousStochasticSystem
```
## Architecture Philosophy {#sec-architecture-philosophy}
**Layered Composition with Strategic Inheritance** - The UI framework achieves:
1. **Zero Code Duplication** - Shared functionality lives in exactly one place
2. **Clean Separation** - Each layer has a single, focused responsibility
3. **Progressive Complexity** - Simple systems inherit from basic layers
4. **Type Safety** - Comprehensive type annotations throughout
5. **Extensibility** - Clear extension points via `define_system()`
6. **Backend Agnosticism** - Multi-backend support transparent to users
The architecture uses **cooperative multiple inheritance** strategically at Layer 2 to combine symbolic machinery with time-domain interfaces, avoiding the duplication that would occur with composition alone.
## Architecture Layers {#sec-architecture-layers}
```
┌─────────────────────────────────────────────────────────────┐
│ Layer 0: SymbolicSystemBase (time-domain agnostic) │
│ • Symbolic variables and parameters │
│ • Code generation and backend management │
│ • Equilibrium handling and configuration │
└────────────────────┬────────────────────────────────────────┘
│
┌──────────┴──────────┐
│ │
┌─────────▼────────┐ ┌─────────▼─────────┐
│ Layer 1: │ │ Layer 1: │
│ Continuous │ │ Discrete │
│ SystemBase │ │ SystemBase │
│ • dx/dt = f │ │ • x[k+1] = f │
│ • integrate() │ │ • step() │
│ • linearize() │ │ • simulate() │
└─────────┬────────┘ └─────────┬─────────┘
│ │
┌─────┴─────┐ ┌─────┴─────┐
│ │ │ │
┌───▼────┐ ┌───▼────┐ ┌─▼──────┐ ┌──▼──────┐
│Layer 2:│ │Layer 3:│ │Layer 2:│ │Layer 3: │
│Cont. │ │Cont. │ │Disc. │ │Disc. │
│Symbolic│ │Stoch. │ │Symbolic│ │Stoch. │
│System │ │System │ │System │ │System │
└────────┘ └────────┘ └────────┘ └─────────┘
```
## Layer 0: Foundation {#sec-layer-0}
### SymbolicSystemBase: Time-Agnostic Foundation {#sec-symbolicsystembase}
**File:** `symbolic_system_base.py`
The `SymbolicSystemBase` provides all symbolic manipulation and backend management functionality, making **no assumptions about continuous vs discrete time**.
**Core responsibilities:**
- **Symbolic variable management** - State, control, and output variables
- **Parameter handling** - Symbolic parameters with numeric values
- **Code generation** - Symbolic → numerical via CodeGenerator
- **Backend management** - NumPy/PyTorch/JAX switching via BackendManager
- **Equilibrium management** - Named equilibria via EquilibriumHandler
- **Configuration persistence** - Save/load system definitions
- **Validation** - Symbolic system validation via SymbolicValidator
**Key design patterns:**
- **Composition** - Delegates to specialized utilities
- **Template Method** - `__init__` orchestrates: define → validate → initialize
- **Abstract Methods** - Forces subclasses to implement `define_system()`
**What Layer 0 does NOT provide:**
- Forward dynamics evaluation (`__call__`, `step`)
- Time integration (`integrate`, `simulate`)
- Linearization computation
- These are time-domain specific and live in Layer 1
**Internal composition (users never access these directly):**
```{python}
#| label: ex-layer0-composition
system = Pendulum()
# Internal utilities (composed automatically)
backend_mgr = system.backend # BackendManager
code_gen = system._code_gen # CodeGenerator
equilibria = system.equilibria # EquilibriumHandler
validator = system._validator # SymbolicValidator
print("✓ Layer 0 composes internal utilities transparently")
```
**User-facing Layer 0 methods:**
```{python}
#| label: ex-layer0-methods
# Backend management
system.set_default_backend('numpy')
system.to_device('cpu')
# Equilibrium management
system.add_equilibrium('origin', x_eq=np.zeros(2), u_eq=np.zeros(1))
x_eq, u_eq = system.get_equilibrium('origin')
equilibria_list = system.list_equilibria()
# Configuration
# config = system.save_config('pendulum_config.json')
# system_loaded = Pendulum.load_config('pendulum_config.json')
print(f"System order: {system.order}")
print(f"State dimension: {system.nx}")
print(f"Control dimension: {system.nu}")
```
**Abstract methods subclasses must implement:**
```python
@abstractmethod
def define_system(self, **params):
"""Define symbolic system components.
Must set:
- self.state_vars: List[sp.Symbol]
- self.control_vars: List[sp.Symbol]
- self._f_sym: sp.Matrix (dynamics)
- self.parameters: Dict[sp.Symbol, float]
- self.order: int
"""
pass
@abstractmethod
def print_equations(self, simplify: bool = True):
"""Print system equations in readable form."""
pass
```
## Layer 1: Time-Domain Interfaces {#sec-layer-1}
### ContinuousSystemBase: Continuous-Time Interface {#sec-continuoussystembase}
**File:** `continuous_system_base.py`
The `ContinuousSystemBase` defines the abstract interface for continuous-time systems described by ODEs.
**Mathematical form:**
$$\frac{dx}{dt} = f(x, u, t)$$
**Core responsibilities:**
- **Dynamics evaluation** - Abstract `__call__(x, u)` for forward dynamics
- **Numerical integration** - Abstract `integrate()` with multi-method support
- **Linearization** - Abstract `linearize()` for Jacobian computation
- **High-level simulation** - Concrete `simulate()` with controller support
**Abstract methods (Layer 2 implements):**
```python
@abstractmethod
def __call__(
self,
x: StateVector,
u: Optional[ControlVector] = None
) -> StateVector:
"""Evaluate dx/dt = f(x, u)."""
pass
@abstractmethod
def integrate(
self,
x0: StateVector,
u: Optional[Union[ControlVector, Callable]] = None,
t_span: TimeSpan = (0, 10),
**kwargs
) -> IntegrationResult:
"""Integrate system over time span."""
pass
@abstractmethod
def linearize(
self,
x_eq: EquilibriumState,
u_eq: EquilibriumControl
) -> Tuple[StateMatrix, InputMatrix]:
"""Compute A = ∂f/∂x, B = ∂f/∂u."""
pass
```
**Concrete method (provided by Layer 1):**
```python
def simulate(
self,
x0: StateVector,
controller: Optional[Callable[[float, StateVector], ControlVector]] = None,
t_span: TimeSpan = (0, 10),
dt: float = 0.01,
**kwargs
) -> Dict[str, np.ndarray]:
"""High-level simulation with regular time grid.
Returns
-------
dict with keys:
'time': Regular time points (T,)
'states': State trajectory (T, nx)
'controls': Control sequence (T, nu)
"""
```
**Usage example:**
```{python}
#| label: ex-continuous-system
system = Pendulum()
# Evaluate dynamics (Layer 1 interface)
x = np.array([0.5, 0.0])
u = np.array([0.0])
dx = system(x, u)
print(f"State derivative: {dx}")
# Integrate (adaptive time grid)
result = system.integrate(x, u=None, t_span=(0, 5), method='RK45')
print(f"Integration steps: {result['nsteps']}")
# Simulate (regular time grid - better for plotting)
sim_result = system.simulate(x, controller=None, t_span=(0, 5), dt=0.01)
print(f"Simulation time points: {len(sim_result['time'])}")
# Linearize
x_eq, u_eq = system.get_equilibrium('origin')
A, B = system.linearize(x_eq, u_eq)
print(f"A matrix: {A.shape}")
```
**Key features:**
- Flexible control input (None, arrays, or callable controllers)
- Multi-backend integration support (scipy, Julia, PyTorch, JAX)
- Dense output and adaptive stepping available
- Comprehensive solver diagnostics
### DiscreteSystemBase: Discrete-Time Interface {#sec-discretesystembase}
**File:** `discrete_system_base.py`
The `DiscreteSystemBase` defines the abstract interface for discrete-time systems.
**Mathematical form:**
$$x[k+1] = f(x[k], u[k], k)$$
**Core responsibilities:**
- **Single step** - Abstract `step(x, u)` for one time step
- **Multi-step simulation** - Abstract `simulate()` for trajectories
- **Linearization** - Abstract `linearize()` for discrete Jacobians
- **Sampling properties** - Concrete `dt` property and `sampling_frequency`
**Abstract property (must implement):**
```python
@property
@abstractmethod
def dt(self) -> float:
"""Sampling period [seconds]."""
pass
```
**Abstract methods (Layer 2 implements):**
```python
@abstractmethod
def step(
self,
x: StateVector,
u: Optional[ControlVector] = None,
k: int = 0
) -> StateVector:
"""Single time step: x[k] → x[k+1]."""
pass
@abstractmethod
def simulate(
self,
x0: StateVector,
u_sequence: Optional[ControlSequence] = None,
n_steps: int = 100,
**kwargs
) -> Dict[str, np.ndarray]:
"""Multi-step simulation."""
pass
@abstractmethod
def linearize(
self,
x_eq: EquilibriumState,
u_eq: EquilibriumControl
) -> Tuple[StateMatrix, InputMatrix]:
"""Compute Ad = ∂f/∂x, Bd = ∂f/∂u (discrete Jacobians)."""
pass
```
**Concrete properties (provided by Layer 1):**
```python
@property
def sampling_frequency(self) -> float:
"""Sampling frequency [Hz]."""
return 1.0 / self.dt
```
**Key conventions:**
- **Time-major ordering** - Arrays are (n_steps, nx), not (nx, n_steps)
- **Control sequences** - Pre-computed controls or None for autonomous systems
- **State feedback** - `rollout()` method for policy-based simulation
## Layer 2: Concrete Symbolic Implementations {#sec-layer-2}
### ContinuousSymbolicSystem: Symbolic + Continuous {#sec-continuoussymbolicsystem}
**File:** `continuous_symbolic_system.py`
**Inheritance:** `SymbolicSystemBase + ContinuousSystemBase` (cooperative multiple inheritance)
The `ContinuousSymbolicSystem` combines symbolic machinery (Layer 0) with continuous-time execution (Layer 1).
**Key internal components (automatically composed):**
- **DynamicsEvaluator** - Evaluates dx/dt = f(x, u)
- **LinearizationEngine** - Computes A = ∂f/∂x, B = ∂f/∂u
- **ObservationEngine** - Evaluates y = h(x), C = ∂h/∂x
- **IntegratorFactory** - Creates appropriate ODE solvers
**Implemented methods (Layer 1 abstractions):**
```python
def __call__(
self,
x: StateVector,
u: Optional[ControlVector] = None
) -> StateVector:
"""Evaluate dx/dt = f(x, u) using DynamicsEvaluator."""
return self._dynamics.evaluate(x, u, backend=self.backend.default_backend)
def integrate(
self,
x0: StateVector,
u: Optional[Union[ControlVector, Callable]] = None,
t_span: TimeSpan = (0, 10),
method: Optional[str] = None,
**kwargs
) -> IntegrationResult:
"""Integrate using IntegratorFactory."""
# Creates appropriate integrator and delegates
def linearize(
self,
x_eq: EquilibriumState,
u_eq: EquilibriumControl
) -> Tuple[StateMatrix, InputMatrix]:
"""Compute Jacobians using LinearizationEngine."""
return self._linearization.linearize_continuous(x_eq, u_eq)
```
**User subclass pattern:**
```{python}
#| label: ex-continuous-symbolic
class SpringMassDamper(ContinuousSymbolicSystem):
def define_system(self, m=1.0, k=10.0, c=0.5):
"""Define mass-spring-damper system."""
# Symbolic variables
x, v = sp.symbols('x v', real=True)
u = sp.symbols('u', real=True)
m_sym, k_sym, c_sym = sp.symbols('m k c', positive=True)
# System definition
self.state_vars = [x, v]
self.control_vars = [u]
self._f_sym = sp.Matrix([
v,
(-k_sym*x - c_sym*v + u)/m_sym
])
self.parameters = {m_sym: m, k_sym: k, c_sym: c}
self.order = 1
def print_equations(self, simplify=True):
print("Mass-Spring-Damper Equations:")
print(" dx/dt = v")
print(" dv/dt = (-k*x - c*v + u)/m")
# Use the system
system = SpringMassDamper(m=2.0, k=5.0)
x0 = np.array([1.0, 0.0])
result = system.simulate(x0, controller=None, t_span=(0, 10))
print(f"✓ Custom continuous symbolic system")
```
### DiscreteSymbolicSystem: Symbolic + Discrete {#sec-discretesymbolicsystem}
**File:** `discrete_symbolic_system.py`
**Inheritance:** `SymbolicSystemBase + DiscreteSystemBase` (cooperative multiple inheritance)
The `DiscreteSymbolicSystem` combines symbolic machinery with discrete-time execution.
**Key internal components:**
- **DynamicsEvaluator** - Evaluates x[k+1] = f(x[k], u[k])
- **LinearizationEngine** - Computes Ad = ∂f/∂x, Bd = ∂f/∂u
- **ObservationEngine** - Evaluates y[k] = h(x[k])
**Critical requirement:**
::: {.callout-important}
## Required: Set `self._dt`
Discrete systems **must** set `self._dt` in `define_system()`. This is the sampling period that defines the discrete-time grid.
:::
**User subclass pattern:**
```{python}
#| label: ex-discrete-symbolic
class DiscreteLinearSystem(DiscreteSymbolicSystem):
def define_system(self, a=0.9, b=0.1, dt=0.01):
"""Define discrete linear system."""
# Symbolic variables
x = sp.symbols('x', real=True)
u = sp.symbols('u', real=True)
a_sym, b_sym = sp.symbols('a b', real=True)
# System definition
self.state_vars = [x]
self.control_vars = [u]
self._f_sym = sp.Matrix([a_sym*x + b_sym*u])
self.parameters = {a_sym: a, b_sym: b}
self._dt = dt # REQUIRED for discrete systems!
self.order = 1
def print_equations(self, simplify=True):
print("Discrete Linear System:")
print(f" x[k+1] = a*x[k] + b*u[k]")
print(f" dt = {self._dt}s")
# Use the system
system = DiscreteLinearSystem(a=0.95, dt=0.1)
x0 = np.array([1.0])
result = system.simulate(x0, u_sequence=None, n_steps=50)
print(f"✓ Custom discrete symbolic system")
```
## Layer 3: Stochastic Extensions {#sec-layer-3}
### ContinuousStochasticSystem: SDE Support {#sec-continuousstochasticsystem}
**File:** `continuous_stochastic_system.py`
**Inheritance:** `ContinuousSymbolicSystem` (single inheritance - extends deterministic)
The `ContinuousStochasticSystem` adds stochastic differential equation (SDE) support to continuous systems.
**Mathematical form:**
$$dx = f(x, u, t)dt + g(x, u, t)dW$$
where:
- $f(x, u, t)$ - Drift (inherited from parent)
- $g(x, u, t)$ - Diffusion matrix (added here)
- $dW$ - Brownian motion increments
**Additional internal components:**
- **DiffusionHandler** - Generates and caches diffusion functions
- **NoiseCharacterizer** - Automatic noise structure analysis
- **SDEValidator** - SDE-specific validation
- **SDEIntegratorFactory** - Stochastic integration methods
**Noise structure types (auto-detected):**
| Type | Structure | Meaning |
|------|-----------|---------|
| ADDITIVE | $g(x,u,t) = \text{constant}$ | Noise intensity independent of state |
| MULTIPLICATIVE | $g$ depends on state | State-dependent noise |
| DIAGONAL | $g$ is diagonal matrix | Independent noise channels |
| SCALAR | Single Wiener process | $n_w = 1$ |
| GENERAL | Full matrix coupling | Correlated noise |
**Additional user-facing methods:**
```python
def drift(self, x: StateVector, u: ControlVector) -> StateVector:
"""Evaluate drift term f(x, u)."""
def diffusion(self, x: StateVector, u: ControlVector) -> DiffusionMatrix:
"""Evaluate diffusion term g(x, u)."""
def is_additive_noise(self) -> bool:
"""Check if noise is additive (state-independent)."""
def recommend_solvers(self, backend: Backend) -> List[str]:
"""Recommend integration methods based on noise structure."""
```
**User subclass pattern:**
```{python}
#| label: ex-stochastic-system
class OrnsteinUhlenbeck(ContinuousStochasticSystem):
def define_system(self, alpha=1.0, sigma=0.5):
"""Define Ornstein-Uhlenbeck process: dx = -α*x*dt + σ*dW"""
# Symbolic variables
x = sp.symbols('x', real=True)
u = sp.symbols('u', real=True)
alpha_sym, sigma_sym = sp.symbols('alpha sigma', positive=True)
# Drift (deterministic part)
self.state_vars = [x]
self.control_vars = [u]
self._f_sym = sp.Matrix([[-alpha_sym * x + u]])
self.parameters = {alpha_sym: alpha, sigma_sym: sigma}
self.order = 1
# Diffusion (stochastic part)
self.diffusion_expr = sp.Matrix([[sigma_sym]])
self.sde_type = 'ito' # or 'stratonovich'
def print_equations(self, simplify=True):
print("Ornstein-Uhlenbeck Process:")
print(" dx = -α*x*dt + σ*dW")
# Use the stochastic system
system = OrnsteinUhlenbeck(alpha=2.0, sigma=0.3)
print(f"Additive noise: {system.is_additive_noise()}")
print(f"Noise dimension: {system.nw}")
# Integrate SDE
x0 = np.array([1.0])
result = system.integrate(
x0, u=None, t_span=(0, 10),
method='EM', # Euler-Maruyama for SDEs
dt=0.01
)
print(f"✓ Stochastic system integration")
```
### DiscreteStochasticSystem: Discrete SDE Support {#sec-discretestochasticsystem}
**File:** `discrete_stochastic_system.py`
**Inheritance:** `DiscreteSymbolicSystem` (single inheritance)
The `DiscreteStochasticSystem` adds stochastic difference equation support.
**Mathematical form:**
$$x[k+1] = f(x[k], u[k]) + g(x[k], u[k]) \cdot w[k]$$
where:
- $f(x[k], u[k])$ - Deterministic dynamics
- $g(x[k], u[k])$ - Diffusion matrix
- $w[k]$ - Discrete-time noise
Similar structure to continuous stochastic but for discrete-time systems.
## Special System: DiscretizedSystem {#sec-special-system}
### DiscretizedSystem: Continuous → Discrete Conversion {#sec-discretizedsystem}
**File:** `discretized_system.py`
The `DiscretizedSystem` creates discrete-time approximations from continuous systems.
**Discretization methods:**
| Method | Order | Description |
|--------|-------|-------------|
| `exact` | N/A | Matrix exponential (linear systems only) |
| `euler` | 1 | Forward Euler approximation |
| `rk4` | 4 | Fourth-order Runge-Kutta |
| `tustin` | 2 | Bilinear (Tustin) transform |
| `backward` | 1 | Backward Euler (implicit) |
| `matched` | N/A | Zero-order hold |
**Usage example:**
```{python}
#| label: ex-discretized-system
#| eval: false
# Start with continuous system
continuous_system = Pendulum()
# Create discrete approximation
from cdesym.systems.base.core.discretized_system import DiscretizedSystem
discrete_system = DiscretizedSystem(
continuous_system,
dt=0.01,
method='rk4'
)
# Use as discrete system
x_next = discrete_system.step(x, u)
result = discrete_system.simulate(x0, u_sequence=None, n_steps=100)
```
**Key features:**
- Multiple discretization methods available
- Preserves symbolic structure when possible
- Handles deterministic and stochastic systems
- Automatic validation of discretization accuracy
## Design Principles {#sec-design-principles}
### 1. Cooperative Multiple Inheritance {#sec-principle-inheritance}
**Where used:** Layer 2 (ContinuousSymbolicSystem, DiscreteSymbolicSystem)
**Why:** Combines symbolic machinery (Layer 0) with time-domain interface (Layer 1) without code duplication.
```python
class ContinuousSymbolicSystem(SymbolicSystemBase, ContinuousSystemBase):
def __init__(self, *args, **kwargs):
# Python's MRO ensures correct initialization order
super().__init__(*args, **kwargs)
```
**Benefit:** Users get both symbolic capabilities and continuous-time interface in one class.
### 2. Composition Over Inheritance {#sec-principle-composition}
**Internal utilities composed, not inherited:**
```python
class SymbolicSystemBase:
def __init__(self):
# Compose specialized utilities
self.backend = BackendManager()
self._code_gen = CodeGenerator(self, self.backend)
self.equilibria = EquilibriumHandler(self.nx, self.nu)
self._dynamics = DynamicsEvaluator(self, self._code_gen, self.backend)
# etc.
```
**Benefit:** Single responsibility, testability, flexibility.
### 3. Template Method Pattern {#sec-principle-template}
**Base class orchestrates, subclass fills details:**
```python
class SymbolicSystemBase:
def __init__(self, *args, **kwargs):
# 1. Call user's define_system()
self.define_system(*args, **kwargs)
# 2. Validate
self._validator.validate(self)
# 3. Initialize utilities
self._setup_utilities()
# 4. Compile functions
self._compile()
@abstractmethod
def define_system(self, **params):
"""User implements this."""
pass
```
**Benefit:** Consistent initialization, clear extension point.
### 4. Separation of Concerns {#sec-principle-separation}
Each layer has a single, focused responsibility:
- **Layer 0:** Symbolic manipulation (time-agnostic)
- **Layer 1:** Time-domain semantics (abstract interfaces)
- **Layer 2:** Concrete execution (symbolic + time-domain)
- **Layer 3:** Specialized extensions (stochastic)
### 5. Zero Code Duplication {#sec-principle-zero-duplication}
**Before refactoring:** ~1,800 lines duplicated between continuous and discrete systems
**After refactoring:** All shared functionality in SymbolicSystemBase
**Eliminated duplication:**
- Parameter handling
- Backend management
- Code generation
- Symbolic validation
- Equilibrium management
- Configuration persistence
## System Properties {#sec-system-properties}
### Automatic Properties (All Systems) {#sec-automatic-properties}
All systems automatically provide these properties:
```{python}
#| label: ex-system-properties
system = Pendulum()
# Dimensions
print(f"State dimension (nx): {system.nx}")
print(f"Control dimension (nu): {system.nu}")
print(f"Output dimension (ny): {system.ny}")
print(f"Physical dimension (nq): {system.nq}")
print(f"System order: {system.order}")
# Backend
print(f"Default backend: {system.backend.default_backend}")
print(f"Device: {system.backend.preferred_device}")
```
### Discrete-Only Properties {#sec-discrete-properties}
```python
# Discrete systems only
dt: float # Sampling period [s]
sampling_frequency: float # 1/dt [Hz]
```
### Stochastic-Only Properties {#sec-stochastic-properties}
```python
# Stochastic systems only
nw: int # Number of Wiener processes
is_additive_noise() -> bool # Noise structure check
is_multiplicative_noise() -> bool
```
## Backend Support {#sec-backend-support}
All systems support multi-backend execution transparently:
| Backend | Execution | Best For |
|---------|-----------|----------|
| **NumPy** | CPU | General purpose, maximum compatibility |
| **PyTorch** | CPU/GPU | Neural networks, GPU acceleration, automatic differentiation |
| **JAX** | CPU/GPU/TPU | Optimization, XLA compilation, functional programming |
| **Julia** (via DiffEqPy) | CPU | High-performance ODE/SDE solvers |
**Switching backends:**
```{python}
#| label: ex-backend-switching
system = Pendulum()
# NumPy (default)
x_np = np.array([1.0, 0.0])
dx_np = system(x_np, np.zeros(1))
# Temporary PyTorch
with system.use_backend('torch'):
x_torch = torch.tensor([1.0, 0.0])
dx_torch = system(x_torch, torch.zeros(1))
print(f"PyTorch result type: {type(dx_torch)}")
# Permanent switch
system.set_default_backend('jax')
x_jax = jnp.array([1.0, 0.0])
dx_jax = system(x_jax, jnp.zeros(1))
print("✓ Backend switching is transparent")
```
## Integration Methods {#sec-integration-methods}
### Continuous Systems (ODE Solvers) {#sec-ode-solvers}
| Backend | Methods | Examples |
|---------|---------|----------|
| **scipy** | Adaptive | RK45, RK23, DOP853, Radau, BDF, LSODA |
| **Julia (DiffEqPy)** | Adaptive | Tsit5, Vern7, Vern9, Rodas5, AutoTsit5 |
| **JAX (diffrax)** | Adaptive | dopri5, tsit5, heun |
| **PyTorch (torchdiffeq)** | Adaptive | dopri5, adaptive_heun |
| **Fixed-step** | All backends | euler, rk4, midpoint |
### Stochastic Systems (SDE Solvers) {#sec-sde-solvers}
| Backend | Methods | Convergence |
|---------|---------|-------------|
| **torchsde** | euler, milstein, srk | Strong 0.5-1.0 |
| **diffrax** | euler, heun | Strong 0.5-1.0 |
| **Julia (DiffEqPy)** | EM, milstein, etc. | Strong/Weak |
## Usage Examples {#sec-usage-examples}
### Example 1: Pendulum (Continuous) {#sec-ex-pendulum}
```{python}
#| label: ex-pendulum-full
class SimplePendulum(ContinuousSymbolicSystem):
def define_system(self, m=1.0, l=0.5, g=9.81, b=0.1):
"""Simple pendulum with damping."""
# Symbolic variables
theta, omega = sp.symbols('theta omega', real=True)
u = sp.symbols('u', real=True)
m_sym, l_sym, g_sym, b_sym = sp.symbols('m l g b', positive=True)
# System definition
self.state_vars = [theta, omega]
self.control_vars = [u]
self._f_sym = sp.Matrix([
omega,
-(g_sym/l_sym)*sp.sin(theta) - (b_sym/(m_sym*l_sym**2))*omega + u/(m_sym*l_sym**2)
])
self.parameters = {m_sym: m, l_sym: l, g_sym: g, b_sym: b}
self.order = 1
def print_equations(self, simplify=True):
print("Simple Pendulum Equations:")
print(" dθ/dt = ω")
print(" dω/dt = -(g/l)sin(θ) - (b/ml²)ω + u/(ml²)")
# Create and use
pendulum = SimplePendulum(m=0.5, l=0.3)
x0 = np.array([0.1, 0.0])
# Evaluate dynamics
dx = pendulum(x0, np.zeros(1))
print(f"State derivative: {dx}")
# Integrate
result = pendulum.integrate(x0, u=None, t_span=(0, 5), method='RK45')
print(f"Integration successful: {result['success']}")
# Linearize
A, B = pendulum.linearize(np.zeros(2), np.zeros(1))
print(f"Linearization: A shape {A.shape}, B shape {B.shape}")
```
### Example 2: Linear System (Discrete) {#sec-ex-discrete-linear}
```{python}
#| label: ex-discrete-full
class DiscreteDoubleIntegrator(DiscreteSymbolicSystem):
def define_system(self, dt=0.01):
"""Discrete double integrator."""
# Symbolic variables
p, v = sp.symbols('p v', real=True)
u = sp.symbols('u', real=True)
dt_sym = sp.symbols('dt', positive=True)
# Discrete dynamics (Euler approximation)
self.state_vars = [p, v]
self.control_vars = [u]
self._f_sym = sp.Matrix([
p + dt_sym * v,
v + dt_sym * u
])
self.parameters = {dt_sym: dt}
self._dt = dt # REQUIRED!
self.order = 1
def print_equations(self, simplify=True):
print("Discrete Double Integrator:")
print(" p[k+1] = p[k] + dt*v[k]")
print(" v[k+1] = v[k] + dt*u[k]")
# Create and use
system = DiscreteDoubleIntegrator(dt=0.1)
x0 = np.array([0.0, 0.0])
# Single step
x_next = system.step(x0, np.array([1.0]))
print(f"Next state: {x_next}")
# Simulate
result = system.simulate(x0, u_sequence=None, n_steps=50)
print(f"Simulation shape: {result['states'].shape}")
# Linearize
Ad, Bd = system.linearize(np.zeros(2), np.zeros(1))
print(f"Discrete linearization: Ad shape {Ad.shape}")
```
### Example 3: Stochastic Process {#sec-ex-stochastic}
```{python}
#| label: ex-stochastic-full
class BrownianMotion(ContinuousStochasticSystem):
def define_system(self, mu=0.0, sigma=1.0):
"""Standard Brownian motion with drift."""
# Symbolic variables
x = sp.symbols('x', real=True)
u = sp.symbols('u', real=True)
mu_sym, sigma_sym = sp.symbols('mu sigma', real=True)
# Drift term
self.state_vars = [x]
self.control_vars = [u]
self._f_sym = sp.Matrix([[mu_sym + u]])
self.parameters = {mu_sym: mu, sigma_sym: sigma}
self.order = 1
# Diffusion term (additive noise)
self.diffusion_expr = sp.Matrix([[sigma_sym]])
self.sde_type = 'ito'
def print_equations(self, simplify=True):
print("Brownian Motion with Drift:")
print(" dx = μ*dt + σ*dW")
# Create and use
brownian = BrownianMotion(mu=0.1, sigma=0.5)
# Check noise properties
print(f"Additive noise: {brownian.is_additive_noise()}")
print(f"Noise dimension: {brownian.nw}")
# Drift and diffusion evaluation
x = np.array([0.5])
f = brownian.drift(x, np.zeros(1))
g = brownian.diffusion(x, np.zeros(1))
print(f"Drift: {f}, Diffusion: {g}")
# Integrate SDE
x0 = np.array([0.0])
result = brownian.integrate(
x0, u=None, t_span=(0, 10),
method='EM', dt=0.01
)
print(f"SDE integration complete: {result['success']}")
```
## Key Strengths {#sec-key-strengths}
::: {.callout-tip}
## UI Framework Advantages
1. **Clean Separation** - Each layer has single responsibility
2. **Zero Duplication** - Symbolic machinery shared across all systems
3. **Type Safety** - Comprehensive TypedDict definitions throughout
4. **Backend Flexibility** - Seamless NumPy/PyTorch/JAX/Julia switching
5. **Extensibility** - Easy to add new system types via `define_system()`
6. **Mathematical Rigor** - Proper handling of ODEs, SDEs, difference equations
7. **Performance** - Multi-backend support enables GPU acceleration
8. **User-Friendly** - Simple subclass pattern for custom systems
9. **Documentation** - Extensive docstrings with mathematical notation
10. **Production-Quality** - Professional software engineering practices
:::
## Summary
The UI Framework provides a **clean, layered architecture** for defining and working with dynamical systems:
- **Layer 0** provides time-agnostic symbolic foundations
- **Layer 1** defines abstract time-domain interfaces
- **Layer 2** combines symbolic + time-domain into concrete systems
- **Layer 3** extends with stochastic capabilities
Users interact primarily at **Layers 2 and 3** by subclassing `ContinuousSymbolicSystem`, `DiscreteSymbolicSystem`, or their stochastic variants. The framework handles all internal complexity transparently, providing a simple `define_system()` extension point while delivering production-grade functionality.
**The architecture eliminates ~1,800 lines of code duplication while maintaining clean separation of concerns and type safety throughout.**