Reduced Wong-Wang BOLD FC Optimization via tvboptim

Complete BOLD functional connectivity simulation & optimization: simulation, exploration, global and regional parameter fitting.

import os

device_count = os.environ.get("TVBO_XLA_DEVICE_COUNT", "1")
os.environ["XLA_FLAGS"] = f"--xla_force_host_platform_device_count={device_count}"

from tvbo import SimulationExperiment, Network

exp = SimulationExperiment.from_db("RWW_BOLD_FC_Optimization")
results = exp.run("tvboptim")
INFO [tvbo.run] [+0s] STEP 1: Running simulation...
INFO [tvbo.run] [+3s]   Simulation period: 120000.0 ms, dt: 4.0 ms
INFO [tvbo.run] [+3s]   Transient period: 120000.0 ms
INFO [tvbo.run] [+3s]   Simulation complete.
INFO [tvbo.run] [+3s] STEP 2: Running explorations...
INFO [tvbo.run] [+3s]   > parameter_landscape
INFO [tvbo.run]   grid batch 3/79 (3%)
INFO [tvbo.run]   grid batch 6/79 (7%)
INFO [tvbo.run]   grid batch 9/79 (11%)
INFO [tvbo.run]   grid batch 12/79 (15%)
INFO [tvbo.run]   grid batch 15/79 (18%)
INFO [tvbo.run]   grid batch 18/79 (22%)
INFO [tvbo.run]   grid batch 21/79 (26%)
INFO [tvbo.run]   grid batch 24/79 (30%)
INFO [tvbo.run]   grid batch 27/79 (34%)
INFO [tvbo.run]   grid batch 30/79 (37%)
INFO [tvbo.run]   grid batch 33/79 (41%)
INFO [tvbo.run]   grid batch 36/79 (45%)
INFO [tvbo.run]   grid batch 39/79 (49%)
INFO [tvbo.run]   grid batch 42/79 (53%)
INFO [tvbo.run]   grid batch 45/79 (56%)
INFO [tvbo.run]   grid batch 48/79 (60%)
INFO [tvbo.run]   grid batch 51/79 (64%)
INFO [tvbo.run]   grid batch 54/79 (68%)
INFO [tvbo.run]   grid batch 57/79 (72%)
INFO [tvbo.run]   grid batch 60/79 (75%)
INFO [tvbo.run]   grid batch 63/79 (79%)
INFO [tvbo.run]   grid batch 66/79 (83%)
INFO [tvbo.run]   grid batch 69/79 (87%)
INFO [tvbo.run]   grid batch 72/79 (91%)
INFO [tvbo.run]   grid batch 75/79 (94%)
INFO [tvbo.run]   grid batch 78/79 (98%)
INFO [tvbo.run]   grid batch 79/79 (100%)
INFO [tvbo.run] [+205s]   Explorations complete.
INFO [tvbo.run] [+205s] STEP 4: Running optimization...
INFO [tvbo.run] [+205s]   Multi-stage optimization: 2 stages
INFO [tvbo.run] [+205s] 
>>> Stage 1/2: global_optimization
INFO [tvbo.run] [+205s]     Free parameters: w, G
INFO [tvbo.run]   step 0/300: loss=0.291598
INFO [tvbo.run]   step 10/300: loss=0.283945
INFO [tvbo.run]   step 20/300: loss=0.253079
INFO [tvbo.run]   step 30/300: loss=0.257406
INFO [tvbo.run]   step 40/300: loss=0.258184
INFO [tvbo.run]   step 50/300: loss=0.247087
INFO [tvbo.run]   step 60/300: loss=0.24912
INFO [tvbo.run]   step 70/300: loss=0.246245
INFO [tvbo.run]   step 80/300: loss=0.24478
INFO [tvbo.run]   step 90/300: loss=0.244793
INFO [tvbo.run]   step 100/300: loss=0.244563
INFO [tvbo.run]   step 110/300: loss=0.244523
INFO [tvbo.run]   step 120/300: loss=0.244534
INFO [tvbo.run]   step 130/300: loss=0.244205
INFO [tvbo.run]   step 140/300: loss=0.244114
INFO [tvbo.run]   step 150/300: loss=0.243969
INFO [tvbo.run]   step 160/300: loss=0.243847
INFO [tvbo.run]   step 170/300: loss=0.243716
INFO [tvbo.run]   step 180/300: loss=0.243584
INFO [tvbo.run]   step 190/300: loss=0.243448
INFO [tvbo.run]   step 200/300: loss=0.243308
INFO [tvbo.run]   step 210/300: loss=0.243165
INFO [tvbo.run]   step 220/300: loss=0.243019
INFO [tvbo.run]   step 230/300: loss=0.24287
INFO [tvbo.run]   step 240/300: loss=0.242719
INFO [tvbo.run]   step 250/300: loss=0.242566
INFO [tvbo.run]   step 260/300: loss=0.242411
INFO [tvbo.run]   step 270/300: loss=0.242255
INFO [tvbo.run]   step 280/300: loss=0.242097
INFO [tvbo.run]   step 290/300: loss=0.241938
INFO [tvbo.run] [+354s] 
>>> Stage 2/2: regional_optimization
INFO [tvbo.run] [+354s]     Free parameters: w, I_o
INFO [tvbo.run] [+354s]     Warmup from: global_optimization
INFO [tvbo.run]   step 0/200: loss=0.241778
INFO [tvbo.run]   step 10/200: loss=0.254507
INFO [tvbo.run]   step 20/200: loss=0.231994
INFO [tvbo.run]   step 30/200: loss=0.199109
INFO [tvbo.run]   step 40/200: loss=0.18676
INFO [tvbo.run]   step 50/200: loss=0.179324
INFO [tvbo.run]   step 60/200: loss=0.171835
INFO [tvbo.run]   step 70/200: loss=0.169147
INFO [tvbo.run]   step 80/200: loss=0.179785
INFO [tvbo.run]   step 90/200: loss=0.170133
INFO [tvbo.run]   step 100/200: loss=0.165327
INFO [tvbo.run]   step 110/200: loss=0.161561
INFO [tvbo.run]   step 120/200: loss=0.164299
INFO [tvbo.run]   step 130/200: loss=0.159098
INFO [tvbo.run]   step 140/200: loss=0.15695
INFO [tvbo.run]   step 150/200: loss=0.156922
INFO [tvbo.run]   step 160/200: loss=0.156178
INFO [tvbo.run]   step 170/200: loss=0.153622
INFO [tvbo.run]   step 180/200: loss=0.154437
INFO [tvbo.run]   step 190/200: loss=0.152177
INFO [tvbo.run] [+449s]   Multi-stage optimization complete
INFO [tvbo.run] [+449s] Experiment complete.

