Paramecium
This is an implementation of a Paramecium in a 2D environment based on the model described in the following paper:
Irene Elices, Anirudh Kulkarni, Nicolas Escoubet, Léa-Laetitia Pontani, Alexis Michel Prevost, Romain Brette, An electrophysiological and kinematic model of Paramecium, the “swimming neuron”, PLoS Comput Biol 19(2): e1010899, 2023.
Code
"""
Full model
"""
from brian2 import *
from brian2.units.allunits import umetre3
from brian2.units.constants import faraday_constant as F, gas_constant as R
from brian2ros import *
from matplotlib.animation import FuncAnimation
set_device("ros_standalone", directory="src/src/brian_project", debug=True)
cell_length = 120.0
cell_width = 35.0
def cell_border(theta=0.0):
"""
Returns cell coordinates in um, centered on 0.
The cell points towards the right.
"""
# Shape coordinates
asymetry = 0.15
xcell = np.linspace(-cell_length * 0.5, cell_length * 0.5, 150)
ycell = (
0.5
* cell_width
* (
(1 - 4 * xcell**2 / cell_length**2) ** 0.5
- asymetry * np.sin(np.pi * 2 * xcell / cell_length)
)
)
ycell = np.hstack([ycell, -ycell[::-1]])
xcell = np.hstack([xcell, xcell[::-1]])
xshape = xcell * np.cos(theta) - ycell * np.sin(theta)
yshape = xcell * np.sin(theta) + ycell * np.cos(theta)
return xshape, yshape
def get_cell_outline(x, y, scale, theta=0.0):
xcell, ycell = cell_border(theta=theta) # pointing up
xcell = np.hstack([xcell, [xcell[0]]]) / cell_length
ycell = np.hstack([ycell, [ycell[0]]]) / cell_length
return x + scale * xcell, y + scale * ycell
constants = {
"C": 275.0 * pfarad,
"Cai0_cilia": 100.0 * nmolar,
"DV": R * (273 + 20.0) * kelvin / F, # 20 °C
"EK": -48.0 * mvolt,
"EL": -23.411751 * mvolt,
"F": F,
"Jpumpmax_cilia": 1.14231898 * khertz,
"K_electromotor": 1.4 * umolar,
"R": R,
"Re": 82.21786662 * Mohm,
"VCa_cilia": 5.235069 * mvolt,
"V_IK": 0.33134 * mvolt,
"a_IK": 100.0 * usecond,
"alpha_cilia": 2.52799157 * hertz,
"angle_min": 0.761927814,
"angle_span": -4.093696582,
"b_IK": 2.973515 * msecond,
"gCa_cilia": 1.0 * uamp,
"gKCa_cilia": 27.8 * namp,
"gL": 11.8 * nsiemens,
"g_IK": 2.31783779 * namp,
"kCa_cilia": 4.659191 * mvolt,
"k_IK": 3.230705 * mvolt,
"nCaM_Ca_cilia": 4.223339529,
"nCaM_KCa_cilia": 1.830536013,
"n_electromotor": 5.592777655,
"n_electromotor_coupling": 2,
"omega_max": 8 * pi / second,
"omega_min": 2 * pi / second,
"pKCa": 3.196528772,
"pKKCa": 7.367400475,
"pK_electromotor": 2.632978236,
"p_ICa": 2.0,
"taue": 0.540317 * msecond,
"taum_Ca_cilia": 1.419777 * msecond,
"theta_max": 90 * pi / 180,
"theta_min": 13 * pi / 180,
"v_cilia": 1700.0 * umetre3,
"v_minus": -0.001 * metre * second**-1,
"v_plus": 0.001 * metre * second**-1,
"v0": 3,
"a_max": 1.86,
"a_min": -1.86,
}
range = LaserScanSubscriber(
name= "range",
output={"ranges": 0}
)
equations = Equations(
"""
dv/dt = (IL+ICa_cilia+IK+IKCa_cilia+I)/C : volt
d = int(range(t,0,0) <= 0.5) : 1
I = 10*nA*d : amp
dx/dt = velocity*cos(orientation) : meter
dy/dt = velocity*sin(orientation) : meter
IL = gL*(EL-v) : amp
IV_K = (EK-v)/DV : 1
IV_Ca = 1/exprel(2*v/DV) : 1
## IK (rectifier)
IK = g_IK * n_IK**2 * IV_K : amp
ninf_IK = 1/(1+exp((V_IK-v)/k_IK)) : 1
dn_IK / dt = (ninf_IK - n_IK)/tau_IK: 1
tau_IK = a_IK + b_IK/(cosh((v-V_IK)/(2*k_IK))) : second
### ICa_cilia (depolarization-activated)
ICa_cilia = gCa_cilia*m_Ca_cilia**2*h_Ca_cilia*IV_Ca : amp
dm_Ca_cilia/dt = (minf_Ca_cilia-m_Ca_cilia)/taum_Ca_cilia : 1
minf_Ca_cilia = 1/(1+exp((VCa_cilia-v)/kCa_cilia)) : 1
h_Ca_cilia = 1/(1+exp(nCaM_Ca_cilia*(pCa-pKCa))) : 1
### IK(Ca)
IKCa_cilia = gKCa_cilia / (1+ exp(-nCaM_KCa_cilia*(pCa-pKKCa)))* IV_K : amp # gKCa is the amplitude at 10 uM
#### Calcium dynamics ####
dpCa/dt = ICa_cilia/(2*F*Cai0_cilia*v_cilia)*exp(-pCa) + alpha_cilia*(exp(-pCa)-1)- Jpump_cilia : 1
Jpump_cilia = Jpumpmax_cilia / (1+exp(pCa)) : 1/second
Cai_cilia = Cai0_cilia*exp(pCa) : mM
### Electromotor coupling
velocity = (-v_plus + 2*v_plus/(1+(Cai_cilia/K_electromotor)**2))*200 : meter/second (constant over dt)
escape_orientation : radian
dorientation/dt = ((orientation - escape_orientation + pi) % (2*pi) - pi)/(50*ms): radian
angular = clip((direction - 0.5)*int(t < t_start + t_turn)*v0,a_min,a_max) : 1 (constant over dt)
direction : 1
t_start : second
t_turn : second
""",
**constants,
)
width, height = 2, 2
neuron = NeuronGroup(
1,
equations,
method="euler",
threshold="velocity > 0*meter/second",
refractory="velocity > 0*meter/second",
reset="direction=rand(); t_turn = rand()*100*ms + 500*ms; t_start = t",
)
neuron.v = constants["EL"]
neuron.orientation = -np.pi / 4
neuron.escape_orientation = -np.pi / 4
motor_commands = TwistPublisher(
rate=200,
input={"linear.x": neuron.velocity, "angular.z": neuron.angular},
)
get_device().add_publisher(motor_commands)
S_sensor = SpikeMonitor(neuron)
pops = PopulationRateMonitor(neuron)
M = StateMonitor(neuron, "v", record=True, dt=1 * ms)
trigger_time = 500 * ms
I0 = 5 * nA
runtime = 99999999 * second
run(runtime, report="text", report_period=10 * second)