Type System Architecture

Foundational Types and Structured Results

Author

Gil Benezer

Published

January 16, 2026

Overview

The Type System is the foundational layer (Layer 0) that provides semantic types, structured results, and type-safe interfaces for the entire framework. It consists of 8 focused modules defining over 200 type aliases and structured dictionaries.

ImportantInternal Framework - Not for Direct Use

This documentation describes internal framework types. Users should NOT directly reference or manipulate these types except through system methods. The type system is an internal implementation detail that provides:

  • Type safety through static checking
  • IDE autocomplete support
  • Clear documentation of expected types
  • Backend-agnostic interfaces
# ✓ CORRECT: Types work transparently through system methods
system = Pendulum()
result = system.integrate(x0, u=None, t_span=(0, 10))
# result is IntegrationResult (TypedDict) - IDE knows fields available

A, B = system.linearize(x_eq, u_eq)
# A is StateMatrix, B is InputMatrix - types guide usage

# ✗ INCORRECT: No need to import or reference types directly
from cdesym.types.core import StateVector, ControlVector
x: StateVector = np.array([1.0, 0.0])  # Unnecessary annotation

The type system enables type checking and IDE support while remaining invisible during normal use. This documentation is provided for framework developers and advanced users implementing custom components.

Architecture Philosophy

Type-Driven Design - Types are not just annotations—they are architecture.

The type system enables:

  1. Semantic Clarity - Names convey mathematical meaning (StateVector, not ArrayLike)
  2. Type Safety - Static checking via mypy/pyright catches errors before runtime
  3. IDE Support - Autocomplete knows result['t'] exists and is TimePoints
  4. Backend Agnosticism - Same types work with NumPy/PyTorch/JAX
  5. Structured Results - TypedDict for dictionaries (not plain dict)
  6. Self-Documentation - Type signatures encode mathematical constraints

Example comparison:

Code
# ✗ Bad: Unclear types and semantics
def bad(x, u):  # What are these? What dimensions? What backend?
    return x + u

# ✓ Good: Clear semantics and constraints
def good(x: StateVector, u: ControlVector) -> StateVector:
    """Clear: state in, control in, state out. Works with any backend."""
    return x + u

Framework Layers

┌────────────────────────────────────────────────────────────┐
│                   APPLICATION LAYER                        │
│  (UI Framework, Delegation Layer, Integration Framework)   │
└─────────────────────┬──────────────────────────────────────┘
                      │ uses types from
                      ↓
┌────────────────────────────────────────────────────────────┐
│                    TYPE SYSTEM (Layer 0)                   │
│                                                            │
│  ┌──────────────────────────────────────────────────────┐  │
│  │  FOUNDATIONAL TYPES                                  │  │
│  │  • core.py           - Vectors, matrices             │  │
│  │  • backends.py       - Backend enums, configs        │  │
│  └──────────────────────────────────────────────────────┘  │
│                                                            │
│  ┌──────────────────────────────────────────────────────┐  │
│  │  DOMAIN TYPES                                        │  │
│  │  • trajectories.py   - Time series results           │  │
│  │  • linearization.py  - Jacobian tuples               │  │
│  │  • symbolic.py       - SymPy types                   │  │
│  │  • control_classical.py - Control design results     │  │
│  └──────────────────────────────────────────────────────┘  │
│                                                            │
│  ┌──────────────────────────────────────────────────────┐  │
│  │  STRUCTURAL TYPES                                    │  │
│  │  • protocols.py      - Abstract interfaces           │  │
│  │  • utilities.py      - Helper types, guards          │  │
│  └──────────────────────────────────────────────────────┘  │
└────────────────────────────────────────────────────────────┘

Foundational Types

core.py: Fundamental Building Blocks

File: core.py

The core module provides fundamental building blocks for all other types in the framework. These types establish semantic clarity and backend agnosticism throughout the library.

Key categories:

Multi-Backend Arrays (20+ types)

Backend-agnostic array types that work transparently with NumPy, PyTorch, and JAX:

ArrayLike = Union[np.ndarray, torch.Tensor, jnp.ndarray]
NumpyArray = np.ndarray
TorchTensor = torch.Tensor
JaxArray = jnp.ndarray
ScalarLike = Union[float, int, np.number, torch.Tensor, jnp.ndarray]
IntegerLike = Union[int, np.integer]

