📐 Control Theory Interactive Lab

Developer Guide — Architecture, Theory & Extension Points


1. Architecture Overview

The lab is a single-file web application (lab.html) using Plotly.js for interactive plots and vanilla JavaScript for the control systems math engine. No backend, no build step — runs entirely in the browser.

Event-Driven Update Loop

Every slider, button, and input selector registers an event listener to the update() function. When any parameter changes, the entire visualization recomputes:

Slider/Button → update() → Build G(s) & C(s) → closedLoop() → simulate() via RK4 → Plotly.react()

Core Functions

Function Purpose
update() Main loop — reads sliders, builds TFs, runs simulation, updates both plots and metrics
simulate(num, den, tEnd, nPts, inputFn) RK4 ODE integration using controllable canonical state-space form. Accepts arbitrary input function
closedLoop(numG, denG, numC, denC) Computes T(s) = C·G / (1 + C·G) via polynomial arithmetic
polyMul(a, b) / polyAdd(a, b) Polynomial multiplication and addition (coefficient arrays, highest power first)
polyRoots(coeffs) Finds complex roots using quadratic/cubic formulas or Durand-Kerner iteration (degree 4+)
openLoopMetrics(z, wn) Analytical Mₚ, tₚ, tₛ for pure second-order systems
responseMetrics(t, y) Numerical metrics from actual simulation output (for closed-loop or non-step inputs)

Tech Stack

Library Role
Plotly.js 2.27 Interactive plotting with Plotly.react() for live updates
Vanilla JavaScript Polynomial math, ODE solver (RK4), root finding, metrics
Vanilla CSS Catppuccin Mocha theme, responsive grid layout

2. Transfer Function Theory

Second-Order Plant

G(s) = ωₙ² / (s² + 2ζωₙs + ωₙ²)

Implemented as polynomial arrays:

const numG = [wn * wn];                    // numerator coefficients
const denG = [1, 2 * z * wn, wn * wn];    // denominator coefficients

PID Controller (Parallel Form)

C(s) = Kp + Ki/s + Kd·s = (Kd·s² + Kp·s + Ki) / s

const numC = [kd, kp, ki];   // Kd·s² + Kp·s + Ki
const denC = [1, 0];         // s

Closed-Loop Transfer Function

T(s) = C(s)·G(s) / (1 + C(s)·G(s))

// JavaScript implementation:
const numCG = polyMul(numC, numG);
const denCG = polyMul(denC, denG);
const clNum = numCG;
const clDen = polyAdd(denCG, numCG);

ODE Simulation (RK4)

The transfer function is converted to controllable canonical state-space form, then integrated via 4th-order Runge-Kutta. The input function inputFn(t, dt) is evaluated at each timestep, supporting step, impulse, ramp, and sinusoidal inputs.

// State equation: ẋ = Ax + Bu,  y = Cx + Du
// A = companion matrix from denominator coefficients
// C = output vector from numerator coefficients (reversed indexing)
// The output uses: C[i] = numPad[n-i] - numPad[0] * denN[n-i]

3. Performance Metrics

Metric Formula Valid When
Max Overshoot (Mₚ) e^(-πζ/√(1-ζ²)) × 100% 0 < ζ < 1
Peak Time (tₚ) π / (ωₙ√(1-ζ²)) 0 < ζ < 1
Settling Time (tₛ) 4 / (ζωₙ) All ζ > 0 (2% criterion)

For closed-loop metrics, the analytical formulas don't apply (the system is no longer a pure second-order). Instead, the lab computes them numerically from the simulation output: peak value, peak time, and the last time the signal exits the 2% settling band.

4. Extending the Lab

Adding a New Preset Scenario

Add a new <button> inside the presets panel in the HTML:

<button class="preset-btn" data-z="0.45" data-wn="8"
        data-cat="Underdamped" onclick="applyPreset(this)">
    New Scenario <span class="cat">· Underdamped</span>
</button>

Adding a New Input Function

Add an entry to the inputFunctions object and a button:

// In the JS section:
inputFunctions.sawtooth = (t) => t % 1;
inputLabels.sawtooth = 'Sawtooth';

// In the HTML:
<button class="input-btn" data-input="sawtooth"
        onclick="setInput(this)">Sawtooth</button>

Custom Plant Transfer Functions

To use a non-standard plant (e.g., the hydraulic transmission G(s) = 29s² / s(s² + 11s + 39)):

// Replace numG and denG in update():
const numG = [29, 0, 0];         // 29s²
const denG = [1, 11, 39, 0];     // s³ + 11s² + 39s

5. Accessing the Lab

# Web version (no install needed):
https://control.gamely.dev/lab.html

# Python version (local, requires dependencies):
cd ~/Control && source .venv/bin/activate && python control_lab.py

The web version runs entirely in the browser — no Python, no packages, no setup. Just open the URL.