Note
This page was generated from a Jupyter notebook.
PathSim Introduction
PathSim is a block-based, time-domain system simulation framework for Python. It lets you model dynamical systems graphically using blocks (sources, integrators, functions, scopes, …) and connections, then simulate them with a choice of ODE solvers.
PathSim is particularly useful for:
Rapid prototyping of control systems.
Running hardware-in-the-loop (HIL) scenarios.
Wrapping external simulation engines (e.g. JSBSim – see
04_pathsim_jsbsim_trim_elevator_doublet.ipynb).
Documentation: https://docs.pathsim.org
Install
pip install pathsim
# or
conda install conda-forge::pathsim
1. Imports
[1]:
# If running on Google Colab, install the required packages.
import sys
if 'google.colab' in sys.modules:
print('Running on Google Colab \u2013 installing pathsim \u2026')
!pip install pathsim
[2]:
import numpy as np
import matplotlib
import matplotlib.pyplot as plt
from pathsim import Simulation, Connection
from pathsim.blocks import (
Integrator,
Amplifier,
Adder,
Scope,
Source,
Constant,
Function,
)
import pathsim
matplotlib.rcParams.update({'figure.dpi': 120, 'axes.grid': True, 'grid.alpha': 0.4})
print(f"PathSim version : {pathsim.__version__}")
PathSim version : 0.20.0
2. Example 1 – Damped harmonic oscillator
We model the second-order ODE
with \(\omega_n = 2\,\text{rad/s}\) and \(\zeta = 0.25\) (under-damped).
The block diagram is:
Adder → Integrator(v0) → Integrator(x0) ──→ Scope
↑ ↓ ↓
└── Amp(-2ζωn) ──┘ Amp(-ωn²) ──┘
[3]:
omega_n = 2.0 # natural frequency [rad/s]
zeta = 0.25 # damping ratio
# Initial conditions: x0 = 1, v0 = 0
int_v = Integrator(0.0) # integrates acceleration → velocity
int_x = Integrator(1.0) # integrates velocity → position
amp_d = Amplifier(-2 * zeta * omega_n) # damping term
amp_k = Amplifier(-omega_n**2) # stiffness term
adder = Adder()
scope = Scope(labels=['position x', 'velocity v'])
sim1 = Simulation(
blocks=[int_v, int_x, amp_d, amp_k, adder, scope],
connections=[
Connection(int_v, int_x, amp_d), # v → x-dot and damping amp
Connection(int_x, amp_k, scope[1]), # x → stiffness amp and scope ch-1
Connection(amp_d, adder), # damping → adder ch-0
Connection(amp_k, adder[1]), # stiffness → adder ch-1
Connection(adder, int_v), # sum → x-ddot
Connection(int_v, scope[0]), # v → scope ch-0 (reorder display)
],
dt=0.01
)
sim1.run(20)
t, scope_data = scope.read()
x_data, v_data = scope_data[0], scope_data[1]
fig, ax = plt.subplots(figsize=(10, 4))
ax.plot(t, x_data, label='position $x$', linewidth=1.5)
ax.plot(t, v_data, label='velocity $\\dot{x}$', linewidth=1.5, linestyle='--')
ax.axhline(0, color='k', linewidth=0.7)
ax.set_xlabel('Time [s]')
ax.set_title(
f'Damped Harmonic Oscillator '
f'($\\omega_n={omega_n}$ rad/s, $\\zeta={zeta}$)'
)
ax.legend()
plt.tight_layout()
plt.savefig('harmonic_oscillator.png', bbox_inches='tight')
plt.show()
15:13:41 - INFO - LOGGING (log: True)
15:13:41 - INFO - BLOCKS (total: 6, dynamic: 2, static: 4, eventful: 0)
15:13:41 - INFO - GRAPH (nodes: 6, edges: 8, alg. depth: 3, loop depth: 0, runtime: 0.073ms)
15:13:41 - INFO - STARTING -> TRANSIENT (Duration: 20.00s)
15:13:41 - INFO - -------------------- 1% | 0.0s<0.4s | 5479.5 it/s
15:13:41 - INFO - ####---------------- 20% | 0.1s<0.2s | 6912.9 it/s
15:13:41 - INFO - ########------------ 40% | 0.1s<0.2s | 6455.0 it/s
15:13:41 - INFO - ############-------- 60% | 0.2s<0.1s | 13251.3 it/s
15:13:41 - INFO - ################---- 80% | 0.2s<0.0s | 13101.9 it/s
15:13:41 - INFO - #################### 100% | 0.3s<--:-- | 10958.3 it/s
15:13:41 - INFO - FINISHED -> TRANSIENT (total steps: 2000, successful: 2000, runtime: 277.51 ms)
3. Example 2 – First-order system step response
A first-order linear system driven by a unit step:
The analytical solution is \(y(t) = 1 - e^{-t/\tau}\).
[4]:
tau = 2.0 # time constant [s]
step_src = Constant(1.0) # unit step input
adder2 = Adder('+-') # error = u - y
amp_tau = Amplifier(1.0 / tau) # 1/τ
integr = Integrator(0.0) # state y
scope2 = Scope(labels=['y', 'u'])
sim2 = Simulation(
blocks=[step_src, adder2, amp_tau, integr, scope2],
connections=[
Connection(step_src, adder2, scope2[1]), # u → error adder and scope
Connection(integr, adder2[1], scope2[0]), # y → error adder (neg) and scope
Connection(adder2, amp_tau), # error → 1/τ
Connection(amp_tau, integr), # ẏ = (u-y)/τ
],
dt=0.01
)
sim2.run(15)
t2, scope2_data = scope2.read()
y2, u2 = scope2_data[0], scope2_data[1]
y_analytic = 1 - np.exp(-np.array(t2) / tau)
fig, ax = plt.subplots(figsize=(10, 4))
ax.plot(t2, u2, 'k--', linewidth=1.0, label='input $u$', alpha=0.5)
ax.plot(t2, y2, color='steelblue', linewidth=2.0, label='PathSim output $y$')
ax.plot(t2, y_analytic, color='orange', linewidth=1.5,
linestyle=':', label='Analytic $1-e^{-t/\\tau}$')
ax.axhline(1.0, color='grey', linewidth=0.7, linestyle='--')
ax.set_xlabel('Time [s]')
ax.set_title(f'First-Order Step Response ($\\tau = {tau}$ s)')
ax.legend()
plt.tight_layout()
plt.savefig('first_order_step.png', bbox_inches='tight')
plt.show()
max_err = np.max(np.abs(np.array(y2) - y_analytic))
print(f"Max absolute error vs analytical: {max_err:.2e}")
15:13:41 - INFO - LOGGING (log: True)
15:13:41 - INFO - BLOCKS (total: 5, dynamic: 1, static: 4, eventful: 0)
15:13:41 - INFO - GRAPH (nodes: 5, edges: 6, alg. depth: 3, loop depth: 0, runtime: 0.069ms)
15:13:41 - INFO - STARTING -> TRANSIENT (Duration: 15.00s)
15:13:41 - INFO - -------------------- 1% | 0.0s<0.2s | 7242.4 it/s
15:13:41 - INFO - ####---------------- 20% | 0.0s<0.1s | 15352.0 it/s
15:13:41 - INFO - ########------------ 40% | 0.0s<0.1s | 16822.7 it/s
15:13:41 - INFO - ############-------- 60% | 0.1s<0.1s | 8802.3 it/s
15:13:42 - INFO - ################---- 80% | 0.1s<0.0s | 16808.7 it/s
15:13:42 - INFO - #################### 100% | 0.1s<--:-- | 16611.6 it/s
15:13:42 - INFO - FINISHED -> TRANSIENT (total steps: 1501, successful: 1501, runtime: 140.00 ms)
Max absolute error vs analytical: 1.54e-06
4. Example 3 – Sinusoidal source and phase portrait
PathSim provides a Source block that wraps an arbitrary time-varying function.
[5]:
import math
# Van der Pol oscillator: x'' - μ(1-x²)x' + x = 0
mu = 1.5
int_vdp_v = Integrator(0.5) # ẋ, initial velocity
int_vdp_x = Integrator(2.0) # x, initial position
# The nonlinear damping term μ(1-x²)ẋ
def vdp_nonlinear(x, v):
return mu * (1 - x**2) * v
nl_block = Function(vdp_nonlinear)
adder_vdp = Adder('+-')
scope_vdp = Scope(labels=['x', 'v'])
sim3 = Simulation(
blocks=[int_vdp_v, int_vdp_x, nl_block, adder_vdp, scope_vdp],
connections=[
Connection(int_vdp_v, int_vdp_x, nl_block[1], scope_vdp[1]), # v
Connection(int_vdp_x, nl_block[0], scope_vdp[0]), # x
Connection(nl_block, adder_vdp), # μ(1-x²)v
Connection(int_vdp_x, adder_vdp[1]), # x (→ -x)
Connection(adder_vdp, int_vdp_v), # ẍ
],
dt=0.01
)
sim3.run(40)
t3, vdp_data = scope_vdp.read()
x3, v3 = vdp_data[0], vdp_data[1]
x3, v3 = np.array(x3), np.array(v3)
fig, axes = plt.subplots(1, 2, figsize=(12, 4))
axes[0].plot(t3, x3, linewidth=1.2, label='$x$', color='steelblue')
axes[0].plot(t3, v3, linewidth=1.2, label='$\\dot{x}$',
color='darkorange', linestyle='--')
axes[0].set_xlabel('Time [s]')
axes[0].set_title(f'Van der Pol Oscillator ($\\mu={mu}$)')
axes[0].legend()
axes[1].plot(x3, v3, linewidth=1.0, color='mediumpurple')
axes[1].plot(x3[0], v3[0], 'go', ms=8, label='start')
axes[1].set_xlabel('$x$')
axes[1].set_ylabel('$\\dot{x}$')
axes[1].set_title('Phase Portrait')
axes[1].legend()
plt.tight_layout()
plt.savefig('van_der_pol.png', bbox_inches='tight')
plt.show()
15:13:42 - INFO - LOGGING (log: True)
15:13:42 - INFO - BLOCKS (total: 5, dynamic: 2, static: 3, eventful: 0)
15:13:42 - INFO - GRAPH (nodes: 5, edges: 8, alg. depth: 3, loop depth: 0, runtime: 0.054ms)
15:13:42 - INFO - STARTING -> TRANSIENT (Duration: 40.00s)
15:13:42 - INFO - -------------------- 1% | 0.0s<0.7s | 5449.2 it/s
15:13:42 - INFO - ####---------------- 20% | 0.1s<0.5s | 6963.1 it/s
15:13:42 - INFO - ########------------ 40% | 0.2s<0.2s | 13459.5 it/s
15:13:42 - INFO - ############-------- 60% | 0.3s<0.1s | 13599.2 it/s
15:13:42 - INFO - ################---- 80% | 0.4s<0.1s | 13061.9 it/s
15:13:42 - INFO - #################### 100% | 0.5s<--:-- | 6963.2 it/s
15:13:42 - INFO - FINISHED -> TRANSIENT (total steps: 4000, successful: 4000, runtime: 458.85 ms)
5. PathView
PathView is the graphical block-diagram editor for PathSim. You can:
Design your block diagram visually in the browser.
Export the diagram as a Python script.
Run the exported script in this environment.
The exported Python file uses the same Simulation, Connection, and Block API shown above.
[6]:
from IPython.display import IFrame
# Display PathView in an iframe (requires internet access)
IFrame(src='https://view.pathsim.org', width='100%', height=500)
[6]:
Summary
You have seen three examples of PathSim block-diagram simulation:
Damped harmonic oscillator – second-order ODE via two cascaded integrators.
First-order step response – verified against an analytical solution.
Van der Pol oscillator – nonlinear system with a custom
Functionblock and phase portrait.
Next: 04_pathsim_jsbsim_trim_elevator_doublet.ipynb – embedding JSBSim directly in a PathSim block diagram (trim + elevator doublet).