Usage in framework:

Code
system = Pendulum()

# Same function works with all backends
x_np = np.array([1.0, 0.0])      # NumPy
x_torch = torch.tensor([1.0, 0.0])  # PyTorch
x_jax = jnp.array([1.0, 0.0])    # JAX

# Backend detected automatically - no type errors
dx_np = system(x_np, np.zeros(1))
dx_torch = system(x_torch, torch.zeros(1))
dx_jax = system(x_jax, jnp.zeros(1))

print("✓ Backend-agnostic type system")

Semantic Vector Types (15+ types)

Vector types that convey mathematical meaning:

StateVector         # x ∈ ℝⁿˣ - System state
ControlVector       # u ∈ ℝⁿᵘ - Control input
OutputVector        # y ∈ ℝⁿʸ - Measured output
NoiseVector         # w ∈ ℝⁿʷ - Stochastic noise
EquilibriumState    # x_eq - Equilibrium state
EquilibriumControl  # u_eq - Equilibrium control
TimeDerivative      # dx/dt - State derivative
StateIncrement      # δx - State deviation
ControlIncrement    # δu - Control deviation

Why semantic types matter:

Code
# ✗ Unclear what arrays represent
def compute(arr1, arr2, arr3):
    return arr1 @ arr2 + arr3

# ✓ Clear mathematical meaning
def compute_control(x: StateVector, K: GainMatrix, u_ff: ControlVector) -> ControlVector:
    """State feedback with feedforward: u = -K*x + u_ff"""
    return -K @ x + u_ff

Matrix Types (30+ types)

Matrix types organized by mathematical purpose:

Dynamics matrices:

StateMatrix         # A ∈ ℝⁿˣˣⁿˣ - ∂f/∂x
InputMatrix         # B ∈ ℝⁿˣˣⁿᵘ - ∂f/∂u  
DiffusionMatrix     # G ∈ ℝⁿˣˣⁿʷ - Noise intensity

Observation matrices:

OutputMatrix        # C ∈ ℝⁿʸˣⁿˣ - ∂h/∂x
FeedthroughMatrix   # D ∈ ℝⁿʸˣⁿᵘ - Direct feedthrough

Control matrices:

GainMatrix          # K ∈ ℝⁿᵘˣⁿˣ - Feedback gain
CostMatrix          # Q ∈ ℝⁿˣˣⁿˣ - State cost
ControlCostMatrix   # R ∈ ℝⁿᵘˣⁿᵘ - Control cost

Stochastic matrices:

CovarianceMatrix    # P ∈ ℝⁿˣˣⁿˣ - Covariance
ProcessNoiseMatrix  # Q ∈ ℝⁿˣˣⁿˣ - Process noise cov
MeasurementNoiseMatrix  # R ∈ ℝⁿʸˣⁿʸ - Measurement noise cov

Special matrices:

IdentityMatrix      # I ∈ ℝⁿˣⁿ
ZeroMatrix         # 0 ∈ ℝᵐˣⁿ
GramianMatrix      # Controllability/observability gramian
ControllabilityMatrix  # [B AB A²B ... Aⁿ⁻¹B]
ObservabilityMatrix    # [C; CA; CA²; ...; CAⁿ⁻¹]

Function Signatures (10+ types)

Callable types that define function interfaces:

DynamicsFunction    # (x, u) → dx/dt
OutputFunction      # (x) → y
ControlPolicy       # (t, x) → u
CostFunction        # (x, u) → scalar
ObserverFunction    # (y, u) → x_hat

Usage example:

Code
from cdesym.types import StateVector, ControlVector
# Function signature guides implementation
def simulate_with_controller(
    system: Callable[[StateVector, ControlVector], StateVector],  # DynamicsFunction
    controller: Callable[[float, StateVector], ControlVector],    # ControlPolicy
    x0: StateVector,
    t_span: Tuple[float, float]
) -> Tuple[np.ndarray, np.ndarray]:
    """Type signatures document the contract."""
    # Implementation here
    pass

print("✓ Function types document interfaces")

System Properties (15+ types)

Types for system dimensions and properties:

