Visualization Framework Architecture

Interactive, Publication-Ready Plotting for Dynamical Systems

Author

Gil Benezer

Published

January 16, 2026

Overview

The Visualization Framework provides interactive, publication-ready plotting for dynamical systems analysis. It consists of 4 core modules organized into a 2-layer architecture: centralized theming + specialized plotters.

NoteFramework Components
  • Theming Layer: Centralized colors, styles, and themes
  • Plotter Layer: Specialized visualization classes
    • TrajectoryPlotter - Time-domain plots
    • PhasePortraitPlotter - State-space plots
    • ControlPlotter - Control analysis plots
ImportantUser Interface

Users should NOT directly instantiate plotter classes. Instead, access plotters through system properties:

# ✓ CORRECT: Access via system properties
system = Pendulum()
fig = system.plotter.plot_trajectory(t, x)  # TrajectoryPlotter
fig = system.phase_plotter.plot_2d(x)       # PhasePortraitPlotter
fig = system.control_plotter.plot_eigenvalue_map(eigs)  # ControlPlotter

# ❌ INCORRECT: Direct instantiation (not recommended)
from cdesym.visualization.trajectory_plotter import TrajectoryPlotter
plotter = TrajectoryPlotter()  # Don't do this!
fig = plotter.plot_trajectory(t, x)

The system properties automatically configure plotters with the correct backend and provide a consistent interface. Direct instantiation is only needed for framework development or advanced customization.

Architecture Philosophy

Themeable Interactive Visualization - The visualization framework enables:

  1. Interactive Plots - Plotly-based with zoom, pan, hover tooltips
  2. Centralized Theming - Consistent colors and styles across all plots
  3. Backend Agnostic - Works with NumPy, PyTorch, JAX seamlessly
  4. Specialized Plotters - Dedicated classes for different visualization types
  5. Publication Ready - High-quality output for papers and presentations
  6. System Integration - Clean system.plotter and system.control_plotter APIs
  7. Accessible Design - Colorblind-safe palettes available

Framework Layers

┌────────────────────────────────────────────────────────────┐
│                   APPLICATION LAYER                        │
│         (ContinuousSystemBase, DiscreteSystemBase)         │
│                                                            │
│  system.plotter          ──────► TrajectoryPlotter         │
│  system.phase_plotter    ──────► PhasePortraitPlotter      │
│  system.control_plotter  ──────► ControlPlotter            │
└──────────────────┬─────────────────────────────────────────┘
                   │ use theming from
                   ↓
┌────────────────────────────────────────────────────────────┐
│                   THEMING LAYER                            │
│                   themes.py                                │
│                                                            │
│  ColorSchemes:            PlotThemes:                      │
│  • PLOTLY                • DEFAULT                         │
│  • D3                    • PUBLICATION                     │
│  • COLORBLIND_SAFE       • DARK                            │
│  • TABLEAU                                                 │
│  • SEQUENTIAL_BLUE                                         │
│  • DIVERGING_RED_BLUE                                      │
└──────────────────┬─────────────────────────────────────────┘
                   │ used by
                   ↓
┌────────────────────────────────────────────────────────────┐
│                   PLOTTER LAYER                            │
│                                                            │
│  TrajectoryPlotter, PhasePortraitPlotter, ControlPlotter   │
└────────────────────────────────────────────────────────────┘

Theming Layer

Color Schemes

The framework provides several built-in color schemes optimized for different purposes:

Code
# Demonstrate different color schemes
schemes = {
    'Plotly (Default)': ['#636EFA', '#EF553B', '#00CC96', '#AB63FA', '#FFA15A'],
    'Colorblind Safe': ['#0173B2', '#DE8F05', '#029E73', '#CC78BC', '#CA9161'],
    'Tableau': ['#4E79A7', '#F28E2B', '#E15759', '#76B7B2', '#59A14F'],
}

fig = make_subplots(
    rows=len(schemes), cols=1,
    subplot_titles=list(schemes.keys()),
    vertical_spacing=0.20
)

