UI Framework Architecture

User-Facing System Classes and Symbolic System Definition

Author

Gil Benezer

Published

January 16, 2026

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.

NoteFramework 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

ImportantUser Interaction Model

Most users interact with the UI framework at two levels:

# 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.

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

┌─────────────────────────────────────────────────────────────┐
│  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

SymbolicSystemBase: Time-Agnostic Foundation

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):

Code
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:

Code
# 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:

@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

ContinuousSystemBase: Continuous-Time Interface

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):

@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):

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:

Code
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

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):

@property
@abstractmethod
def dt(self) -> float:
    """Sampling period [seconds]."""
    pass

Abstract methods (Layer 2 implements):

@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):

@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

ContinuousSymbolicSystem: Symbolic + Continuous

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):

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:

Code
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

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:

ImportantRequired: 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:

Code
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

ContinuousStochasticSystem: SDE Support

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:

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:

Code
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

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

DiscretizedSystem: Continuous → Discrete Conversion

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:

Code
# 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

1. Cooperative Multiple Inheritance

Where used: Layer 2 (ContinuousSymbolicSystem, DiscreteSymbolicSystem)

Why: Combines symbolic machinery (Layer 0) with time-domain interface (Layer 1) without code duplication.

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

Internal utilities composed, not inherited:

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

Base class orchestrates, subclass fills details:

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

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

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

Automatic Properties (All Systems)

All systems automatically provide these properties:

Code
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

# Discrete systems only
dt: float                    # Sampling period [s]
sampling_frequency: float    # 1/dt [Hz]

Stochastic-Only Properties

# Stochastic systems only
nw: int                      # Number of Wiener processes
is_additive_noise() -> bool  # Noise structure check
is_multiplicative_noise() -> bool

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:

Code
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

Continuous Systems (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)

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

Example 1: Pendulum (Continuous)

Code
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)

Code
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

Code
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

TipUI 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.