Initial commit

Co-authored-by: Aradhana Dube <a.dube@rug.nl>
Co-authored-by: Renzo I. Barraza Altamirano <r.i.barraza.altamirano@rug.nl>
Co-authored-by: Paolo Gibertini <p.gibertini@rug.nl>
Co-authored-by: Luca D. Fehlings <l.d.fehlings@rug.nl>
This commit is contained in:
75 files changed
+447515

No files matched your search

@@ -0,0 +1,847 @@
{
"cells": [
{
"cell_type": "code",
"execution_count": 1,
"id": "593c2428",
"metadata": {},
"outputs": [],
"source": [
"import diffrax\n",
"import jax\n",
"import jax.numpy as jnp\n",
"import matplotlib.pyplot as plt\n",
"import numpy as np\n",
"\n",
"plt.style.use(\"seaborn-v0_8-whitegrid\")\n",
"plt.rcParams[\"figure.figsize\"] = [14, 10]\n",
"plt.rcParams[\"font.size\"] = 11\n",
"plt.rcParams[\"axes.labelsize\"] = 12\n",
"plt.rcParams[\"axes.titlesize\"] = 13"
]
},
{
"cell_type": "markdown",
"id": "ebbc1603",
"metadata": {},
"source": [
"## System definition"
]
},
{
"cell_type": "code",
"execution_count": 2,
"id": "075bfd5d",
"metadata": {},
"outputs": [
{
"data": {
"text/plain": [
"2.58e-13"
]
},
"execution_count": 2,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"129e-15 / 0.5"
]
},
{
"cell_type": "code",
"execution_count": 3,
"id": "f40d4a9b",
"metadata": {},
"outputs": [],
"source": [
"# Neuromorphic system parameters (dimensionless)\n",
"alpha = 0.5 # I_n0 / I_bias ratio\n",
"beta = 1.5 # k / U_t (inverse thermal scale)\n",
"gamma = 0.26 # coupling coefficient\n",
"kappa = 5.0 # tanh steepness\n",
"sigma = 0.6 # bias scaling (s * I_bias normalized)\n",
"\n",
"# Classic Lotka-Volterra parameters\n",
"a = 1.0 # prey growth rate\n",
"b = 0.5 # predation rate\n",
"c = 0.5 # predator death rate\n",
"d = 0.5 # predator growth from prey\n",
"\n",
"\n",
"def z_func(u, v):\n",
" \"\"\"Coupling function\"\"\"\n",
" return jnp.tanh(kappa * (u - v))\n",
"\n",
"\n",
"def neuromorphic_system(t, state, args):\n",
" \"\"\"\n",
" Neuromorphic oscillator interpreted as predator-prey\n",
" u = prey, v = predator\n",
" \"\"\"\n",
" u, v = state\n",
" z = z_func(u, v)\n",
"\n",
" # Prey dynamics\n",
" du = z * (1 - alpha * jnp.exp(beta * v) * (1 + gamma * (0.5 - u))) - sigma\n",
"\n",
" # Predator dynamics\n",
" dv = z * (-1 + alpha * jnp.exp(beta * u) * (1 + gamma * (0.5 - v))) - sigma\n",
"\n",
" return jnp.array([du, dv])\n",
"\n",
"\n",
"def lotka_volterra(t, state, args):\n",
" \"\"\"Classic predator-prey system\"\"\"\n",
" x, y = state\n",
" dx = x * (a - b * y)\n",
" dy = y * (-c + d * x)\n",
" return jnp.array([dx, dy])\n",
"\n",
"\n",
"def generalized_predator_prey(state, t):\n",
" \"\"\"\n",
" Modified Lotka-Volterra with saturation (for fairer comparison)\n",
" \"\"\"\n",
" x, y = state\n",
" K = 3.0 # carrying capacity\n",
" dx = x * (a * (1 - x / K) - b * y)\n",
" dy = y * (-c + d * x)\n",
" return jnp.array([dx, dy])\n",
"\n",
"\n",
"def solve(dyn, y0, p, T, n=1000):\n",
" sol = diffrax.diffeqsolve(\n",
" diffrax.ODETerm(dyn),\n",
" diffrax.Tsit5(),\n",
" t0=0.0,\n",
" t1=T,\n",
" dt0=0.01,\n",
" y0=y0,\n",
" args=p,\n",
" saveat=diffrax.SaveAt(ts=jnp.linspace(0, T, n)),\n",
" stepsize_controller=diffrax.PIDController(rtol=1e-6, atol=1e-8),\n",
" max_steps=50000,\n",
" )\n",
" return sol.ts, sol.ys"
]
},
{
"cell_type": "code",
"execution_count": 4,
"id": "c7d5821a",
"metadata": {},
"outputs": [],
"source": [
"def compute_nullclines_neuromorphic(u_range, v_range, resolution=200):\n",
" \"\"\"\n",
" Compute nullclines for neuromorphic system\n",
" du/dt = 0 (u-nullcline)\n",
" dv/dt = 0 (v-nullcline)\n",
" \"\"\"\n",
" u_vals = jnp.linspace(u_range[0], u_range[1], resolution)\n",
" v_vals = jnp.linspace(v_range[0], v_range[1], resolution)\n",
" U, V = jnp.meshgrid(u_vals, v_vals)\n",
"\n",
" Z = z_func(U, V)\n",
"\n",
" # du/dt\n",
" dU = Z * (1 - alpha * jnp.exp(beta * V) * (1 + gamma * (0.5 - U))) - sigma\n",
"\n",
" # dv/dt\n",
" dV = Z * (-1 + alpha * jnp.exp(beta * U) * (1 + gamma * (0.5 - V))) - sigma\n",
"\n",
" return U, V, dU, dV\n",
"\n",
"\n",
"def compute_nullclines_lotka_volterra(x_range, y_range, resolution=200):\n",
" \"\"\"Compute nullclines for classic Lotka-Volterra\"\"\"\n",
" x_vals = jnp.linspace(x_range[0], x_range[1], resolution)\n",
" y_vals = jnp.linspace(y_range[0], y_range[1], resolution)\n",
" X, Y = jnp.meshgrid(x_vals, y_vals)\n",
"\n",
" # dx/dt = x(a - by)\n",
" dX = X * (a - b * Y)\n",
"\n",
" # dy/dt = y(-c + dx)\n",
" dY = Y * (-c + d * X)\n",
"\n",
" return X, Y, dX, dY"
]
},
{
"cell_type": "markdown",
"id": "fd15b70a",
"metadata": {},
"source": [
"## Fixpoint analysis"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "f5a7f679",
"metadata": {},
"outputs": [
{
"data": {
"text/plain": [
"[[0.0, 0.0], [1.0, 2.0]]"
]
},
"execution_count": 5,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"def newton_fp(f, x0, p, tol=1e-12, iters=50):\n",
" x = jnp.array(x0)\n",
" for _ in range(iters):\n",
" fx = f(0.0, x, p)\n",
" J = jax.jacrev(lambda y: f(0.0, y, p))(x)\n",
" x = x - jnp.linalg.solve(J, fx)\n",
" if jnp.linalg.norm(fx) < tol:\n",
" return x, True\n",
" return x, False\n",
"\n",
"\n",
"def find_fixed_points(fn):\n",
" \"\"\"Find fixed points numerically\"\"\"\n",
" fixed_points = []\n",
"\n",
" # Try multiple initial guesses\n",
" initial_guesses = [\n",
" [0.0, 0.0],\n",
" [0.5, 0.5],\n",
" [-0.5, -0.5],\n",
" [1.0, 0.0],\n",
" [0.0, 1.0],\n",
" [-1.0, 0.0],\n",
" [0.5, -0.5],\n",
" [-0.5, 0.5],\n",
" [0.3, 0.3],\n",
" [2.0, 0.0],\n",
" [0.0, 2.0],\n",
" [-2.0, 0.0],\n",
" [1.5, -1.5],\n",
" [-1.5, 1.5],\n",
" [1.3, 1.3],\n",
" [2.0, -1.5],\n",
" [-2.5, 1.5],\n",
" [2, 1.5],\n",
" ]\n",
"\n",
" for guess in initial_guesses:\n",
" point, _ = newton_fp(fn, guess, None)\n",
"\n",
" # Check if it's actually a fixed point\n",
" residual = jnp.linalg.norm(fn(0, point, None))\n",
" if residual < 1e-6:\n",
" # Check if we already have this point\n",
" is_new = True\n",
" for fp in fixed_points:\n",
" if jnp.linalg.norm(jnp.array(fp) - jnp.array(point)) < 0.01:\n",
" is_new = False\n",
" break\n",
" if is_new:\n",
" fixed_points.append(point.tolist())\n",
"\n",
" return fixed_points\n",
"\n",
"\n",
"# find_fixed_points(neuromorphic_system)\n",
"find_fixed_points(lotka_volterra)"
]
},
{
"cell_type": "markdown",
"id": "daaa31a8",
"metadata": {},
"source": [
"## Stability"
]
},
{
"cell_type": "code",
"execution_count": 6,
"id": "ec312bf8",
"metadata": {},
"outputs": [],
"source": [
"def calc_jacobian(u, v, fn, eps=1e-6):\n",
" \"\"\"Numerical Jacobian for neuromorphic system\"\"\"\n",
" jac = jax.jacobian(fn, argnums=1)(0, [u, v], None)\n",
"\n",
" J = jnp.zeros((2, 2))\n",
" # Partial derivatives\n",
" J = J.at[0, 0].set(jac[0][0])\n",
" J = J.at[0, 1].set(jac[0][1])\n",
" J = J.at[1, 0].set(jac[1][0])\n",
" J = J.at[1, 1].set(jac[1][1])\n",
"\n",
" return J\n",
"\n",
"\n",
"def analyze_stability(J):\n",
" \"\"\"Analyze stability from Jacobian eigenvalues\"\"\"\n",
" eigenvalues = jnp.linalg.eigvals(J)\n",
"\n",
" real_parts = jnp.real(eigenvalues)\n",
" imag_parts = jnp.imag(eigenvalues)\n",
"\n",
" # Classification\n",
" if all(r < 0 for r in real_parts):\n",
" if any(abs(i) > 1e-10 for i in imag_parts):\n",
" stability = \"Stable Focus (spiral sink)\"\n",
" else:\n",
" stability = \"Stable Node\"\n",
" elif all(r > 0 for r in real_parts):\n",
" if any(abs(i) > 1e-10 for i in imag_parts):\n",
" stability = \"Unstable Focus (spiral source)\"\n",
" else:\n",
" stability = \"Unstable Node\"\n",
" elif real_parts[0] * real_parts[1] < 0:\n",
" stability = \"Saddle Point\"\n",
" else:\n",
" if any(abs(i) > 1e-10 for i in imag_parts):\n",
" stability = \"Center (neutral)\"\n",
" else:\n",
" stability = \"Non-hyperbolic\"\n",
"\n",
" return eigenvalues, stability"
]
},
{
"cell_type": "markdown",
"id": "37996ccd",
"metadata": {},
"source": [
"# Main"
]
},
{
"cell_type": "code",
"execution_count": 7,
"id": "640c686a",
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"======================================================================\n",
"PREDATOR-PREY ANALYSIS: NEUROMORPHIC vs LOTKA-VOLTERRA\n",
"======================================================================\n",
"\n",
"======================================================================\n",
"1. FIXED POINT ANALYSIS\n",
"======================================================================\n",
"\n",
"--- Neuromorphic System ---\n",
"Found 2 fixed point(s):\n",
" FP1: (u*, v*) = (0.6762, -0.1182)\n",
" Eigenvalues: λ₁ = -0.1247+1.1803j, λ₂ = -0.1247-1.1803j\n",
" Classification: Stable Focus (spiral sink)\n",
" FP2: (u*, v*) = (-0.1182, 0.6762)\n",
" Eigenvalues: λ₁ = -0.1247+1.1803j, λ₂ = -0.1247-1.1803j\n",
" Classification: Stable Focus (spiral sink)\n",
"\n",
"--- Lotka-Volterra System ---\n",
"Fixed points:\n",
" FP1: (x*, y*) = (0.0000, 0.0000)\n",
" (Origin - typically a saddle)\n",
" FP2: (x*, y*) = (1.0000, 2.0000)\n",
" Eigenvalues: λ₁ = 0.0000+0.7071j, λ₂ = 0.0000-0.7071j\n",
" Classification: Center (neutral)\n",
"\n",
"======================================================================\n",
"2. NULLCLINE EQUATIONS\n",
"======================================================================\n",
"\n",
"--- Neuromorphic System ---\n",
"u-nullcline (du/dt = 0):\n",
" z·[1 - α·exp(βv)·(1 + γ(½-u))] = σ\n",
" where z = tanh(κ(u-v))\n",
"\n",
"v-nullcline (dv/dt = 0):\n",
" z·[-1 + α·exp(βu)·(1 + γ(½-v))] = σ\n",
"\n",
"--- Lotka-Volterra System ---\n",
"x-nullcline (dx/dt = 0):\n",
" x = 0 OR y = a/b\n",
"\n",
"y-nullcline (dy/dt = 0):\n",
" y = 0 OR x = c/d\n"
]
}
],
"source": [
"print(\"=\" * 70)\n",
"print(\"PREDATOR-PREY ANALYSIS: NEUROMORPHIC vs LOTKA-VOLTERRA\")\n",
"print(\"=\" * 70)\n",
"\n",
"# --- Fixed Points ---\n",
"print(\"\\n\" + \"=\" * 70)\n",
"print(\"1. FIXED POINT ANALYSIS\")\n",
"print(\"=\" * 70)\n",
"\n",
"print(\"\\n--- Neuromorphic System ---\")\n",
"fp_neuro = find_fixed_points(neuromorphic_system)\n",
"print(f\"Found {len(fp_neuro)} fixed point(s):\")\n",
"for i, fp in enumerate(fp_neuro):\n",
" print(f\" FP{i + 1}: (u*, v*) = ({fp[0]:.4f}, {fp[1]:.4f})\")\n",
" J = calc_jacobian(fp[0], fp[1], neuromorphic_system)\n",
" eigs, stability = analyze_stability(J)\n",
" print(f\" Eigenvalues: λ₁ = {eigs[0]:.4f}, λ₂ = {eigs[1]:.4f}\")\n",
" print(f\" Classification: {stability}\")\n",
"\n",
"print(\"\\n--- Lotka-Volterra System ---\")\n",
"fp_lv = find_fixed_points(lotka_volterra)\n",
"print(\"Fixed points:\")\n",
"for i, fp in enumerate(fp_lv):\n",
" print(f\" FP{i + 1}: (x*, y*) = ({fp[0]:.4f}, {fp[1]:.4f})\")\n",
" if fp[0] > 0 or fp[1] > 0: # avoid division issues at origin\n",
" J = calc_jacobian(fp[0], fp[1], lotka_volterra)\n",
" eigs, stability = analyze_stability(J)\n",
" print(f\" Eigenvalues: λ₁ = {eigs[0]:.4f}, λ₂ = {eigs[1]:.4f}\")\n",
" print(f\" Classification: {stability}\")\n",
" else:\n",
" print(\" (Origin - typically a saddle)\")\n",
"\n",
"print(\"\\n\" + \"=\" * 70)\n",
"print(\"2. NULLCLINE EQUATIONS\")\n",
"print(\"=\" * 70)\n",
"\n",
"print(\"\\n--- Neuromorphic System ---\")\n",
"print(\"u-nullcline (du/dt = 0):\")\n",
"print(\" z·[1 - α·exp(βv)·(1 + γ(½-u))] = σ\")\n",
"print(\" where z = tanh(κ(u-v))\")\n",
"print(\"\\nv-nullcline (dv/dt = 0):\")\n",
"print(\" z·[-1 + α·exp(βu)·(1 + γ(½-v))] = σ\")\n",
"\n",
"print(\"\\n--- Lotka-Volterra System ---\")\n",
"print(\"x-nullcline (dx/dt = 0):\")\n",
"print(\" x = 0 OR y = a/b\")\n",
"print(\"\\ny-nullcline (dy/dt = 0):\")\n",
"print(\" y = 0 OR x = c/d\")"
]
},
{
"cell_type": "markdown",
"id": "a81cdac9",
"metadata": {},
"source": [
"## Visualization"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "e1adaf80",
"metadata": {},
"outputs": [
{
"name": "stderr",
"output_type": "stream",
"text": [
"/tmp/ipykernel_2751701/1982221629.py:146: UserWarning: Glyph 8320 (\\N{SUBSCRIPT ZERO}) missing from font(s) Arial.\n",
" plt.tight_layout()\n",
"/home/p306945/Projects/felice/.venv/lib/python3.13/site-packages/IPython/core/pylabtools.py:170: UserWarning: Glyph 8320 (\\N{SUBSCRIPT ZERO}) missing from font(s) Arial.\n",
" fig.canvas.print_figure(bytes_io, **kw)\n"
]
},
{
"data": {
"image/png": "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 truncated
"text/plain": [
"<Figure size 1600x1400 with 4 Axes>"
]
},
"metadata": {},
"output_type": "display_data"
},
{
"name": "stdout",
"output_type": "stream",
"text": [
"\n",
"======================================================================\n",
"3. PHASE PORTRAIT SAVED\n",
"======================================================================\n",
"Saved: phase_portrait_comparison.png\n"
]
}
],
"source": [
"fig = plt.figure(figsize=(16, 14))\n",
"\n",
"# --- Plot 1: Neuromorphic Phase Portrait ---\n",
"ax1 = fig.add_subplot(2, 2, 1)\n",
"\n",
"u_range = [-1.5, 1.5]\n",
"v_range = [-1.5, 1.5]\n",
"U, V, dU, dV = compute_nullclines_neuromorphic(u_range, v_range)\n",
"\n",
"# Nullclines\n",
"ax1.contour(U, V, dU, levels=[0], colors=\"blue\", linewidths=2, linestyles=\"-\")\n",
"ax1.contour(U, V, dV, levels=[0], colors=\"red\", linewidths=2, linestyles=\"-\")\n",
"\n",
"# Vector field\n",
"u_sparse = np.linspace(u_range[0], u_range[1], 20)\n",
"v_sparse = np.linspace(v_range[0], v_range[1], 20)\n",
"Us, Vs = np.meshgrid(u_sparse, v_sparse)\n",
"dUs = np.zeros_like(Us)\n",
"dVs = np.zeros_like(Vs)\n",
"for i in range(Us.shape[0]):\n",
" for j in range(Us.shape[1]):\n",
" deriv = neuromorphic_system(0, [Us[i, j], Vs[i, j]], None)\n",
" dUs[i, j] = deriv[0]\n",
" dVs[i, j] = deriv[1]\n",
"\n",
"# Normalize for visualization\n",
"magnitude = np.sqrt(dUs**2 + dVs**2)\n",
"magnitude[magnitude == 0] = 1\n",
"dUs_norm = dUs / magnitude\n",
"dVs_norm = dVs / magnitude\n",
"\n",
"ax1.quiver(Us, Vs, dUs_norm, dVs_norm, magnitude, cmap=\"viridis\", alpha=0.6)\n",
"\n",
"# Fixed points\n",
"for fp in fp_neuro:\n",
" ax1.plot(\n",
" fp[0], fp[1], \"ko\", markersize=10, markerfacecolor=\"yellow\", markeredgewidth=2\n",
" )\n",
"\n",
"# Trajectories\n",
"t = 50 # np.linspace(0, 50, 2000)\n",
"initial_conditions = [\n",
" [0.8, 0.2],\n",
" [-0.5, 0.5],\n",
" [0.3, -0.3],\n",
" [1.0, 1.0],\n",
" [-1.0, -0.5],\n",
" [0.1, 0.8],\n",
"]\n",
"colors = plt.cm.plasma(np.linspace(0.2, 0.8, len(initial_conditions)))\n",
"\n",
"for ic, color in zip(initial_conditions, colors):\n",
" # sol = odeint(neuromorphic_system, ic, t)\n",
" ts, ys = solve(neuromorphic_system, jnp.array(ic), None, t)\n",
" ax1.plot(ys[:, 0], ys[:, 1], \"-\", color=color, linewidth=1.5, alpha=0.8)\n",
" ax1.plot(ic[0], ic[1], \"o\", color=color, markersize=6)\n",
"\n",
"ax1.set_xlabel(\"u (Prey analogue)\")\n",
"ax1.set_ylabel(\"v (Predator analogue)\")\n",
"ax1.set_title(\"Neuromorphic System: Phase Portrait\")\n",
"ax1.legend([\"u-nullcline (du/dt=0)\", \"v-nullcline (dv/dt=0)\"], loc=\"upper right\")\n",
"ax1.set_xlim(u_range)\n",
"ax1.set_ylim(v_range)\n",
"ax1.axhline(y=0, color=\"gray\", linestyle=\"--\", alpha=0.3)\n",
"ax1.axvline(x=0, color=\"gray\", linestyle=\"--\", alpha=0.3)\n",
"\n",
"# --- Plot 2: Lotka-Volterra Phase Portrait ---\n",
"ax2 = fig.add_subplot(2, 2, 2)\n",
"\n",
"x_range = [0, 4]\n",
"y_range = [0, 4]\n",
"X, Y, dX, dY = compute_nullclines_lotka_volterra(x_range, y_range)\n",
"\n",
"# Nullclines (analytical)\n",
"ax2.axvline(x=0, color=\"blue\", linewidth=2, label=\"x-nullcline (x=0)\")\n",
"ax2.axhline(\n",
" y=a / b,\n",
" color=\"blue\",\n",
" linewidth=2,\n",
" linestyle=\"--\",\n",
" label=f\"x-nullcline (y={a / b:.1f})\",\n",
")\n",
"ax2.axhline(y=0, color=\"red\", linewidth=2, label=\"y-nullcline (y=0)\")\n",
"ax2.axvline(\n",
" x=c / d,\n",
" color=\"red\",\n",
" linewidth=2,\n",
" linestyle=\"--\",\n",
" label=f\"y-nullcline (x={c / d:.1f})\",\n",
")\n",
"\n",
"# Vector field\n",
"x_sparse = np.linspace(0.1, x_range[1], 18)\n",
"y_sparse = np.linspace(0.1, y_range[1], 18)\n",
"Xs, Ys = np.meshgrid(x_sparse, y_sparse)\n",
"dXs = np.zeros_like(Xs)\n",
"dYs = np.zeros_like(Ys)\n",
"for i in range(Xs.shape[0]):\n",
" for j in range(Xs.shape[1]):\n",
" deriv = lotka_volterra(0, [Xs[i, j], Ys[i, j]], None)\n",
" dXs[i, j] = deriv[0]\n",
" dYs[i, j] = deriv[1]\n",
"\n",
"magnitude = np.sqrt(dXs**2 + dYs**2)\n",
"magnitude[magnitude == 0] = 1\n",
"dXs_norm = dXs / magnitude\n",
"dYs_norm = dYs / magnitude\n",
"\n",
"ax2.quiver(Xs, Ys, dXs_norm, dYs_norm, magnitude, cmap=\"viridis\", alpha=0.6)\n",
"\n",
"# Fixed point\n",
"ax2.plot(c / d, a / b, \"ko\", markersize=10, markerfacecolor=\"yellow\", markeredgewidth=2)\n",
"\n",
"# Trajectories\n",
"initial_conditions_lv = [\n",
" [0.5, 0.5],\n",
" [1.5, 0.5],\n",
" [2.5, 1.5],\n",
" [0.5, 2.0],\n",
" [3.0, 3.0],\n",
" [1.0, 3.0],\n",
"]\n",
"\n",
"for ic, color in zip(initial_conditions_lv, colors):\n",
" ts, ys = solve(lotka_volterra, jnp.array(ic), None, t)\n",
" ax2.plot(ys[:, 0], ys[:, 1], \"-\", color=color, linewidth=1.5, alpha=0.8)\n",
" ax2.plot(ic[0], ic[1], \"o\", color=color, markersize=6)\n",
"\n",
"ax2.set_xlabel(\"x (Prey)\")\n",
"ax2.set_ylabel(\"y (Predator)\")\n",
"ax2.set_title(\"Classic Lotka-Volterra: Phase Portrait\")\n",
"ax2.set_xlim([0, x_range[1]])\n",
"ax2.set_ylim([0, y_range[1]])\n",
"ax2.legend(loc=\"upper right\", fontsize=9)\n",
"\n",
"ax3 = fig.add_subplot(2, 2, 3)\n",
"\n",
"t_long = 100\n",
"ic_neuro = [0.8, 0.2]\n",
"ts, ys = solve(neuromorphic_system, jnp.array(ic_neuro), None, t_long)\n",
"\n",
"ax3.plot(ts, ys[:, 0], \"b-\", linewidth=2, label=\"u (Prey analogue)\")\n",
"ax3.plot(ts, ys[:, 1], \"r-\", linewidth=2, label=\"v (Predator analogue)\")\n",
"ax3.set_xlabel(\"Time τ\")\n",
"ax3.set_ylabel(\"Population\")\n",
"ax3.set_title(\n",
" f\"Neuromorphic System: Time Series (IC: u₀={ic_neuro[0]}, v₀={ic_neuro[1]})\"\n",
")\n",
"ax3.legend()\n",
"ax3.axhline(y=0, color=\"gray\", linestyle=\"--\", alpha=0.3)\n",
"\n",
"ax4 = fig.add_subplot(2, 2, 4)\n",
"\n",
"ic_lv = [1.5, 0.5]\n",
"ts, ys = solve(lotka_volterra, jnp.array(ic_lv), None, t_long)\n",
"\n",
"ax4.plot(ts, ys[:, 0], \"b-\", linewidth=2, label=\"x (Prey)\")\n",
"ax4.plot(ts, ys[:, 1], \"r-\", linewidth=2, label=\"y (Predator)\")\n",
"ax4.set_xlabel(\"Time t\")\n",
"ax4.set_ylabel(\"Population\")\n",
"ax4.set_title(f\"Lotka-Volterra: Time Series (IC: x₀={ic_lv[0]}, y₀={ic_lv[1]})\")\n",
"ax4.legend()\n",
"\n",
"plt.tight_layout()\n",
"# plt.savefig('/home/claude/phase_portrait_comparison.png', dpi=150, bbox_inches='tight')\n",
"# plt.close()\n",
"plt.show()\n",
"\n",
"print(\"\\n\" + \"=\" * 70)\n",
"print(\"3. PHASE PORTRAIT SAVED\")\n",
"print(\"=\" * 70)\n",
"print(\"Saved: phase_portrait_comparison.png\")"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "1ba0d377",
"metadata": {},
"outputs": [
{
"data": {
"image/png": "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 truncated
"text/plain": [
"<Figure size 1600x1000 with 6 Axes>"
]
},
"metadata": {},
"output_type": "display_data"
}
],
"source": [
"fig2, axes = plt.subplots(2, 3, figsize=(16, 10))\n",
"\n",
"# --- Bifurcation-like analysis: vary sigma ---\n",
"ax = axes[0, 0]\n",
"sigmas = [0.01, 0.05, 0.1, 0.2, 0.3]\n",
"colors_sigma = plt.cm.coolwarm(np.linspace(0, 1, len(sigmas)))\n",
"\n",
"for sig, col in zip(sigmas, colors_sigma):\n",
"\n",
" def neuro_temp(t, state, args):\n",
" u, v = state\n",
" z = jnp.tanh(kappa * (u - v))\n",
" du = z * (1 - alpha * jnp.exp(beta * v) * (1 + gamma * (0.5 - u))) - sig\n",
" dv = z * (-1 + alpha * jnp.exp(beta * u) * (1 + gamma * (0.5 - v))) - sig\n",
" return jnp.asarray([du, dv])\n",
"\n",
" ts, ys = solve(neuro_temp, jnp.array([0.5, 0.3]), None, 80)\n",
" ax.plot(\n",
" ys[:, 0], ys[:, 1], \"-\", color=col, linewidth=1.5, label=f\"σ={sig}\", alpha=0.8\n",
" )\n",
"\n",
"ax.set_xlabel(\"u\")\n",
"ax.set_ylabel(\"v\")\n",
"ax.set_title(\"Neuromorphic: Effect of σ (mortality)\")\n",
"ax.legend(fontsize=8)\n",
"\n",
"ax = axes[0, 1]\n",
"kappas = [1, 3, 5, 10, 20]\n",
"colors_kappa = plt.cm.viridis(np.linspace(0.2, 0.9, len(kappas)))\n",
"\n",
"for kap, col in zip(kappas, colors_kappa):\n",
"\n",
" def neuro_temp(t, state, args):\n",
" u, v = state\n",
" z = jnp.tanh(kap * (u - v))\n",
" du = z * (1 - alpha * jnp.exp(beta * v) * (1 + gamma * (0.5 - u))) - sigma\n",
" dv = z * (-1 + alpha * jnp.exp(beta * u) * (1 + gamma * (0.5 - v))) - sigma\n",
" return jnp.asarray([du, dv])\n",
"\n",
" ts, ys = solve(neuro_temp, jnp.array([0.6, 0.2]), None, 60)\n",
" ax.plot(\n",
" ys[:, 0], ys[:, 1], \"-\", color=col, linewidth=1.5, label=f\"κ={kap}\", alpha=0.8\n",
" )\n",
"\n",
"ax.set_xlabel(\"u\")\n",
"ax.set_ylabel(\"v\")\n",
"ax.set_title(\"Neuromorphic: Effect of κ (switching sharpness)\")\n",
"ax.legend(fontsize=8)\n",
"\n",
"# --- Vary alpha (interaction strength) ---\n",
"ax = axes[0, 2]\n",
"alphas = [0.2, 0.4, 0.6, 0.8, 1.0]\n",
"colors_alpha = plt.cm.plasma(np.linspace(0.2, 0.9, len(alphas)))\n",
"\n",
"for alp, col in zip(alphas, colors_alpha):\n",
"\n",
" def neuro_temp(t, state, args):\n",
" u, v = state\n",
" z = jnp.tanh(kappa * (u - v))\n",
" du = z * (1 - alp * jnp.exp(beta * v) * (1 + gamma * (0.5 - u))) - sigma\n",
" dv = z * (-1 + alp * jnp.exp(beta * u) * (1 + gamma * (0.5 - v))) - sigma\n",
" return jnp.array([du, dv])\n",
"\n",
" ts, sol = solve(neuro_temp, jnp.array([0.5, 0.3]), None, 60)\n",
" ax.plot(\n",
" sol[:, 0], sol[:, 1], \"-\", color=col, linewidth=1.5, label=f\"α={alp}\", alpha=0.8\n",
" )\n",
"\n",
"ax.set_xlabel(\"u\")\n",
"ax.set_ylabel(\"v\")\n",
"ax.set_title(\"Neuromorphic: Effect of α (interaction strength)\")\n",
"ax.legend(fontsize=8)\n",
"\n",
"# --- Compare dynamics qualitatively ---\n",
"ax = axes[1, 0]\n",
"\n",
"# Neuromorphic\n",
"t_comp, sol1 = solve(neuro_temp, jnp.array([0.7, 0.3]), None, 50)\n",
"ax.plot(t_comp, sol1[:, 0], \"b-\", linewidth=2, label=\"Neuro: u\")\n",
"ax.plot(t_comp, sol1[:, 1], \"b--\", linewidth=2, label=\"Neuro: v\")\n",
"\n",
"ax.set_xlabel(\"Time\")\n",
"ax.set_ylabel(\"State\")\n",
"ax.set_title(\"Neuromorphic Dynamics\")\n",
"ax.legend()\n",
"\n",
"# --- Lotka-Volterra comparison ---\n",
"ax = axes[1, 1]\n",
"t_comp, sol2 = solve(lotka_volterra, jnp.array([2.0, 1.0]), None, 50)\n",
"ax.plot(t_comp, sol2[:, 0], \"r-\", linewidth=2, label=\"LV: x (prey)\")\n",
"ax.plot(t_comp, sol2[:, 1], \"r--\", linewidth=2, label=\"LV: y (predator)\")\n",
"\n",
"ax.set_xlabel(\"Time\")\n",
"ax.set_ylabel(\"Population\")\n",
"ax.set_title(\"Lotka-Volterra Dynamics\")\n",
"ax.legend()\n",
"\n",
"# --- Summary comparison ---\n",
"ax = axes[1, 2]\n",
"ax.axis(\"off\")\n",
"\n",
"summary_text = \"\"\"\n",
"COMPARISON SUMMARY\n",
"══════════════════════════════════════\n",
"\n",
"┌─────────────────┬───────────────────┬───────────────────┐\n",
"│ Feature │ Neuromorphic │ Lotka-Volterra │\n",
"├─────────────────┼───────────────────┼───────────────────┤\n",
"│ Interaction │ Exponential │ Bilinear (xy) │\n",
"│ │ exp(βx) │ │\n",
"├─────────────────┼───────────────────┼───────────────────┤\n",
"│ Coupling │ Soft switch │ Direct │\n",
"│ │ tanh(κ(u-v)) │ multiplication │\n",
"├─────────────────┼───────────────────┼───────────────────┤\n",
"│ Equilibrium │ Stable focus or │ Center (neutral │\n",
"│ │ limit cycle │ stability) │\n",
"├─────────────────┼───────────────────┼───────────────────┤\n",
"│ Oscillations │ Damped or │ Persistent │\n",
"│ │ sustained │ (conservative) │\n",
"├─────────────────┼───────────────────┼───────────────────┤\n",
"│ Biological │ More realistic │ Idealized │\n",
"│ Realism │ (saturation) │ (no bounds) │\n",
"└─────────────────┴───────────────────┴───────────────────┘\n",
"\n",
"KEY INSIGHT: The neuromorphic system includes\n",
"natural saturation mechanisms (tanh, exp bounds)\n",
"that prevent unbounded growth - more realistic\n",
"for actual biological populations.\n",
"\"\"\"\n",
"\n",
"ax.text(\n",
" 0.05,\n",
" 0.95,\n",
" summary_text,\n",
" transform=ax.transAxes,\n",
" fontsize=10,\n",
" verticalalignment=\"top\",\n",
" fontfamily=\"monospace\",\n",
" bbox=dict(boxstyle=\"round\", facecolor=\"wheat\", alpha=0.5),\n",
")\n",
"\n",
"plt.tight_layout()\n",
"# plt.savefig('/home/claude/detailed_comparison.png', dpi=150, bbox_inches='tight')\n",
"# plt.close()\n",
"plt.show()"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "f0539c9f",
"metadata": {},
"outputs": [],
"source": []
}
],
"metadata": {
"kernelspec": {
"display_name": "felice",
"language": "python",
"name": "python3"
},
"language_info": {
"codemirror_mode": {
"name": "ipython",
"version": 3
},
"file_extension": ".py",
"mimetype": "text/x-python",
"name": "python",
"nbconvert_exporter": "python",
"pygments_lexer": "ipython3",
"version": "3.13.5"
}
},
"nbformat": 4,
"nbformat_minor": 5
}
@@ -0,0 +1,189 @@
{
"cells": [
{
"cell_type": "code",
"execution_count": 10,
"id": "cbf60fc3",
"metadata": {},
"outputs": [],
"source": [
"import diffrax as dfx\n",
"import jax\n",
"import jax.numpy as jnp\n",
"import jax.random as jrand\n",
"import matplotlib as mpl\n",
"import matplotlib.pyplot as plt\n",
"\n",
"from felice.neuron_models import WereRabbit\n",
"\n",
"jax.config.update(\"jax_enable_x64\", True)"
]
},
{
"cell_type": "code",
"execution_count": 11,
"id": "5f28e42f",
"metadata": {},
"outputs": [],
"source": [
"key = jrand.key(0)\n",
"max_time = 40\n",
"\n",
"model = WereRabbit(dtype=jnp.float64)\n",
"\n",
"\n",
"def state_at_t(comp_times):\n",
" sol = dfx.diffeqsolve(\n",
" terms=dfx.ODETerm(model.dynamics),\n",
" solver=dfx.Tsit5(),\n",
" t0=0.0,\n",
" t1=max_time,\n",
" dt0=1e-3,\n",
" y0=model.init_state(1)\n",
" + jrand.uniform(key, shape=(1, 2), minval=0.1, maxval=0.5),\n",
" saveat=dfx.SaveAt(ts=comp_times),\n",
" max_steps=100000,\n",
" )\n",
"\n",
" return sol.ts, sol.ys"
]
},
{
"cell_type": "code",
"execution_count": 12,
"id": "28924cfa",
"metadata": {},
"outputs": [],
"source": [
"comp_times = jnp.linspace(0.0, max_time, 2000)\n",
"_, state = state_at_t(comp_times)"
]
},
{
"cell_type": "code",
"execution_count": 13,
"id": "9d4d0829",
"metadata": {},
"outputs": [],
"source": [
"def compute_nullclines(snn, u_range, v_range, resolution=200):\n",
" \"\"\"\n",
" Compute nullclines\n",
" du/dt = 0 (u-nullcline)\n",
" dv/dt = 0 (v-nullcline)\n",
" \"\"\"\n",
" u_vals = jnp.linspace(u_range[0], u_range[1], resolution)\n",
" v_vals = jnp.linspace(v_range[0], v_range[1], resolution)\n",
" U, V = jnp.meshgrid(u_vals, v_vals)\n",
"\n",
" UV = jnp.stack(\n",
" [U.reshape(-1), V.reshape(-1), jnp.ones((resolution * resolution,))], axis=1\n",
" )\n",
" dS = snn.vector_field(UV)\n",
" dU = dS[:, 0].reshape(U.shape)\n",
" dV = dS[:, 1].reshape(V.shape)\n",
"\n",
" return U, V, dU, dV\n",
"\n",
"\n",
"def plot_vf(ax, snn, u_range, v_range):\n",
" import numpy as np\n",
"\n",
" u_sparse = jnp.linspace(u_range[0], u_range[1], 20)\n",
" v_sparse = jnp.linspace(v_range[0], v_range[1], 20)\n",
"\n",
" Us, Vs = jnp.meshgrid(u_sparse, v_sparse)\n",
"\n",
" U, V, dU, dV = compute_nullclines(snn, u_range, v_range, 200)\n",
"\n",
" UVs = jnp.stack([Us.reshape(-1), Vs.reshape(-1), jnp.ones((20 * 20,))], axis=1)\n",
" dS = snn.vector_field(UVs)\n",
" dUs = dS[:, 0].reshape(Us.shape)\n",
" dVs = dS[:, 1].reshape(Vs.shape)\n",
"\n",
" # Normalize for visualization\n",
" magnitude = np.sqrt(dUs**2 + dVs**2)\n",
" magnitude[magnitude == 0] = 1\n",
" dUs_norm = dUs / magnitude\n",
" dVs_norm = dVs / magnitude\n",
"\n",
" # Nullclines\n",
" ax.contour(U, V, dU, levels=[0], colors=\"blue\", linewidths=1, linestyles=\"-\")\n",
" ax.contour(U, V, dV, levels=[0], colors=\"red\", linewidths=1, linestyles=\"-\")\n",
"\n",
" # Vector field\n",
" ax.quiver(Us, Vs, dUs_norm, dVs_norm, magnitude, cmap=\"viridis\", alpha=0.6)\n",
"\n",
" ax.set_xlabel(\"u (Prey)\")\n",
" ax.set_ylabel(\"v (Predator)\")\n",
" ax.set_title(\"Wererabbit: Phase Portrait\")\n",
" ax.legend([\"u-nullcline (du/dt=0)\", \"v-nullcline (dv/dt=0)\"], loc=\"upper right\")\n",
" ax.set_xlim(u_range)\n",
" ax.set_ylim(v_range)\n",
" ax.axhline(y=0, color=\"gray\", linestyle=\"--\", alpha=0.3)\n",
" ax.axvline(x=0, color=\"gray\", linestyle=\"--\", alpha=0.3)"
]
},
{
"cell_type": "code",
"execution_count": 14,
"id": "b1e166ef",
"metadata": {},
"outputs": [
{
"data": {
"image/png": "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WQghhBBCCCFE71BdXa2o1WoFUO6++26P5z777DMFUMaNG+da9sorryiAcsUVV3i0/fnnn10ZbB988IHHc1dccYUCKEOGDFEqKip8jmP+/PkeWa7u3DNgIyMjldzcXJ/baGhoUGJjYxVAueWWWxS73e6z3cMPP6wASkxMjNLQ0ODxnHuG3uTJk5Wmpiaf2+iI2tpaJT093edr0na/jEajsnfvXq82BQUFrn0aMmSI1/POrFxAiY+PVwoLC73a7N69WwkICFAA5eKLL/a7jbaZvYrimY3ZNovXF2fbk5nZW1VV5XpN1Gq1kpeX53rOPfMyKChI2bdvn9f6+fn5rgzoRx555LjH19zcrEyaNEkBlL/+9a9ezzsztB988MHj2u7s2bNd+/TTTz/5bFNVVeXKGr733nuPe+wdyexdu3atolKpFEAZMGCAa/kDDzzgWnfOnDle69lsNuXKK690tdm1a5fH8+7HEqDMmzev3bEea5xO3XnO+svsdXJmw0sWr+huktkrhBBCCCGEEALAVQsUvLN13ev1Ojkfr1692qPGrvu67tnC2dnZ/PDDDwC88sorrgzcti666CJXzc3PP//c73gfeughkpOTfT731VdfUVxcjNFo5M033/SZOQnw1FNPYTKZKCkpYdGiRX77evHFF9Hr9X6fP5agoCCuvvpqABYvXtxu23vuuYeMjAyv5QkJCTz++OOAY7KsDRs2+N3GE088QVxcnNfyIUOG8Ic//AGABQsWnNQJtxRFQVGUk1LL12w2s3jxYqZNm0ZxcTEAt912G0lJST7b33fffQwaNMhreWJiIr/61a8A2LRp03GPQ6vVcvPNNwO+31er1Qo43rvj8corrwBw0003ceGFF/psExoa6joevvjiC791rjujrKyMTz75hCuvvNK13b/85S8A2Gw2V53cSy65hCuuuMJrfbVazRtvvIFWqwXgvffe89vXhRdeyMUXX9xlY3fqznNWiN5Egr1CCCGEEEIIIVycwdm9e/d6TE60atUqwDPYO2LECIKDgykvL/e4xd8Z7E1MTCQ9Pd21fOnSpSiKQkBAAOPGjaOurs7vjzPo3F4A7pJLLvH7nLPMwcSJE9FoNH77sdvtriCgv74iIyMZP368376crFYrH3/8MZdffjkpKSkYjUaPSZleeOEFAA4cONDudpwBJl+uueYa1+M1a9ac0DbsdvtxTfjWkz7++GOP1zIoKIjzzz+fLVu2ADB16lTefPNNv+tfdNFFfp8bOHAgQLulEGbPns31119Penq6ayJA588999wD+H5fncfxCy+8wLx583xOZthWfX09a9euBWDatGntnifOydMqKirIyso65rb9aTvxWXR0NNOnT6ekpASAGTNmcMcLine truncated
"text/plain": [
"<Figure size 1380x520 with 2 Axes>"
]
},
"metadata": {},
"output_type": "display_data"
}
],
"source": [
"with mpl.style.context(\"boilerplot.ieeetran\"):\n",
" fig, ax = plt.subplots(1, 2, figsize=(6.9, 2.6), dpi=200)\n",
" ax[0].plot(comp_times, state[:, 0, 0], label=\"x1\")\n",
" ax[0].plot(comp_times, state[:, 0, 1], label=\"x2\")\n",
" ax[0].legend([\"x1\", \"x2\"])\n",
"\n",
" plot_vf(ax[1], model, [-0.2, 0.5], [-0.2, 0.5])\n",
" ax[1].plot(state[:, 0, 0], state[:, 0, 1])\n",
" ax[1].plot(state[0, 0, 0], state[0, 0, 1], \".\", label=\"start\")\n",
" ax[1].plot(state[-1, 0, 0], state[-1, 0, 1], \".\", label=\"end\")\n",
" ax[1].legend()\n",
" plt.show()"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "f87852ec",
"metadata": {},
"outputs": [],
"source": []
}
],
"metadata": {
"kernelspec": {
"display_name": "felice",
"language": "python",
"name": "python3"
},
"language_info": {
"codemirror_mode": {
"name": "ipython",
"version": 3
},
"file_extension": ".py",
"mimetype": "text/x-python",
"name": "python",
"nbconvert_exporter": "python",
"pygments_lexer": "ipython3",
"version": "3.13.5"
}
},
"nbformat": 4,
"nbformat_minor": 5
}
@@ -0,0 +1,318 @@
import marimo
__generated_with = "0.19.4"
app = marimo.App(width="medium")
@app.cell
def _():
import marimo as mo
from wigglystuff import Slider2D
return Slider2D, mo
@app.cell
def _():
import diffrax
import jax
import jax.numpy as jnp
import matplotlib.pyplot as plt
import numpy as np
return diffrax, jax, jnp, np, plt
@app.cell
def _(diffrax, jax, jnp):
def vector_field(t, state, args):
u, v = state
alpha, beta, gamma, kappa, sigma, delta = args
z = jax.nn.tanh(kappa * (u - v))
# Prey dynamics
du = z * (1 - alpha * jnp.exp(beta * v) * (1 + gamma * (0.5 - u))) - sigma
# Predator dynamics
dv = z * (-1 + alpha * jnp.exp(beta * u) * u * (1 + gamma * (0.5 - v))) - sigma
return jnp.array([du, dv])
def vector_field_prod(t, state, args):
u, v = state
alpha, beta, gamma, kappa, sigma, delta = args
z = jax.nn.tanh(kappa * (u - v))
# Prey dynamics
du = (
z * (1 - alpha * jnp.exp(beta * v) * (1 + gamma * (0.5 - u))) - sigma
# + sigma * jnp.maximum(0, delta - u) / (delta + 1e-16)
)
# Predator dynamics
dv = (
z * (-1 + alpha * jnp.exp(beta * u) * (1 + gamma * (0.5 - v))) - sigma
# + sigma * jnp.maximum(0, delta - v) / (delta + 1e-16)
)
dv = jnp.where(jnp.allclose(z, 0.0), dv * jnp.sign(v), dv)
du = jnp.where(jnp.allclose(z, 0.0), du * jnp.sign(u), du)
return jnp.array([du, dv])
def vector_field_exp(t, state, args):
u, v = state
alpha, beta, gamma, kappa, sigma, delta = args
z = jax.nn.tanh(kappa * (u - v))
# Prey dynamics
du = (
z * (1 - alpha * jnp.exp(beta * v) * (1 + gamma * (0.5 - u)))
- sigma
+ sigma * jnp.exp(-u / delta)
)
# Predator dynamics
dv = (
z * (-1 + alpha * jnp.exp(beta * u) * u * (1 + gamma * (0.5 - v)))
- sigma
+ sigma * jnp.exp(-v / delta)
)
return jnp.array([du, dv])
def physical_vector_field(t, state, args):
x1, x2 = state
alpha, beta, gamma, kappa, sigma, delta = args
In0 = 129e-15 # fixed by design
C = 0.1e-12 # fixed by design
kk = 0.39 # fixed by tech
Ut = 0.025 # temperature dependent
Ibias = In0 / alpha
Ia = Ibias * sigma
x3 = jax.nn.tanh(kappa * (x1 - x2))
dx1 = (
x3 * Ibias
- (In0 * jnp.exp(kk * x2 / Ut)) * (x3 + 26e-2 * (0.5 - x1) * x3)
- Ia
) / C
dx2 = (
-x3 * Ibias
+ In0 * jnp.exp(kk * x1 / Ut) * (x3 + 26e-2 * (0.5 - x2) * x3)
- Ia
) / C
return jnp.array([dx1, dx2])
def compute_nullclines(vector_field, u_range, v_range, args, resolution=200):
"""
Compute nullclines
du/dt = 0 (u-nullcline)
dv/dt = 0 (v-nullcline)
"""
alpha, beta, gamma, kappa, sigma, delta = args
u_vals = jnp.linspace(u_range[0], u_range[1], resolution)
v_vals = jnp.linspace(v_range[0], v_range[1], resolution)
U, V = jnp.meshgrid(u_vals, v_vals)
dU, dV = vector_field(0, [U, V], args)
return U, V, dU, dV
def solve(dyn, y0, p, T, n=1000):
sol = diffrax.diffeqsolve(
diffrax.ODETerm(dyn),
diffrax.Tsit5(),
t0=0.0,
t1=T,
dt0=0.01,
y0=y0,
args=p,
saveat=diffrax.SaveAt(ts=jnp.linspace(0, T, n)),
stepsize_controller=diffrax.PIDController(rtol=1e-7, atol=1e-8),
max_steps=50000,
)
return sol.ts, sol.ys
return compute_nullclines, solve, vector_field_prod
@app.cell
def _(Slider2D, mo):
# alpha = 0.5 # I_n0 / I_bias ratio
# beta = 0.39/0.025 # k / U_t (inverse thermal scale)
# gamma = 0.26 # coupling coefficient
# kappa = 5.0 # tanh steepness
# sigma = 0.6 # bias scaling (s * I_bias normalized)
# y0 = jnp.array([0.2, 0.4])
# ts, ys = solve(vector_field, y0, params, 140)
alpha = mo.ui.slider(
0.0004, 0.012, 0.00001, 0.00129, label="alpha", orientation="vertical"
)
beta = mo.ui.slider(
0.0, 30, 0.00001, 0.39 / 0.025, label="beta", orientation="vertical"
)
gamma = mo.ui.slider(0, 1, 0.01, 0.26, label="gamma", orientation="vertical")
kappa = mo.ui.slider(0, 10, 0.1, 5.0, label="kappa", orientation="vertical")
sigma = mo.ui.slider(0, 1, 0.01, 0.6, label="sigma", orientation="vertical")
delta = mo.ui.slider(0, 0.1, 0.001, 0.02, label="delta", orientation="vertical")
# v0 = mo.ui.slider(0, 1.0, 0.01, 0.3, label="v0")
# u0 = mo.ui.slider(0, 1.0, 0.01, 0.2, label="u0", orientation="vertical")
state0 = mo.ui.anywidget(
Slider2D(
width=150,
height=150,
x_bounds=(-1.0, 1.5),
y_bounds=(-1.0, 1.5),
)
)
mo.hstack(
[
mo.plain_text("""
alpha: I_n0 / I_bias ratio
beta: k / U_t ratio
gamma: coupling coefficient
kappa: tanh steepness
sigma: bias scaling (s * I_bias)
"""),
mo.hstack(
[state0, alpha, beta, gamma, kappa, sigma, delta], justify="start"
),
]
)
return alpha, beta, delta, gamma, kappa, sigma, state0
@app.cell
def _(
alpha,
beta,
compute_nullclines,
delta,
gamma,
jnp,
kappa,
np,
plt,
sigma,
solve,
state0,
vector_field_prod,
):
params = (
alpha.value,
beta.value,
gamma.value,
kappa.value,
sigma.value,
delta.value,
)
ic_neuro = [state0.x, state0.y]
u_range = [-1.0, 1.5]
v_range = [-1.0, 1.5]
u_sparse = jnp.linspace(u_range[0], u_range[1], 20)
v_sparse = jnp.linspace(v_range[0], v_range[1], 20)
Us, Vs = jnp.meshgrid(u_sparse, v_sparse)
def plot_vf(ax, vector_field):
U, V, dU, dV = compute_nullclines(vector_field, u_range, v_range, params)
dUs, dVs = vector_field(0, [Us, Vs], params)
# Normalize for visualization
magnitude = np.sqrt(dUs**2 + dVs**2)
magnitude[magnitude == 0] = 1
dUs_norm = dUs / magnitude
dVs_norm = dVs / magnitude
# Nullclines
ax.contour(U, V, dU, levels=[0], colors="blue", linewidths=2, linestyles="-")
ax.contour(U, V, dV, levels=[0], colors="red", linewidths=2, linestyles="-")
ax.quiver(Us, Vs, dUs_norm, dVs_norm, magnitude, cmap="viridis", alpha=0.6)
# Trajectories
color = plt.cm.plasma(0.2)
ts, ys = solve(vector_field, jnp.array(ic_neuro), params, 50)
ax.plot(ys[:, 0], ys[:, 1], "-", color=color, linewidth=1.5, alpha=0.8)
ax.plot(ic_neuro[0], ic_neuro[1], "o", color=color, markersize=6)
ax.set_xlabel("u (Prey)")
ax.set_ylabel("v (Predator)")
ax.set_title("Wererabbit: Phase Portrait")
ax.legend(["u-nullcline (du/dt=0)", "v-nullcline (dv/dt=0)"], loc="upper right")
ax.set_xlim(u_range)
ax.set_ylim(v_range)
ax.axhline(y=0, color="gray", linestyle="--", alpha=0.3)
ax.axvline(x=0, color="gray", linestyle="--", alpha=0.3)
def plot_trj(ax, vector_field):
ts, ys = solve(vector_field, jnp.array(ic_neuro), params, 50)
ax.plot(ts, ys[:, 0], "b-", linewidth=2, label="u (Prey)")
ax.plot(ts, ys[:, 1], "r-", linewidth=2, label="v (Predator)")
ax.set_xlabel("Time τ")
ax.set_ylabel("Population")
ax.set_title(
f"Wererabbit: Time Series (IC: u₀={ic_neuro[0]:.2f}, v₀={ic_neuro[1]:.2f})"
)
ax.legend()
ax.axhline(y=0, color="gray", linestyle="--", alpha=0.3)
fig = plt.figure(figsize=(10, 4))
# --- Plot 1: Wererabbit Phase Portrait ---
ax1 = fig.add_subplot(1, 2, 1)
plot_vf(ax1, vector_field_prod)
ax2 = fig.add_subplot(1, 2, 2)
plot_trj(ax2, vector_field_prod)
# ax3 = fig.add_subplot(3, 2, 3)
# plot_vf(ax3, vector_field_prod)
# ax4 = fig.add_subplot(3, 2, 4)
# plot_trj(ax4, vector_field_prod)
# ax5 = fig.add_subplot(3, 2, 5)
# plot_vf(ax5, vector_field_exp)
# ax6 = fig.add_subplot(3, 2, 6)
# plot_trj(ax6, vector_field_exp)
plt.tight_layout()
fig
return
@app.cell
def _():
return
@app.cell
def _():
return
@app.cell
def _():
return
if __name__ == "__main__":
app.run()