|
| 1 | +""" |
| 2 | +**Example**: Stability analysis of a heterogeneous network. |
| 3 | +
|
| 4 | +This example extends heterogeneous_network.py to demonstrate how to |
| 5 | +analyze the stability of a network's fixed point by computing the |
| 6 | +eigenvalues of the Jacobian matrix. |
| 7 | +
|
| 8 | +""" |
| 9 | +import os |
| 10 | +import jax |
| 11 | +import jax.numpy as jp |
| 12 | +from jax.tree_util import tree_flatten, tree_unflatten |
| 13 | +import vbjax as vb |
| 14 | +import collections |
| 15 | +import matplotlib.pyplot as plt |
| 16 | + |
| 17 | +# Create a directory for saving images |
| 18 | +os.makedirs('images', exist_ok=True) |
| 19 | + |
| 20 | +# 1. Define the network structure (same as heterogeneous_network.py) |
| 21 | +n_jr_nodes = 4 |
| 22 | +n_mpr_nodes = 2 |
| 23 | +n_total_nodes = n_jr_nodes + n_mpr_nodes |
| 24 | + |
| 25 | +connectivity = jp.zeros((n_total_nodes, n_total_nodes)) |
| 26 | +connectivity = connectivity.at[:n_jr_nodes, :n_jr_nodes].set(0.2) |
| 27 | +connectivity = connectivity.at[n_jr_nodes:, n_jr_nodes:].set(0.2) |
| 28 | +connectivity = connectivity.at[n_jr_nodes:, :n_jr_nodes].set(0.1) |
| 29 | +connectivity = connectivity.at[jp.diag_indices(n_total_nodes)].set(0) |
| 30 | + |
| 31 | +# 2. Define PyTrees for the heterogeneous network |
| 32 | +HeteroState = collections.namedtuple('HeteroState', ['jr', 'mpr']) |
| 33 | +HeteroTheta = collections.namedtuple('HeteroTheta', ['jr', 'mpr']) |
| 34 | + |
| 35 | +# 3. Define the network dynamics function (dfun) |
| 36 | +def hetero_dfun(state, p, coupling_strength): |
| 37 | + jr_state, mpr_state = state |
| 38 | + jr_p, mpr_p = p |
| 39 | + |
| 40 | + jr_r = jr_state.y0 |
| 41 | + mpr_r = mpr_state.r |
| 42 | + all_r = jp.concatenate([jr_r, mpr_r]) |
| 43 | + |
| 44 | + coupling_input = coupling_strength * connectivity @ all_r |
| 45 | + jr_coupling_input = coupling_input[:n_jr_nodes] |
| 46 | + mpr_coupling_input = (coupling_input[n_jr_nodes:], 0.0) |
| 47 | + |
| 48 | + d_jr_array = vb.jr_dfun(jr_state, jr_coupling_input, jr_p) |
| 49 | + d_mpr_array = vb.mpr_dfun(mpr_state, mpr_coupling_input, mpr_p) |
| 50 | + |
| 51 | + d_jr = vb.JRState(*[d_jr_array[i] for i in range(len(vb.JRState._fields))]) |
| 52 | + d_mpr = vb.MPRState(*[d_mpr_array[i] for i in range(len(vb.MPRState._fields))]) |
| 53 | + |
| 54 | + return HeteroState(jr=d_jr, mpr=d_mpr) |
| 55 | + |
| 56 | +# 4. Set up simulation parameters |
| 57 | +dt = 0.1 |
| 58 | +coupling_strength = 0.05 |
| 59 | + |
| 60 | +# Initial states and parameters |
| 61 | +jr_state_init = vb.JRState(*[jp.ones(n_jr_nodes) * v for v in (0.1, 0.2, 0.3, 0.4, 0.5, 0.6)]) |
| 62 | +mpr_state_init = vb.MPRState(r=jp.ones(n_mpr_nodes) * 0.1, V=jp.ones(n_mpr_nodes) * -2.0) |
| 63 | +initial_state = HeteroState(jr=jr_state_init, mpr=mpr_state_init) |
| 64 | +params = HeteroTheta(jr=vb.jr_default_theta, mpr=vb.mpr_default_theta) |
| 65 | + |
| 66 | +# Define a deterministic dfun for finding the fixed point |
| 67 | +dfun = lambda state, p: hetero_dfun(state, p, coupling_strength) |
| 68 | + |
| 69 | +# 5. Find a fixed point of the system |
| 70 | +# We'll do this by running a deterministic simulation until it settles. |
| 71 | +# This gives an approximation of a stable fixed point. |
| 72 | +print("Finding fixed point...") |
| 73 | +run_sim = vb.make_ode(dt, dfun) |
| 74 | +# Run for a bit to let transients pass away |
| 75 | +fp_state = run_sim(initial_state, params, time=1000.0) |
| 76 | +print("Fixed point found.") |
| 77 | + |
| 78 | +# 6. Linearize the system and find eigenvalues |
| 79 | +# To use jax.jacobian, we need to work with flat vectors, not PyTrees. |
| 80 | +# We'll create functions to flatten the state PyTree and wrap the dfun. |
| 81 | + |
| 82 | +# Get the PyTree structure definition from our initial state |
| 83 | +_, state_tree_def = tree_flatten(initial_state) |
| 84 | + |
| 85 | +def flatten_state(state_pytree): |
| 86 | + "Flattens a state PyTree into a single 1D array." |
| 87 | + leaves, _ = tree_flatten(state_pytree) |
| 88 | + return jp.concatenate([leaf.ravel() for leaf in leaves]) |
| 89 | + |
| 90 | +def unflatten_state(flat_state_vec, tree_def): |
| 91 | + "Unflattens a 1D array back into a state PyTree." |
| 92 | + # Get the shapes of the leaves from a template PyTree |
| 93 | + template_leaves, _ = tree_flatten(tree_def.unflatten([0]*tree_def.num_leaves)) |
| 94 | + leaf_shapes = [leaf.shape for leaf in template_leaves] |
| 95 | + |
| 96 | + # Unflatten the vector |
| 97 | + leaf_sizes = [jp.prod(jp.array(s)) for s in leaf_shapes] |
| 98 | + indices = jp.cumsum(jp.array([0] + leaf_sizes)) |
| 99 | + leaves = [ |
| 100 | + flat_state_vec[indices[i]:indices[i+1]].reshape(leaf_shapes[i]) |
| 101 | + for i in range(len(leaf_shapes)) |
| 102 | + ] |
| 103 | + return tree_unflatten(tree_def, leaves) |
| 104 | + |
| 105 | +def flat_dfun(flat_state_vec, p): |
| 106 | + "A wrapper for dfun that works with flattened state vectors." |
| 107 | + state = unflatten_state(flat_state_vec, state_tree_def) |
| 108 | + d_state_pytree = dfun(state, p) |
| 109 | + return flatten_state(d_state_pytree) |
| 110 | + |
| 111 | +# Now we can compute the Jacobian at the fixed point |
| 112 | +print("Computing Jacobian and eigenvalues...") |
| 113 | +fp_flat = flatten_state(fp_state) |
| 114 | +jacobian_matrix = jax.jacobian(flat_dfun)(fp_flat, params) |
| 115 | +eigenvalues = jp.linalg.eigvals(jacobian_matrix) |
| 116 | +print("Done.") |
| 117 | + |
| 118 | +# 7. Plot the eigenvalues in the complex plane |
| 119 | +print("Plotting results...") |
| 120 | +plt.figure(figsize=(8, 8)) |
| 121 | +plt.scatter(jp.real(eigenvalues), jp.imag(eigenvalues), c='b', marker='o') |
| 122 | +plt.axvline(0, color='r', linestyle='--', label='Stability Boundary (Re=0)') |
| 123 | +plt.title(f'Eigenvalues of the Jacobian (g={coupling_strength})') |
| 124 | +plt.xlabel('Real Part') |
| 125 | +plt.ylabel('Imaginary Part') |
| 126 | +plt.grid(True) |
| 127 | +plt.legend() |
| 128 | +plt.axis('equal') |
| 129 | +plt.savefig('images/heterogeneous_network_eigenvalues.png') |
| 130 | +plt.show() |
| 131 | + |
| 132 | +print("Plot saved to images/heterogeneous_network_eigenvalues.png") |
| 133 | + |
| 134 | +# Interpretation: |
| 135 | +# If all eigenvalues are in the left half-plane (real part < 0), the |
| 136 | +# fixed point is stable. If any eigenvalue has a positive real part, |
| 137 | +# the fixed point is unstable. By running this analysis for different |
| 138 | +# values of `coupling_strength`, you can find bifurcations where the |
| 139 | +# system's stability changes. |
0 commit comments