Results

# | fig-cap: "**RWW BOLD FC Optimization.** (A) Neural activity. (B) BOLD signal. (C) Parameter landscape with trajectory. (D-F) FC matrices: target, global, regional. (G-H) FC scatter plots. (I-J) Fitted regional parameters."

import bsplot
from matplotlib.colors import Normalize
import matplotlib.pyplot as plt
import matplotlib.patheffects as path_effects
import jax.numpy as jnp
import numpy as np
from tvboptim.observations.observation import fc_corr

mosaic = """
AABBCC
DDEEFF
GGHHII
"""
fig, axes = plt.subplot_mosaic(mosaic, layout='compressed', figsize=(8, 6))
cmap = plt.cm.cividis

fc_target = results.integration.observations.empirical_fc

# A: Neural Activity
ax = axes["A"]
t_max = int(1000 / exp.integration.step_size)
data = np.asarray(results.integration.data[:t_max, 0, :])
norm = Normalize(vmin=data.mean(0).min(), vmax=data.mean(0).max())
for i in range(data.shape[1]):
    ax.plot(
        np.asarray(results.integration.time[:t_max]),
        data[:, i],
        color=cmap(norm(data.mean(0)[i])),
        lw=0.5,
    )
ax.set(xlabel="Time [ms]", ylabel="S [a.u.]", title="Neural Activity")

# B: BOLD Signal
ax = axes["B"]
bold = results.integration.observations.bold
data = np.asarray(bold.data[:60, 0, :])
norm = Normalize(vmin=data.mean(0).min(), vmax=data.mean(0).max())
for i in range(data.shape[1]):
    ax.plot(np.asarray(bold.time[:60]), data[:, i], color=cmap(norm(data.mean(0)[i])), lw=0.8)
ax.set(xlabel="Time [s]", ylabel="BOLD [a.u.]", title="BOLD Signal")

# C: Parameter Landscape with Trajectory
ax = axes["C"]
expl = results.exploration.parameter_landscape
# Labeled loss landscape: as_grid() is an xarray keyed by the dotted parameters,
# so orient it by name (order-independent) and read the coords straight off it.
loss = expl.as_grid().transpose("ReducedWongWang.w", "FastLinearCoupling.G")
w_vals, G_vals = loss["ReducedWongWang.w"].values, loss["FastLinearCoupling.G"].values
im = ax.imshow(
    loss.values,
    cmap="cividis_r",
    extent=[G_vals.min(), G_vals.max(), w_vals.min(), w_vals.max()],
    origin="lower",
    aspect="auto",
)
plt.colorbar(im, ax=ax, label="Loss", shrink=0.8)