StateDimension      # nx - Number of states
ControlDimension    # nu - Number of controls
OutputDimension     # ny - Number of outputs
NoiseDimension      # nw - Number of Wiener processes
SystemOrder         # order - Differential order

EquilibriumPoint    # (x_eq, u_eq) - Tuple
EquilibriumName     # str - Named equilibrium

backends.py: Backend Configuration

File: backends.py

The backends module defines backend selection, device management, and method specification types.

Key categories:

Backend Types

Backend = Literal["numpy", "torch", "jax"]
Device = str  # 'cpu', 'cuda:0', 'mps', 'tpu'

class BackendConfig(TypedDict, total=False):
    backend: Backend
    device: Optional[Device]
    dtype: Optional[str]  # 'float32', 'float64'

Usage in framework:

Code
system = Pendulum()

# Backend configuration handled internally
print(f"Default backend: {system.backend.default_backend}")
print(f"Default device: {system.backend.preferred_device}")

# Users interact through simple methods
system.set_default_backend('numpy')
system.to_device('cpu')

with system.use_backend('torch'):
    print(f"Temporary backend: {system.backend.default_backend}")

Integration Methods

IntegrationMethod = str  # 'RK45', 'dopri5', 'tsit5', etc.

# Specific categories
OdeMethod = str          # Deterministic methods
SdeMethod = str          # Stochastic methods
FixedStepMethod = str    # Fixed-step methods
AdaptiveMethod = str     # Adaptive methods

Discretization Methods

DiscretizationMethod = Literal[
    "exact",      # Matrix exponential
    "euler",      # Forward Euler
    "tustin",     # Bilinear transform
    "backward",   # Backward Euler
    "matched",    # Zero-order hold
]

SDE Types

SDEType = Literal["ito", "stratonovich"]

class NoiseType(Enum):
    ADDITIVE = "additive"
    MULTIPLICATIVE = "multiplicative"
    MULTIPLICATIVE_DIAGONAL = "multiplicative_diagonal"
    MULTIPLICATIVE_SCALAR = "multiplicative_scalar"
    MULTIPLICATIVE_GENERAL = "multiplicative_general"
    UNKNOWN = "unknown"

class ConvergenceType(Enum):
    STRONG = "strong"  # Pathwise convergence
    WEAK = "weak"      # Distribution convergence

System Configuration

class SystemConfig(TypedDict, total=False):
    """Complete system configuration."""
    nx: int              # State dimension
    nu: int              # Control dimension
    ny: int              # Output dimension
    nw: int              # Noise dimension
    order: int           # System order
    dt: Optional[float]  # Time step (discrete)
    backend: Backend
    device: Device

Domain Types

trajectories.py: Time Series Results

File: trajectories.py

The trajectories module defines types for time series data and simulation results.

Key categories:

Trajectory Types

StateTrajectory = ArrayLike      # (T, nx) or (T, batch, nx)
ControlSequence = ArrayLike      # (T, nu) or (T, batch, nu)
OutputSequence = ArrayLike       # (T, ny)
NoiseSequence = ArrayLike        # (T, nw)

Convention: Time-major ordering - All trajectories use (T, ...) format where T is the first dimension.

Time Types

TimePoints = ArrayLike           # (T,) - Time grid
TimeSpan = Tuple[float, float]   # (t0, tf) - Interval
TimeStep = float                 # dt - Step size

Integration Results (TypedDict)

Deterministic ODE Integration:

class IntegrationResult(TypedDict, total=False):
    """ODE integration result."""
    t: TimePoints              # Time points
    x: StateTrajectory         # State trajectory (T, nx)
    success: bool              # Integration succeeded
    message: str               # Status message
    nfev: int                  # Function evaluations
    nsteps: int                # Integration steps
    integration_time: float    # Wall time (seconds)
    solver: str                # Integrator name
    
    # Optional fields (adaptive methods)
    njev: int                  # Jacobian evaluations
    nlu: int                   # LU decompositions
    status: int                # Solver status code
    sol: Any                   # Dense output object
    dense_output: bool         # Dense output available

Stochastic SDE Integration:

