import numpy as np
import matplotlib.pyplot as plt
from matplotlib import cmPyRates Parameter Analysis
This guide demonstrates parameter sweeps and grid searches using TVBO’s SimulationExperiment.explorations with PyRates as the backend. Grid searches are declared declaratively — no manual loops, no YAML export plumbing. TVBO translates each Exploration into a PyRates-native grid_search() call automatically.
Workflow
- Define the model with
Dynamics - Declare the exploration with
SimulationExperiment.explorations - Run
exp.run('pyrates')— TVBO invokes PyRatesgrid_search()natively - Access results via
result.explorations[name](ExplorationResult)
Setup
Example 1: Van der Pol Parameter Sweep
Explore how the damping parameter μ affects oscillation behaviour.
Define Model and Exploration
from tvbo import Dynamics, SimulationExperiment
from tvbo.datamodel.schema import Exploration, Parameter, Range
from IPython.display import Markdown, display
# Van der Pol oscillator
vdp = Dynamics("Dynamics")
vdp.name = "VdP"
vdp.add_parameter("mu", value=1.0, description="Damping parameter")
vdp.add_state_variable("x", equation="z", initial_value=0.1)
vdp.add_state_variable("z", equation="mu*(1 - x**2)*z - x", initial_value=0.0)
display(Markdown(vdp.generate_report(format="markdown")))VdP
State Equations
\[ \dot{x} = z \] \[ \dot{z} = - x + \mu*z*\left(1 - x^{2}\right) \]
Parameters
| Parameter | Value | Unit | Description |
|---|---|---|---|
| \(\mu\) | 1.0 | N/A | Damping parameter |
Declare Exploration and Run
exp = SimulationExperiment(dynamics=vdp)
exp.integration.duration = 50.0
exp.integration.step_size = 1e-2
# Declare 1-D sweep over mu — TVBO translates this to PyRates grid_search()
exp.explorations['mu_sweep'] = Exploration(
name='mu_sweep',
label='μ sweep',
description='Effect of damping coefficient μ on VdP oscillations',
parameters=[Parameter(name='mu', domain=Range(lo=0.1, hi=10.0, n=6))],
mode='product',
)
print("Running μ sweep via PyRates grid_search …")
result = exp.run('pyrates')
sweep = result.explorations['mu_sweep']
mu_values = sweep.axes[0].explored_values
n_time = sweep.results.shape[1]
time = np.arange(n_time) * sweep.dt
x_idx = sweep.output_names.index('x')
z_idx = sweep.output_names.index('z')
print(f"Sweep complete — {len(mu_values)} conditions × {n_time} time steps")
print(f"ExplorationResult: {sweep}")Running μ sweep via PyRates grid_search …
Compilation Progress
--------------------
(1) Translating the circuit template into a networkx graph representation...
...finished.
(2) Preprocessing edge transmission operations...
...finished.
(3) Parsing the model equations into a compute graph...
...finished.
Model compilation was finished.
Simulation Progress
-------------------
(1) Generating the network run function...
(2) Processing output variables...
...finished.
(3) Running the simulation...
...finished after 0.02262170799986052s.
Compilation Progress
--------------------
(1) Translating the circuit template into a networkx graph representation...
...finished.
(2) Preprocessing edge transmission operations...
...finished.
(3) Parsing the model equations into a compute graph...
...finished.
Model compilation was finished.
Simulation Progress
-------------------
(1) Generating the network run function...
(2) Processing output variables...
...finished.
(3) Running the simulation...
...finished after 0.05941066699961084s.
Sweep complete — 6 conditions × 5000 time steps
ExplorationResult: ExplorationResult(name='mu_sweep', grid=6, timeseries=(6, 5000, 2))
Visualize Time Series
fig, axes = plt.subplots(2, 3, figsize=(14, 8))
axes = axes.flatten()
for i, mu in enumerate(mu_values):
ax = axes[i]
ax.plot(time, sweep.results[i, :, x_idx], label='x')
ax.plot(time, sweep.results[i, :, z_idx], alpha=0.7, label='z')
ax.set_title(f'μ = {mu:.2g}')
ax.set_xlabel('Time')
ax.set_ylabel('State')
ax.legend(loc='upper right')
plt.suptitle('Van der Pol: Effect of Damping (μ)')
plt.tight_layout()
plt.show()
Phase Portraits
fig, axes = plt.subplots(2, 3, figsize=(14, 8))
axes = axes.flatten()
colors = cm.viridis(np.linspace(0, 1, len(mu_values)))
for i, (mu, color) in enumerate(zip(mu_values, colors)):
ax = axes[i]
x = sweep.results[i, :, x_idx]
z = sweep.results[i, :, z_idx]
ax.plot(x, z, color=color, linewidth=0.8)
ax.scatter(x[0], z[0], color='green', s=50, zorder=5, label='Start')
ax.scatter(x[-1], z[-1], color='red', s=50, zorder=5, label='End')
ax.set_title(f'μ = {mu:.2g}')
ax.set_xlabel('x')
ax.set_ylabel('z')
ax.set_box_aspect(1)
plt.suptitle('Van der Pol Phase Portraits')
plt.tight_layout()
plt.show()
Example 2: 2D Parameter Grid Search
Explore two FitzHugh-Nagumo parameters simultaneously using a full Cartesian product.
Define Model and 2D Exploration
fhn = Dynamics("Dynamics")
fhn.name = "FHN"
fhn.add_parameter("a", value=0.7)
fhn.add_parameter("b", value=0.8)
fhn.add_parameter("tau", value=12.5)
fhn.add_parameter("I_ext", value=0.5)
fhn.add_state_variable("v", equation="v - v**3/3 - w + I_ext", initial_value=0.0)
fhn.add_state_variable("w", equation="(v + a - b*w)/tau", initial_value=0.0)
display(Markdown(fhn.generate_report(format="markdown")))FHN
State Equations
\[ \dot{v} = I_{ext} + v - w - \frac{v^{3}}{3} \] \[ \dot{w} = \frac{a + v - b*w}{\tau} \]
Parameters
| Parameter | Value | Unit | Description |
|---|---|---|---|
| \(a\) | 0.7 | N/A | None |
| \(b\) | 0.8 | N/A | None |
| \(\tau\) | 12.5 | N/A | None |
| \(I_{ext}\) | 0.5 | N/A | None |
exp2 = SimulationExperiment(dynamics=fhn)
exp2.integration.duration = 100.0
exp2.integration.step_size = 5e-3
# 2-D product sweep: 3 × 3 = 9 conditions
# axes[0] = 'a' varies fastest (innermost), axes[1] = 'I_ext' varies slowest
exp2.explorations['amp_landscape'] = Exploration(
name='amp_landscape',
label='a × I_ext amplitude landscape',
parameters=[
Parameter(name='a', domain=Range(lo=0.5, hi=1.0, n=3)),
Parameter(name='I_ext', domain=Range(lo=0.0, hi=1.0, n=3)),
],
mode='product',
)
print("Running 2-D grid search …")
result2 = exp2.run('pyrates')
sweep2 = result2.explorations['amp_landscape']
a_vals = sweep2.axes[0].explored_values # varies fastest
I_vals = sweep2.axes[1].explored_values # varies slowest
n_a, n_I = len(a_vals), len(I_vals)
n_time2 = sweep2.results.shape[1]
v_idx = sweep2.output_names.index('v')
print(f"Grid: {n_a}×{n_I} = {n_a*n_I} conditions, {n_time2} time steps")Running 2-D grid search …
Compilation Progress
--------------------
(1) Translating the circuit template into a networkx graph representation...
...finished.
(2) Preprocessing edge transmission operations...
...finished.
(3) Parsing the model equations into a compute graph...
...finished.
Model compilation was finished.
Simulation Progress
-------------------
(1) Generating the network run function...
(2) Processing output variables...
...finished.
(3) Running the simulation...
...finished after 0.14529229199979454s.
Compilation Progress
--------------------
(1) Translating the circuit template into a networkx graph representation...
...finished.
(2) Preprocessing edge transmission operations...
...finished.
(3) Parsing the model equations into a compute graph...
...finished.
Model compilation was finished.
Simulation Progress
-------------------
(1) Generating the network run function...
(2) Processing output variables...
...finished.
(3) Running the simulation...
...finished after 0.8711053329989227s.
Grid: 3×3 = 9 conditions, 20000 time steps
Amplitude Heatmap
# Steady-state amplitude for each condition
amps = np.array([
np.ptp(sweep2.results[i, n_time2 // 2:, v_idx])
for i in range(n_a * n_I)
])
# Reshape: conditions ordered (a varies fastest → rows=I, cols=a)
amps_grid = amps.reshape(n_I, n_a)
fig, ax = plt.subplots(figsize=(7, 5))
im = ax.imshow(
amps_grid, origin='lower', aspect='auto',
extent=[a_vals[0], a_vals[-1], I_vals[0], I_vals[-1]],
cmap='viridis',
)
ax.set_xlabel('Parameter a')
ax.set_ylabel('External current I_ext')
ax.set_title('FitzHugh-Nagumo: Oscillation Amplitude')
plt.colorbar(im, ax=ax, label='Amplitude (max − min)')
plt.tight_layout()
plt.show()
Example 3: Bifurcation-like Sweep
Scan the background drive η of a QIF mean-field model to find the transition from silence to oscillation.
Define Model and Sweep
qif = Dynamics("Dynamics")
qif.name = "QIF"
qif.add_parameter("tau", value=1.0)
qif.add_parameter("eta", value=-5.0, description="Background drive")
qif.add_parameter("J", value=15.0, description="Coupling strength")
qif.add_parameter("Delta", value=1.0, description="Heterogeneity")
qif.add_state_variable("r", equation="Delta/(tau*pi) + 2*r*v", initial_value=0.1)
qif.add_state_variable("v", equation="v**2 + eta + J*r*tau - (pi*r*tau)**2", initial_value=-2.0)
display(Markdown(qif.generate_report(format="markdown")))QIF
State Equations
\[ \dot{r} = 2*r*v + \frac{\Delta}{\pi*\tau} \] \[ \dot{v} = \eta + v^{2} + J*r*\tau - \pi^{2}*r^{2}*\tau^{2} \]
Parameters
| Parameter | Value | Unit | Description |
|---|---|---|---|
| \(\tau\) | 1.0 | N/A | None |
| \(\eta\) | -5.0 | N/A | Background drive |
| \(J\) | 15.0 | N/A | Coupling strength |
| \(\Delta\) | 1.0 | N/A | Heterogeneity |
exp3 = SimulationExperiment(dynamics=qif)
exp3.integration.duration = 200.0
exp3.integration.step_size = 5e-3
exp3.explorations['eta_sweep'] = Exploration(
name='eta_sweep',
label='η bifurcation sweep',
parameters=[Parameter(name='eta', domain=Range(lo=-10.0, hi=5.0, n=15))],
mode='product',
)
print("Running η bifurcation sweep …")
result3 = exp3.run('pyrates')
sweep3 = result3.explorations['eta_sweep']
eta_values = sweep3.axes[0].explored_values
n_time3 = sweep3.results.shape[1]
r_idx = sweep3.output_names.index('r')
print(f"Sweep complete — {len(eta_values)} conditions × {n_time3} time steps")Running η bifurcation sweep …
Compilation Progress
--------------------
(1) Translating the circuit template into a networkx graph representation...
...finished.
(2) Preprocessing edge transmission operations...
...finished.
(3) Parsing the model equations into a compute graph...
...finished.
Model compilation was finished.
Simulation Progress
-------------------
(1) Generating the network run function...
(2) Processing output variables...
...finished.
(3) Running the simulation...
...finished after 0.385886207999647s.
Compilation Progress
--------------------
(1) Translating the circuit template into a networkx graph representation...
...finished.
(2) Preprocessing edge transmission operations...
...finished.
(3) Parsing the model equations into a compute graph...
...finished.
Model compilation was finished.
Simulation Progress
-------------------
(1) Generating the network run function...
(2) Processing output variables...
...finished.
(3) Running the simulation...
...finished after 4.052057500000956s.
Sweep complete — 15 conditions × 40000 time steps
Bifurcation Diagram
steady_start = int(0.8 * n_time3)
r_min = [float(np.min( sweep3.results[i, steady_start:, r_idx])) for i in range(len(eta_values))]
r_max = [float(np.max( sweep3.results[i, steady_start:, r_idx])) for i in range(len(eta_values))]
r_mean = [float(np.mean(sweep3.results[i, steady_start:, r_idx])) for i in range(len(eta_values))]
fig, axes = plt.subplots(1, 2, figsize=(14, 5))
ax = axes[0]
ax.fill_between(eta_values, r_min, r_max, alpha=0.3, label='Oscillation range')
ax.plot(eta_values, r_mean, 'k-', linewidth=2, label='Mean firing rate')
ax.set_xlabel('η (background drive)')
ax.set_ylabel('Firing rate r')
ax.set_title('QIF: Bifurcation-like Diagram')
ax.legend()
ax.grid(True, alpha=0.3)
time3 = np.arange(n_time3) * sweep3.dt
ax = axes[1]
sample_indices = [0, 5, 9, 14]
colors_ = plt.cm.viridis(np.linspace(0, 1, len(sample_indices)))
for idx, color in zip(sample_indices, colors_):
start = int(0.6 * n_time3)
ax.plot(time3[start:] - time3[start], sweep3.results[idx, start:, r_idx],
color=color, label=f'η = {eta_values[idx]:.1f}')
ax.set_xlabel('Time')
ax.set_ylabel('Firing rate r')
ax.set_title('Sample Time Series')
ax.legend()
plt.tight_layout()
plt.show()
Summary
TVBO’s explorations declaration maps directly to PyRates grid_search():
| TVBO concept | PyRates concept |
|---|---|
Exploration.parameters + Range(lo, hi, n) |
param_grid = {name: linspace(lo, hi, n)} |
Exploration.mode = 'product' |
permute_grid=True (Cartesian product) |
Exploration.mode = 'zip' |
permute_grid=False (pairwise) |
result.explorations[name] |
returned (results_df, results_map) DataFrames |
from tvbo import Dynamics, SimulationExperiment
from tvbo.datamodel.schema import Exploration, Parameter, Range
exp = SimulationExperiment(dynamics=model)
exp.integration.duration = T
exp.integration.step_size = dt
exp.explorations['my_sweep'] = Exploration(
name='my_sweep',
parameters=[Parameter(name='mu', domain=Range(lo=0.1, hi=10.0, n=6))],
mode='product', # or 'zip'
)
result = exp.run('pyrates')
sweep = result.explorations['my_sweep']
# Access: sweep.axes[i].explored_values, sweep.results[cond, time, var], sweep.output_namesNext Steps
- PyRates Bifurcation: Numerical continuation with AUTO-07p
- PyRates Interoperability: Basic round-trip examples