Heterogeneous Edges

Synapse-like relay nodes on the connection path: short-term plasticity modelled as its own dynamics, still a single-scale network.

Part of the running example, where stage 5 adds a connectome and a coupling function.

An edge can carry its own dynamics. Instead of a static weight, the connection between two nodes passes through a relay node with its own equations, here a short-term–plasticity synapse (depression, facilitation, Tsodyks–Markram). A pre-synaptic node drives three parallel synapse nodes, each filtering the signal differently before it reaches the post-synaptic node.

Still single-scale, still heterogeneous

A relay node is just another node on the same graph: the synapse is a node, peer to the neurons it connects. This is heterogeneity on one scale, not multi-scale nesting: nothing here contains a finer network with its own node-space or clock.

Backend: PyRates

A relay’s transmitted signal is input-dependent: r_eff = r_in · x · u reads what the synapse receives, not just its own state. PyRates routes such algebraic edge outputs natively, so this example runs on PyRates.

The tvboptim (JAX) engine behind heterogeneous nodes transports signals derived from a node’s own state; input-dependent relay outputs are a deferred milestone there. To model short-term plasticity on tvboptim today, put the synaptic state (x, u) in the receiving node and drive it through ordinary coupling.

flowchart LR
    Pre["Pre-synaptic<br/>Neuron"]

    Dep["Depression<br/>Synapse"]
    Fac["Facilitation<br/>Synapse"]
    TM["Tsodyks-Markram<br/>Synapse"]

    Post["Post-synaptic<br/>Neuron"]

    Pre -->|"r_out"| Dep
    Pre -->|"r_out"| Fac
    Pre -->|"r_out"| TM

    Dep -->|"r_eff"| Post
    Fac -->|"r_eff"| Post
    TM -->|"r_eff"| Post

from tvbo import Dynamics

# Tsodyks-Markram short-term plasticity model
# Combines depression (x) and facilitation (u)
# r_eff is an input-dependent output (r_in * x * u); the PyRates backend routes it
tsodyks = Dynamics.from_string("""
name: TsodyksMarkram
description: "Short-term synaptic plasticity with depression and facilitation (canonical Tsodyks-Markram)"
parameters:
  tau_D:
    value: 200.0
    description: "Depression recovery time constant (ms)"
  tau_F:
    value: 600.0
    description: "Facilitation time constant (ms)"
  U0:
    value: 0.2
    description: "Baseline release probability"
state_variables:
  x:
    equation:
      rhs: "(1 - x)/tau_D - u*x*r_in"
    initial_value: 1.0
    description: "Available synaptic resources (depression variable)"
  u:
    equation:
      rhs: "(U0 - u)/tau_F + U0*(1 - u)*r_in"
    initial_value: 0.2
    description: "Release probability (facilitation variable)"
coupling_inputs:
  r_in: {}
derived_variables:
  r_eff:
    equation:
      rhs: "r_in*x*u"
    description: "Effective synaptic transmission (release probability x resources x rate)"
output:
    - r_eff
""")

# Pure synaptic depression model
depression = Dynamics.from_string("""
name: Depression
description: "Short-term synaptic depression only (fixed release probability)"
parameters:
  tau_D:
    value: 300.0
    description: "Depression recovery time constant (ms)"
  U:
    value: 0.5
    description: "Release probability (fixed)"
state_variables:
  x:
    equation:
      rhs: "(1 - x)/tau_D - U*x*r_in"
    initial_value: 1.0
    description: "Available synaptic resources"
coupling_inputs:
  r_in: {}
derived_variables:
  r_eff:
    equation:
      rhs: "U*r_in*x"
    description: "Effective transmission with depression"
output:
    - r_eff
""")