class SDEIntegrationResult(TypedDict, total=False):
    """SDE integration result (extends IntegrationResult)."""
    # All IntegrationResult fields, plus:
    diffusion_evals: int       # Diffusion function calls
    noise_samples: NoiseVector # Brownian increments used
    n_paths: int               # Number of trajectories
    convergence_type: str      # 'strong' or 'weak'
    sde_type: str              # 'ito' or 'stratonovich'
    noise_type: str            # Noise structure

Batch Simulation:

class BatchSimulationResult(TypedDict):
    """Batched simulation result."""
    t: TimePoints                    # (T,)
    x: StateTrajectory               # (T, batch, nx)
    u: ControlSequence               # (T, batch, nu)
    batch_size: int
    statistics: Dict[str, ArrayLike] # Mean, std, etc.

Why TypedDict:

Code
system = Pendulum()
x0 = np.array([1.0, 0.0])

# TypedDict result provides IDE support
result = system.integrate(x0, u=None, t_span=(0, 10))

# IDE knows these fields exist and their types
t = result['t']              # TimePoints
x = result['x']              # StateTrajectory
success = result['success']  # bool
solver = result['solver']    # str

print(f"Integration {'succeeded' if success else 'failed'}")
print(f"Solver: {solver}")

linearization.py: Jacobian Types

File: linearization.py

The linearization module defines return types for linearization operations.

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]\]

Type definitions:

Basic Linearization

DeterministicLinearization = Tuple[StateMatrix, InputMatrix]
# Returns: (A, B) where
#   A = ∂f/∂x - State Jacobian
#   B = ∂f/∂u - Control Jacobian

StochasticLinearization = Tuple[StateMatrix, InputMatrix, DiffusionMatrix]
# Returns: (A, B, G) where
#   A = ∂f/∂x
#   B = ∂f/∂u  
#   G = ∂g/∂x or g(x_eq) - Diffusion

LinearizationResult = Union[DeterministicLinearization, StochasticLinearization]
# Polymorphic: works with both

Output Linearization

ObservationLinearization = Tuple[OutputMatrix, FeedthroughMatrix]
# Returns: (C, D) where
#   C = ∂h/∂x - Output Jacobian
#   D = ∂h/∂u - Feedthrough (usually 0)

Full State-Space

FullLinearization = Tuple[StateMatrix, InputMatrix, OutputMatrix, FeedthroughMatrix]
# Returns: (A, B, C, D)

FullStochasticLinearization = Tuple[
    StateMatrix, InputMatrix, DiffusionMatrix, OutputMatrix, FeedthroughMatrix
]
# Returns: (A, B, G, C, D)

Usage example:

Code
system = Pendulum()

# Get equilibrium
x_eq, u_eq = system.get_equilibrium('origin')

# Type annotation guides usage
A, B = system.linearize(x_eq, u_eq)  # DeterministicLinearization

# Natural tuple unpacking
print(f"A shape: {A.shape}")  # (nx, nx)
print(f"B shape: {B.shape}")  # (nx, nu)

symbolic.py: SymPy Integration

File: symbolic.py

The symbolic module provides types for SymPy symbolic expressions.

Key categories:

Symbolic Variables

SymbolicVariable = sp.Symbol        # Single variable
SymbolicVector = sp.Matrix          # Vector of symbols
SymbolicMatrix = sp.Matrix          # Matrix expression
SymbolicExpression = sp.Expr        # General expression

System Components

DynamicsExpression = sp.Matrix      # f(x, u) symbolic
OutputExpression = sp.Matrix        # h(x) symbolic
DiffusionExpression = sp.Matrix     # g(x, u) symbolic
ParameterDict = Dict[sp.Symbol, float]  # Parameter values

Jacobian Expressions

JacobianExpression = sp.Matrix      # ∂f/∂x symbolic
HessianExpression = sp.Matrix       # ∂²f/∂x² symbolic
GradientExpression = sp.Matrix      # ∇f symbolic

Usage in framework:

Code
# Define custom system using symbolic types
class MySystem(ContinuousSymbolicSystem):
    def define_system(self, m=1.0, k=10.0):
        # SymbolicVariable
        x, v = sp.symbols('x v', real=True)
        u = sp.symbols('u', real=True)
        
        # SymbolicVector
        self.state_vars = [x, v]
        self.control_vars = [u]
        
        # DynamicsExpression (SymbolicMatrix)
        self._f_sym = sp.Matrix([v, -k*x/m + u/m])
        
        # ParameterDict
        m_sym, k_sym = sp.symbols('m k', positive=True)
        self.parameters = {m_sym: m, k_sym: k}
        
        self.order = 1

