01. Quickstart Guide to ctrlpy¶
A Modern, High-Performance Control Systems Library for Python
This notebook provides a hands-on introduction to modeling Linear Time-Invariant (LTI) systems, performing block diagram algebra, simulating time-domain responses, extracting dynamic performance metrics, and generating frequency-domain visualizations using ctrlpy.
import matplotlib.pyplot as plt
import numpy as np
import ctrlpy as cp
# Ensure inline plotting
%matplotlib inline
1. Defining Linear Time-Invariant (LTI) Systems¶
ctrlpy supports both continuous-time Transfer Functions (TransferFunction, tf) and State-Space models (StateSpace, ss).
Let's define a second-order plant: $$G(s) = \frac{10}{s^2 + 3s + 2}$$
# Create a transfer function: G(s) = 10 / (s^2 + 3s + 2)
G = cp.tf([10.0], [1.0, 3.0, 2.0])
print("Transfer Function G(s):")
print(G)
print(f"Poles: {G.poles()}")
print(f"Zeros: {G.zeros()}")
print(f"DC Gain (s=0): {G.num[-1] / G.den[-1]:.4f}")
In Jupyter notebooks, ctrlpy systems natively render as formatted LaTeX mathematical equations:
# Display native LaTeX representation
G
Bidirectional Model Conversions (tf $\leftrightarrow$ ss)¶
We can convert any transfer function to state-space form in controllable canonical realization, and vice-versa:
# Convert Transfer Function to State-Space
sys_ss = G.to_ss()
print("State-Space Representation:")
print(sys_ss)
# Convert back to Transfer Function
G_recovered = sys_ss.to_tf()
print("\nRecovered Transfer Function:")
print(G_recovered)
2. Block Diagram Algebra & Interconnections¶
ctrlpy overloads standard Python operators (+, -, *, /) and provides high-level interconnection functions:
cp.series(G1, G2)orG1 * G2(Cascade connection)cp.parallel(G1, G2)orG1 + G2(Parallel summation)cp.feedback(G_forward, H_feedback)(Closed-loop feedback loop)
# Define a lead-lag controller C(s) = 2(s + 1) / (s + 5)
C = cp.tf([2.0, 2.0], [1.0, 5.0])
# Series cascade (Forward path): G_open(s) = C(s) * G(s)
G_open = cp.series(C, G)
print("Open-Loop Transfer Function G_open(s):")
print(G_open)
# Closed-loop unity feedback: T(s) = G_open / (1 + G_open)
T_closed = cp.feedback(G_open, 1.0)
print("\nClosed-Loop Transfer Function T_closed(s):")
print(T_closed)
print(f"Closed-Loop Poles: {T_closed.poles()}")
3. Time-Domain Simulations & Dynamic Metrics¶
ctrlpy provides fast, vectorized simulation functions:
cp.step_response(sys)cp.impulse_response(sys)cp.forced_response(sys, T, U)
The returned TimeResponseData container automatically calculates key transient specifications using sub-interval interpolation.
# Compute step response of the closed-loop system
step_data = cp.step_response(T_closed, T=6.0)
print("--- Step Response Performance Metrics ---")
print(f"Steady-State Value (yss) : {step_data.steady_state_value():.4f}")
print(f"Rise Time tr (10% to 90%): {step_data.rise_time():.4f} s")
print(f"Settling Time ts (2% band): {step_data.settling_time(tolerance=0.02):.4f} s")
print(f"Percent Overshoot (%OS) : {step_data.overshoot():.2f} %")
print(f"Peak Time tp : {step_data.peak_time():.4f} s")
# Plot Step Response using Matplotlib
fig, ax = cp.plot_step(T_closed, T=6.0)
ax.set_title("Closed-Loop Step Response")
plt.show()
# Arbitrary input simulation: Sinusoidal forced response
t_sim = np.linspace(0.0, 10.0, 1000)
u_sin = np.sin(2.0 * np.pi * 0.5 * t_sim) # 0.5 Hz sine wave
forced_data = cp.forced_response(T_closed, T=t_sim, U=u_sin)
plt.figure(figsize=(9, 4))
plt.plot(t_sim, u_sin, "r--", label="Input u(t) [Sine 0.5 Hz]")
plt.plot(forced_data.t, forced_data.y, "b-", label="Output y(t)")
plt.title("Forced Response to Sinusoidal Input")
plt.xlabel("Time [s]")
plt.ylabel("Amplitude")
plt.grid(True)
plt.legend()
plt.tight_layout()
plt.show()
4. Frequency-Domain Analysis & Stability Margins¶
Compute exact stability margins including Gain Margin ($GM$), Phase Margin ($PM$), Gain Crossover Frequency ($\omega_{cg}$), and Phase Crossover Frequency ($\omega_{cp}$):
# Compute exact stability margins for the open-loop system
sm = cp.margin(G_open)
print("--- Stability Margins ---")
print(f"Gain Margin (GM) : {sm.gm_db:.2f} dB")
print(f"Phase Margin (PM) : {sm.pm_deg:.2f}°")
print(f"Gain Crossover Freq (wcg) : {sm.wcg:.4f} rad/s")
print(f"Phase Crossover Freq (wcp) : {sm.wcp:.4f} rad/s")
# Bode Plot with stability margin annotations
fig_bode, (ax_mag, ax_phase) = cp.plot_bode(G_open, margins=True)
plt.show()
5. Interactive Visualizations with Plotly¶
ctrlpy provides built-in interactive Plotly charts under ctrlpy.plotting_plotly:
from ctrlpy.plotting_plotly import (
plot_bode_plotly,
plot_root_locus_plotly,
)
# Interactive Root Locus diagram
fig_rl = plot_root_locus_plotly(G_open)
fig_rl.show()
# Interactive Bode diagram with crossover markers
fig_bode_plotly = plot_bode_plotly(G_open, margins=True)
fig_bode_plotly.show()
Summary¶
You now know how to:
- Create and convert
TransferFunctionandStateSpacemodels. - Form complex feedback interconnections with algebraic operators.
- Simulate step, impulse, and forced responses and extract metrics.
- Compute stability margins and plot Bode, Nyquist, and Root Locus diagrams.
Next, explore 02_dc_motor_control.ipynb for a practical electromechanical case study!