for i, (name, colors) in enumerate(schemes.items(), 1):
    for j, color in enumerate(colors):
        fig.add_trace(
            go.Bar(x=[j], y=[1], marker_color=color, showlegend=False),
            row=i, col=1
        )
    fig.update_xaxes(showticklabels=False, row=i, col=1)  # Hide x ticks
    fig.update_yaxes(showticklabels=False, row=i, col=1)

# Add single x-axis label at bottom
fig.add_annotation(
    text="Color Index",
    xref="paper", yref="paper",
    x=0.5, y=-0.05,
    showarrow=False,
    font=dict(size=12)
)

fig.update_layout(
    height=450,
    title_text="Color Scheme Comparison",
    showlegend=False,
    margin=dict(b=60)
)
fig

Built-in color schemes with accessibility options

Plot Themes

Three built-in themes provide different visual styles:

Theme Use Case Features
default Interactive exploration Plotly default, colorful
publication Papers, presentations Clean, high-contrast, serif fonts
dark Presentations, demos Dark background, reduced eye strain

TrajectoryPlotter: Time-Domain Visualization

TipIntegration Methods for Visualization

For visualization purposes, use simulate() instead of integrate():

  • simulate() returns regular time grids (uniform spacing) - ideal for plotting
  • integrate() returns adaptive time points (variable spacing) - can create uneven plots
# ✓ RECOMMENDED for plotting: Regular time grid
result = system.simulate(x0, controller, t_span=(0, 10), dt=0.01)
fig = system.plotter.plot_trajectory(result['time'], result['states'])

# ⚠️ Works but may have uneven time spacing: Adaptive grid
result = system.integrate(x0, u=None, t_span=(0, 10), method='RK45')
# Or use t_eval for regular grid:
result = system.integrate(x0, u=None, t_span=(0, 10), t_eval=np.linspace(0, 10, 1001))
fig = system.plotter.plot_trajectory(result['t'], result['x'])

Why this matters: - Adaptive methods choose time points based on error control (dense where dynamics change, sparse where smooth) - This can create visually uneven plots with more points in some regions - Regular grids ensure uniform visual appearance - For comparison plots, all trajectories must have same time grid

Basic Trajectory Plot

The most common visualization: state variables vs time.

Code
# Simulate pendulum with regular time grid (recommended for plotting)
x0 = np.array([0.5, 0.0])
result = system.simulate(x0, controller=None, t_span=(0, 10), dt=0.01)

# Plot using system's built-in plotter
fig = system.plotter.plot_trajectory(
    result['time'],
    result['states'],
    state_names=['θ (rad)', 'ω (rad/s)'],
    title='Pendulum Dynamics'
)
fig

Simple pendulum trajectory showing angle and angular velocity over time

NoteWhy simulate() for Plotting

This example uses simulate() rather than integrate() because:

  • Regular time grid (uniform dt=0.01) makes for smooth, evenly-spaced plots
  • Time-major output result['states'] is (T, nx) - natural for plotting
  • Reconstructed controls available if controller provided

For comparison, integrate() returns adaptive time points that may create visually uneven plots.

Batched Trajectories (Monte Carlo)

Automatically handles multiple trajectories. This example uses the stochastic pendulum to show genuine stochastic dynamics:

Code
# Use torch backend (now fixed!)
stochastic_system.set_default_backend('torch')

# Run Monte Carlo simulation
n_trials = 20
x0 = np.array([0.5, 0.0])

# Define output times
t_eval = np.linspace(0, 10, 1001)

# Collect trajectories
results = []
for i in range(n_trials):
    result = stochastic_system.integrate(
        x0, 
        u=None, 
        t_span=(0, 10),
        method='euler',
        dt=0.01,
        t_eval=t_eval,
        seed=i
    )
    # Convert torch tensor to numpy
    x_np = result['x'].cpu().numpy() if hasattr(result['x'], 'cpu') else result['x']
    results.append(x_np)

# Stack: (n_trials, T, nx)
x_batch = np.stack(results)
t_np = result['t'].cpu().numpy() if hasattr(result['t'], 'cpu') else result['t']

print(f"Batch shape: {x_batch.shape} (n_trials={n_trials}, T={len(t_np)}, nx=2)")