system = MySystem()
print("✓ Symbolic types guide system definition")

control_classical.py: Control Design Results

File: control_classical.py

The control_classical module provides TypedDict results for classical control theory operations.

Key categories:

System Analysis Results

Stability Analysis:

class StabilityInfo(TypedDict):
    """Stability analysis result.
    
    Stability Criteria:
    - Continuous: All Re(λ) < 0 (left half-plane)
    - Discrete: All |λ| < 1 (inside unit circle)
    """
    eigenvalues: np.ndarray          # Complex eigenvalues
    magnitudes: np.ndarray           # |λ| values
    max_magnitude: float             # Spectral radius
    spectral_radius: float           # Same as max_magnitude
    is_stable: bool                  # Asymptotically stable
    is_marginally_stable: bool       # On stability boundary
    is_unstable: bool                # Unstable

Controllability:

class ControllabilityInfo(TypedDict, total=False):
    """Controllability analysis result.
    
    Test: rank(C) = nx where C = [B AB A²B ... Aⁿ⁻¹B]
    """
    controllability_matrix: ControllabilityMatrix  # (nx, nx*nu)
    rank: int                        # Rank of C
    is_controllable: bool            # rank == nx
    uncontrollable_modes: Optional[np.ndarray]  # Eigenvalues

Observability:

class ObservabilityInfo(TypedDict, total=False):
    """Observability analysis result.
    
    Test: rank(O) = nx where O = [C; CA; CA²; ...; CAⁿ⁻¹]
    """
    observability_matrix: ObservabilityMatrix  # (nx*ny, nx)
    rank: int                        # Rank of O
    is_observable: bool              # rank == nx
    unobservable_modes: Optional[np.ndarray]  # Eigenvalues

Control Design Results

LQR Controller:

class LQRResult(TypedDict):
    """Linear Quadratic Regulator result.
    
    Minimizes: J = ∫(x'Qx + u'Ru)dt  (continuous)
               J = Σ(x'Qx + u'Ru)     (discrete)
    
    Control law: u = -Kx
    """
    gain: GainMatrix                 # Feedback gain K (nu, nx)
    cost_to_go: CovarianceMatrix     # Riccati solution P (nx, nx)
    closed_loop_eigenvalues: np.ndarray  # eig(A - BK)
    stability_margin: float          # Phase/gain margin

Kalman Filter:

class KalmanFilterResult(TypedDict):
    """Kalman Filter (optimal estimator) result.
    
    System:
        x[k+1] = Ax[k] + Bu[k] + w[k],  w ~ N(0,Q)
        y[k] = Cx[k] + v[k],            v ~ N(0,R)
    
    Estimator: x̂[k+1] = Ax̂[k] + Bu[k] + L(y[k] - Cx̂[k])
    """
    gain: GainMatrix                 # Kalman gain L (nx, ny)
    error_covariance: CovarianceMatrix  # Error cov P (nx, nx)
    innovation_covariance: CovarianceMatrix  # Innovation S (ny, ny)
    observer_eigenvalues: np.ndarray  # eig(A - LC)

LQG Controller:

class LQGResult(TypedDict):
    """Linear Quadratic Gaussian controller result.
    
    Combines LQR (optimal control) + Kalman (optimal estimation)
    via separation principle.
    
    Controller: u = -Kx̂
    Estimator: x̂[k+1] = Ax̂[k] + Bu[k] + L(y[k] - Cx̂[k])
    """
    control_gain: GainMatrix         # LQR gain K (nu, nx)
    estimator_gain: GainMatrix       # Kalman gain L (nx, ny)
    control_cost_to_go: CovarianceMatrix  # Controller Riccati P
    estimation_error_covariance: CovarianceMatrix  # Estimator Riccati P
    separation_verified: bool        # Separation principle holds
    closed_loop_stable: bool         # Overall stability
    controller_eigenvalues: np.ndarray  # eig(A - BK)
    estimator_eigenvalues: np.ndarray   # eig(A - LC)

