codex/neuronal-paired-pulse-event-readout.py

neuronal-paired-pulse-event-readout.py
"""Original ideal event readout for a stipulated recovery rule; no voltage trace.

Tested: Python 3.14.4, matplotlib 3.10.7, numpy 2.3.5.
Output matching basename.png in cwd. Each row is an independently reset trial.
Conditioning success at time zero is assumed; successful tests emit at onset.
"""
import math
from pathlib import Path

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

HOLD_MS = 2.0
TAU_MS = 3.0
BASE_NA = 0.2
EXCESS = 2.0
TEST_NA = 0.35
WIDTH_MS = 0.2


def required_current(delay_ms):
    if delay_ms < HOLD_MS:
        return np.inf
    return BASE_NA * (1 + EXCESS*np.exp(-(delay_ms-HOLD_MS)/TAU_MS))


def test_passes(delay_ms, current_na=TEST_NA):
    if delay_ms < HOLD_MS:
        return False
    required = required_current(delay_ms)
    # Include mathematical equality despite a few ULPs of floating-point error.
    return current_na >= required or math.isclose(
        current_na, required, rel_tol=4*np.finfo(float).eps, abs_tol=0.0)


def pulse(ax, start, height, color):
    ax.plot([start, start, start+WIDTH_MS, start+WIDTH_MS],
            [0, height, height, 0], color=color, lw=2.3)


def main():
    plt.rcParams.update({'font.size': 11, 'font.family': 'DejaVu Sans'})
    fig, axes = plt.subplots(3, 2, figsize=(10.0, 6.1), sharex=True,
                             gridspec_kw={'width_ratios': [1.05, 1]}, layout='constrained')
    fig.set_facecolor('white')
    blue, orange = '#225f91', '#a74321'
    for row, delta in enumerate([1.0, 4.0, 6.0]):
        inp, out = axes[row]
        for ax in [inp, out]:
            ax.set_facecolor('white')
            ax.axvspan(0, HOLD_MS, color='#eeeeee', zorder=0)
            ax.spines[['top', 'right']].set_visible(False)
            ax.set_xlim(-0.5, 8)
            ax.set_xticks([0, 2, 4, 6, 8])
        inp.plot([-0.5, 8], [0, 0], color='#444444', lw=1)
        pulse(inp, 0, 0.65, blue)
        pulse(inp, delta, TEST_NA, orange)
        inp.set_ylim(-0.04, 0.88)
        inp.set_yticks([0, 0.35, 0.65])
        inp.set_ylabel('Current (nA)')
        inp.set_title(f'Delay Δ = {delta:g} ms', loc='left', fontsize=12)
        if row == 0:
            inp.text(0.3, 0.73, 'Conditioning', color=blue, fontsize=10)
            inp.text(1.4, 0.38, 'Test: 0.35 nA', color=orange, fontsize=10)
        out.set_ylim(0, 1.55)
        out.set_yticks([])
        out.spines['left'].set_visible(False)
        out.hlines(0.55, -0.5, 8, color='#aaaaaa', lw=1)
        out.vlines(0, 0.35, 1.0, color=blue, lw=3)
        out.text(0.2, 1.05, 'Conditioning event', color=blue, fontsize=10)
        if test_passes(delta):
            out.vlines(delta, 0.35, 1.0, color=orange, lw=3)
            out.text(delta, 1.05, 'Test event', color=orange, ha='center', fontsize=10)
            reason = f'Pass: 0.35 ≥ {required_current(delta):.3f} nA'
        else:
            out.plot(delta, 0.55, marker='x', color=orange, ms=9, mew=2)
            reason = ('Fails during absolute hold' if delta < HOLD_MS else
                      f'Fails: 0.35 < {required_current(delta):.3f} nA')
        out.text(7.9, 0.12, reason, ha='right', color=orange, fontsize=11)
    axes[-1, 0].set_xlabel('Pulse onset time (ms)')
    axes[-1, 1].set_xlabel('Ideal event time (ms)')
    fig.suptitle('Paired-pulse readout: equal tests, different outcomes', fontsize=16)
    fig.supxlabel('Rectangles: imposed pulses. Ticks: events. Crosses: failed tests. Grey: chosen 2 ms hold.', fontsize=10)
    axes[0, 1].set_title('Abstract event readout; not membrane voltage', loc='left', fontsize=11)
    fig.savefig(Path(__file__).with_suffix('.png').name, dpi=120,
                facecolor='white', bbox_inches='tight')
    plt.close(fig)


if __name__ == '__main__':
    main()