# Plot individual trajectories
fig = stochastic_system.plotter.plot_trajectory(
    t_np,
    x_batch,
    state_names=['θ (rad)', 'ω (rad/s)'],
    title=f'Stochastic Pendulum Monte Carlo (n={n_trials}, σ=0.3)',
    color_scheme='colorblind_safe'
)
fig
Batch shape: (20, 1001, 2) (n_trials=20, T=1001, nx=2)

Monte Carlo simulation of stochastic pendulum showing 20 independent noise realizations

NoteStochastic Monte Carlo Visualization

This demonstrates true stochastic dynamics from Wiener process noise, described by the SDE:

\[dx = f(x, u)dt + g(x, u)dW\]

What you see:

  • Individual trajectories: Each colored line shows one noise realization
  • Stochastic spread: Trajectories diverge over time from accumulated Brownian forcing
  • Nonlinear effects: Pendulum damping limits growth but uncertainty remains

Key distinction: The trajectory variability comes from random Brownian forcing \(dW(t)\), not just different initial conditions. Starting from identical states (θ=0.5, ω=0), the 20 trajectories spread due to different noise realizations, visualizing inherent stochastic uncertainty.

For comparison, deterministic Monte Carlo shows variability only from initial condition uncertainty, not from dynamic randomness.

TipSDE Integration

If you still want to use integrate directly despite the recommendation for simulate, specify output times with t_eval:

# Define output times
t_eval = np.linspace(0, 10, 1001)  # 1001 uniformly spaced points

result = system.integrate(
    x0,
    u=None,
    t_span=(0, 10),
    method='euler',       # PyTorch torchsde method
    dt=0.01,              # Integration time step
    t_eval=t_eval,        # Output time points
    seed=42
)

Integration Parameters:

  • dt=0.01: Internal time step (controls accuracy)
  • t_eval: Which points to return in output (controls density)
TipBackend Conversion for Plotting

The plotter requires NumPy arrays, but integration returns arrays of default backend type:

# Convert torch tensor to numpy
x_np = result['x'].cpu().numpy()  # Move to CPU, then numpy
t_np = result['t'].cpu().numpy()  # Same for time

Why this is needed:

  • PyTorch solvers work with torch tensors (for autodiff/GPU)
  • JAX solvers work with jax.numpy.array arrays (for TPU/JIT performance)
  • Plotly requires NumPy arrays for visualization

State and Control Together

Visualize both states and control inputs:

Code
# Simple proportional controller
def controller(x, t):
    K = np.array([[-0.5, -0.15]])
    return -K @ x

# Simulate with controller
from cdesym.systems.base.core.continuous_system_base import ContinuousSystemBase
result = system.simulate(x0, controller=controller, t_span=(0, 10), dt=0.01)

# Plot states and controls
fig = system.plotter.plot_state_and_control(
    result['time'],
    result['states'],
    result['controls'],
    state_names=['θ (rad)', 'ω (rad/s)'],
    control_names=['Torque (N⋅m)']
)
fig

Combined state and control visualization

Comparing Multiple Designs

Compare different control strategies:

Code
# Simulate with different controllers
# Note: All must use same time grid for comparison
controllers = {
    'P (K=1)': lambda x, t: np.array([-1.0 * x[0] - 0.5 * x[1]]),
    'P (K=5)': lambda x, t: np.array([-5.0 * x[0] - 2.0 * x[1]]),
    'No Control': None
}

# Use common dt for all simulations
dt = 0.01
trajectories = {}
t_common = np.arange(0, 10 + dt, dt)

for name, ctrl in controllers.items():
    # Use simulate() to ensure regular time grid
    result = system.simulate(x0, controller=ctrl, t_span=(0, 10), dt=dt)
    trajectories[name] = result['states']

# Compare trajectories (all have same time grid)
fig = system.plotter.plot_comparison(
    t_common,
    trajectories,
    state_names=['θ (rad)', 'ω (rad/s)']
)
fig

Comparison of different control strategies

ImportantRegular Time Grids Required for Comparison

When comparing multiple trajectories, all must use the same time grid. Use simulate() with the same dt for all runs, or use integrate() with the same t_eval array.