# Initial & fitted points
for label, G, w, dy in [
    (
        "Initial",
        results.state.coupling.FastLinearCoupling.G,
        results.state.dynamics.w,
        0.03,
    ),
    (
        "Optimized",
        results.global_optimization.fitted_params.coupling.FastLinearCoupling.G.value,
        results.global_optimization.fitted_params.dynamics.w.value,
        -0.05,
    ),
]:
    ax.scatter(
        G, w, color="white", s=80, marker="o", edgecolors="k", linewidths=2, zorder=5
    )
    ax.annotate(
        label,
        (G, w),
        xytext=(G, w + dy),
        color="white",
        fontweight="bold",
        ha="center",
        path_effects=[path_effects.withStroke(linewidth=3, foreground="black")],
    )

# Trajectory
route = results.optimizations.global_optimization.state_trajectory
ax.scatter(
    [s.coupling.FastLinearCoupling.G.value for s in route],
    [s.dynamics.w.value for s in route],
    color="white",
    s=10,
    marker="o",
    edgecolors="k",
    linewidths=0.5,
    zorder=4,
)
ax.set(xlabel="G", ylabel="w", title="Exploration")

# D-F: FC Matrices
for key, fc, title in [
    ("D", np.array(results.optimizations.global_optimization.simulation.observations.fc), f"Global"),
    ("E", np.array(results.optimizations.regional_optimization.simulation.observations.fc), f"Regional"),
    ("F", fc_target, "Target FC"),
]:
    ax = axes[key]
    fc_plot = np.copy(fc)
    np.fill_diagonal(fc_plot, np.nan)
    ax.imshow(fc_plot, cmap="cividis", vmin=0, vmax=0.9)
    ax.set(xticks=[], yticks=[], title=title)

# G-H: Scatter Plots
triu = np.triu_indices_from(fc_target, k=1)
for key, fc, title in [
    ("G", results.optimizations.global_optimization.simulation.observations.fc, "Global Fit"),
    ("H", results.optimizations.regional_optimization.simulation.observations.fc, "Regional Fit"),
]:
    ax = axes[key]
    ax.scatter(fc_target[triu], np.array(fc)[triu], alpha=0.3, s=8, color="royalblue")
    ax.plot([0, 1], [0, 1], "k--", lw=1.5)
    ax.set(
        xlabel="Empirical FC",
        ylabel="Simulated FC",
        title=f"r={fc_corr(fc, fc_target):.3f}",
        aspect="equal",
    )

# I: Fitted Regional Parameters (dual y-axis)
mean_conn = exp.network.weights.mean(axis=1)
opt_g, opt_r = results.global_optimization, results.regional_optimization

ax1 = axes["I"]

ax2 = ax1.twinx()

# Left axis: w (blue)
ax1.scatter(
    mean_conn,
    opt_r.fitted_params.dynamics.w.value.flatten(),
    alpha=0.7,
    s=5,
    color="royalblue",
    edgecolors="k",
    lw=0.5,
    label="w (regional)",
)
ax1.axhline(
    opt_g.fitted_params.dynamics.w.value,
    color="royalblue",
    ls="--",
    lw=2,
    label=f"w (global): {float(opt_g.fitted_params.dynamics.w.value):.3f}",
)
ax1.set_xlabel("Mean Connectivity")
ax1.set_ylabel("w", color="royalblue")
ax1.tick_params(axis="y", labelcolor="royalblue")

# Right axis: I_o (orange)
ax2.scatter(
    mean_conn,
    opt_r.fitted_params.dynamics.I_o.value.flatten(),
    alpha=0.7,
    s=5,
    color="darkorange",
    edgecolors="k",
    lw=0.5,
    label="I_o (regional)",
)
ax2.axhline(
    opt_g.fitted_params.dynamics.I_o,
    color="darkorange",
    ls="--",
    lw=2,
    label=f"I_o (global): {float(opt_g.fitted_params.dynamics.I_o):.3f}",
)
ax2.set_ylabel("I_o", color="darkorange")
ax2.tick_params(axis="y", labelcolor="darkorange")

ax1.set_title("Regional Parameters")
# lines1, labels1 = ax1.get_legend_handles_labels()
# lines2, labels2 = ax2.get_legend_handles_labels()
# ax1.legend(lines1 + lines2, labels1 + labels2, loc="upper right", fontsize=8)

plt.suptitle(
    "Reduced Wong-Wang BOLD FC Optimization", fontsize=14, fontweight="bold", y=1
)
bsplot.style.format_fig(fig)
##
Figure 1