Files
organism/neuron/appunti/traditional_python_models/Tripartite-Synapse.py
T

691 lines
30 KiB
Python
Raw Normal View History

2026-03-30 14:14:18 +02:00
# Tripartite Synapse - Multi-Scale Computational Model
# =====================================================
# Presynaptic + Postsynaptic perspectives, fully integrated.
#
# Change log:
# ORIG - present from the original document
# NEW - added in the missing-behavior integration pass
# DET - deterministic Ca2+-driven vesicle release
# NKA - explicit Na/K-ATPase V_pre decay and ATP cost
# POST-ATP - postsynaptic Ca2+ dynamics and ATP loop
# FIX - corrections applied in this pass:
# * NT_released_this_window accumulator (was missing entirely)
# * k_rec_fast / k_rec_slow converted to /s, recruitment moved to Loop 2
# * dt_slow_s added
# * mGluR now reads NT_released_this_window (not NT_cleft snapshot)
# * IP3 now reads NT_released_this_window (not cleared_NT residual)
# * wave_active flag + conversion_efficiency boost on astrocyte wave
# * CDI rise gated to spike window only
#
# Clock structure:
# Loop 1 - dt = 1 ms (Ca2+, vesicle release, traces, postsynaptic fast)
# Loop 2 - dt = 1000 ms (astrocyte clearance, eCB, mGluR, recruitment)
# Loop 3 - dt = 60000 ms (glutamine shuttle, metabolic health)
#
# =======================================================================
# THREE CLOSED LOOPS
# =======================================================================
#
# PRESYNAPTIC:
# NT loop : release (ms) -> cleft -> astrocyte clearance (s) ->
# glutamine shuttle (min) -> RP refill -> RRP -> release
# Ca2+ loop : VGCC influx (ms) -> Tr_Ca -> recruitment speed (s) ->
# eCB retrograde from post (s) -> VGCC suppression
# ATP loop : NKA + pump costs (ms) -> ATP_demand (min) -> ATP_level ->
# pump_scale -> Ca2+ clearance rate -> CDI recovery
#
# POSTSYNAPTIC:
# NT detection loop : NT_cleft -> AMPA -> V_post -> desensitization ->
# reduces next response
# Ca2+ coincidence : NMDA (NT + V_post) -> Ca_post -> eCB -> pre brake
# ATP loop : NKA + PMCA costs (ms) -> ATP_demand_post (min) ->
# ATP_level_post -> pump_scale_post -> Ca_post clearance
#
# SHARED:
# eCB_level : post synthesises -> pre reads (retrograde brake)
# NT_cleft : pre releases -> post detects -> astrocyte clears
# Glucose_level : astrocyte supplies both sides from same budget
#
# =======================================================================
# METABOLIC SILENCING CASCADE (presynaptic)
# =======================================================================
# [CASCADE 1] HIGH FIRING -> VESICLE DEPLETION (~seconds)
# release rate >> recruitment rate -> N_RRP -> 0
# [CASCADE 2] HIGH FIRING -> ATP DEPLETION (~minutes)
# NKA + PMCA + docking demand > glucose-driven supply
# [CASCADE 3] LOW ATP -> PUMP FAILURE
# pump_scale = Hill(ATP_level) -> cleared_PMCA/SERCA fall
# [CASCADE 4] PUMP FAILURE -> RESIDUAL Ca2+ STAYS HIGH
# Ca_micro persists between spikes
# [CASCADE 5] RESIDUAL Ca2+ -> CDI LOCKS VGCCs SHUT
# CDI rise (spike only) + recovery blocked by Ca2+ -> CDI -> 1
# [CASCADE 6] SYNAPSE SILENCES (excitotoxicity protection)
# effective_conductance = N_VGCC*(1-eCB)*(1-CDI)*(1-mGluR*alpha)
# -> 0; NCX auto-reset when drive stops
#
# POSTSYNAPTIC ATP CASCADE (no CDI equivalent -> dangerous):
# [POST-ATP 1] HIGH V_post + NMDA -> ATP_demand_post rises
# [POST-ATP 2] ATP_level_post falls -> pump_scale_post falls
# [POST-ATP 3] Ca_post clearance slows -> Ca_post stays elevated
# [POST-ATP 4] Ca_post > eCB_threshold without real coincidence
# -> false retrograde signal suppresses presynapse
# [POST-ATP 5] Critically low ATP_post -> runaway Ca_post -> excitotoxicity
# =======================================================================
import numpy as np
# -----------------------------------------------------------------------
# CLOCK
# -----------------------------------------------------------------------
dt = 1.0 # ms
dt_slow = 1000.0 # ms
dt_meta = 60_000.0 # ms
High_Freq_Multiplier = int(dt_slow / dt) # 1000
Metabolic_Multiplier = int(dt_meta / dt) # 60000
dt_s = dt / 1000.0 # 0.001 s/step - for /s rate constants in Loop 1
dt_slow_s = dt_slow / 1000.0 # 1.0 s/step - for /s rate constants in Loop 2
# -----------------------------------------------------------------------
# PRESYNAPTIC PARAMETERS
# -----------------------------------------------------------------------
# -- Voltage / membrane --
tau_V_pre = 2.0 # ms - AP waveform decay (Na/K-ATPase recharge)
V_pre_peak = 1.0 # a.u. - normalised AP peak
V_rest = 0.0 # a.u. - resting potential
V_pre_voltage = -10.0 # mV - driving force for compute_flux
NKA_cost_per_AP = 0.002 # ATP units per AP (dominant drain at high rates)
# -- Ca2+ influx & buffering --
N_VGCC = 100 # number of VGCCs (ceiling of effective_conductance)
k_flux = 0.05 # Ca2+ influx per open channel per unit driving force
B_total = 1.0 # total buffer capacity (normalised)
tau_buffer_rebind = 200.0 # ms - buffer recharge time constant
# -- Ca2+ clearance (/ms constants) --
k_PMCA = 0.03 # ATP-dependent primary pump
k_NCX = 0.10 # ATP-independent floor
k_SERCA = 0.01 # ATP-dependent ER pump
ATP_half = 0.3 # Hill half-saturation for presynaptic pumps
ATP_cost_PMCA = 0.0005 # ATP per unit Ca2+ extruded by PMCA
ATP_cost_SERCA = 0.0002 # ATP per unit Ca2+ pumped into ER
ATP_cost_docking = 0.001 # ATP per vesicle docked (RP->RRP)
# -- Deterministic release (Hill + NT suppression) --
k_rel = 0.5 # max releasable fraction of RRP per spike
KD_rel = 1.0 # half-saturation [Ca2+]
n_rel = 4 # Hill cooperativity (synaptotagmin-1)
NT_suppression_weight = 0.3 # max NT_cleft brake on release fraction
NT_suppression_sat = 50.0 # NT_cleft level that saturates suppression
# -- CDI --
k_CDI_rise = 0.8 # /s - CDI build rate (applied * dt_s, spike only)
Ca_micro_saturation = 2.0 # normalisation ceiling for CDI recovery
k_CDI_rec = 0.015 # /s - CDI de-inactivation rate (applied * dt_s)
# -- Vesicle pools --
Max_RRP = 20
Max_RP = 200
# -- Calcium trace --
tau_Tr_Ca = 1000.0 # ms
T_high = 0.6 # Tr_Ca threshold -> fast recruitment
T_low = 0.2 # Tr_Ca threshold -> slow recruitment
# -- RP->RRP recruitment (/s, runs in Loop 2) --
k_rec_fast = 5.0 # /s - fast recruitment (at Tr_Ca > T_high)
k_rec_slow = 0.5 # /s - slow recruitment (at Tr_Ca < T_low)
# -- NT accumulator for Loop 2 signals --
NT_window_sat = 40.0 # vesicles/s that saturates mGluR and IP3
# at 20 Hz releasing ~2/spike = 40/s
# -- eCB retrograde brake --
tau_eCB_rise = 2000.0
tau_eCB_decay = 10_000.0
eCB_threshold = 0.7 # Ca_post level that triggers eCB synthesis
# -- mGluR presynaptic autoreceptor --
Km_mGluR = 0.5
tau_mGluR = 2000.0 # ms
alpha_mGluR = 0.4 # max fractional VGCC suppression
# -- Astrocyte / IP3 --
tau_IP3 = 3000.0 # ms
IP3_threshold = 0.8
wave_boost = 0.2 # conversion_efficiency boost when wave fires
tau_wave_decay = 2 # metabolic cycles before boost decays back
# -- Glutamine shuttle --
conversion_efficiency_base = 0.8
# -- NT cleft --
tau_NT_decay = 5.0 # ms
# -----------------------------------------------------------------------
# POSTSYNAPTIC PARAMETERS
# -----------------------------------------------------------------------
# -- NMDA coincidence detection --
k_NMDA = 0.08 # Ca_post influx per unit NT * (1 - Mg_block) per ms
V_NMDA_half = 0.3 # V_post at which Mg block is 50% lifted
# -- Ca_post clearance --
k_Ca_post_clear = 0.05 # /ms - ATP-dependent PMCA in spine
k_Ca_post_NCX = 0.02 # /ms - ATP-independent NCX floor
ATP_half_post = 0.3 # Hill half-saturation for postsynaptic pumps
# -- Postsynaptic ATP costs --
NKA_cost_per_bAP_post = 0.002 # ATP per unit V_post per s (continuous)
ATP_cost_Ca_post_pump = 0.0005 # ATP per unit Ca_post cleared
ATP_demand_scale_post = 50.0 # normalisation (same as presynaptic)
# -- Receptor desensitization --
tau_membrane = 20.0 # ms
tau_desensitization = 500.0 # ms
# -----------------------------------------------------------------------
# HELPER FUNCTIONS
# -----------------------------------------------------------------------
def compute_flux(conductance, voltage):
return k_flux * conductance * abs(voltage)
def deterministic_release(N_RRP, Ca_micro, NT_cleft):
# Hill equation: Ca2+ sensor cooperativity (synaptotagmin-1, n=4)
Ca_n = Ca_micro ** n_rel
release_frac = k_rel * Ca_n / (Ca_n + KD_rel ** n_rel)
# NT suppression: physical crowding + fast local autoreceptors
NT_norm = min(1.0, NT_cleft / NT_suppression_sat)
release_frac = release_frac * (1.0 - NT_suppression_weight * NT_norm)
release_frac = np.clip(release_frac, 0.0, 1.0)
return max(0.0, release_frac * N_RRP)
def map_trace_to_speed(Tr_Ca):
# Returns /s recruitment rate based on Tr_Ca level
if Tr_Ca > T_high:
return k_rec_fast
elif Tr_Ca < T_low:
return k_rec_slow
else:
t = (Tr_Ca - T_low) / (T_high - T_low)
return k_rec_slow + t * (k_rec_fast - k_rec_slow)
def compute_pump_atp_factor(atp, atp_half):
# Hill function: ATP gates pump speed (shared by pre and post)
return (atp ** 2) / (atp ** 2 + atp_half ** 2)
def compute_EPSP(receptor_conductance):
return receptor_conductance * 0.1
def compute_astrocyte_metabolic_health(Glucose_level, ATP_demand_accumulated,
demand_scale=50.0):
# Converts glucose supply and accumulated demand into ATP_level (0->1)
# and conversion_efficiency (0->1). Both sides use this function with
# their own demand accumulators but the same Glucose_level — shared
# metabolic vulnerability.
health = np.clip(Glucose_level - ATP_demand_accumulated / demand_scale,
0.0, 1.0)
return health, health # (conversion_efficiency, ATP_level)
def trigger_slow_astrocyte_calcium_wave():
# Placeholder - gliotransmitter release over ~10 s
pass
# -----------------------------------------------------------------------
# STATE VARIABLES
# -----------------------------------------------------------------------
# -- Presynaptic membrane --
V_pre_state = 0.0
# -- Presynaptic Ca2+ --
Ca_micro = 0.0
Ca_ER = 0.5
Ca_buffer_bound = 0.0
B_free = B_total
# -- CDI --
CDI_factor = 0.0
# -- Vesicle pools --
N_RRP = 15.0
N_RP = 150.0
# -- Calcium trace --
Tr_Ca = 0.0
# -- NT cleft --
NT_cleft = 0.0
# -- NT accumulator for slow signals --
# FIX: this was missing. Accumulates every ms in Loop 1,
# consumed by mGluR and IP3 in Loop 2, reset each second.
NT_released_this_window = 0.0
# -- Postsynaptic membrane + receptors --
V_post = 0.0
receptor_conductance = 0.0
Desensitization_level = 0.0
V_post_history = []
# -- Postsynaptic Ca2+ (spine compartment) --
Ca_post = 0.0
# Driven by NMDA coincidence (NT + V_post). Cleared by PMCA (ATP-gated)
# and NCX (always). Drives eCB synthesis. No CDI equivalent ->
# elevated Ca_post under ATP failure has no self-limiting mechanism.
# -- Retrograde / autoreceptor --
eCB_level = 0.0
mGluR_activation = 0.0
# -- Astrocyte --
IP3 = 0.0
wave_active = 0 # countdown: cycles remaining of wave boost
Glutamine_pool = 50.0
# -- Presynaptic ATP --
ATP_level = 1.0
ATP_demand = 0.0
conversion_efficiency = conversion_efficiency_base
Glucose_level = 1.0 # set < 1.0 to engage metabolic silencing
# -- Postsynaptic ATP --
ATP_level_post = 1.0 # separate pool; same glucose budget as presynaptic
ATP_demand_post = 0.0 # accumulates from NKA (V_post) and PMCA (Ca_post)
# -----------------------------------------------------------------------
# MAIN SIMULATION LOOP
# -----------------------------------------------------------------------
def run_simulation(spike_train, total_steps, bAP_train=None):
"""
spike_train : list of int - presynaptic AP timestep indices
total_steps : int
bAP_train : list of int - postsynaptic bAP timestep indices (optional)
if None, no bAPs are delivered
"""
global V_pre_state
global Ca_micro, Ca_ER, Ca_buffer_bound, B_free
global CDI_factor
global N_RRP, N_RP, Tr_Ca, NT_cleft, NT_released_this_window
global V_post, receptor_conductance, Desensitization_level, V_post_history
global Ca_post
global eCB_level, mGluR_activation
global IP3, wave_active, Glutamine_pool
global ATP_level, ATP_demand, conversion_efficiency, Glucose_level
global ATP_level_post, ATP_demand_post
log = {k: [] for k in [
"V_pre_state", "Ca_micro", "Ca_ER", "CDI_factor", "B_free",
"N_RRP", "N_RP", "Tr_Ca", "NT_cleft",
"V_post", "Ca_post", "eCB_level", "mGluR_activation",
"released_NT", "ATP_level", "ATP_demand",
"ATP_level_post", "ATP_demand_post",
]}
spike_set = set(spike_train)
bAP_set = set(bAP_train) if bAP_train else set()
for step in range(total_steps):
# ==============================================================
# LOOP 1 — HIGH-FREQUENCY (dt = 1 ms)
# ==============================================================
V_pre = 1 if step in spike_set else 0
bAP = 1 if step in bAP_set else 0
released_NT = 0.0
# -- 1A. PRESYNAPTIC MEMBRANE / Na-K-ATPase -------------------
# AP fires: membrane jumps to peak, then decays with tau_V_pre.
# Ca2+ influx uses V_pre_state (continuous) not binary V_pre,
# giving a temporal influx profile that tapers as membrane repolarises.
if V_pre == 1:
V_pre_state = V_pre_peak
ATP_demand += NKA_cost_per_AP # dominant presynaptic ATP cost
V_pre_state += (V_rest - V_pre_state) * dt / tau_V_pre
# -- 1B. PRESYNAPTIC Ca2+ INFLUX ------------------------------
# Three multiplicative brakes on effective_conductance:
# eCB_level : retrograde brake from postsynapse (Loop 2)
# CDI_factor : Ca2+-dependent inactivation (below)
# mGluR_activation : autoreceptor brake (Loop 2)
effective_conductance = (
N_VGCC
* (1.0 - eCB_level)
* (1.0 - CDI_factor)
* (1.0 - mGluR_activation * alpha_mGluR)
)
raw_influx = compute_flux(effective_conductance, V_pre_state)
# Buffer proteins capture a fraction immediately (fast sponge).
# B_free -> 0 during sustained bursting -> capture_fraction -> 0
# -> full raw_influx enters Ca_micro (CASCADE 4 acceleration).
capture_fraction = B_free / B_total
captured = raw_influx * capture_fraction
B_free = max(0.0, B_free - captured)
Ca_buffer_bound += captured
Ca_micro += (raw_influx - captured)
# -- 1C. VESICLE RELEASE --------------------------------------
# Deterministic: Hill Ca2+ sensor * NT suppression * N_RRP.
# Runs every ms that Ca_micro > 0 (release profile follows Ca2+
# transient, not locked to spike flag).
if N_RRP > 0 and Ca_micro > 0:
released_NT = deterministic_release(N_RRP, Ca_micro, NT_cleft)
released_NT = min(released_NT, N_RRP)
N_RRP -= released_NT
NT_cleft += released_NT
# FIX: accumulate for Loop 2 mGluR and IP3 signals.
# This is the only correct way to feed slow signals from fast
# events — snapshot of NT_cleft at Loop 2 time would be ~0
# because passive diffusion has already cleared it.
NT_released_this_window += released_NT
# Passive NT diffusion out of cleft each ms.
NT_cleft *= (1.0 - dt / tau_NT_decay)
NT_cleft = max(0.0, NT_cleft)
# -- 1D. PRESYNAPTIC Ca2+ CLEARANCE ---------------------------
# pump_scale: Hill(ATP_level) — bridges Loop 3 ATP to Loop 1 clearance.
# NCX is ATP-independent (floor); PMCA and SERCA are ATP-gated.
pump_scale = compute_pump_atp_factor(ATP_level, ATP_half)
cleared_PMCA = k_PMCA * Ca_micro * pump_scale
cleared_NCX = k_NCX * Ca_micro
cleared_SERCA = k_SERCA * Ca_micro * pump_scale
Ca_micro -= (cleared_PMCA + cleared_NCX + cleared_SERCA)
Ca_micro = max(0.0, Ca_micro)
Ca_ER += cleared_SERCA
ATP_demand += ATP_cost_PMCA * cleared_PMCA
ATP_demand += ATP_cost_SERCA * cleared_SERCA
# Buffer recharge: bound Ca2+ slowly re-releases back to cytosol.
# During pump failure this sustains Ca_micro elevation (CASCADE 4).
rebind_flux = Ca_buffer_bound * dt / tau_buffer_rebind
Ca_micro += rebind_flux
Ca_buffer_bound = max(0.0, Ca_buffer_bound - rebind_flux)
B_free = B_total - Ca_buffer_bound
# -- 1E. CDI — RISE (spike only) AND RECOVERY (every ms) ------
# RISE: Ca2+ entering through open channels inactivates them locally.
# Gated to spike window — requires channels to be open.
# (Running every ms was wrong: CDI needs Ca2+ flowing through
# the channel, not ambient cytosolic Ca2+.)
if V_pre == 1:
CDI_factor += k_CDI_rise * Ca_micro * dt_s
# RECOVERY: continuous, suppressed when Ca_micro is high.
# Self-locking: pump failure -> Ca_micro high -> recovery ~0
# -> CDI_factor -> 1 -> effective_conductance -> 0 (CASCADE 5-6).
CDI_recovery_rate = k_CDI_rec * (1.0 - Ca_micro / Ca_micro_saturation)
CDI_factor = np.clip(CDI_factor - CDI_recovery_rate * dt_s, 0.0, 1.0)
# -- 1F. CALCIUM TRACE ----------------------------------------
# Leaky integrator — integrates full Ca2+ waveform every ms
# including inter-spike clearance. Drives Loop 2 recruitment speed.
Tr_Ca = Tr_Ca + (Ca_micro - Tr_Ca / tau_Tr_Ca) * dt
# -- 1G. POSTSYNAPTIC: NT DETECTION & AMPA --------------------
# Desensitization reduces effective NT — sustained NT exposure
# progressively silences receptors (postsynaptic equivalent of CDI).
effective_NT = released_NT * (1.0 - Desensitization_level)
receptor_conductance += effective_NT * 0.05
receptor_conductance *= (1.0 - dt / tau_membrane)
V_post += compute_EPSP(receptor_conductance) - (V_post / tau_membrane) * dt
V_post = max(0.0, V_post)
Desensitization_level += NT_cleft * 0.001 * dt
Desensitization_level -= (Desensitization_level / tau_desensitization) * dt
Desensitization_level = np.clip(Desensitization_level, 0.0, 1.0)
V_post_history.append(V_post)
if len(V_post_history) > 5000:
V_post_history.pop(0)
# -- 1H. POSTSYNAPTIC: NMDA COINCIDENCE DETECTION -------------
# Ca_post enters only when BOTH conditions hold simultaneously:
# (1) NT_cleft > 0 — ligand gate (glutamate present)
# (2) V_post elevated — voltage gate (Mg2+ block lifted)
# Mg block removal is a sigmoid of V_post.
# bAP (backpropagating AP) boosts V_post further, enabling
# full NMDA opening and larger Ca_post surge.
V_post_effective = V_post + (bAP * 0.5) # bAP adds depolarisation
Mg_block_removal = V_post_effective / (V_post_effective + V_NMDA_half)
NMDA_Ca_influx = k_NMDA * NT_cleft * Mg_block_removal
Ca_post += NMDA_Ca_influx
# Postsynaptic NKA: membrane recharge cost proportional to V_post.
# [POST-ATP 1] Dominant postsynaptic ATP drain at high activity.
ATP_demand_post += NKA_cost_per_bAP_post * V_post * dt_s
# -- 1I. POSTSYNAPTIC: Ca_post CLEARANCE ----------------------
# pump_scale_post: Hill(ATP_level_post) — same structure as presynaptic.
# NCX is ATP-independent floor (enables auto-reset after ATP recovery).
# [POST-ATP 3] When pump_scale_post falls, Ca_post stays elevated ->
# eCB threshold crossed without genuine coincidence -> false retrograde.
pump_scale_post = compute_pump_atp_factor(ATP_level_post, ATP_half_post)
cleared_Ca_post_pump = k_Ca_post_clear * Ca_post * pump_scale_post
cleared_Ca_post_NCX = k_Ca_post_NCX * Ca_post
Ca_post -= (cleared_Ca_post_pump + cleared_Ca_post_NCX)
Ca_post = max(0.0, Ca_post)
# [POST-ATP 2] ATP cost of postsynaptic PMCA.
ATP_demand_post += ATP_cost_Ca_post_pump * cleared_Ca_post_pump
# -- RECORD ---------------------------------------------------
log["V_pre_state"].append(V_pre_state)
log["Ca_micro"].append(Ca_micro)
log["Ca_ER"].append(Ca_ER)
log["CDI_factor"].append(CDI_factor)
log["B_free"].append(B_free)
log["N_RRP"].append(N_RRP)
log["N_RP"].append(N_RP)
log["Tr_Ca"].append(Tr_Ca)
log["NT_cleft"].append(NT_cleft)
log["V_post"].append(V_post)
log["Ca_post"].append(Ca_post)
log["eCB_level"].append(eCB_level)
log["mGluR_activation"].append(mGluR_activation)
log["released_NT"].append(released_NT)
log["ATP_level"].append(ATP_level)
log["ATP_demand"].append(ATP_demand)
log["ATP_level_post"].append(ATP_level_post)
log["ATP_demand_post"].append(ATP_demand_post)
# ==============================================================
# LOOP 2 — SLOW / ASTROCYTE (dt_slow = 1 s)
# ==============================================================
if (step % High_Freq_Multiplier) == 0:
# Astrocyte EAAT clearance — active NT removal from cleft.
cleared_NT = NT_cleft * 0.3
NT_cleft = max(0.0, NT_cleft - cleared_NT)
# FIX: IP3 integrates NT_released_this_window (total release
# since last Loop 2), not the post-diffusion NT_cleft residual
# which is ~0 by the time Loop 2 runs.
IP3 += NT_released_this_window - (IP3 / tau_IP3) * dt_slow
IP3 = max(0.0, IP3)
if IP3 > IP3_threshold:
trigger_slow_astrocyte_calcium_wave()
# FIX: wave boosts conversion_efficiency in the next mins cycle.
# The astrocyte responds to heavy load by upregulating its
# recycling machinery — shipping more glutamine back to the
# presynapse. Boost decays over tau_wave_decay metabolic cycles.
wave_active = tau_wave_decay
# FIX: mGluR reads NT_released_this_window (accumulated release
# load), not NT_cleft snapshot. NT_cleft is ~0 at Loop 2 time
# due to diffusion; the accumulator correctly represents the
# burst load the autoreceptor has sensed during this window.
NT_window_norm = min(1.0, NT_released_this_window / NT_window_sat)
mGluR_target = NT_window_norm
mGluR_activation += (mGluR_target - mGluR_activation) * (dt_slow / tau_mGluR)
mGluR_activation = np.clip(mGluR_activation, 0.0, 1.0)
# FIX: reset accumulator for next window.
NT_released_this_window = 0.0
# eCB retrograde synthesis: now driven by Ca_post (spine Ca2+),
# not V_post_history. The actual eCB synthesis in the spine is
# triggered by Ca2+-dependent enzymes (DAGL, PLC), not voltage.
# Under normal conditions Ca_post only rises with coincidence.
# Under POST-ATP failure Ca_post stays elevated without genuine
# coincidence -> false retrograde signal (POST-ATP 4).
recent_Ca_post = (np.mean(log["Ca_post"][-2000:])
if len(log["Ca_post"]) >= 2000
else (np.mean(log["Ca_post"]) if log["Ca_post"] else 0.0))
eCB_signal = max(0.0, recent_Ca_post - eCB_threshold)
if eCB_signal > 0:
eCB_level += eCB_signal * (dt_slow / tau_eCB_rise)
else:
eCB_level -= eCB_level * (dt_slow / tau_eCB_decay)
eCB_level = np.clip(eCB_level, 0.0, 1.0)
# FIX: RP->RRP recruitment moved here from Loop 1.
# Biological timescale: vesicle docking and priming take seconds,
# not milliseconds. k_rec_fast/slow are /s; * dt_slow_s = 1.0 s
# gives dimensionless per-step fraction — no hidden unit scaling.
current_recruitment_rate = map_trace_to_speed(Tr_Ca) # /s
refill_amount = (current_recruitment_rate * dt_slow_s
* N_RP * (Max_RRP - N_RRP) / Max_RRP)
refill_amount = max(0.0, refill_amount)
refill_amount = min(refill_amount, N_RP)
N_RRP = min(N_RRP + refill_amount, Max_RRP)
N_RP = max(0.0, N_RP - refill_amount)
ATP_demand += ATP_cost_docking * refill_amount
# ==============================================================
# LOOP 3 — METABOLIC (dt_meta = 1 min)
# ==============================================================
if (step % Metabolic_Multiplier) == 0:
# Presynaptic ATP: glucose supply minus accumulated demand.
conversion_efficiency, ATP_level = compute_astrocyte_metabolic_health(
Glucose_level, ATP_demand
)
ATP_demand = 0.0
# FIX: wave boost applied to conversion_efficiency.
# Astrocyte calcium wave (triggered by high IP3) upregulates
# glutamine synthetase -> faster NT recycling -> more RP refill.
# Boost decays over tau_wave_decay cycles.
if wave_active > 0:
conversion_efficiency = min(1.0, conversion_efficiency + wave_boost)
wave_active -= 1
# Glutamine shuttle: astrocyte converts cleared NT to glutamine,
# presynapse repackages it into vesicles -> N_RP replenished.
refill_RP = Glutamine_pool * conversion_efficiency
N_RP = min(Max_RP, N_RP + refill_RP)
Glutamine_pool = max(0.0, Glutamine_pool - refill_RP)
# Postsynaptic ATP: same glucose budget, own demand accumulator.
# Both sides draw from Glucose_level -> shared metabolic vulnerability.
# Presynaptic silence reduces NT -> less NMDA -> less Ca_post ->
# less ATP_demand_post: presynaptic protection indirectly
# protects the postsynapse.
_, ATP_level_post = compute_astrocyte_metabolic_health(
Glucose_level, ATP_demand_post, ATP_demand_scale_post
)
ATP_demand_post = 0.0
return log
# -----------------------------------------------------------------------
# EXAMPLE USAGE
# -----------------------------------------------------------------------
if __name__ == "__main__":
import matplotlib.pyplot as plt
total_steps = 10_000 # 10 seconds
# Presynaptic 20 Hz burst for 2 s.
spike_train = list(range(0, 2000, 50))
# Postsynaptic bAPs coincident with every 5th presynaptic spike
# (simulates partial coincidence for NMDA activation).
bAP_train = list(range(0, 2000, 250))
results = run_simulation(spike_train, total_steps, bAP_train)
t = np.arange(total_steps) * dt
fig, axes = plt.subplots(8, 1, figsize=(12, 18), sharex=True)
fig.suptitle("Tripartite Synapse — Presynaptic + Postsynaptic", fontsize=13)
axes[0].plot(t, results["V_pre_state"], color="slateblue", lw=0.8)
axes[0].set_ylabel("V_pre")
axes[0].set_title("Presynaptic membrane (AP waveform)", fontsize=9, loc="left")
axes[1].plot(t, results["Ca_micro"], color="darkorange", lw=0.8)
axes[1].set_ylabel("[Ca2+] pre")
axes[1].set_title("CASCADE 4 — presynaptic Ca2+", fontsize=9, loc="left")
axes[2].plot(t, results["CDI_factor"], color="firebrick", lw=0.8, label="CDI")
axes[2].plot(t, results["B_free"], color="steelblue", lw=0.8, label="Buffer free")
axes[2].set_ylabel("CDI / Buffer")
axes[2].set_title("CASCADE 5 — CDI lock-out", fontsize=9, loc="left")
axes[2].legend(fontsize=8)
axes[3].plot(t, results["N_RRP"], color="teal", lw=0.8, label="RRP")
axes[3].plot(t, results["N_RP"], color="purple", lw=0.8, label="RP")
axes[3].set_ylabel("Vesicles")
axes[3].set_title("CASCADE 1 — vesicle depletion", fontsize=9, loc="left")
axes[3].legend(fontsize=8)
axes[4].plot(t, results["NT_cleft"], color="darkgreen", lw=0.8, label="NT cleft")
axes[4].plot(t, results["mGluR_activation"], color="saddlebrown", lw=0.8, label="mGluR")
axes[4].plot(t, results["eCB_level"], color="crimson", lw=0.8, label="eCB")
axes[4].set_ylabel("Cleft / Feedback")
axes[4].set_title("CASCADE 6 — three brakes on conductance", fontsize=9, loc="left")
axes[4].legend(fontsize=8)
axes[5].plot(t, results["V_post"], color="navy", lw=0.8, label="V_post")
axes[5].plot(t, results["Ca_post"], color="coral", lw=0.8, label="Ca_post (spine)")
axes[5].set_ylabel("Postsynaptic")
axes[5].set_title("Postsynaptic potential + NMDA spine Ca2+", fontsize=9, loc="left")
axes[5].legend(fontsize=8)
axes[6].plot(t, results["ATP_level"], color="goldenrod", lw=0.8, label="ATP pre")
axes[6].plot(t, results["ATP_level_post"], color="darkorange", lw=0.8, label="ATP post")
axes[6].set_ylabel("ATP level")
axes[6].set_title("CASCADE 2 / POST-ATP — presynaptic and postsynaptic ATP", fontsize=9, loc="left")
axes[6].legend(fontsize=8)
axes[7].plot(t, results["ATP_demand"], color="tomato", lw=0.8, label="demand pre")
axes[7].plot(t, results["ATP_demand_post"], color="orangered", lw=0.8, label="demand post")
axes[7].set_ylabel("ATP demand")
axes[7].set_title("Accumulated ATP demand (resets each min cycle)", fontsize=9, loc="left")
axes[7].set_xlabel("Time (ms)")
axes[7].legend(fontsize=8)
plt.tight_layout()
plt.savefig("./synapse_simulation.png", dpi=150)
plt.close()
print("Done.")