# ✓ CORRECT: Same time grid
for name, ctrl in controllers.items():
    result = system.simulate(x0, controller=ctrl, t_span=(0, 10), dt=0.01)
    
# ❌ WRONG: Different adaptive grids
for name, ctrl in controllers.items():
    result = system.integrate(x0, u=ctrl, t_span=(0, 10))
    # Different number of time points! Cannot compare directly.

PhasePortraitPlotter: State-Space Visualization

2D Phase Portrait

Visualize dynamics in state space:

Code
# Simulate from multiple initial conditions (use simulate for regular grids)
initial_conditions = [
    np.array([0.3, 0.0]),
    np.array([1.0, 0.0]),
    np.array([1.5, 0.5]),
]

trajectories = []
for x0_i in initial_conditions:
    result = system.simulate(x0_i, controller=None, t_span=(0, 15), dt=0.01)
    trajectories.append(result['states'])

x_all = np.stack(trajectories)  # (n_traj, T, nx)

# Plot phase portrait
fig = system.phase_plotter.plot_2d(
    x_all,
    state_names=('θ (rad)', 'ω (rad/s)'),
    equilibria=[np.array([0, 0])],
    show_direction=True
)
fig

2D phase portrait showing state-space trajectory

With Vector Field

Add vector field to show dynamics everywhere:

Code
# Define vector field function
def pendulum_field(theta, omega):
    """Compute derivatives at any point in phase space."""
    x = np.array([theta, omega])
    dx = system(x, u=np.zeros(1))
    return dx[0], dx[1]

# Plot with vector field
fig = system.phase_plotter.plot_2d(
    x_all,
    state_names=('θ (rad)', 'ω (rad/s)'),
    vector_field=pendulum_field,
    equilibria=[np.array([0, 0]), np.array([np.pi, 0])],
    show_direction=True
)
fig

Phase portrait with vector field overlay

ControlPlotter: Control Analysis

Eigenvalue Map

Visualize closed-loop stability with comparison:

Code
# Get linearization at origin
x_eq, u_eq = system.get_equilibrium('origin')
A, B = system.linearize(x_eq, u_eq)

# Design LQR controller
Q = np.diag([10, 1])
R = np.array([[0.1]])
lqr_result = system.control.design_lqr(A, B, Q, R, system_type='continuous')

# Plot eigenvalues using dictionary format (recommended for comparison)
eigenvalues_ol = np.linalg.eigvals(A)
eigenvalues_cl = lqr_result['controller_eigenvalues']

eigenvalue_sets = {
    'Open-loop': eigenvalues_ol,
    'Closed-loop (LQR)': eigenvalues_cl,
}

fig = system.control_plotter.plot_eigenvalue_map(
    eigenvalue_sets,
    system_type='continuous',
    color_scheme='colorblind_safe',
    theme='publication'
)
fig

Eigenvalue map comparing open-loop and closed-loop poles

TipComparing Eigenvalue Sets

The plot_eigenvalue_map() method supports multiple input formats:

Dictionary format (recommended for comparison):

eigenvalue_sets = {
    'Open-loop': eigs_ol,
    'Closed-loop (LQR)': eigs_cl,
}
fig = system.control_plotter.plot_eigenvalue_map(eigenvalue_sets)

Concatenated with per-eigenvalue labels:

eigs_all = np.concatenate([eigs_ol, eigs_cl])
labels = ['Open-loop'] * len(eigs_ol) + ['Closed-loop'] * len(eigs_cl)
fig = system.control_plotter.plot_eigenvalue_map(eigs_all, labels=labels)

Single set:

fig = system.control_plotter.plot_eigenvalue_map(eigs_cl, labels='LQR')

Each set gets a unique color and marker symbol for easy distinction!

Stochastic System Visualization

Comparing Noise Levels

Visualize impact of different noise intensities:

Code
# Create systems with different noise levels
noise_levels = [0.1, 0.3, 0.5]
trajectories_noise = {}

# Use common time grid for comparison
dt = 0.01
t_common = np.arange(0, 15 + dt, dt)