# Pure synaptic facilitation model
facilitation = Dynamics.from_string("""
name: Facilitation
description: "Short-term synaptic facilitation only (full resources)"
parameters:
  tau_F:
    value: 500.0
    description: "Facilitation time constant (ms)"
  U0:
    value: 0.2
    description: "Baseline release probability"
state_variables:
  u:
    equation:
      rhs: "(U0 - u)/tau_F + U0*(1 - u)*r_in"
    initial_value: 0.2
    description: "Release probability (facilitation variable)"
coupling_inputs:
  r_in: {}
derived_variables:
  r_eff:
    equation:
      rhs: "r_in*u"
    description: "Effective transmission with facilitation"
output:
 - r_eff
""")

# Simple rate neuron (I_ext=0.0, will receive external stimulus)
rate_neuron = Dynamics.from_string("""
name: RateNeuron
description: "Simple rate-based neuron with external input"
parameters:
  tau:
    value: 10.0
  I_ext:
    value: 0.0
state_variables:
  r:
    equation:
      rhs: "(-r + I_ext + r_in)/tau"
    initial_value: 0.0
coupling_inputs:
  r_in: {}
""")

print(f"Created plasticity models: {tsodyks.name}, {depression.name}, {facilitation.name}")
Created plasticity models: TsodyksMarkram, Depression, Facilitation
import yaml
from tvbo import SimulationExperiment
from tvbo import Network

# Network: Pre-synaptic neuron → three parallel synaptic pathways → Post-synaptic neuron
# This allows direct comparison of how different plasticity affects the SAME post-synaptic target

network_yaml = """
label: SynapticPlasticityComparison
number_of_nodes: 5
nodes:
  - id: 0
    label: PreSynaptic
    dynamics: RateNeuron
    position:
      x: 0.0
      y: 0.5
      z: 0
  - id: 1
    label: DepressionSynapse
    dynamics: Depression
    position:
      x: 0.5
      y: 0.8
      z: 0
  - id: 2
    label: FacilitationSynapse
    dynamics: Facilitation
    position:
      x: 0.5
      y: 0.5
      z: 0
  - id: 3
    label: TsodyksSynapse
    dynamics: TsodyksMarkram
    position:
      x: 0.5
      y: 0.2
      z: 0
  - id: 4
    label: PostSynaptic
    dynamics: RateNeuron
    position:
      x: 1.0
      y: 0.5
      z: 0
edges:
  # Pre-synaptic drives all three synaptic relays
  - source: 0
    target: 1
    parameters:
      weight:
        value: 1.0
    source_var: r_out
    target_var: r_in
  - source: 0
    target: 2
    parameters:
      weight:
        value: 1.0
    source_var: r_out
    target_var: r_in
  - source: 0
    target: 3
    parameters:
      weight:
        value: 1.0
    source_var: r_out
    target_var: r_in
  # All synapses converge onto the post-synaptic neuron
  - source: 1
    target: 4
    parameters:
      weight:
        value: 0.33
    source_var: r_eff
    target_var: r_in
  - source: 2
    target: 4
    parameters:
      weight:
        value: 0.33
    source_var: r_eff
    target_var: r_in
  - source: 3
    target: 4
    parameters:
      weight:
        value: 0.33
    source_var: r_eff
    target_var: r_in
"""

# Parse network and create experiment with all components
network_plasticity = Network.from_string(network_yaml)

network_plasticity.plot_graph(node_size=150, edge_cmap="bwr", fontsize=13)

Run Simulation with Pulsed Stimulus

We create a pulsed input pattern that drives the pre-synaptic neuron. This matches Use Case 2 from the PyRates tutorial:

import numpy as np

# Create time array and pulsed input signal
duration = 1000.0
step_size = 0.1
time = np.arange(0, duration, step_size)

# Pulse train with longer recovery periods to show plasticity effects
# Two bursts of rapid pulses separated by a long gap
pulse_times = [
    50,
    80,
    110,
    140,
    170,  # First burst (rapid)
    500,
    530,
    560,
    590,
    620,
]  # Second burst after recovery
input_signal = np.zeros_like(time)
for pt in pulse_times:
    mask = (time >= pt) & (time < pt + 15)
    input_signal[mask] = 5.0

