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)