for sigma in noise_levels:
    sys_i = ContinuousStochasticPendulum(g=9.81, L=1.0, b=0.5, sigma=sigma)
    sys_i.set_default_backend('torch')
    x0 = np.array([0.3, 0.0])
    
    result = sys_i.integrate(
        x0, u=None, t_span=(0, 15), 
        method='euler', dt=dt, seed=42
    )
    
    # Convert to numpy for plotting
    x_np = result['x'].cpu().numpy() if hasattr(result['x'], 'cpu') else result['x']
    trajectories_noise[f'σ={sigma}'] = x_np

# Compare trajectories (pass t and trajectories separately)
fig = stochastic_system.plotter.plot_comparison(
    t_common,
    trajectories_noise,
    state_names=['θ (rad)', 'ω (rad/s)']
)
fig.update_layout(title='Effect of Noise Intensity on Stochastic Pendulum')
fig

Effect of noise intensity on pendulum dynamics

TipNoise Impact

Notice how increasing noise intensity (\(\sigma\)):

  • Creates larger amplitude fluctuations
  • Can induce transitions over energy barriers
  • Affects regularity of oscillations
  • Changes effective damping behavior

At moderate noise levels, the system exhibits stochastic resonance - an optimal noise level that actually enhances response to periodic forcing.

Stochastic Phase Portrait

Phase portraits reveal noise-induced behavior:

Code
# Switch to torch for SDE integration
stochastic_system.set_default_backend('torch')

# Multiple stochastic trajectories from same initial condition
n_trajectories = 3
stochastic_trajectories = []

for i in range(n_trajectories):
    result = stochastic_system.integrate(
        np.array([np.pi/2, 0.0]),
        u=None,
        t_span=(0, 20),
        method='euler',
        dt=0.01,
        seed=i
    )
    # Convert to numpy
    x_np = result['x'].cpu().numpy() if hasattr(result['x'], 'cpu') else result['x']
    stochastic_trajectories.append(x_np)

x_stochastic = np.stack(stochastic_trajectories)

# Reset backend
stochastic_system.set_default_backend('numpy')

# Plot stochastic phase portrait
fig = stochastic_system.phase_plotter.plot_2d(
    x_stochastic,
    state_names=('θ (rad)', 'ω (rad/s)'),
    equilibria=[np.array([0, 0])],
    show_direction=True
)
fig.update_layout(title='Stochastic Pendulum Phase Portrait (Same IC, Different Noise)')
fig

Phase portrait of stochastic pendulum showing noise-induced wandering

NoteStochastic vs Deterministic Phase Portraits

Unlike deterministic systems where each initial condition produces one unique trajectory, stochastic systems produce a cloud of possible trajectories from the same initial condition. This visualization shows:

  • Trajectory spreading due to random forcing
  • Noise-induced wandering in phase space
  • Probabilistic barrier crossing (not deterministic separatrix)
  • Effective diffusion in state space

The phase portrait becomes a probability distribution rather than a single curve!

ControlPlotter: Control Analysis

Gain Comparison

Compare feedback gains for different designs:

Code
# Design LQR with different Q weights
Q_values = [1, 10, 100]
gains = {}
for q in Q_values:
    Q_i = q * np.diag([10, 1])
    result = system.control.design_lqr(A, B, Q_i, R, system_type='continuous')
    gains[f'Q={q}'] = result['gain']

# Plot gain comparison
fig = system.control_plotter.plot_gain_comparison(
    gains,
    labels=['θ', 'ω']
)
fig

Heatmap comparing LQR gains for different Q weights

Step Response

Analyze closed-loop performance:

Code
# Create closed-loop system with reference tracking
from scipy import signal

# Output: measure angle only
C = np.array([[1, 0]])
D = np.zeros((1, 1))

# Closed-loop dynamics with state feedback u = -Kx
A_cl = A - B @ lqr_result['gain']

# For proper reference tracking, we need feedforward compensation.
# With u = -Kx + K_r*r, the steady-state output tracks the reference r.
# K_r is chosen so that y_ss = r, which requires: K_r = -1/(C @ inv(A_cl) @ B)
dc_gain = C @ np.linalg.inv(A_cl) @ B
K_r = -1.0 / dc_gain[0, 0]

