codex/ion-channel-current-and-reversal.py

ion-channel-current-and-reversal.py
"""Ohmic toy currents; writes an opaque PNG in the current directory.

Tested with Python 3.14.4, NumPy 2.3.5 and Matplotlib 3.10.7.
All parameters are stipulated, not empirical. V = inside minus outside;
conventional current is inward-positive. Units: nS * mV = pA.
"""
from pathlib import Path

import matplotlib
matplotlib.use("Agg")
import matplotlib.pyplot as plt
import numpy as np


BLUE = "#207ea2"
ORANGE = "#bc5b22"
INK = "#263749"


def current(voltage, conductance, reversal):
    """Inward-positive current in pA for mV and nS inputs."""
    return conductance * (reversal - np.asarray(voltage))


def main():
    plt.rcParams.update({"font.family": "DejaVu Sans", "font.size": 10,
                         "axes.spines.top": False, "axes.spines.right": False})
    fig, (single, pair) = plt.subplots(1, 2, figsize=(11.0, 5.5), facecolor="white")
    fig.subplots_adjust(left=.085, right=.98, top=.75, bottom=.2, wspace=.3)
    fig.suptitle("Reversal means zero NET current; ion movement can continue", y=.97,
                 fontsize=15, color=INK)
    fig.text(.5, .9, "V = inside − outside • conventional current: inward positive, outward negative",
             ha="center", fontsize=11, color=INK)
    v = np.linspace(-115., 60., 701)
    for ax in [single, pair]:
        ax.axhline(0, color="#82909c", lw=.9)
        ax.set(xlabel="Membrane potential V (mV)",
               ylabel="Inward-positive current (pA)")
        ax.grid(alpha=.17)

    single.plot(v, current(v, 10., -80.), color=BLUE, lw=2.4)
    single.set(title="A. One Ohmic pathway: g = 10 nS, E = −80 mV",
               xlim=(-115, -45), ylim=(-400, 400))
    single.set_xticks([-110, -100, -80, -60, -50])
    for probe, label, delta in [(-100., "+200 pA", (5, 8)),
                                (-80., "0 pA", (9, 10)),
                                (-60., "−200 pA", (-62, -22))]:
        y = current(probe, 10., -80.)
        single.plot(probe, y, "o", color=BLUE, ms=6)
        single.annotate(label, (probe, y), xytext=delta, textcoords="offset points",
                        color=BLUE, fontsize=10)
    single.axvline(-80, color=BLUE, ls=":", lw=1)
    single.text(-112, 335, "Positive: net positive charge enters", fontsize=9, color=INK)
    single.text(-112, -355, "Negative: net positive charge leaves", fontsize=9, color=INK)

    balance = (10.*-80. + 2.*50.) / 12.
    pair.plot(v, current(v, 10., -80.), color=ORANGE, lw=2,
              label="K: 10 nS, E = −80 mV")
    pair.plot(v, current(v, 2., 50.), color=BLUE, lw=2,
              label="Na: 2 nS, E = +50 mV")
    pair.plot(v, current(v, 10., -80.) + current(v, 2., 50.),
              color=INK, lw=2.2, ls="--", label="Total: 12 nS, E = −58.3333 mV")
    pair.set(title="B. Two pathways across the same membrane",
             xlim=(-110, 60), ylim=(-1500, 650))
    pair.set_xticks([-100, -80, -60, 0, 50])
    pair.legend(loc="lower left", fontsize=9, frameon=True)
    pair.axvline(balance, color="#82909c", ls=":", lw=1)
    for g, e, color in [(10., -80., ORANGE), (2., 50., BLUE)]:
        pair.plot(balance, current(balance, g, e), "o", color=color, ms=5)
    pair.plot(balance, 0, "o", color=INK, ms=5)
    pair.annotate("At −58.3333 mV:\nNa: +216.6667 pA\nK: −216.6667 pA\nTotal: 0 pA",
                  xy=(balance, 0), xytext=(-10, 390), fontsize=9, color=INK,
                  va="top", arrowprops={"arrowstyle": "->", "color": INK},
                  bbox={"boxstyle": "round,pad=.3", "fc": "white", "ec": "#ccd4dc"})
    fig.text(.5, .07, "Stipulated constant conductances and fixed gradients • "
             "instantaneous passive current balance, not a full resting-voltage model",
             ha="center", fontsize=9.5, color=INK)
    fig.savefig(Path.cwd() / (Path(__file__).stem + ".png"), dpi=120,
                facecolor="white", transparent=False)
    plt.close(fig)


if __name__ == "__main__":
    main()