Nested Subsystems¶
Demonstration of hierarchical modeling using nested subsystems for a Van der Pol oscillator.
You can also find this example as a single file in the GitHub repository.
Why Use Subsystems?¶
Subsystems allow you to:
Organize complex systems into logical modules
Reuse components across different models
Abstract implementation details
Scale to large systems with many components
Debug and test individual modules separately
The Van der Pol Oscillator Revisited¶
The stiff Van der Pol oscillator is described by:
With \(\mu = 1000\) (very stiff!)
Hierarchical Structure¶
This example demonstrates hierarchical modeling using Subsystem and Interface blocks for modular system design.
[1]:
import numpy as np
import matplotlib.pyplot as plt
# Apply PathSim docs matplotlib style for consistent, theme-friendly figures
plt.style.use('../pathsim_docs.mplstyle')
from pathsim import Simulation, Connection, Interface, Subsystem
from pathsim.blocks import Integrator, Scope, Function, Multiplier, Adder, Amplifier, Constant, Pow
from pathsim.solvers import ESDIRK43
System Parameters¶
[2]:
# Initial conditions
x1_0 = 2.0
x2_0 = 0.0
# Van der Pol parameter (high stiffness!)
mu = 1000
# Simulation timestep
dt = 0.01
Level 1: ODE Function Subsystem¶
First, we create a subsystem that computes \(f(x_1, x_2) = \mu(1 - x_1^2)x_2 - x_1\)
This subsystem:
Takes two inputs: \(x_1\) and \(x_2\)
Returns one output: the computed derivative
Is self-contained and reusable
The Interface block defines the subsystem’s inputs and outputs and this is how it looks like in pathsim:
[3]:
# Interface for the ODE function subsystem
# Input 0: x1
# Input 1: x2
# Output: μ(1 - x1²)x2 - x1
In = Interface()
M1 = Multiplier() # For x2 * (1 - x1²)
C1 = Constant(1) # The constant 1
A1 = Amplifier(mu) # Multiply by μ
P1 = Adder("+-") # Sum: μ(1 - x1²)x2 - x1
P2 = Adder("+-") # Compute: 1 - x1²
S1 = Pow(2) # Square x1
fn_blocks = [In, M1, C1, A1, P1, P2, S1]
fn_connections = [
Connection(In[0], S1), # x1 → x1²
Connection(In[1], M1[0]), # x2 → multiplier
Connection(S1, P2[1]), # x1² to adder (-)
Connection(C1, P2[0]), # 1 to adder
Connection(P2, M1[1]), # (1 - x1²) to multiplier
Connection(M1, A1), # x2(1 - x1²) → multiply by μ
Connection(A1, P1[0]), # μ(...) to final adder
Connection(In[0], P1[1]), # x1 to final adder (-)
Connection(P1, In) # Result to output
]
# Create the ODE function subsystem
Fn = Subsystem(fn_blocks, fn_connections)
Level 2: Van der Pol Subsystem¶
Now we create a subsystem that contains:
Two integrators (for \(x_1\) and \(x_2\))
The ODE function subsystem we just created
This implements the complete Van der Pol ODE system:
[4]:
# Interface for VDP subsystem (no inputs, two outputs)
If = Interface()
I1 = Integrator(x1_0) # Integrator for x1
I2 = Integrator(x2_0) # Integrator for x2
vdp_blocks = [If, I1, I2, Fn]
vdp_connections = [
Connection(I2, I1, Fn[1], If[1]), # x2 → I1, Fn, and output
Connection(I1, Fn, If), # x1 → Fn and output[0]
Connection(Fn, I2) # dx2/dt → I2
]
# Create the Van der Pol subsystem
VDP = Subsystem(vdp_blocks, vdp_connections)
Level 3: Top-Level System¶
Finally, we create the top-level system that contains:
The VDP subsystem
A Scope for visualization
At this level, the VDP subsystem looks like a simple block with two outputs, hiding all its internal complexity
[5]:
# Top-level system
Sco = Scope(labels=[r"$x_1(t)$", r"$x_2(t)$"])
blocks = [VDP, Sco]
connections = [
Connection(VDP, Sco), # x1 to scope
# Connection(VDP[1], Sco[1]) # x2 to scope
]
Simulation Setup¶
We use a stiff solver (ESDIRK43) because :math:`mu = 1000makes this a very stiff system.
[6]:
# Initialize simulation
Sim = Simulation(
blocks,
connections,
dt=dt,
log=True,
Solver=ESDIRK43,
tolerance_lte_abs=1e-6,
tolerance_lte_rel=1e-4,
tolerance_fpi=1e-7
)
# Run simulation (note: 2*mu for long-term dynamics)
Sim.run(2*mu)
07:09:33 - INFO - LOGGING (log: True)
07:09:33 - INFO - BLOCKS (total: 2, dynamic: 1, static: 1, eventful: 0)
07:09:33 - INFO - GRAPH (nodes: 2, edges: 1, alg. depth: 1, loop depth: 0, runtime: 0.022ms)
07:09:33 - INFO - STARTING -> TRANSIENT (Duration: 2000.00s)
07:09:33 - INFO - -------------------- 1% | 0.4s<11.4s | 45.5 it/s
07:09:33 - WARNING - implicit solver not converged, reverting step at t=557.471131
Subsystem: 1.19e-01
07:09:33 - INFO - #####--------------- 27% | 0.7s<1.1s | 27.6 it/s
07:09:34 - WARNING - implicit solver not converged, reverting step at t=676.479189
Subsystem: 8.00e-03
07:09:34 - WARNING - implicit solver not converged, reverting step at t=744.641850
Subsystem: 5.03e-02
07:09:34 - WARNING - implicit solver not converged, reverting step at t=716.038014
Subsystem: 1.11e-04
07:09:34 - WARNING - implicit solver not converged, reverting step at t=754.836313
Subsystem: 3.80e-04
07:09:34 - WARNING - implicit solver not converged, reverting step at t=784.117711
Subsystem: 3.89e-02
07:09:34 - WARNING - implicit solver not converged, reverting step at t=797.308454
Subsystem: 1.49e-02
07:09:34 - WARNING - implicit solver not converged, reverting step at t=800.647586
Subsystem: 2.40e-05
07:09:34 - INFO - ########------------ 40% | 1.2s<8.0s | 29.4 it/s
07:09:34 - WARNING - implicit solver not converged, reverting step at t=803.643644
Subsystem: 8.47e-07
07:09:34 - WARNING - implicit solver not converged, reverting step at t=805.302856
Subsystem: 3.50e-06
07:09:34 - WARNING - implicit solver not converged, reverting step at t=806.501125
Subsystem: 6.08e-04
07:09:36 - INFO - ########------------ 41% | 2.9s<6.8s | 67.4 it/s
07:09:36 - WARNING - implicit solver not converged, reverting step at t=1250.693446
Subsystem: 1.79e-03
07:09:36 - INFO - ############-------- 62% | 3.1s<0.6s | 34.1 it/s
07:09:36 - WARNING - implicit solver not converged, reverting step at t=1412.300094
Subsystem: 2.35e-03
07:09:36 - WARNING - implicit solver not converged, reverting step at t=1331.496770
Subsystem: 3.14e-06
07:09:36 - WARNING - implicit solver not converged, reverting step at t=1443.543101
Subsystem: 3.00e-05
07:09:36 - WARNING - implicit solver not converged, reverting step at t=1525.065843
Subsystem: 7.51e-03
07:09:36 - WARNING - implicit solver not converged, reverting step at t=1484.304472
Subsystem: 3.73e-05
07:09:36 - WARNING - implicit solver not converged, reverting step at t=1541.546358
Subsystem: 9.09e-03
07:09:36 - WARNING - implicit solver not converged, reverting step at t=1580.432813
Subsystem: 2.23e-03
07:09:36 - WARNING - implicit solver not converged, reverting step at t=1580.432813
Subsystem: 3.48e-04
07:09:36 - WARNING - implicit solver not converged, reverting step at t=1585.468308
Subsystem: 7.52e-03
07:09:37 - WARNING - implicit solver not converged, reverting step at t=1600.952536
Subsystem: 5.22e-02
07:09:37 - WARNING - implicit solver not converged, reverting step at t=1594.454722
Subsystem: 1.03e-05
07:09:37 - WARNING - implicit solver not converged, reverting step at t=1611.057481
Subsystem: 2.58e+00
07:09:37 - INFO - ################---- 80% | 3.9s<3.4s | 21.6 it/s
07:09:37 - WARNING - implicit solver not converged, reverting step at t=1607.880672
Subsystem: 3.77e-05
07:09:37 - WARNING - implicit solver not converged, reverting step at t=1607.880672
Subsystem: 4.21e-04
07:09:37 - WARNING - implicit solver not converged, reverting step at t=1610.569142
Subsystem: 4.27e-06
07:09:37 - WARNING - implicit solver not converged, reverting step at t=1612.041876
Subsystem: 1.60e-06
07:09:37 - WARNING - implicit solver not converged, reverting step at t=1613.632269
Subsystem: 1.52e-03
07:09:39 - INFO - ################---- 81% | 5.7s<3.8s | 79.4 it/s
07:09:39 - INFO - #################### 100% | 5.9s<--:-- | 38.1 it/s
07:09:39 - INFO - FINISHED -> TRANSIENT (total steps: 363, successful: 246, runtime: 5894.86 ms)
[6]:
{'total_steps': 363, 'successful_steps': 246, 'runtime_ms': 5894.859768999595}
Results: Time Series¶
The Van der Pol oscillator with \(\mu = 1000\) exhibits relaxation oscillations - fast transitions between slow phases. This requires a stiff solver to handle efficiently.
[7]:
Sco.plot(".-", lw=1.5)
plt.show()
Subsystem Benefits Demonstrated¶
This example shows several advantages of subsystems:
Modularity: The ODE function is completely separate from the integration
Reusability: The
Fnsubsystem could be used in other modelsClarity: The top level is clean - just VDP and Scope
Debugging: Each subsystem can be tested independently
Abstraction: Inner complexity is hidden from higher levels
Comparison with ODE Block¶
Compare this hierarchical approach with using a single ODE block:
# Alternative: Using ODE block (simpler but less modular)
def vdp_ode(x, u, t):
return np.array([x[1], mu*(1 - x[0]**2)*x[1] - x[0]])
VDP = ODE(vdp_ode, np.array([x1_0, x2_0]))
Both approaches work! Use subsystems when:
You need modularity and reusability
The system is complex with many components
You want to visualize internal signals
You’re building block diagram models