Pole Placement:

class PolePlacementResult(TypedDict):
    """Pole placement (eigenvalue assignment) result.
    
    Design K such that eig(A - BK) = desired poles
    """
    gain: GainMatrix                 # State feedback gain K
    desired_poles: np.ndarray        # Desired eigenvalues
    achieved_poles: np.ndarray       # Actual eig(A - BK)
    is_controllable: bool            # Arbitrary placement possible

Luenberger Observer:

class LuenbergerObserverResult(TypedDict):
    """Luenberger observer (deterministic estimator) result.
    
    Observer: x̂̇ = Ax̂ + Bu + L(y - Cx̂)
    Error dynamics: ė = (A - LC)e
    """
    gain: GainMatrix                 # Observer gain L (nx, ny)
    desired_poles: np.ndarray        # Desired observer poles
    achieved_poles: np.ndarray       # Actual eig(A - LC)
    is_observable: bool              # Arbitrary placement possible

Usage example:

Code
# Stability analysis (TypedDict provides structure)
stability: StabilityInfo = system.control.analyze_stability(A, system_type='continuous')
if stability['is_stable']:
    print(f"Stable with spectral radius {stability['spectral_radius']:.3f}")

# LQR design (clear result structure)
lqr: LQRResult = system.control.design_lqr(A, B, Q, R, system_type='continuous')
K = lqr['gain']
closed_loop_eigs = lqr['controller_eigenvalues']

# Kalman filter (all fields documented)
kalman: KalmanFilterResult = system.control.design_kalman(
    A, C, Q_process, R_measurement, system_type='discrete'
)
L = kalman['gain']

# LQG controller (separation principle results)
lqg: LQGResult = system.control.design_lqg(
    A, B, C, Q, R, Q_process, R_measurement, system_type='discrete'
)
K = lqg['controller_gain']
L = lqg['estimator_gain']

Structural Types

protocols.py: Abstract Interfaces

File: protocols.py

The protocols module defines abstract interfaces via Protocol (structural typing).

Key categories:

System Protocols

class DynamicalSystemProtocol(Protocol):
    """Abstract interface for dynamical systems."""
    @property
    def nx(self) -> int: ...
    @property
    def nu(self) -> int: ...
    def __call__(self, x: StateVector, u: ControlVector) -> StateVector: ...

class ContinuousSystemProtocol(DynamicalSystemProtocol, Protocol):
    """Continuous-time system interface."""
    def integrate(
        self,
        x0: StateVector,
        u_func: Callable,
        t_span: TimeSpan
    ) -> IntegrationResult: ...

class DiscreteSystemProtocol(DynamicalSystemProtocol, Protocol):
    """Discrete-time system interface."""
    @property
    def dt(self) -> float: ...
    def step(self, x: StateVector, u: ControlVector) -> StateVector: ...

class StochasticSystemProtocol(Protocol):
    """Stochastic system interface."""
    @property
    def nw(self) -> int: ...
    def drift(self, x: StateVector, u: ControlVector) -> StateVector: ...
    def diffusion(self, x: StateVector, u: ControlVector) -> DiffusionMatrix: ...

Controller Protocols

class ControllerProtocol(Protocol):
    """Controller interface."""
    def compute_control(self, x: StateVector) -> ControlVector: ...

class FeedbackControllerProtocol(ControllerProtocol, Protocol):
    """Linear feedback controller."""
    @property
    def K(self) -> GainMatrix: ...

Observer Protocols

class ObserverProtocol(Protocol):
    """State observer interface."""
    def observe(self, x: StateVector) -> OutputVector: ...
    def estimate(self, y: OutputVector, u: ControlVector) -> StateVector: ...

Why Protocol:

Code
# No inheritance needed - structural typing
class MyCustomSystem:
    @property
    def nx(self) -> int:
        return 2
    
    @property  
    def nu(self) -> int:
        return 1
    
    def __call__(self, x: StateVector, u: ControlVector) -> StateVector:
        return x + u

# Satisfies protocol structurally (duck typing with type safety)
system: DynamicalSystemProtocol = MyCustomSystem()  # ✓ Type checker approves!

utilities.py: Helper Types

File: utilities.py