# Create tracking system: ẋ = A_cl*x + B*K_r*r, y = Cx
# When r is a unit step, the input to the system is B*K_r
B_ref = B * K_r
sys_tracking = signal.StateSpace(A_cl, B_ref, C, D)

# Step response (now properly tracks the reference)
t_step = np.linspace(0, 5, 500)
t_out, y_out = signal.step(sys_tracking, T=t_step)

# Plot with metrics
fig = system.control_plotter.plot_step_response(
    t_out,
    y_out.squeeze(),
    reference=1.0,
    show_metrics=True
)
fig

Step response showing rise time, overshoot, and settling time

NoteReference Tracking with State Feedback

LQR designs a state feedback controller u = -Kx that regulates the state to zero. To track a non-zero reference, we add feedforward compensation:

\[u = -Kx + K_r \cdot r\]

where \(K_r\) is chosen to achieve zero steady-state error. For the closed-loop system \(\dot{x} = A_{cl}x + BK_r r\) with output \(y = Cx\), the DC gain must equal 1:

\[K_r = -\frac{1}{C A_{cl}^{-1} B}\]

This ensures the output tracks step references with the stability and transient performance designed by LQR.

Publication-Ready Output

Publication Theme

Switch to publication theme for papers:

Code
# Get a fresh result for this example
x0_pub = np.array([0.5, 0.0])
result_pub = system.simulate(x0_pub, controller=None, t_span=(0, 10), dt=0.01)

# Same trajectory, publication theme
fig = system.plotter.plot_trajectory(
    result_pub['time'],
    result_pub['states'],
    state_names=['θ (rad)', 'ω (rad/s)'],
    title='Pendulum Dynamics',
    theme='publication',
    color_scheme='colorblind_safe'
)
fig

Same plot with publication theme (serif fonts, clean styling)

Exporting Figures

Plotly figures can be exported in multiple formats:

# Export as HTML (interactive)
fig.write_html('pendulum_trajectory.html')

# Export as static image (requires kaleido)
fig.write_image('pendulum_trajectory.pdf', width=800, height=600)
fig.write_image('pendulum_trajectory.png', width=800, height=600, scale=2)

# Export as SVG (vector graphics)
fig.write_image('pendulum_trajectory.svg')

Design Patterns

Pattern 1: Centralized Theming

All plotters use centralized themes:

# ❌ BAD: Hardcoded colors
class BadPlotter:
    def plot(self):
        fig.add_trace(go.Scatter(line=dict(color='#1f77b4')))

# ✓ GOOD: Centralized theming
class GoodPlotter:
    def plot(self, color_scheme='plotly', theme='default'):
        colors = ColorSchemes.get_colors(color_scheme)
        fig.add_trace(go.Scatter(line=dict(color=colors[0])))
        fig = PlotThemes.apply_theme(fig, theme)

Pattern 2: Backend Agnostic

Automatically converts PyTorch/JAX to NumPy:

def plot_trajectory(self, t, x):
    # Convert to NumPy for Plotly
    t_np = self._to_numpy(t)
    x_np = self._to_numpy(x)
    
    # Plot with Plotly (requires NumPy)
    fig = go.Figure()
    fig.add_trace(go.Scatter(x=t_np, y=x_np[:, 0]))
    return fig

def _to_numpy(self, arr):
    if hasattr(arr, 'cpu'):  # PyTorch
        return arr.cpu().numpy()
    elif hasattr(arr, '__array__'):  # JAX
        return np.array(arr)
    return arr

Pattern 3: Adaptive Layouts

Optimal subplot arrangements:

def _determine_subplot_layout(self, n_plots: int) -> Tuple[int, int]:
    """
    Determine optimal subplot grid.
    
    1-2 plots: (n, 1) vertical
    3-4 plots: (2, 2) square
    5-6 plots: (2, 3)
    7-9 plots: (3, 3)
    """
    if n_plots <= 2:
        return n_plots, 1
    elif n_plots <= 4:
        return 2, 2
    else:
        cols = int(np.ceil(np.sqrt(n_plots)))
        rows = int(np.ceil(n_plots / cols))
        return rows, cols