network_plasticity.dynamics.update(
    {
        "RateNeuron": rate_neuron,
        "Depression": depression,
        "Facilitation": facilitation,
        "TsodyksMarkram": tsodyks,
    }
),
# Create experiment
exp_plasticity = SimulationExperiment(
    network=network_plasticity,
)

exp_plasticity.integration.duration = duration
exp_plasticity.integration.step_size = step_size

# Run with pulsed input to PreSynaptic neuron
pyrates_inputs = {"PreSynaptic/RateNeuron_op/I_ext": input_signal}
res = exp_plasticity.run("pyrates", inputs=pyrates_inputs)
print(res)
* Owlready2 * Warning: ignoring cyclic subclass of/subproperty of, involving:
  http://uri.interlex.org/tgbugs/uris/readable/atlas/Space
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.22746691689826548s.
Experiment
└── integration
        data: (10000, 4, 5)

Visualize Results

import matplotlib.pyplot as plt

fig, axes = plt.subplots(4, 1, figsize=(10, 10), sharex=True)
t = res.time

# Row 1: Pulsed input stimulus
ax = axes[0]
ax.plot(t, input_signal, 'k', lw=2)
ax.set_ylabel("Input\nStimulus")
ax.set_title("Short-Term Synaptic Plasticity: Pre → Synapses → Post")
ax.set_ylim(-0.5, 6)

# Row 2: Pre-synaptic activity (state variable: r)
ax = axes[1]
pre_r = res.get_region('PreSynaptic').get_state_variable('r')
ax.plot(t, pre_r.data.squeeze(), 'k', lw=2, label="Pre-synaptic Rate")
ax.set_ylabel("Pre-synaptic\nRate")
ax.legend(loc='upper right')

# Row 3: Synaptic variables
ax = axes[2]
dep_x = res.get_region('DepressionSynapse').get_state_variable('x')
fac_u = res.get_region('FacilitationSynapse').get_state_variable('u')
tso_x = res.get_region('TsodyksSynapse').get_state_variable('x')
tso_u = res.get_region('TsodyksSynapse').get_state_variable('u')

ax.plot(t, dep_x.data.squeeze(), 'C0', lw=2, label="Depression (x)")
ax.plot(t, fac_u.data.squeeze(), 'C1', lw=2, label="Facilitation (u)")
ax.plot(t, tso_x.data.squeeze(), 'C2', lw=2, label="Tsodyks x")
ax.plot(t, tso_u.data.squeeze(), 'C3', lw=2, label="Tsodyks u")
ax.set_ylabel("Synaptic\nVariables")
ax.legend(loc='right')
ax.axhline(1.0, color='gray', ls=':', alpha=0.5)

# Row 4: Post-synaptic response (state variable: r)
ax = axes[3]
post_r = res.get_region('PostSynaptic').get_state_variable('r')
ax.plot(t, post_r.data.squeeze(), 'C4', lw=2, label="Post-synaptic Rate")
ax.set_ylabel("Post-synaptic\nRate")
ax.set_xlabel("Time (ms)")
ax.legend(loc='upper right')

plt.tight_layout()
plt.show()
/var/folders/ym/9kw1g21j1nd7kwfn8c0z3st40000gn/T/ipykernel_71632/4283454728.py:43: UserWarning: Glyph 8594 (\N{RIGHTWARDS ARROW}) missing from font(s) Helvetica.
  plt.tight_layout()
/Users/leonmartin_bih/tools/tvbo/.venv/lib/python3.12/site-packages/IPython/core/pylabtools.py:170: UserWarning: Glyph 8594 (\N{RIGHTWARDS ARROW}) missing from font(s) Helvetica.
  fig.canvas.print_figure(bytes_io, **kw)