The utilities module provides helper types, type guards, and performance tracking.

Key categories:

Type Guards

def is_numpy(arr: ArrayLike) -> bool:
    """Check if array is NumPy."""
    return isinstance(arr, np.ndarray)

def is_torch(arr: ArrayLike) -> bool:
    """Check if array is PyTorch."""
    return hasattr(arr, '__module__') and 'torch' in arr.__module__

def is_jax(arr: ArrayLike) -> bool:
    """Check if array is JAX."""
    return hasattr(arr, '__module__') and 'jax' in arr.__module__

Shape Utilities

def is_batched(arr: ArrayLike, expected_dims: int = 1) -> bool:
    """Check if array is batched."""
    return arr.ndim > expected_dims

def get_batch_size(arr: ArrayLike) -> Optional[int]:
    """Get batch size if batched."""
    return arr.shape[0] if is_batched(arr) else None

def get_state_dim(x: StateVector) -> int:
    """Get state dimension."""
    return x.shape[-1] if x.ndim > 0 else 1

Performance Types

class ExecutionStats(TypedDict):
    """Performance statistics."""
    count: int              # Number of calls
    total_time: float       # Total time (seconds)
    avg_time: float         # Average time
    min_time: float         # Fastest call
    max_time: float         # Slowest call

Validation Types

class ValidationResult(TypedDict):
    """Validation result."""
    valid: bool
    errors: List[str]
    warnings: List[str]
    info: Dict[str, Any]

Design Principles

1. Semantic Over Structural

Principle: Names convey mathematical meaning, not implementation details.

Code
# ✗ Structural (what it is)
def compute(arr1: np.ndarray, arr2: np.ndarray) -> np.ndarray:
    return arr1 @ arr2

# ✓ Semantic (what it means)
def compute_control(x: StateVector, K: GainMatrix) -> ControlVector:
    """u = -Kx"""
    return -K @ x

2. Backend Agnosticism

Principle: Same types work with NumPy/PyTorch/JAX.

Code
# Same function signature works for all backends
def dynamics(x: StateVector, u: ControlVector) -> StateVector:
    # Works with NumPy, PyTorch, JAX
    return x + u  # Backend detected from input

# Works with all backends
dx_np = dynamics(np.array([1.0, 0.0]), np.array([0.0]))
dx_torch = dynamics(torch.tensor([1.0, 0.0]), torch.tensor([0.0]))
dx_jax = dynamics(jnp.array([1.0, 0.0]), jnp.array([0.0]))

print("✓ Backend-agnostic types")

3. TypedDict for Structured Results

Principle: Never return plain dictionaries—use TypedDict for structure and safety.

Code
# ✗ Plain dict (no IDE support, no type checking)
def integrate() -> dict:
    return {'t': t, 'x': x, 'success': True}

# ✓ TypedDict (type-safe, self-documenting)
def integrate() -> IntegrationResult:
    return {
        't': t,              # TimePoints - IDE knows this
        'x': x,              # StateTrajectory - IDE knows this
        'success': True,     # bool - IDE knows this
        'nfev': 100,        # int - Required field
        'integration_time': 0.5,
        'solver': 'RK45'
    }

4. Optional Fields with total=False

Principle: Use total=False for optional fields in TypedDict.

class IntegrationResult(TypedDict, total=False):
    # Required fields (always present)
    t: TimePoints
    x: StateTrajectory
    success: bool
    
    # Optional fields (may not be present)
    njev: int  # Only adaptive methods
    sol: Any   # Dense output (optional)

5. Polymorphic Types via Union

Principle: Use Union for polymorphic return types.

LinearizationResult = Union[
    Tuple[StateMatrix, InputMatrix],           # Deterministic
    Tuple[StateMatrix, InputMatrix, DiffusionMatrix]  # Stochastic
]

# Single function handles both
def analyze(result: LinearizationResult):
    A, B = result[0], result[1]
    if len(result) == 3:
        G = result[2]  # Stochastic

6. Protocol for Interfaces

Principle: Define interfaces via Protocol (structural typing) not inheritance.

Code
# No inheritance needed - structural typing
class MySystem:
    @property
    def nx(self) -> int:
        return 2
    
    def __call__(self, x: StateVector, u: ControlVector) -> StateVector:
        return x + u

