cdi traces

This commit is contained in:
2026-03-22 16:28:09 +01:00
parent b5d6ad3a3a
commit 8c1ba7d362
3 changed files with 41 additions and 1155 deletions
+41 -14
View File
@@ -39,7 +39,7 @@ container: BEH-AXO
-- CDI rises with Ca2+ each ms: accumulates across inter-spike intervals under pump failure -- CDI rises with Ca2+ each ms: accumulates across inter-spike intervals under pump failure
-- CDI recovers when Ca2+ is low: rate -> 0 when Ca2+ is high — the self-locking feedback -- CDI recovers when Ca2+ is low: rate -> 0 when Ca2+ is high — the self-locking feedback
- Ca²⁺ trace integrates toward CaTraces - Ca²⁺ trace integrates toward CaTraces
- Vesicles release from RRP, based on CaMicro and RRP, suppressed by NT_cleft - Vesicles release from RRP, based on Ca2+ and RRP, suppressed by NT_cleft
- NT added to cleft - NT added to cleft
- NT_released_this_window accumulates (feeds Medium) - NT_released_this_window accumulates (feeds Medium)
- NT passively diffuses out of cleft (Astrocyte behavior) - NT passively diffuses out of cleft (Astrocyte behavior)
@@ -133,25 +133,25 @@ context: RRPConcentration
out_context: RRPFull out_context: RRPFull
``` ```
#### Ca2MicroConcentration: Context #### Ca2+Concentration: Context
```Gen ```Gen
context: Ca2MicroConcentration context: Ca2+Concentration
contained_by: BEH-PRE contained_by: BEH-PRE
in_context: AP in_context: AP
rf: ( active: 60x ) rf: ( active: 60x )
condition: (Ca2Micro medium) condition: (Ca2+ medium)
out_context: Ca2MicroMedium out_context: Ca2+Medium
condition: (Ca2Micro full) condition: (Ca2+ full)
out_context: Ca2MicroFull out_context: Ca2+Full
``` ```
#### NTrelease: Episodes #### NTrelease: Episodes
Ci sono 4 casi che dipendono da RRP, Ca2+ e NT. L'idea e' che la quantita' di RRP sia il driver principale. Gli NT liberati sono di piu' al crescere di RRP e CaMicro e di meno al crescere di NT. Gli NT nella sinapsi fanno da moderazione alla ulteriore liberazione di NT, ma non bloccano mai totalmente. NT suppression only matters when everything else is already at maximum — which is exactly the biological purpose: it prevents runaway release during peak activity, not during moderate activity. Ci sono 4 casi che dipendono da RRP, Ca2+ e NT. L'idea e' che la quantita' di RRP sia il driver principale. Gli NT liberati sono di piu' al crescere di RRP e Ca2+ e di meno al crescere di NT. Gli NT nella sinapsi fanno da moderazione alla ulteriore liberazione di NT, ma non bloccano mai totalmente. NT suppression only matters when everything else is already at maximum — which is exactly the biological purpose: it prevents runaway release during peak activity, not during moderate activity.
ATP cost of Na/K-ATPase recharge on each AP. The cost is per action potential. Here we charge it at every release of NT. This is the dominant ATP drain at high firing rates. ATP cost of Na/K-ATPase recharge on each AP. The cost is per action potential. Here we charge it at every release of NT. This is the dominant ATP drain at high firing rates.
@@ -161,7 +161,7 @@ ATP cost of Na/K-ATPase recharge on each AP. The cost is per action potential. H
episode: NTreleaseMaximum episode: NTreleaseMaximum
contained_by: BEH-PRE contained_by: BEH-PRE
in_context: (Ca2MicroFull AND RRPFull) in_context: (Ca2+Full AND RRPFull)
rf: ( active: 6x ) # Maximum rf: ( active: 6x ) # Maximum
hypothesis: (NT empty) hypothesis: (NT empty)
@@ -175,7 +175,7 @@ episode: NTreleaseMaximum
episode: NTreleaseHigh episode: NTreleaseHigh
contained_by: BEH-PRE contained_by: BEH-PRE
in_context: (Ca2MicroFull AND RRPFull) in_context: (Ca2+Full AND RRPFull)
rf: ( active: 6x ) # High rf: ( active: 6x ) # High
hypothesis: NOT (NT empty) # solo in questo caso NT modera! hypothesis: NOT (NT empty) # solo in questo caso NT modera!
@@ -189,7 +189,7 @@ episode: NTreleaseHigh
episode: NTreleaseMedium episode: NTreleaseMedium
contained_by: BEH-PRE contained_by: BEH-PRE
in_context: (Ca2MicroFull AND RRPMedium) OR (Ca2MicroMedium AND RRPFull) in_context: (Ca2+Full AND RRPMedium) OR (Ca2+Medium AND RRPFull)
rf: ( active: 6x ) # Medium rf: ( active: 6x ) # Medium
hypothesis: (NT empty) OR NOT (NT empty) # In tutti i casi hypothesis: (NT empty) OR NOT (NT empty) # In tutti i casi
@@ -203,7 +203,7 @@ episode: NTreleaseMedium
episode: NTreleaseLow episode: NTreleaseLow
contained_by: BEH-PRE contained_by: BEH-PRE
in_context: (Ca2MicroMedium AND RRPMedium) in_context: (Ca2+Medium AND RRPMedium)
rf: ( active: 6x ) # Low rf: ( active: 6x ) # Low
hypothesis: (NT empty) OR NOT (NT empty) # In tutti i casi hypothesis: (NT empty) OR NOT (NT empty) # In tutti i casi
@@ -233,9 +233,36 @@ Limita rilascio NT: Dipende da quanti NT sono stati gia' rilasciati nella Syn
Limita rilascio NT: Dipende da POST che tende a bloccare rialascio di NT se non servono Limita rilascio NT: Dipende da POST che tende a bloccare rialascio di NT se non servono
#### CDI/CaTraces concentration #### CDIs concentration
Limita rilascio NT: Dipende dalla concentrazione alta di Ca2+ nella PRE. probabilmente sta dentro VGCC? Possiamo metterlo come un controllo su fullness di Ca2+ e cmq spiegare che e' un floor a questo comportamento di CDI
CDI is calcium-dependent inactivation of VGCCs. The inactivation happens because Ca²⁺ enters through the channel and binds to a calmodulin tethered to the channel's intracellular face, physically blocking it from reopening. This is a local, channel-specific event — it requires Ca²⁺ to be flowing through that channel right now, not residual Ca²⁺ drifting in the cytosol between spikes.
So the rise increment should only fire when V_pre == 1 (the spike window when channels are actually open and Ca²⁺ is entering). The recovery, by contrast, should run every millisecond unconditionally — CDI de-inactivation is a continuous process that proceeds whenever Ca²⁺ dissociates from calmodulin, which depends on the ambient Ca_micro level at all times.
Limita rilascio NT: Dipende dalla concentrazione alta di Ca2+ nella PRE. Driver Ca2+ > 0 between spikes CDI -> 1, effective_conductance -> 0.
```Gen
context: CDIConcentration
contained_by: BEH-PRE
in_context: NOT AP # prima volta NOT in un contesto
rf: ( active: 60x )
condition: (Ca2+ full)
out_context: IncreaseCDIFast
condition: (Ca2+ medium)
out_context: IncreaseCDIMedium
condition: (Ca2+ empty)
out_context: DecreaseCDI
```
#### CaTrace concentration
Serve a dare la velocita' al trasporto di vesicles da RP a RRP. Ha un decadimento proprio il che dice alla Presinapsi di accellerare se da poco c'e' stata una spike, altrimenti di andare piu' piano. So after one second of silence Tr_Ca has fallen to ~37% of its peak value, after two seconds to ~14%, after three seconds to ~5%. It asymptotes toward zero but never exactly reaches it. Between spikes, Ca2+ falls toward zero as the pumps clear it.
#### RP->RRP shuttling #### RP->RRP shuttling
@@ -1,493 +0,0 @@
import numpy as np
import matplotlib as plt
import matplotlib.pyplot as plt # <-- this is essential
plt.rcdefaults() # Restore default rc parameters
# ============================================================================
# TRIPARTITE SYNAPSE MODEL
# ============================================================================
# This code simulates a single synapse with presynapse, postsynapse, and astrocyte.
# Timescales range from milliseconds (AP, Ca2+ influx) to minutes (vesicle replenishment,
# astrocyte Ca2+ waves, synaptic weight changes).
#
# Units:
# - Time: seconds (s) or milliseconds (ms) as noted. Internal time step is in seconds.
# - Concentrations: micromolar (µM)
# - Vesicle pools: number of vesicles (can be fractional)
# - Conductances: nanosiemens (nS)
# - Membrane potential: millivolts (mV)
# ============================================================================
# ----------------------------------------------------------------------------
# Presynapse Class
# ----------------------------------------------------------------------------
class Presynapse:
"""
Models the presynaptic terminal.
State variables:
Cf (float): fast calcium concentration (µM)
Cs (float): slow calcium concentration (µM)
R (float): vesicles in Readily Releasable Pool (RRP)
P (float): vesicles in Reserve Pool (RP)
N_VGCC (int): number of functional VGCCs (can be modulated by astrocyte)
release_prob (float): probability of vesicle fusion per AP (computed)
NT_released (float): amount of neurotransmitter ejected (arbitrary units)
"""
def __init__(self, params):
self.p = params # store parameters
# Initial states
self.Cf = 0.0
self.Cs = 0.0
self.R = self.p['R_max'] # start with full RRP
self.P = self.p['P_max'] # start with full RP
self.N_VGCC = self.p['N_VGCC0'] # baseline VGCC number
self.release_prob = 0.0
self.NT_released = 0.0
def ap_event(self):
"""
Called when an action potential arrives.
Increases calcium (fast and slow components) based on available VGCCs.
Triggers vesicle release and updates pools.
"""
# 1. Calcium influx (instantaneous jump)
self.Cf += self.p['alpha'] * self.N_VGCC
self.Cs += self.p['beta'] * self.N_VGCC # small slow component
# 2. Compute release probability (Hill function of total calcium)
C_tot = self.Cf + self.Cs
self.release_prob = self.p['p_max'] * (C_tot**self.p['n_Hill']) / \
(C_tot**self.p['n_Hill'] + self.p['Kd']**self.p['n_Hill'])
# 3. Vesicles released from RRP
released = self.R * self.release_prob
self.NT_released = self.p['gamma'] * released
self.R -= released
# Ensure pools don't go negative (shouldn't happen if dt small)
self.R = max(self.R, 0)
def step(self, dt):
"""
Update presynaptic states between APs.
- Calcium decay (fast and slow)
- Vesicle mobilization (RP -> RRP) depending on calcium trace
- RP replenishment from an infinite source
"""
# Total calcium for trace classification
C_tot = self.Cf + self.Cs
# 1. Calcium decay (first order)
self.Cf -= dt * (self.Cf / self.p['tau_f'])
self.Cs -= dt * (self.Cs / self.p['tau_s'])
# 2. Vesicle mobilization rate based on calcium trace
if C_tot < self.p['theta_low']:
k_mob = self.p['k_slow']
elif C_tot < self.p['theta_high']:
k_mob = self.p['k_med']
else:
k_mob = self.p['k_fast']
# Move vesicles from RP to RRP
move = k_mob * self.P * dt
self.R += move
self.P -= move
# 3. RP replenishment (slow refilling)
replenish = self.p['k_rep'] * (self.p['P_max'] - self.P) * dt
self.P += replenish
# Optional: keep pools within bounds
self.R = min(self.R, self.p['R_max'])
self.P = min(self.P, self.p['P_max'])
self.P = max(self.P, 0)
def set_vgcc_modulation(self, factor):
"""
Allow astrocyte to modulate VGCC availability.
factor: multiplier (0..1) applied to baseline N_VGCC.
"""
self.N_VGCC = self.p['N_VGCC0'] * factor
# ----------------------------------------------------------------------------
# Postsynapse Class
# ----------------------------------------------------------------------------
class Postsynapse:
"""
Models the postsynaptic density and spine.
State variables:
G (float): cleft glutamate concentration (a.u.)
gA (float): AMPA conductance (nS)
gN (float): NMDA conductance (nS)
Vm (float): membrane potential (mV)
C_post (float): postsynaptic calcium (µM)
w (float): synaptic weight (dimensionless, modulates AMPAR conductance)
"""
def __init__(self, params):
self.p = params
self.G = 0.0
self.gA = 0.0
self.gN = 0.0
self.Vm = self.p['E_rest']
self.C_post = self.p['C_post_rest']
self.w = 1.0 # initial weight
def receive_nt(self, NT_released):
"""
Neurotransmitter released into cleft. Instantaneous rise,
then decay handled in step().
"""
self.G += NT_released # instantaneous jump
def step(self, dt, astrocyte_dserine_factor=1.0):
"""
Update postsynaptic states.
- Glutamate clearance
- AMPA/NMDA conductance kinetics
- Membrane potential (Euler)
- Postsynaptic calcium dynamics
- Synaptic weight plasticity (slow)
"""
# 1. Glutamate clearance (first order)
self.G -= dt * (self.G / self.p['tau_glu'])
# 2. AMPA receptor kinetics
# Target conductance = max conductance * (G/(G+K)) * weight
target_A = self.p['gA_max'] * (self.G / (self.G + self.p['K_glu'])) * self.w
self.gA += dt * ((target_A - self.gA) / self.p['tau_AMPA'])
# 3. NMDA receptor kinetics (with voltage-dependent Mg block)
Mg_block = 1.0 / (1.0 + (self.p['Mg'] / 3.57) * np.exp(-0.062 * self.Vm))
# astrocyte D-serine enhances NMDA conductance (multiplying max)
target_N = self.p['gN_max'] * astrocyte_dserine_factor * \
(self.G / (self.G + self.p['K_glu'])) * Mg_block
self.gN += dt * ((target_N - self.gN) / self.p['tau_NMDA'])
# 4. Membrane potential (current balance)
I_syn = self.gA * (self.Vm - self.p['E_AMPA']) + self.gN * (self.Vm - self.p['E_NMDA'])
I_leak = self.p['gL'] * (self.Vm - self.p['E_rest'])
dVm = (-I_leak - I_syn) / self.p['Cm']
self.Vm += dt * dVm
# 5. Postsynaptic calcium influx (via NMDA and VGCCs)
# Simplified: calcium current proportional to NMDA conductance and voltage driving force
I_Ca_NMDA = self.p['eta_N'] * self.gN * (self.Vm - self.p['E_Ca'])
# VGCC contribution (activated by depolarisation)
vgcc_act = 1.0 / (1.0 + np.exp(-(self.Vm - self.p['V_half'])/self.p['k_vgcc']))
I_Ca_VGCC = self.p['eta_V'] * vgcc_act * (self.Vm - self.p['E_Ca'])
self.C_post += dt * (I_Ca_NMDA + I_Ca_VGCC - (self.C_post - self.p['C_post_rest'])/self.p['tau_Ca_post'])
# 6. Synaptic weight plasticity (slow, calcium-driven)
# LTP and LTD thresholds based on calcium control hypothesis
if self.C_post > self.p['theta_LTP']:
dw = self.p['gamma_LTP'] * (1.0 - self.w) * dt
elif self.C_post > self.p['theta_LTD']:
dw = -self.p['gamma_LTD'] * self.w * dt
else:
dw = 0.0
self.w += dw
self.w = np.clip(self.w, 0.0, 2.0) # keep within bounds
# ----------------------------------------------------------------------------
# Astrocyte Class
# ----------------------------------------------------------------------------
class Astrocyte:
"""
Models an astrocytic process enwrapping the synapse.
State variables:
I (float): IP3 concentration (µM)
A_Ca (float): astrocyte calcium (µM)
S (float): gliotransmitter (ATP) concentration (a.u.)
uptake_efficiency (float): modulates glutamate clearance (not used directly)
"""
def __init__(self, params):
self.p = params
self.I = 0.0
self.A_Ca = self.p['A_Ca_rest']
self.S = 0.0
self.uptake = 1.0 # can be modulated
def sense_glutamate(self, G):
"""
Glutamate spillover activates mGluRs, producing IP3.
IP3 production is proportional to low-pass filtered glutamate.
"""
# Low-pass filter of G (simple exponential moving average)
# We'll use a separate state for filtered G for simplicity.
# For now, we directly update IP3 based on G with a delay.
# Here we use a simplified approach: IP3 production rate depends on G.
pass
def step(self, dt, G):
"""
Update astrocyte states.
- IP3 dynamics (production from mGluR activation, decay)
- Calcium release from internal stores (IP3-dependent)
- Gliotransmitter release when calcium high
"""
# 1. IP3 production (driven by glutamate spillover) and decay
# Use a Hill function for mGluR activation
mGluR_act = (G**self.p['n_mGluR']) / (G**self.p['n_mGluR'] + self.p['K_mGluR']**self.p['n_mGluR'])
prod = self.p['beta_IP3'] * mGluR_act
self.I += dt * (prod - self.I / self.p['tau_IP3'])
# 2. Astrocyte calcium (simplified Li-Rinzel type)
# IP3 opens ER channels; calcium-induced calcium release not explicitly modeled
# We use a simple equation: increase when IP3 high, decay otherwise.
# More detailed: dA_Ca/dt = r_IP3 * (I^n/(I^n+K_I^n)) * (A_store - A_Ca) - A_Ca/tau_A_Ca
# For simplicity, use a direct dependence:
target = self.p['A_max'] * (self.I**self.p['n_IP3']) / (self.I**self.p['n_IP3'] + self.p['K_IP3']**self.p['n_IP3'])
self.A_Ca += dt * ((target - self.A_Ca) / self.p['tau_A_rise'] - (self.A_Ca - self.p['A_Ca_rest'])/self.p['tau_A_decay'])
# 3. Gliotransmitter release (ATP) when calcium above threshold
if self.A_Ca > self.p['theta_S']:
self.S = self.p['S_max'] # instantaneous release, can be made graded
else:
self.S = 0.0
# Gliotransmitter decays
self.S *= np.exp(-dt / self.p['tau_S'])
def get_vgcc_modulation(self):
"""
Compute factor to modulate presynaptic VGCCs (e.g., via adenosine).
"""
# ATP released by astrocyte is converted to adenosine, which activates A1 receptors.
# We model it as a simple inhibition: factor = 1 - delta * S/(S+K_S)
factor = 1.0 - self.p['delta_A1'] * (self.S / (self.S + self.p['K_A1']))
return max(factor, 0.0)
def get_dserine_modulation(self):
"""
D-serine enhances NMDA receptor function.
Return a factor >1 when astrocyte calcium is high.
"""
# Simple relation: factor = 1 + dserine_max * (A_Ca/(A_Ca+K_D))
factor = 1.0 + self.p['dserine_max'] * (self.A_Ca / (self.A_Ca + self.p['K_D']))
return factor
# ============================================================================
# Parameter Sets (with timescales and biological references)
# ============================================================================
# All times are in seconds unless noted.
presyn_params = {
# Calcium dynamics
'alpha': 0.5, # fast Ca influx per VGCC (µM)
'beta': 0.02, # slow Ca influx per VGCC (µM)
'tau_f': 0.020, # fast Ca decay time constant (20 ms)
'tau_s': 1.0, # slow Ca decay time constant (1 s)
# VGCC
'N_VGCC0': 10, # baseline number of functional VGCCs
# Release
'p_max': 0.8, # max release probability
'Kd': 2.0, # Ca half-activation (µM)
'n_Hill': 4, # Hill coefficient (cooperativity)
'gamma': 1.0, # conversion factor from vesicles to NT units
# Vesicle pools
'R_max': 50, # max RRP size (vesicles)
'P_max': 200, # max RP size (vesicles)
'k_fast': 2.0, # fast mobilization rate (s^-1) under high Ca
'k_med': 0.5, # medium mobilization rate (s^-1)
'k_slow': 0.1, # slow mobilization rate (s^-1) under low Ca
'k_rep': 0.03, # RP replenishment rate (s^-1) (time constant ~33 s)
# Calcium trace thresholds (µM)
'theta_low': 1.0,
'theta_high': 3.0,
}
postsyn_params = {
# Glutamate dynamics
'tau_glu': 0.002, # glutamate decay in cleft (2 ms)
'K_glu': 1.0, # glutamate half-activation for receptors (a.u.)
# AMPA receptors
'gA_max': 5.0, # max AMPA conductance (nS)
'tau_AMPA': 0.002, # AMPA rise/decay time (2 ms, simplified)
'E_AMPA': 0.0, # reversal potential (mV)
# NMDA receptors
'gN_max': 2.0, # max NMDA conductance (nS)
'tau_NMDA': 0.050, # NMDA decay time (50 ms)
'E_NMDA': 0.0, # reversal potential (mV)
'Mg': 1.0, # extracellular Mg concentration (mM)
# Membrane parameters
'Cm': 1.0, # membrane capacitance (µF/cm^2, scaled)
'gL': 0.1, # leak conductance (nS)
'E_rest': -70.0, # resting potential (mV)
'E_Ca': 120.0, # calcium reversal potential (mV)
# Postsynaptic calcium
'eta_N': 0.02, # NMDA calcium influx factor (µM/ms/nA? simplified)
'eta_V': 0.01, # VGCC calcium influx factor
'V_half': -20.0, # half-activation for VGCC (mV)
'k_vgcc': 5.0, # slope factor for VGCC activation
'tau_Ca_post': 0.050, # calcium decay time (50 ms)
'C_post_rest': 0.05, # resting calcium (µM)
# Plasticity thresholds
'theta_LTD': 0.5, # LTD threshold (µM)
'theta_LTP': 1.5, # LTP threshold (µM)
'gamma_LTD': 0.001, # LTD rate (s^-1)
'gamma_LTP': 0.002, # LTP rate (s^-1)
}
astro_params = {
# IP3 dynamics
'beta_IP3': 0.1, # IP3 production rate per glutamate (µM/s)
'tau_IP3': 2.0, # IP3 decay time constant (2 s)
'K_mGluR': 0.5, # glutamate half-activation for mGluR (a.u.)
'n_mGluR': 2, # cooperativity for mGluR
# Astrocyte calcium
'A_max': 2.0, # max calcium release (µM)
'K_IP3': 0.5, # IP3 half-activation (µM)
'n_IP3': 2, # cooperativity for IP3
'tau_A_rise': 1.0, # rise time for Ca release (1 s)
'tau_A_decay': 5.0, # decay time for Ca (5 s)
'A_Ca_rest': 0.1, # resting calcium (µM)
# Gliotransmitter release
'theta_S': 0.8, # Ca threshold for release (µM)
'S_max': 1.0, # max gliotransmitter concentration (a.u.)
'tau_S': 1.0, # gliotransmitter decay time (1 s)
# Feedback parameters
'delta_A1': 0.5, # maximum inhibition of VGCCs (fraction)
'K_A1': 0.3, # half-saturation for adenosine inhibition (a.u.)
'dserine_max': 0.5, # maximum fractional increase in NMDA conductance
'K_D': 0.5, # half-saturation for D-serine modulation (µM)
}
# ============================================================================
# Simulation Setup
# ============================================================================
# Create instances
pre = Presynapse(presyn_params)
post = Postsynapse(postsyn_params)
astro = Astrocyte(astro_params)
# Simulation parameters
dt = 0.0001 # time step = 0.1 ms
T_total = 30.0 # simulate 30 seconds
time = np.arange(0, T_total, dt)
n_steps = len(time)
# Storage for plotting
store = {
't': [],
'Cf': [], 'Cs': [], 'R': [], 'P': [],
'G': [], 'gA': [], 'gN': [], 'Vm': [], 'C_post': [], 'w': [],
'I': [], 'A_Ca': [], 'S': [],
'AP': []
}
# Define spike times (example: two bursts)
spike_times = []
# 5 spikes at 20 Hz starting at 1.0 s
spike_times.extend(np.linspace(1.0, 1.2, 5, endpoint=False))
# 10 spikes at 50 Hz starting at 5.0 s
spike_times.extend(np.linspace(5.0, 5.18, 10, endpoint=False))
# 3 spikes at 10 Hz starting at 15.0 s
spike_times.extend(np.linspace(15.0, 15.2, 3, endpoint=False))
spike_times.sort()
spike_index = 0
next_spike = spike_times[spike_index] if spike_times else np.inf
# Main simulation loop
for i, t in enumerate(time):
# Check for AP
AP = 0
if t >= next_spike:
AP = 1
spike_index += 1
next_spike = spike_times[spike_index] if spike_index < len(spike_times) else np.inf
# --- Presynapse ---
if AP:
pre.ap_event()
pre.step(dt)
# --- Astrocyte modulation (feedback) ---
# Astrocyte senses glutamate from cleft (post.G)
astro.step(dt, post.G)
vgcc_factor = astro.get_vgcc_modulation()
pre.set_vgcc_modulation(vgcc_factor)
dserine_factor = astro.get_dserine_modulation()
# --- Postsynapse ---
# Transfer released NT
if AP:
post.receive_nt(pre.NT_released)
post.step(dt, dserine_factor)
# Store data every 1 ms to save memory
if i % int(0.001/dt) == 0:
store['t'].append(t)
store['Cf'].append(pre.Cf)
store['Cs'].append(pre.Cs)
store['R'].append(pre.R)
store['P'].append(pre.P)
store['G'].append(post.G)
store['gA'].append(post.gA)
store['gN'].append(post.gN)
store['Vm'].append(post.Vm)
store['C_post'].append(post.C_post)
store['w'].append(post.w)
store['I'].append(astro.I)
store['A_Ca'].append(astro.A_Ca)
store['S'].append(astro.S)
store['AP'].append(AP)
# ============================================================================
# Plot Results
# ============================================================================
fig, axes = plt.subplots(4, 1, figsize=(10, 12), sharex=True)
t_plot = np.array(store['t'])
# Presynapse
axes[0].plot(t_plot, store['Cf'], label='Fast Ca', color='C0')
axes[0].plot(t_plot, store['Cs'], label='Slow Ca', color='C1')
axes[0].set_ylabel('Ca (µM)')
axes[0].legend(loc='upper right')
axes[0].set_title('Presynapse')
ax2 = axes[0].twinx()
ax2.plot(t_plot, store['R'], label='RRP', color='C2', linestyle='--')
ax2.plot(t_plot, store['P'], label='RP', color='C3', linestyle='--')
ax2.set_ylabel('Vesicles')
ax2.legend(loc='lower right')
# Postsynapse conductances and potential
axes[1].plot(t_plot, store['gA'], label='AMPA', color='C4')
axes[1].plot(t_plot, store['gN'], label='NMDA', color='C5')
axes[1].set_ylabel('Conductance (nS)')
axes[1].legend(loc='upper left')
ax2 = axes[1].twinx()
ax2.plot(t_plot, store['Vm'], label='Vm', color='C6', linestyle='-')
ax2.set_ylabel('Vm (mV)')
ax2.legend(loc='upper right')
# Postsynaptic calcium and weight
axes[2].plot(t_plot, store['C_post'], label='Post Ca', color='C7')
axes[2].set_ylabel('Ca (µM)')
axes[2].legend(loc='upper left')
ax2 = axes[2].twinx()
ax2.plot(t_plot, store['w'], label='Weight w', color='C8', linestyle='--')
ax2.set_ylabel('Synaptic weight')
ax2.legend(loc='upper right')
# Astrocyte
axes[3].plot(t_plot, store['I'], label='IP3', color='C9')
axes[3].plot(t_plot, store['A_Ca'], label='Astro Ca', color='C10')
axes[3].plot(t_plot, store['S'], label='Gliotrans.', color='C11', linestyle=':')
axes[3].set_ylabel('Conc. (µM / a.u.)')
axes[3].set_xlabel('Time (s)')
axes[3].legend(loc='upper right')
axes[3].set_title('Astrocyte')
plt.tight_layout()
plt.tight_layout()
# plt.show() # Comment out or replace
plt.savefig('tripartite_simulation.png', dpi=150, bbox_inches='tight')
print("Simulation finished. Plot saved as 'tripartite_simulation.png'.")
# plt.show()
@@ -1,648 +0,0 @@
# Tripartite Synapse - Multi-Scale Computational Model
import numpy as np
# -----------------------------------------------------------------------
# PARAMETERS
# -----------------------------------------------------------------------
dt = 1.0 # ms - high-freq timestep
dt_slow = 1000.0 # ms - astrocyte / slow loop timestep
dt_meta = 60_000.0 # ms - metabolic loop timestep
High_Freq_Multiplier = int(dt_slow / dt) # 1000
Metabolic_Multiplier = int(dt_meta / dt) # 60000
# Unit-conversion scalar — derived once, used wherever a biological rate
# constant is expressed in /s but the loop timestep is in ms.
# Multiplying a /s rate by dt_s gives a dimensionless per-step increment:
# increment = k [/s] * Ca * dt_s [s/step] = dimensionless / step
dt_s = dt / 1000.0 # ms -> s (= 0.001 s per 1 ms step)
# -- Calcium influx & buffering --
N_VGCC = 100 # ORIG: number of presynaptic VGCCs
# [CASCADE 6] Sets the absolute ceiling of Ca2+ influx.
# effective_conductance can only reduce from here.
V_pre_voltage = -10.0 # ORIG: mV - membrane potential during AP
k_flux = 0.05 # ORIG: Ca2+ influx per open channel (a.u.)
B_total = 1.0 # ORIG: total buffer capacity (normalised)
# [CASCADE 4] When B_free -> 0 (buffer saturated),
# all raw_influx enters Ca_micro directly,
# accelerating residual Ca2+ build-up.
tau_buffer_rebind = 200.0 # NEW: ms - time for saturated buffer to recharge
# [CASCADE 4] Slow recharge means buffer stays saturated
# during sustained bursting, removing its
# protective role against Ca2+ accumulation.
# -- Ca2+ clearance rate constants (per ms) --
# [CASCADE 3] These three constants define maximum clearance capacity.
# PMCA and SERCA are multiplied by pump_scale (ATP-gated);
# NCX runs independently but cannot fully compensate when they fail.
k_PMCA = 0.03 # NEW: plasma membrane Ca-ATPase - primary high-affinity pump
# [CASCADE 3] First pump to fail under low ATP. Its rate
# constant is the largest ATP-dependent term;
# halving pump_scale loses 0.015/ms of clearance.
k_NCX = 0.10 # NEW: sodium-calcium exchanger - fast, NOT ATP-dependent
# [CASCADE 3 note] NCX keeps clearing during pump failure,
# but k_NCX alone cannot prevent Ca2+
# accumulation when PMCA + SERCA are both
# impaired - it is a floor, not a rescue.
k_SERCA = 0.01 # NEW: ER pump - slowest, ATP-dependent
# [CASCADE 3] Compounds PMCA failure; also stops loading
# Ca_ER, so the ER store cannot buffer spikes.
ATP_half = 0.3 # NEW: Hill half-saturation for ATP-dependent pumps
# [CASCADE 3] At ATP_level = ATP_half pumps run at 50% speed.
# Below this value pump failure is disproportionately
# severe because the Hill curve is steepest here.
# -- CDI (calcium-dependent inactivation) --
k_CDI_rise = 0.8 # ORIG: CDI build rate — units /s per unit Ca_micro
# Biological rate constant expressed in per-second units.
# Applied inside map_calcium_to_inactivation via * dt_s.
# [CASCADE 5] Higher value = VGCCs lock shut sooner at any [Ca2+].
Ca_micro_saturation = 2.0 # NEW: normalisation ceiling for CDI recovery
# [CASCADE 5] When Ca_micro = Ca_micro_saturation,
# CDI_recovery_rate -> 0 completely.
k_CDI_rec = 0.015 # NEW: CDI de-inactivation rate — units /s
# Biological rate constant expressed in per-second units.
# Applied in the loop via * dt_s.
# [CASCADE 5] Lower value = slower recovery = longer silence
# window after pump failure.
# -- Vesicle pools --
Max_RRP = 20 # ORIG: maximum readily-releasable pool size
# [CASCADE 1] Smaller Max_RRP -> exhausted in fewer spikes.
Max_RP = 200 # ORIG: maximum reserve pool size
# [CASCADE 1] Once depleted, only the glutamine shuttle
# (Loop 3, minutes timescale) can refill it.
p_release_base = 0.1 # ORIG: base release probability per vesicle per ms
# [CASCADE 1] Multiplied by Ca_micro inside stochastic_release,
# so higher Ca_micro during a burst draws MORE
# vesicles per spike - a positive feedback that
# accelerates RRP depletion.
# -- Trace integrator --
tau_Tr_Ca = 1000.0 # ORIG: ms - calcium trace decay
T_high = 0.6 # ORIG: trace threshold -> fast recruitment
T_low = 0.2 # ORIG: trace threshold -> slow recruitment
k_rec_fast = 0.005 # ORIG: fast RP->RRP recruitment rate (/ms)
# [CASCADE 1] Even at maximum, recruitment lags release at
# high firing. When N_RP also falls, the term
# (rate * N_RP * headroom) shrinks on both sides,
# accelerating collapse toward N_RRP = 0.
k_rec_slow = 0.0005 # ORIG: slow RP->RRP recruitment rate (/ms)
# -- Postsynaptic --
tau_membrane = 20.0 # ORIG: ms - membrane time constant
tau_desensitization = 500.0 # ORIG: ms - receptor desensitization decay
# -- eCB retrograde brake --
tau_eCB_rise = 2000.0 # ORIG: ms - rise time constant
# [CASCADE 6] eCB is a slow-onset brake (~2 s rise).
# Provides early partial conductance
# suppression before CDI lock-out is complete.
tau_eCB_decay = 10_000.0 # ORIG: ms - decay time constant
# [CASCADE 6] Slow decay (10 s) means eCB persists as
# a partial brake even as the burst ends,
# prolonging the silence window.
eCB_threshold = 0.7 # ORIG: V_post level that triggers eCB synthesis
# [CASCADE 6] Requires sustained postsynaptic depolarisation;
# acts as an integrating safety threshold.
# -- mGluR presynaptic autoreceptor --
Km_mGluR = 0.5 # NEW: Michaelis-Menten half-saturation for NT_cleft
# [CASCADE 6] Lower Km -> autoreceptor saturates at smaller
# NT_cleft -> faster VGCC suppression onset.
tau_mGluR = 2000.0 # NEW: ms - mGluR activation / desensitization time constant
# [CASCADE 6] Governs how quickly the autoreceptor engages
# and releases as NT_cleft rises and falls.
alpha_mGluR = 0.4 # NEW: max fractional VGCC suppression by mGluR
# [CASCADE 6] Upper bound of this term. Combined with eCB,
# both brakes together can suppress ~70% of
# conductance before CDI becomes significant.
# -- Astrocyte / IP3 --
tau_IP3 = 3000.0 # ORIG: ms
IP3_threshold = 0.8 # ORIG
# -- Glutamine shuttle --
conversion_efficiency_base = 0.8 # ORIG: fraction of Gln pool converted per cycle
# [CASCADE 1] Sets max RP-refill rate.
# Degraded by metabolic fatigue via
# conversion_efficiency in Loop 3.
# -----------------------------------------------------------------------
# HELPER FUNCTIONS
# -----------------------------------------------------------------------
def compute_flux(conductance, voltage):
# ORIG: Ca2+ influx into microdomain.
# [CASCADE 4] Raw influx that, after buffer capture, loads Ca_micro.
# Collapses to near zero once CASCADE 6 locks effective_conductance.
return k_flux * conductance * abs(voltage)
def stochastic_release(N_RRP, Ca_micro):
# ORIG: Binomial vesicle release from RRP.
# [CASCADE 1] p scales with Ca_micro: higher Ca_micro during the early burst
# draws MORE vesicles per spike than at rest - a positive feedback
# that accelerates RRP depletion beyond what recruitment can match.
p = min(1.0, p_release_base * Ca_micro)
return int(np.random.binomial(int(N_RRP), p))
def map_trace_to_speed(Tr_Ca):
# ORIG: Maps calcium trace to RP->RRP recruitment rate.
# [CASCADE 1] Even at maximum (k_rec_fast = 0.005/ms), recruitment lags
# release at high firing. As N_RP and headroom both fall,
# refill_amount shrinks and N_RRP collapses.
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 map_calcium_to_inactivation(Ca_micro):
# ORIG: Ca2+ drives CDI increment each ms.
# k_CDI_rise is in /s. Multiplying by dt_s converts it to a
# dimensionless per-step increment:
# increment = k_CDI_rise [/s] * Ca_micro * dt_s [s/step]
# Using dt_s here (not dt) makes the unit conversion explicit and
# avoids the implicit /1000 anti-pattern.
# [CASCADE 5] Fires every ms that Ca_micro > 0, including inter-spike
# intervals under pump failure. Result: CDI_factor accumulates
# across spikes instead of resetting, climbing toward 1.
return k_CDI_rise * Ca_micro * dt_s
def compute_pump_atp_factor(ATP_level):
# NEW: Hill function - ATP gates PMCA and SERCA speed.
# [CASCADE 3] Mechanistic bridge from CASCADE 2 (ATP_level, written by
# Loop 3 once per minute) to CASCADE 4 (Ca_micro staying elevated).
# ATP_level = 1.0 -> pump_scale ~= 0.92 (near full speed)
# ATP_level = 0.3 -> pump_scale = 0.50 (half speed)
# ATP_level = 0.1 -> pump_scale ~= 0.10 (severe failure)
# Hill exponent of 2 gives a sigmoidal response: small drops
# have modest effect; drops below ATP_half are disproportionate.
return (ATP_level ** 2) / (ATP_level ** 2 + ATP_half ** 2)
def compute_EPSP(receptor_conductance):
# ORIG: postsynaptic potential increment.
return receptor_conductance * 0.1
def compute_postsynaptic_eCB_signal(V_post_history):
# ORIG: eCB synthesis triggered by sustained high postsynaptic activity.
# [CASCADE 6] Integrates V_post over a 2 s window. Acts as a slow retrograde
# warning signal; its tau_eCB_decay of 10 s means it outlasts
# the burst and keeps conductance partially suppressed during recovery.
recent_mean = (np.mean(V_post_history[-2000:])
if len(V_post_history) >= 2000
else np.mean(V_post_history))
if recent_mean > eCB_threshold:
return recent_mean - eCB_threshold
return 0.0
def compute_astrocyte_metabolic_health(Glucose_level, step):
# ORIG + NEW ATP output.
# [CASCADE 2] Glucose_level is the root upstream variable of the slow cascade.
# Under sustained high firing, demand outpaces vascular delivery
# and Glucose_level falls. This function converts that into:
#
# ATP_level -> [CASCADE 2->3 bridge]
# Read every ms by compute_pump_atp_factor() in Loop 1.
# This is how minute-scale metabolic state reaches the
# millisecond Ca2+ clearance equations.
#
# conversion_efficiency -> [CASCADE 1, slow refill arm]
# Gates glutamine shuttle throughput; low efficiency starves
# N_RP refill, prolonging vesicle depletion.
#
# Both outputs degrade together as Glucose_level -> 0,
# tightening CASCADE 1 and CASCADE 3-5 simultaneously.
health = np.clip(Glucose_level, 0.0, 1.0)
return health, health # (conversion_efficiency, ATP_level)
def trigger_slow_astrocyte_calcium_wave():
# ORIG: placeholder - would release gliotransmitters over ~10 s.
pass
# -----------------------------------------------------------------------
# STATE VARIABLES (initial values)
# -----------------------------------------------------------------------
# -- Presynaptic Ca2+ --
Ca_micro = 0.0 # ORIG: free cytosolic [Ca2+] in microdomain
# [CASCADE 4] Central victim variable.
# Normally returns to ~0 between spikes;
# stays elevated when pumps fail.
Ca_ER = 0.5 # NEW: Ca2+ stored in ER (loaded by SERCA)
Ca_buffer_bound = 0.0 # NEW: Ca2+ currently held by buffer proteins
B_free = 1.0 # NEW: free buffer binding sites (= B_total at rest)
# [CASCADE 4] B_free -> 0 during sustained bursting.
# Once saturated, capture_fraction -> 0 and
# raw_influx enters Ca_micro unattenuated.
# -- CDI --
CDI_factor = 0.0 # ORIG: fractional VGCC inactivation (0 -> 1)
# [CASCADE 5] Primary silence variable; rises with Ca_micro,
# recovery suppressed by Ca_micro.
# [CASCADE 6] Appears as (1 - CDI_factor) in effective_conductance;
# when CDI_factor -> 1 conductance collapses to 0.
# -- Vesicle pools --
N_RRP = 15 # ORIG: readily-releasable pool (docked vesicles)
# [CASCADE 1] Depleted in seconds at high firing rate.
N_RP = 150 # ORIG: reserve pool
# [CASCADE 1] Depleted over tens of seconds; only the glutamine
# shuttle (Loop 3) can refill it on minute timescales.
# -- Calcium trace --
Tr_Ca = 0.0 # ORIG: integrative calcium memory; drives RP->RRP recruitment speed.
# -- NT in cleft --
NT_cleft = 0.0 # ORIG: normalised NT concentration
# [CASCADE 6] Drives mGluR_activation (fastest conductance brake).
# -- Postsynaptic --
V_post = 0.0 # ORIG
receptor_conductance = 0.0 # ORIG
Desensitization_level = 0.0 # ORIG
V_post_history = [] # ORIG: rolling buffer for eCB computation
# -- Retrograde / autoreceptor --
eCB_level = 0.0 # ORIG: endocannabinoid retrograde brake (0 -> 1)
# [CASCADE 6] Second multiplicative suppressor of
# effective_conductance. Slow onset (~2 s),
# slow decay (~10 s).
mGluR_activation = 0.0 # NEW: presynaptic autoreceptor occupancy (0 -> 1)
# [CASCADE 6] Third multiplicative suppressor. Fastest
# onset because it reads NT_cleft directly,
# no postsynaptic relay required.
# -- Astrocyte --
IP3 = 0.0 # ORIG
Glutamine_pool = 50.0 # ORIG
# -- Metabolic --
ATP_level = 1.0 # NEW: normalised ATP (1 = replete)
# [CASCADE 2->3] Bridge variable: written by Loop 3
# (minutes), read every ms in Loop 1
# via compute_pump_atp_factor().
conversion_efficiency = 0.8 # ORIG
Glucose_level = 1.0 # ORIG: external glucose supply (0 -> 1)
# [CASCADE 2] Root input of the slow cascade arm.
# Set < 1.0 to activate metabolic silencing.
# Try 0.2 to observe full pump failure.
# -- NT cleft decay --
tau_NT_decay = 5.0 # ms - passive NT diffusion / dilution out of cleft
# -----------------------------------------------------------------------
# MAIN SIMULATION LOOP
# -----------------------------------------------------------------------
def run_simulation(spike_train, total_steps):
"""
spike_train : list of int timestep indices at which an AP arrives
total_steps : int number of 1 ms steps to simulate
"""
global Ca_micro, Ca_ER, Ca_buffer_bound, B_free
global CDI_factor
global N_RRP, N_RP
global Tr_Ca, NT_cleft
global V_post, receptor_conductance, Desensitization_level, V_post_history
global eCB_level, mGluR_activation
global IP3, Glutamine_pool
global ATP_level, conversion_efficiency, Glucose_level
log = {k: [] for k in [
"Ca_micro", "Ca_ER", "CDI_factor", "B_free",
"N_RRP", "N_RP", "Tr_Ca", "NT_cleft",
"V_post", "eCB_level", "mGluR_activation",
"released_NT", "ATP_level",
]}
spike_set = set(spike_train)
for step in range(total_steps):
# ==============================================================
# LOOP 1 - HIGH-FREQUENCY (dt = 1 ms)
# ==============================================================
V_pre = 1 if step in spike_set else 0
released_NT = 0
# -- 1A. PRESYNAPTIC SPIKE HANDLING ----------------------------
if V_pre == 1:
# [CASCADE 6] OUTCOME of the full cascade.
# All three suppression terms must be near zero
# for significant Ca2+ influx to occur.
#
# Onset timing during a burst:
# mGluR*alpha_mGluR rises first (~seconds, reads NT_cleft)
# eCB_level rises second (~2-10 s, retrograde)
# CDI_factor locks last but is irreversible until
# Ca_micro falls (needs pump recovery)
effective_conductance = (
N_VGCC
* (1.0 - eCB_level) # [CASCADE 6] retrograde brake
* (1.0 - CDI_factor) # [CASCADE 5->6] CDI lock-out
* (1.0 - mGluR_activation * alpha_mGluR) # [CASCADE 6] autoreceptor brake
)
raw_influx = compute_flux(effective_conductance, V_pre_voltage)
# NEW: Buffer proteins capture a fraction of raw_influx immediately.
# [CASCADE 4] capture_fraction = B_free / B_total.
# Early in a burst B_free ~= B_total so most influx is
# captured and Ca_micro rises slowly (buffer is protective).
# As bursting continues B_free -> 0 (buffer saturates),
# capture_fraction -> 0 and full raw_influx enters Ca_micro,
# accelerating the residual Ca2+ accumulation of CASCADE 4.
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)
# ORIG: NT release from RRP.
# [CASCADE 1] Each spike draws from N_RRP via stochastic_release.
# Draw rate (p * N_RRP) exceeds refill rate
# (k_rec_fast * N_RP * headroom) at high firing,
# driving N_RRP -> 0.
if N_RRP > 0:
released_NT = stochastic_release(N_RRP, Ca_micro)
N_RRP = max(0, N_RRP - released_NT) # floor guard
NT_cleft += released_NT
# -- 1B. Ca2+ CLEARANCE (runs every ms, spike or not) ----------
# [CASCADE 3] pump_scale links CASCADE 2 (ATP depletion) to
# CASCADE 4 (residual Ca2+ accumulation).
# Written from ATP_level which is updated once per minute
# in Loop 3; read here every millisecond.
pump_scale = compute_pump_atp_factor(ATP_level)
# [CASCADE 3->4] PMCA: primary high-affinity Ca2+ extrusion pump.
# ATP-dependent. First pump to be impaired; largest
# single loss of clearance capacity when pump_scale drops.
cleared_PMCA = k_PMCA * Ca_micro * pump_scale
# [CASCADE 3 note] NCX: fast, NOT ATP-dependent.
# Continues clearing during metabolic failure.
# Acts as a partial floor but cannot alone prevent
# Ca2+ accumulation when PMCA + SERCA are impaired.
# Also enables the auto-reset: once high-frequency
# drive stops, NCX gradually lowers Ca_micro and
# allows CDI recovery even without restored ATP.
cleared_NCX = k_NCX * Ca_micro
# [CASCADE 3->4] SERCA: ER pump, ATP-dependent.
# Compounds PMCA failure. Also stops loading Ca_ER,
# so the ER store cannot buffer subsequent spikes.
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 # SERCA fills the ER store when ATP is available
# NEW: Buffer recharge - bound Ca2+ slowly re-releases back to cytosol.
# [CASCADE 4] During pump failure, released buffer Ca2+ is not promptly
# cleared, so it adds to Ca_micro and sustains its elevation
# between spikes - a secondary positive feedback within 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
# -- 1C. CDI - RISE AND RECOVERY --------------------------------
# [CASCADE 5] CDI increment: fires every ms proportional to Ca_micro.
# Under normal conditions Ca_micro returns to ~0 between
# spikes and CDI is balanced by recovery below.
# Under pump failure (CASCADE 4) Ca_micro stays > 0
# between spikes, so this increment fires continuously
# and CDI_factor climbs monotonically toward 1.
CDI_factor += map_calcium_to_inactivation(Ca_micro)
# [CASCADE 5] CDI recovery: maximised when Ca_micro = 0, suppressed
# as Ca_micro approaches Ca_micro_saturation.
# This is the self-locking mechanism of the cascade:
# pump failure -> Ca_micro stays high
# Ca_micro high -> CDI_recovery_rate -> 0
# CDI no recovery -> CDI_factor -> 1
# CDI_factor -> 1 -> effective_conductance -> 0 (CASCADE 6)
# k_CDI_rec is in /s; multiply by dt_s for a per-step decrement.
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)
# -- 1D. TRACE INTEGRATOR --------------------------------------
# ORIG: integrates Ca_micro; drives RP->RRP recruitment speed.
Tr_Ca = Tr_Ca + (Ca_micro - Tr_Ca / tau_Tr_Ca) * dt
# -- 1E. RP -> RRP RECRUITMENT (with pool guards) --------------
# [CASCADE 1] Only counter-force to vesicle depletion.
# refill_amount = rate * N_RP * (Max_RRP - N_RRP)
# As N_RP falls (reservoir empties) and N_RRP -> 0
# (headroom = Max_RRP, maximised), the N_RP term collapses
# and refill becomes negligible - depletion becomes
# self-sustaining once N_RP is exhausted.
current_recruitment_rate = map_trace_to_speed(Tr_Ca)
refill_amount = current_recruitment_rate * N_RP * (Max_RRP - N_RRP)
refill_amount = max(0.0, refill_amount) # never negative
refill_amount = min(refill_amount, N_RP) # cannot exceed RP supply
N_RRP = min(N_RRP + refill_amount, Max_RRP) # ceiling guard
N_RP = max(0.0, N_RP - refill_amount) # floor guard
# -- 1F. POSTSYNAPTIC FAST RESPONSE ----------------------------
# ORIG: receptor activation and desensitization.
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)
# ORIG: NT diffuses / dilutes out of cleft each ms.
NT_cleft *= (1.0 - dt / tau_NT_decay)
NT_cleft = max(0.0, NT_cleft)
V_post_history.append(V_post)
if len(V_post_history) > 5000:
V_post_history.pop(0)
# -- RECORD -----------------------------------------------------
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["eCB_level"].append(eCB_level)
log["mGluR_activation"].append(mGluR_activation)
log["released_NT"].append(released_NT)
log["ATP_level"].append(ATP_level)
# ==============================================================
# LOOP 2 - SLOW / ASTROCYTE (dt_slow = 1 s)
# ==============================================================
if (step % High_Freq_Multiplier) == 0:
# ORIG: Astrocyte clears NT from cleft via EAATs.
cleared_NT = NT_cleft * 0.3
NT_cleft = max(0.0, NT_cleft - cleared_NT)
# ORIG: IP3 integrates clearance load.
IP3 += cleared_NT - (IP3 / tau_IP3) * dt_slow
IP3 = max(0.0, IP3)
if IP3 > IP3_threshold:
trigger_slow_astrocyte_calcium_wave()
# ORIG: eCB retrograde signal.
# [CASCADE 6] Second conductance brake. Onset ~2 s after sustained
# postsynaptic activity crosses eCB_threshold. Its slow
# decay (10 s) means it persists as a partial brake
# during the recovery window, preventing premature
# re-engagement of the synapse after a silence event.
eCB_signal = compute_postsynaptic_eCB_signal(V_post_history)
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)
# NEW: mGluR presynaptic autoreceptor feedback.
# [CASCADE 6] Fastest conductance brake. Reads NT_cleft directly
# (no postsynaptic relay), so it rises within seconds
# of burst onset when NT_cleft is highest.
# At saturation it suppresses up to alpha_mGluR = 40%
# of conductance, acting as the first partial brake
# and slightly slowing Ca2+ influx before CDI builds.
# Decays rapidly when NT_cleft falls (e.g. after vesicle
# depletion in CASCADE 1), so it does not contribute to
# the irreversible lock-out - that role belongs to CDI.
mGluR_target = NT_cleft / (NT_cleft + Km_mGluR)
mGluR_activation += (mGluR_target - mGluR_activation) * (dt_slow / tau_mGluR)
mGluR_activation = np.clip(mGluR_activation, 0.0, 1.0)
# ==============================================================
# LOOP 3 - METABOLIC (dt_meta = 1 min)
# ==============================================================
if (step % Metabolic_Multiplier) == 0:
# [CASCADE 2] Root of the slow cascade arm.
# Glucose_level is the external input that sets the
# pace of ATP replenishment. Under sustained demand
# it falls, reducing ATP_level.
#
# ATP_level written here is the CASCADE 2->3 bridge:
# it is read every millisecond in Loop 1 by
# compute_pump_atp_factor(), carrying the minute-scale
# metabolic state into the Ca2+ clearance equations.
#
# conversion_efficiency (also written here) is the
# CASCADE 1 slow-refill arm: low efficiency starves
# N_RP, making vesicle depletion more severe and
# prolonged after each burst episode.
conversion_efficiency, ATP_level = compute_astrocyte_metabolic_health(
Glucose_level, step
)
# [CASCADE 1 - slow refill] Glutamine shuttle rebuilds N_RP.
# Gated by conversion_efficiency; when low, N_RP cannot
# recover between bursts, so successive episodes deplete
# faster - a cumulative fatigue effect.
refill_RP = Glutamine_pool * conversion_efficiency
N_RP = min(Max_RP, N_RP + refill_RP)
Glutamine_pool = max(0.0, Glutamine_pool - refill_RP)
return log
# -----------------------------------------------------------------------
# EXAMPLE USAGE
# -----------------------------------------------------------------------
if __name__ == "__main__":
import matplotlib.pyplot as plt
total_steps = 10_000 # 10 seconds of simulated time
# 20 Hz burst for 2 s, then silence.
# Set Glucose_level = 0.2 before calling run_simulation() to engage
# the full metabolic cascade (CASCADES 2-5).
spike_train = list(range(0, 2000, 50))
results = run_simulation(spike_train, total_steps)
t = np.arange(total_steps) * dt
fig, axes = plt.subplots(6, 1, figsize=(12, 14), sharex=True)
fig.suptitle("Tripartite Synapse - Metabolic Silencing Cascade", fontsize=13)
axes[0].plot(t, results["Ca_micro"], color="darkorange", lw=0.8)
axes[0].set_ylabel("[Ca2+] free (a.u.)")
axes[0].set_title("CASCADE 4 - residual Ca2+ stays high under pump failure",
fontsize=9, loc="left")
axes[1].plot(t, results["CDI_factor"], color="firebrick", lw=0.8, label="CDI factor")
axes[1].plot(t, results["B_free"], color="steelblue", lw=0.8, label="Buffer free")
axes[1].set_ylabel("CDI / Buffer (0-1)")
axes[1].set_title("CASCADE 5 - CDI lock-out | CASCADE 4 secondary - buffer saturation",
fontsize=9, loc="left")
axes[1].legend(fontsize=8)
axes[2].plot(t, results["N_RRP"], color="teal", lw=0.8, label="RRP")
axes[2].plot(t, results["N_RP"], color="purple", lw=0.8, label="RP")
axes[2].set_ylabel("Vesicles")
axes[2].set_title("CASCADE 1 - vesicle depletion (fast)", fontsize=9, loc="left")
axes[2].legend(fontsize=8)
axes[3].plot(t, results["NT_cleft"], color="darkgreen", lw=0.8, label="NT cleft")
axes[3].plot(t, results["mGluR_activation"], color="saddlebrown", lw=0.8, label="mGluR (CASCADE 6)")
axes[3].plot(t, results["eCB_level"], color="crimson", lw=0.8, label="eCB (CASCADE 6)")
axes[3].set_ylabel("Cleft / Feedback")
axes[3].set_title("CASCADE 6 - three multiplicative brakes on effective_conductance",
fontsize=9, loc="left")
axes[3].legend(fontsize=8)
axes[4].plot(t, results["V_post"], color="navy", lw=0.8)
axes[4].set_ylabel("V_post (a.u.)")
axes[4].set_title("CASCADE 6 result - postsynaptic silence", fontsize=9, loc="left")
axes[5].plot(t, results["ATP_level"], color="goldenrod", lw=0.8)
axes[5].set_ylabel("ATP level (0-1)")
axes[5].set_title("CASCADE 2 - set Glucose_level < 1.0 to activate this arm",
fontsize=9, loc="left")
axes[5].set_xlabel("Time (ms)")
plt.tight_layout()
plt.savefig("./synapse_simulation.png", dpi=150)
plt.close()
print("Done.")