Pattern 4: Batch Detection

Automatically detect and process batched data:

def _process_batch(self, x: np.ndarray):
    if x.ndim == 3:  # (n_batch, T, nx)
        return np.mean(x, axis=0), np.std(x, axis=0), True
    elif x.ndim == 2:  # (T, nx)
        return x, None, False
    else:
        raise ValueError(f"Invalid shape: {x.shape}")

Integration with Systems

Property-Based Access (Recommended)

All systems provide plotter properties for convenient access:

# ✓ Access via system properties (recommended)
system.plotter           # TrajectoryPlotter instance
system.phase_plotter     # PhasePortraitPlotter instance
system.control_plotter   # ControlPlotter instance

# Convenience method
system.plot(result)      # Quick trajectory plot
WarningDo Not Instantiate Plotters Directly

Plotters are internal framework components that should be accessed through system properties, not instantiated directly:

# ❌ WRONG: Direct instantiation
from cdesym.visualization.trajectory_plotter import TrajectoryPlotter
from cdesym.visualization.phase_portrait import PhasePortraitPlotter
from cdesym.visualization.control_plots import ControlPlotter

plotter = TrajectoryPlotter(backend='numpy')  # Don't do this!
phase = PhasePortraitPlotter(backend='numpy')  # Don't do this!
control = ControlPlotter(backend='numpy')      # Don't do this!

# ✓ CORRECT: Access via system
system = Pendulum()
fig = system.plotter.plot_trajectory(t, x)
fig = system.phase_plotter.plot_2d(x)
fig = system.control_plotter.plot_eigenvalue_map(eigs)

The system automatically creates and configures plotters with the correct backend. Direct instantiation is only needed for: - Framework development - Testing plotter components in isolation - Advanced customization outside the system context

Typical Workflow

# 1. Create system
system = Pendulum()

# 2. Simulate
result = system.simulate(x0, controller, t_span=(0, 10))

# 3. Visualize (plotters accessed through system)
fig = system.plotter.plot_trajectory(
    result['time'],
    result['states'],
    theme='publication'
)
fig

Available Plot Types

Summary Table

Plotter Method Purpose
TrajectoryPlotter
plot_trajectory() States vs time
plot_state_and_control() States + controls
plot_comparison() Compare multiple runs
PhasePortraitPlotter
plot_2d() 2D phase portrait
plot_3d() 3D phase portrait
plot_limit_cycle() Periodic orbits
ControlPlotter
plot_eigenvalue_map() Stability analysis
plot_gain_comparison() Compare gains
plot_step_response() Step response metrics
plot_impulse_response() Impulse response
plot_frequency_response() Bode plots
plot_nyquist() Nyquist diagram
plot_root_locus() Root locus
plot_controllability_gramian() Gramian heatmap
plot_observability_gramian() Gramian heatmap
plot_riccati_convergence() Solver convergence

Key Strengths

TipFramework Advantages
  1. Centralized Theming - Single source of truth for colors/styles
  2. Backend Agnostic - NumPy/PyTorch/JAX transparent
  3. Interactive - Plotly-based with zoom, pan, hover
  4. Publication Ready - Professional defaults and themes
  5. Accessible - Colorblind-safe palettes
  6. Specialized Plotters - Right tool for each visualization type
  7. System Integration - Clean system.plotter APIs
  8. Adaptive Layouts - Optimal subplot arrangements
  9. Batch Support - Monte Carlo visualization automatic
  10. Comprehensive - 16+ plot types covering all needs

Summary

The visualization framework provides publication-quality interactive plotting seamlessly integrated with ControlDESymulation’s dynamical systems. All plots are:

  • ✅ Interactive with Plotly
  • ✅ Themeable for different contexts
  • ✅ Backend-agnostic
  • ✅ Automatically adaptive
  • ✅ Publication-ready
ImportantUsage Reminder

Access visualization through system properties:

system.plotter           # Time-domain plots
system.phase_plotter     # Phase space plots
system.control_plotter   # Control analysis plots

Do not directly instantiate TrajectoryPlotter, PhasePortraitPlotter, or ControlPlotter classes—the system handles this automatically with proper configuration.