codex/synaptic-spatial-conductance-passive-integration.py

synaptic-spatial-conductance-passive-integration.py
"""Original passive compartment and conductance comparisons.
Python 3.14, NumPy 2.3.5, Matplotlib 3.10.7. Run from _media.
Exact constant-coefficient solutions, pF / nS = ms, nS * mV = pA.
No active channels, firing/reset, fitted physiology, or cable approximation.
"""
from pathlib import Path
import numpy as np
import matplotlib.pyplot as plt


def compartment_voltage(t, currents):
    # Deviations from -65 mV; soma and dendrite each C=100 pF, leak=5 nS.
    # Axial coupling 10 nS. Eigenvalues of K/C set both decay rates.
    k = np.array([[15., -10.], [-10., 15.]])
    rates, vectors = np.linalg.eigh(k / 100.)
    steady = np.linalg.solve(k, np.asarray(currents, dtype=float))
    on = np.maximum(t - 10., 0.)
    duration = np.minimum(on, 50.)
    after = np.maximum(on - 50., 0.)
    modal = vectors.T @ steady
    response = vectors @ (modal[:, None] * (-np.expm1(-rates[:, None] * duration)) * np.exp(-rates[:, None] * after))
    return response


def conductance_voltage(t, ge, gi=0.):
    # Excitatory reversal 0 mV; shunt reversal and leak reversal -65 mV.
    total = 10. + ge + gi
    steady_rise = 65. * ge / total
    on = np.maximum(t - 10., 0.)
    duration = np.minimum(on, 50.)
    after = np.maximum(on - 50., 0.)
    return steady_rise * (-np.expm1(-duration * total / 200.)) * np.exp(-after * 10. / 200.)


def main():
    t = np.linspace(0, 100, 10001)
    pulse = ((t >= 10.) & (t < 60.)).astype(float)
    fig, axes = plt.subplots(2, 3, figsize=(12, 6.4), layout='constrained')
    schematic = axes[0, 0]
    schematic.set_axis_off()
    schematic.set_xlim(0, 1)
    schematic.set_ylim(0, 1)
    for x, label in [(0.24, 'Soma'), (0.76, 'Dendrite')]:
        schematic.text(x, 0.62, label, ha='center', bbox={'boxstyle': 'round,pad=0.5', 'fc': 'white', 'ec': '#1565c0'})
        schematic.annotate('100 pA', (x, 0.7), (x, 0.92), ha='center', arrowprops={'arrowstyle': '->'})
        schematic.text(x, 0.37, 'C = 100 pF\nleak = 5 nS', ha='center', fontsize=9)
    schematic.plot([0.37, 0.63], [0.62, 0.62], color='0.3', lw=2)
    schematic.text(0.5, 0.5, '10 nS', ha='center')
    schematic.text(0.5, 0.13, 'Steps: 10–60 ms; rest −65 mV\nSoma alone, dendrite alone, or both', ha='center', fontsize=9)
    schematic.set_title('A. Locations with fixed current inputs')
    for currents, label, color in [([100, 0], 'Soma input', '#1565c0'), ([0, 100], 'Dendrite input', '#c45c10'), ([100, 100], 'Both: exact sum', '#178346')]:
        axes[1, 0].plot(t, -65 + compartment_voltage(t, currents)[0], label=label, color=color, lw=2)
    axes[1, 0].set_ylabel('Passive somatic voltage (mV)')
    axes[0, 1].step(t, 2 * pulse, where='post', color='#1565c0', lw=2, label='Each input: 2 nS')
    axes[0, 1].step(t, 4 * pulse, where='post', color='#c45c10', lw=2, label='Joint excitation: 4 nS')
    axes[0, 1].set_title('B. One compartment, two conductances')
    axes[1, 1].plot(t, -65 + 2 * conductance_voltage(t, 2), ls='--', color='0.4', lw=2, label='Sum of separate voltage changes')
    axes[1, 1].plot(t, -65 + conductance_voltage(t, 4), color='#c45c10', lw=2, label='Joint conductance response')
    axes[0, 2].step(t, 4 * pulse, where='post', color='#c45c10', lw=2, label='Excitation: 4 nS')
    axes[0, 2].step(t, 20 * pulse, where='post', color='#7b3294', lw=2, label='Shunt: 20 nS, reversal −65 mV')
    axes[0, 2].set_title('C. Same excitation, added shunt')
    axes[1, 2].plot(t, -65 + conductance_voltage(t, 4), color='#c45c10', lw=2, label='Excitation alone')
    axes[1, 2].plot(t, -65 + conductance_voltage(t, 4, 20), color='#7b3294', lw=2, label='Excitation + shunt')
    for col in range(3):
        axes[1, col].set_ylim(-66, -42)
        axes[1, col].set_xlabel('Time (ms)')
        for row in range(2):
            if col == 0 and row == 0:
                continue
            axes[row, col].set_xlim(0, 100)
            axes[row, col].grid(alpha=0.18)
            axes[row, col].legend(fontsize=7.6, loc='upper right')
    for ax in axes[0, 1:]:
        ax.set_xlabel('Time (ms)')
        ax.set_ylabel('Activated conductance (nS)')
    axes[1, 1].legend(fontsize=7.6, loc='lower left')
    axes[0, 1].set_ylim(-0.3, 5.5)
    axes[0, 2].set_ylim(-1, 25)
    fig.suptitle('Passive integration: location, driving force and shunting\nB–C: C = 200 pF, leak = 10 nS, rest = −65 mV, excitatory reversal = 0 mV; no spikes', fontsize=13)
    fig.savefig(Path(__file__).stem + '.png', dpi=120, facecolor='white', transparent=False)
    plt.close(fig)


if __name__ == '__main__':
    main()