---
title: "Delegation Layer Architecture"
subtitle: "Internal Support Framework for System Components"
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 **Delegation Layer** (also called the **Support Framework**) provides **specialized internal services** to the UI framework through **composition rather than inheritance**. This layer consists of **11 focused utility classes** that handle backend management, code generation, dynamics evaluation, and stochastic analysis.
::: {.callout-important}
## Internal Framework - Not for Direct Use
**This documentation describes internal framework components.** Users should NOT directly instantiate or interact with these classes. The delegation layer is an internal implementation detail accessed through the high-level system interface:
```python
# ❌ INCORRECT: Direct delegation layer access
code_gen = CodeGenerator(system)
dynamics = DynamicsEvaluator(system, code_gen, backend_mgr)
# ✓ CORRECT: Use system interface (delegation happens automatically)
system = Pendulum()
dx = system(x, u) # DynamicsEvaluator called internally
A, B = system.linearize(x_eq, u_eq) # LinearizationEngine called internally
```
The system classes (e.g., `ContinuousSymbolicSystem`) automatically compose these utilities and expose their functionality through clean, user-facing APIs. This documentation is provided for framework developers and advanced users who need to understand the internal architecture.
:::
```{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 cdesym import ContinuousSymbolicSystem, Pendulum, LangevinDynamics
from cdesym import ContinuousStochasticSystem
from cdesym.systems.builtin.deterministic.continuous.pendulum import SymbolicPendulum2ndOrder
import warnings
# Suppress warnings
warnings.filterwarnings('ignore', category=UserWarning)
# Create system instances for examples
continuous_pendulum = Pendulum()
continuous_pendulum_2 = SymbolicPendulum2ndOrder()
langevin = LangevinDynamics()
```
## Architecture Philosophy {#sec-architecture-philosophy}
### Composition Over Inheritance
Instead of creating deep inheritance hierarchies, the UI framework composes these specialized utilities as private attributes. This internal composition is transparent to users:
```{python}
#| label: ex-composition-internal
# Internal framework structure (users never access these directly)
backend_manager = continuous_pendulum.backend # BackendManager
code_generator = continuous_pendulum._code_gen # CodeGenerator
dynamics_evaluator = continuous_pendulum._dynamics # DynamicsEvaluator
linearization_engine = continuous_pendulum._linearization # LinearizationEngine
observation_engine = continuous_pendulum._observation # ObservationEngine
equilibrium_handler = continuous_pendulum.equilibria # EquilibriumHandler
# Stochastic systems add additional handlers
diffusion_handler = langevin.diffusion_handler # DiffusionHandler
print("✓ Framework internally composed - users interact with system methods only")
```
### Design Benefits
This architecture provides:
- **Single Responsibility** - Each class does one thing well
- **Reusability** - Utilities can be composed by different system types
- **Testability** - Each component tested in isolation
- **Flexibility** - Easy to swap implementations internally
- **Clarity** - Clear separation of concerns
- **Encapsulation** - Internal complexity hidden from users
## Architecture Layers {#sec-architecture-layers}
```
┌─────────────────────────────────────────────────────────────┐
│ UI FRAMEWORK │
│ (SymbolicSystemBase, ContinuousSymbolicSystem, etc.) │
│ │
│ User-facing methods that delegate to utilities: │
│ • system(x, u) → delegates to DynamicsEvaluator │
│ • system.linearize() → delegates to LinearizationEngine │
│ • system.set_default_backend() → delegates to BackendMgr │
└────────────────────┬────────────────────────────────────────┘
│ uses (composition)
↓
┌─────────────────────────────────────────────────────────────┐
│ DELEGATION LAYER (Internal) │
│ │
│ ┌───────────────────────────────────────────────────────┐ │
│ │ CORE UTILITIES (Universal) │ │
│ │ • BackendManager - Multi-backend support │ │
│ │ • CodeGenerator - Symbolic → numerical │ │
│ │ • EquilibriumHandler - Equilibrium management │ │
│ │ • SymbolicValidator - System validation │ │
│ └───────────────────────────────────────────────────────┘ │
│ │
│ ┌───────────────────────────────────────────────────────┐ │
│ │ DETERMINISTIC EVALUATION (ODE Systems) │ │
│ │ • DynamicsEvaluator - Forward dynamics │ │
│ │ • LinearizationEngine - Jacobian computation │ │
│ │ • ObservationEngine - Output evaluation │ │
│ └───────────────────────────────────────────────────────┘ │
│ │
│ ┌───────────────────────────────────────────────────────┐ │
│ │ STOCHASTIC SUPPORT (SDE Systems) │ │
│ │ • DiffusionHandler - Diffusion generation │ │
│ │ • NoiseCharacterizer - Noise analysis │ │
│ │ • SDEValidator - SDE validation │ │
│ └───────────────────────────────────────────────────────┘ │
│ │
│ ┌───────────────────────────────────────────────────────┐ │
│ │ LOW-LEVEL UTILITIES │ │
│ │ • codegen_utils - SymPy code generation │ │
│ └───────────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────┘
```
## Core Utilities (Universal) {#sec-core-utilities}
### BackendManager: Multi-Backend Array Handling {#sec-backendmanager}
**File:** `backend_manager.py`
The `BackendManager` handles all backend-specific array operations and device management. Users interact with this functionality through system-level methods, never directly.
**Internal responsibilities:**
- Backend detection from array types
- Array conversion between backends (NumPy ↔ PyTorch ↔ JAX)
- Backend availability checking
- Device management (CPU, CUDA, TPU)
- Default backend configuration
- Temporary backend switching (context manager)
**User-facing interface (system-level):**
```{python}
#| label: ex-backend-manager
# ✓ User interacts with system methods
system = Pendulum()
# Check current backend
print(f"Default backend: {system.backend.default_backend}")
# Set backend for system
system.set_default_backend('numpy')
system.to_device('cpu')
# Temporary backend switching
with system.use_backend('torch'):
x_torch = torch.tensor([1.0, 0.0])
u_torch = torch.tensor([0.0])
dx = system(x_torch, u_torch)
print(f"Result type: {type(dx)}")
print("✓ Backend management through system interface")
```
**Supported backends:**
| Backend | Execution | Best For |
|---------|-----------|----------|
| NumPy | CPU | General purpose, maximum compatibility |
| PyTorch | CPU/GPU | Neural networks, GPU acceleration, autograd |
| JAX | CPU/GPU/TPU | Optimization, XLA compilation, functional programming |
**Internal capabilities (framework use only):**
```python
# Framework code (users never write this)
backend = backend_manager.detect(array)
x_torch = backend_manager.convert(array, target_backend='torch')
x_cuda = backend_manager.convert(array, target_backend='torch', device='cuda:0')
```
### CodeGenerator: Symbolic to Numerical Compilation {#sec-codegenerator}
**File:** `code_generator.py`
The `CodeGenerator` orchestrates the symbolic-to-numerical code generation pipeline with intelligent caching. This is completely internal—users simply call system methods that trigger code generation when needed.
**Internal responsibilities:**
- Generate dynamics functions: f(x, u) → dx/dt
- Generate output functions: h(x) → y
- Generate Jacobian functions: A, B, C
- Per-backend function caching
- Compilation and warmup
- Cache invalidation on parameter changes
**How users benefit (transparent):**
```{python}
#| label: ex-code-generation
system = Pendulum()
# First call: Code generated and cached automatically
x = np.array([1.0, 0.0])
u = np.array([0.0])
dx1 = system(x, u) # CodeGenerator works internally
# Subsequent calls: Use cached compiled function (fast)
dx2 = system(x, u) # Instant - cached function reused
# When you change parameters, cache automatically invalidates
# (parameters are accessed via system.parameters dict)
m_sym = [k for k in system.parameters.keys() if str(k) == 'm'][0]
l_sym = [k for k in system.parameters.keys() if str(k) == 'l'][0]
system.parameters[m_sym] = 2.0
system.parameters[l_sym] = 1.0
system.reset_caches() # Explicitly clear cache after parameter change
dx3 = system(x, u) # Uses new parameter values
print("✓ Code generation happens transparently")
```
**Caching strategy (internal):**
- Functions cached per backend
- Symbolic Jacobians computed once, then compiled per backend
- Automatic cache invalidation on parameter changes
- Lazy generation (only when first needed)
**Backend-specific optimizations (internal):**
- **NumPy:** Common subexpression elimination (CSE)
- **PyTorch:** Expression simplification for autograd
- **JAX:** JIT compilation via `jax.jit`
### EquilibriumHandler: Named Equilibrium Management {#sec-equilibriumhandler}
**File:** `equilibrium_handler.py`
The `EquilibriumHandler` manages multiple named equilibrium points with automatic backend conversion. Users interact through system methods.
**Internal responsibilities:**
- Store equilibria as NumPy arrays (backend-neutral)
- Convert to any backend on demand
- Named equilibrium management
- Equilibrium verification
- Metadata storage (stability, description, etc.)
**User-facing interface:**
```{python}
#| label: ex-equilibrium
system = Pendulum()
# Add equilibrium (system delegates to EquilibriumHandler)
system.add_equilibrium(
'upright',
x_eq=np.array([np.pi, 0]),
u_eq=np.zeros(1),
verify=True,
metadata={'stability': 'unstable', 'description': 'Inverted pendulum'}
)
# Get equilibrium (backend conversion automatic)
x_eq, u_eq = system.get_equilibrium('upright')
# List all equilibria
equilibria = system.list_equilibria()
print(f"Available equilibria: {equilibria}")
# Get metadata
meta = system.get_equilibrium_metadata('upright')
if meta:
print(f"Stability: {meta.get('stability', 'unknown')}")
else:
print("No metadata available")
```
**Automatic features:**
- Origin equilibrium always present
- Dimension validation on add
- Finite value checking (no NaN/Inf)
- Optional verification with tolerance
- Backend-agnostic storage with on-demand conversion
### SymbolicValidator: System Definition Validation {#sec-symbolicvalidator}
**File:** `symbolic_validator.py`
The `SymbolicValidator` performs comprehensive validation of symbolic system definitions during initialization. Users benefit from clear error messages without directly interacting with the validator.
**Validation checks (automatic during system creation):**
**Required validations:**
- ✓ `state_vars` is list of `sp.Symbol`
- ✓ `control_vars` is list of `sp.Symbol`
- ✓ `_f_sym` is `sp.Matrix` with correct dimensions
- ✓ `parameters` keys are `sp.Symbol` (not strings)
- ✓ `order` divides `nx` evenly (nx % order == 0)
**Output function validations (if defined):**
- ✓ `_h_sym` is `sp.Matrix`
- ✓ `_h_sym` only depends on `state_vars` (not control)
- ✓ `output_vars` matches `_h_sym` dimensions
**Example validation (automatic):**
```python
# This validation happens automatically during system creation
class MySystem(ContinuousSymbolicSystem):
def define_system(self):
x = sp.Symbol('x')
# ❌ This will fail validation with clear error message
self.parameters = {'m': 1.0} # String key instead of Symbol
# ✓ Correct - validator passes
m = sp.Symbol('m')
self.parameters = {m: 1.0} # Symbol key
```
Error messages are clear and actionable, helping users fix issues quickly.
## Deterministic Evaluation Components {#sec-deterministic-evaluation}
### DynamicsEvaluator: Forward Dynamics Computation {#sec-dynamicsevaluator}
**File:** `dynamics_evaluator.py`
The `DynamicsEvaluator` handles forward dynamics evaluation (dx/dt = f(x, u)) across all backends. Users call `system(x, u)` which internally delegates to this evaluator.
**Internal responsibilities:**
- Evaluate dx/dt = f(x, u) for controlled systems
- Evaluate dx/dt = f(x) for autonomous systems
- Handle batched vs single evaluation
- Backend-specific dispatch
- Input shape validation
- Performance tracking
**User-facing interface:**
```{python}
#| label: ex-dynamics
system = Pendulum()
# Single evaluation (DynamicsEvaluator called internally)
x = np.array([1.0, 0.0])
u = np.array([0.0])
dx = system(x, u)
print(f"State derivative shape: {dx.shape}")
# Batched evaluation (automatically detected)
x_batch = np.random.randn(100, 2)
u_batch = np.zeros((100, 1))
dx_batch = system(x_batch, u_batch)
print(f"Batch derivatives shape: {dx_batch.shape}")
# Works seamlessly with different backends
with system.use_backend('torch'):
x_torch = torch.tensor([1.0, 0.0])
u_torch = torch.tensor([0.0])
dx_torch = system(x_torch, u_torch)
print(f"PyTorch result type: {type(dx_torch)}")
```
**Automatic features:**
- Backend detection from input arrays
- Shape validation and broadcasting
- Batched evaluation detection
- Performance statistics tracking
### LinearizationEngine: Jacobian Computation {#sec-linearizationengine}
**File:** `linearization_engine.py`
The `LinearizationEngine` computes system linearizations (Jacobians) at equilibrium points. Users access this through `system.linearize()`.
**Mathematical forms:**
**Continuous systems:**
$$\delta\dot{x} = A\delta x + B\delta u$$
where:
$$A = \frac{\partial f}{\partial x}\bigg|_{(x_{eq}, u_{eq})} \in \mathbb{R}^{n_x \times n_x}$$
$$B = \frac{\partial f}{\partial u}\bigg|_{(x_{eq}, u_{eq})} \in \mathbb{R}^{n_x \times n_u}$$
**Discrete systems:**
$$\delta x[k+1] = A_d\delta x[k] + B_d\delta u[k]$$
**User-facing interface:**
```{python}
#| label: ex-linearization
system = Pendulum()
# Get equilibrium point
x_eq, u_eq = system.get_equilibrium('origin')
# Compute linearization (LinearizationEngine called internally)
A, B = system.linearize(x_eq, u_eq)
print(f"A matrix shape: {A.shape}")
print(f"B matrix shape: {B.shape}")
# Works with any backend
with system.use_backend('torch'):
x_eq_torch = torch.tensor([0.0, 0.0])
u_eq_torch = torch.tensor([0.0])
A_torch, B_torch = system.linearize(x_eq_torch, u_eq_torch)
print(f"PyTorch A type: {type(A_torch)}")
```
**Higher-order system handling:**
For `order=n` systems where the state is `[q, q̇, ..., q^(n-1)]` and only `q^(n)` is defined:
1. Automatically constructs full state-space representation
2. Computes Jacobian of complete state derivative
3. Returns proper $(n_x \times n_x)$ and $(n_x \times n_u)$ matrices
### ObservationEngine: Output Function Evaluation {#sec-observationengine}
**File:** `observation_engine.py`
The `ObservationEngine` handles output function evaluation (y = h(x)) and output Jacobian computation. Users access through `system.output()` and `system.output_jacobian()`.
**Mathematical form:**
$$y = h(x)$$
Output linearization:
$$\delta y = C\delta x$$
where
$$C = \frac{\partial h}{\partial x}\bigg|_{x_{eq}} \in \mathbb{R}^{n_y \times n_x}$$
**User-facing interface:**
```{python}
#| label: ex-observation
# Create system with output function
class ObservableSystem(ContinuousSymbolicSystem):
def define_system(self):
x, v = sp.symbols('x v', real=True)
u = sp.symbols('u', real=True)
self.state_vars = [x, v]
self.control_vars = [u]
self._f_sym = sp.Matrix([v, -x + u])
# Output: only position is measured
self._h_sym = sp.Matrix([x])
self.output_vars = [sp.Symbol('y')]
self.order = 1
system = ObservableSystem()
# Evaluate output
x = np.array([1.5, 0.2])
y = system.h(x) # h() method evaluates output
print(f"Output: {y}")
# Compute output Jacobian
C = system.linearized_observation(x)
print(f"C matrix shape: {C.shape}")
```
## Stochastic Support Components {#sec-stochastic-support}
### DiffusionHandler: SDE Diffusion Management {#sec-diffusionhandler}
**File:** `diffusion_handler.py`
The `DiffusionHandler` generates and caches diffusion functions for stochastic systems. Users interact through `system.diffusion()` method.
**Mathematical form:**
$$dx = f(x, u)dt + g(x, u)dW$$
Diffusion matrix $g(x, u) \in \mathbb{R}^{n_x \times n_w}$
where:
- $n_x$: state dimension
- $n_w$: number of independent Wiener processes
**Noise structure types (auto-detected):**
| Type | Structure | Optimization |
|------|-----------|-------------|
| Additive | $g(x, u) = \text{constant}$ | Return constant matrix (no recomputation) |
| Multiplicative Diagonal | $g$ is diagonal | Independent noise channels |
| Multiplicative Scalar | $n_w = 1$ | Single Wiener process |
| General | Full matrix coupling | Complete computation |
**User-facing interface:**
```{python}
#| label: ex-diffusion
# Stochastic system automatically sets up DiffusionHandler
system = LangevinDynamics()
# Evaluate diffusion (DiffusionHandler called internally)
# LangevinDynamics has nx=2 (position and velocity)
x = np.array([1.0, 0.5])
u = np.array([0.0])
g = system.diffusion(x, u)
print(f"Diffusion matrix shape: {g.shape}")
# Check noise properties
print(f"Is additive noise: {system.is_additive_noise()}")
print(f"Noise dimensions: {system.nw}")
# Get solver recommendations based on noise structure
solvers = system.recommend_solvers('torch')
print(f"Recommended solvers: {solvers[:3] if len(solvers) > 3 else solvers}")
```
**Automatic optimizations (internal):**
- **Additive noise:** Returns constant matrix, no function calls needed
- **Diagonal noise:** Specialized handling for independent channels
- **Scalar noise:** Simplified operations for single Wiener process
### SDEValidator: SDE-Specific Validation {#sec-sdevalidator}
**File:** `sde_validator.py`
The `SDEValidator` performs SDE-specific validation during system creation. Validation happens automatically with clear error messages.
**Validation checks (automatic):**
- ✓ `diffusion_expr` is `sp.Matrix`
- ✓ Dimensions: $(n_x, n_w)$ where $n_w \geq 1$
- ✓ `diffusion_expr` only uses `state_vars` and `control_vars`
- ✓ `sde_type` is 'ito' or 'stratonovich'
- ✓ Compatibility between drift and diffusion
- ✓ No division by zero in diffusion terms
**Example validation (automatic):**
```python
class MyStochasticSystem(ContinuousStochasticSystem):
def define_system(self):
x = sp.Symbol('x')
sigma = sp.Symbol('sigma', positive=True)
# ❌ Wrong dimensions - validator catches this
self.diffusion_expr = sp.Matrix([[sigma]]) # nx=2 but only 1 row
# ✓ Correct dimensions
self.diffusion_expr = sp.Matrix([[sigma], [0]]) # (2, 1) ✓
```
## Low-Level Utilities {#sec-low-level-utilities}
### codegen_utils: SymPy Code Generation {#sec-codegenutils}
**File:** `codegen_utils.py`
Low-level utilities for converting SymPy expressions to executable functions. This is completely internal to the framework.
**Optimization strategies (internal):**
**NumPy:**
- Common subexpression elimination (CSE)
- Fast numerical modules ('numpy', 'scipy')
- Matrix operation optimization
**PyTorch:**
- Symbolic simplification before generation
- Automatic differentiation compatibility
- GPU tensor operation support
**JAX:**
- JIT compilation via `jax.jit`
- Pure functional style for XLA
- Automatic vectorization
## Internal Composition Patterns {#sec-composition-patterns}
::: {.callout-note}
## Framework Implementation Details
These patterns show how the framework internally composes delegation layer components. Users never write this code—it's handled automatically by system classes.
:::
### Pattern 1: Core System Utilities
Used by all symbolic systems:
```python
# Internal framework code (in SymbolicSystemBase.__init__)
self.backend = BackendManager(default_backend, default_device)
self._code_gen = CodeGenerator(self, self.backend)
self._validator = SymbolicValidator()
self.equilibria = EquilibriumHandler(nx, nu)
```
### Pattern 2: Deterministic Extensions
Added by continuous and discrete systems:
```python
# Internal framework code (in ContinuousSymbolicSystem.__init__)
super().__init__(*args, **kwargs) # Core utilities
# Add deterministic evaluators
self._dynamics = DynamicsEvaluator(self, self._code_gen, self.backend)
self._linearization = LinearizationEngine(self, self._code_gen, self.backend)
self._observation = ObservationEngine(self, self._code_gen, self.backend)
```
### Pattern 3: Stochastic Extensions
Added by stochastic systems:
```python
# Internal framework code (in ContinuousStochasticSystem.__init__)
super().__init__(*args, **kwargs) # Deterministic utilities
# Add stochastic support
self.diffusion_handler = DiffusionHandler(self, self._code_gen, self.backend)
self.noise_characteristics = NoiseCharacterizer().analyze(self.diffusion_expr)
self._sde_validator = SDEValidator()
```
## Design Principles {#sec-design-principles}
### Single Responsibility Principle
Each class has one clear, focused purpose:
- `BackendManager` → Backend management **only**
- `CodeGenerator` → Code generation **only**
- `DynamicsEvaluator` → Dynamics evaluation **only**
- `LinearizationEngine` → Linearization **only**
### Composition Over Inheritance
Systems compose utilities as private attributes rather than inheriting functionality:
```python
# ❌ NOT: Deep inheritance
class System(BackendManager, CodeGenerator, DynamicsEvaluator):
pass
# ✓ YES: Composition
class System:
def __init__(self):
self.backend = BackendManager()
self._code_gen = CodeGenerator()
self._dynamics = DynamicsEvaluator()
```
### Dependency Injection
Utilities receive dependencies explicitly through constructors:
```python
# Clear dependency chain (internal framework code)
backend_mgr = BackendManager()
code_gen = CodeGenerator(system, backend_mgr)
dynamics = DynamicsEvaluator(system, code_gen, backend_mgr)
```
### Interface Segregation
Each utility exposes a minimal, focused interface:
- `BackendManager`: detect, convert, to_backend
- `CodeGenerator`: generate_dynamics, generate_jacobians
- `DynamicsEvaluator`: evaluate
### Lazy Initialization
Functions generated and cached only when first needed:
```python
# First call: generates and caches
f = code_gen.generate_dynamics('numpy')
# Subsequent calls: returns cached
f_again = code_gen.generate_dynamics('numpy')
assert f is f_again # Same function object
```
### Backend Agnosticism
All utilities work transparently across backends:
```python
# Same interface, different backends (internal)
dx_numpy = evaluator.evaluate(x_np, u_np, backend='numpy')
dx_torch = evaluator.evaluate(x_torch, u_torch, backend='torch')
dx_jax = evaluator.evaluate(x_jax, u_jax, backend='jax')
```
## Practical Examples {#sec-practical-examples}
::: {.callout-note}
## User-Facing Examples
These examples show how users benefit from the delegation layer through the system interface. Users never directly instantiate delegation layer components.
:::
### Example 1: Creating a System (Delegation Automatic)
```{python}
#| label: ex-system-creation
class SpringMassDamper(ContinuousSymbolicSystem):
def define_system(self, m=1.0, k=10.0, c=0.5):
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)
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
# Delegation layer automatically composed internally
system = SpringMassDamper()
# User interacts with clean system interface
x = np.array([1.0, 0.0])
u = np.array([0.5])
dx = system(x, u) # DynamicsEvaluator called internally
A, B = system.linearize(np.zeros(2), np.zeros(1)) # LinearizationEngine
print(f"System created with automatic delegation")
```
### Example 2: Multi-Backend Usage (Transparent)
```{python}
#| label: ex-multibackend
system = Pendulum()
# NumPy computation
x_np = np.array([1.0, 0.0])
u_np = np.array([0.0])
dx_np = system(x_np, u_np)
# PyTorch computation (backend conversion automatic)
with system.use_backend('torch'):
x_torch = torch.tensor([1.0, 0.0])
u_torch = torch.tensor([0.0])
dx_torch = system(x_torch, u_torch)
print(f"PyTorch result: {type(dx_torch)}")
# JAX computation
with system.use_backend('jax'):
x_jax = jnp.array([1.0, 0.0])
u_jax = jnp.array([0.0])
dx_jax = system(x_jax, u_jax)
print(f"JAX result: {type(dx_jax)}")
print("✓ Backend management completely transparent")
```
### Example 3: Stochastic System (Full Delegation)
```{python}
#| label: ex-stochastic-system
class BrownianMotion(ContinuousStochasticSystem):
def define_system(self, mu=0.0, sigma=1.0):
x = sp.Symbol('x', real=True)
u = sp.Symbol('u', real=True)
mu_sym, sigma_sym = sp.symbols('mu sigma', real=True)
self.state_vars = [x]
self.control_vars = [u]
self._f_sym = sp.Matrix([mu_sym + u])
self.diffusion_expr = sp.Matrix([[sigma_sym]])
self.parameters = {mu_sym: mu, sigma_sym: sigma}
self.sde_type = 'ito'
self.order = 1
# Stochastic delegation automatic
sde_system = BrownianMotion()
# User interacts through clean interface
x = np.array([0.0])
u = np.array([0.0])
# Drift and diffusion evaluated automatically
f = sde_system.drift(x, u) # DynamicsEvaluator
g = sde_system.diffusion(x, u) # DiffusionHandler
print(f"Noise type: {sde_system.is_additive_noise()}")
print(f"Recommended methods: {sde_system.recommend_solvers('numpy')[:3]}")
```
### Example 4: Equilibrium Management (Delegated)
```{python}
#| label: ex-equilibrium-management
system = Pendulum()
# Add multiple equilibria (delegates to EquilibriumHandler)
system.add_equilibrium(
'upright',
x_eq=np.array([np.pi, 0.0]),
u_eq=np.zeros(1),
verify=True,
metadata={'stability': 'unstable'}
)
system.add_equilibrium(
'downright',
x_eq=np.array([-np.pi, 0.0]),
u_eq=np.zeros(1),
metadata={'stability': 'unstable'}
)
# List and access equilibria
equilibria = system.list_equilibria()
print(f"Available: {equilibria}")
# Get equilibrium in any backend
x_eq_np, u_eq_np = system.get_equilibrium('upright')
with system.use_backend('torch'):
x_eq_torch, u_eq_torch = system.get_equilibrium('upright')
print(f"Backend conversion automatic: {type(x_eq_torch)}")
```
### Example 5: Linearization Workflow (Seamless)
```{python}
#| label: ex-linearization-workflow
system = SpringMassDamper()
# Get equilibrium
x_eq, u_eq = system.get_equilibrium('origin')
# Linearize (LinearizationEngine called internally)
A, B = system.linearize(x_eq, u_eq)
# Analyze stability
eigenvalues = np.linalg.eigvals(A)
print(f"Eigenvalues: {eigenvalues}")
print(f"Stable: {np.all(eigenvalues.real < 0)}")
# Linearization works with any backend
with system.use_backend('torch'):
A_torch, B_torch = system.linearize(
torch.zeros(2),
torch.zeros(1)
)
print(f"PyTorch linearization: {type(A_torch)}")
```
## Key Strengths {#sec-key-strengths}
1. **Clean Separation** - Each utility has one clear responsibility
2. **Encapsulation** - Internal complexity hidden from users
3. **Reusability** - Components can be composed by different system types
4. **Testability** - Easy to test each component in isolation
5. **Flexibility** - Internal implementation can change without affecting users
6. **Performance** - Intelligent caching and lazy initialization
7. **Multi-Backend** - Seamless backend switching without user intervention
8. **Type Safety** - TypedDict and semantic types throughout
9. **Documentation** - Clear purpose and interface for each component
10. **Maintainability** - Easy to understand, modify, and extend
11. **User-Friendly** - Complex functionality exposed through simple interfaces
## Summary
The delegation layer provides robust internal services that power the ControlDESymulation framework. Through careful composition and encapsulation, these utilities enable powerful functionality while maintaining a clean, user-friendly system interface.
Users benefit from this architecture without needing to understand its internal workings—they simply interact with intuitive system methods that delegate to the appropriate internal components automatically.