# Satisfies DynamicalSystemProtocol structurally
system: DynamicalSystemProtocol = MySystem()  # ✓ Type checker approves!

Usage Throughout Framework

In UI Framework

class ContinuousSymbolicSystem(SymbolicSystemBase, ContinuousSystemBase):
    def __call__(
        self, 
        x: StateVector, 
        u: Optional[ControlVector] = None
    ) -> StateVector:
        """Evaluate dynamics (types guide implementation)."""
        return self._dynamics.evaluate(x, u, backend=self.backend.default_backend)
    
    def linearize(
        self,
        x_eq: EquilibriumState,
        u_eq: EquilibriumControl,
        backend: Backend = "numpy"
    ) -> DeterministicLinearization:
        """Compute linearization (return type documents structure)."""
        return self._linearization.linearize_continuous(x_eq, u_eq, backend)

In Delegation Layer

class DynamicsEvaluator:
    def evaluate(
        self,
        x: StateVector,
        u: Optional[ControlVector],
        backend: Backend
    ) -> StateVector:
        """Evaluate forward dynamics (types ensure correctness)."""
        f_func: DynamicsFunction = self.code_gen.generate_dynamics(backend)
        return f_func(x, u)
    
    def get_stats(self) -> ExecutionStats:
        """Get performance statistics (TypedDict result)."""
        return {
            'count': self._call_count,
            'total_time': self._total_time,
            'avg_time': self._total_time / self._call_count,
            'min_time': self._min_time,
            'max_time': self._max_time
        }

In Integration Framework

class ScipyIntegrator(IntegratorBase):
    def integrate(
        self,
        x0: StateVector,
        u_func: Callable[[ScalarLike, StateVector], Optional[ControlVector]],
        t_span: TimeSpan,
        t_eval: Optional[TimePoints] = None
    ) -> IntegrationResult:
        """Integrate using scipy (TypedDict ensures complete result)."""
        # ... implementation ...
        
        result: IntegrationResult = {
            't': sol.t,
            'x': sol.y.T,
            'success': sol.success,
            'message': sol.message,
            'nfev': sol.nfev,
            'nsteps': sol.nfev,
            'integration_time': elapsed,
            'solver': self.name
        }
        
        # Optional fields (type system allows this)
        if hasattr(sol, 'njev'):
            result['njev'] = sol.njev
        
        return result

Type Statistics

Type Distribution

Category Count Examples
Vector Types 15+ StateVector, ControlVector, OutputVector
Matrix Types 30+ StateMatrix, GainMatrix, CovarianceMatrix
Function Types 10+ DynamicsFunction, ControlPolicy
Backend Types 20+ Backend, Device, NoiseType
Trajectory Types 15+ StateTrajectory, IntegrationResult
Linearization Types 15+ DeterministicLinearization
Symbolic Types 10+ SymbolicExpression, DynamicsExpression
Protocol Types 20+ DynamicalSystemProtocol
Utility Types 20+ ExecutionStats, ValidationResult
TypedDict Results 15+ IntegrationResult, LQRResult, StabilityInfo
TOTAL 200+ Complete type system

Key Strengths

TipType System Benefits
  1. Semantic Clarity - Names convey mathematical meaning
  2. Type Safety - Static checking prevents errors
  3. IDE Support - Autocomplete and inline documentation
  4. Backend Agnostic - Works with NumPy/PyTorch/JAX transparently
  5. Structured Results - TypedDict not plain dict
  6. Self-Documenting - Types encode constraints and invariants
  7. Composition - Types compose naturally
  8. Extensible - Easy to add new types
  9. Consistent - Same conventions throughout framework
  10. Testable - Type-driven testing patterns

Summary

The type system is the foundational layer that enables clean, type-safe architecture throughout ControlDESymulation. By providing semantic types, structured results, and protocol-based interfaces, it supports:

  • Type-driven development - Types guide implementation
  • Static verification - Catch errors before runtime
  • Multi-backend support - Transparent backend switching
  • Clear contracts - Function signatures document expectations
  • Maintainability - Types make code self-documenting

The type system is infrastructure—users benefit from it without needing to understand it. This documentation is provided for framework developers and advanced users who need to understand the internal type architecture.