Delegation Layer Architecture

Internal Support Framework for System Components

Author

Gil Benezer

Published

January 16, 2026

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.

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

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

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:

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

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

BackendManager: Multi-Backend Array Handling

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

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

# 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

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

Code
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

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:

Code
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

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

# 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

DynamicsEvaluator: Forward Dynamics Computation

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:

Code
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

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:

Code
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

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:

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

DiffusionHandler: SDE Diffusion Management

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:

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

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

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

codegen_utils: SymPy Code Generation

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

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

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

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

# 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

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:

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

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

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

# 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

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

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

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

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

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

Code
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

  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.