"""
.. module:: cls
:synopsis: Module containing basic cls abstract class
.. moduleauthor:: Marc Javin
"""
import pylab as plt
from cycler import cycler
from abc import ABC, abstractmethod
import numpy as np
from odynn import utils
from odynn.utils import classproperty
import tensorflow as tf
import scipy as sp
[docs]class Neuron(ABC):
V_pos = 0
"""int, Default position of the voltage in state vectors"""
_ions = {}
"""dictionnary, name of ions in the vector states and their positions"""
default_init_state = None
"""array, Initial values for the vector of state variables"""
def __init__(self, dt=0.1):
self.dt = dt
self._init_state = self.default_init_state
@property
def num(self):
"""int, Number of neurons being modeled in this object"""
return self._num
@property
def init_state(self):
"""ndarray, Initial state vector"""
return self._init_state
@classproperty
def ions(self):
"""dict, contains the names of modeled ion concentrations as keys and their position in init_state as values"""
return self._ions
[docs] @abstractmethod
def step(self, X, i):
"""
Integrate and update state variable (voltage and possibly others) after one time step
Args:
X(ndarray): State variables
i(float): Input current
Returns:
ndarray: updated state vector
"""
pass
[docs] @classmethod
def plot_output(cls, ts, i_inj, states, y_states=None, suffix="", show=True, save=False, l=1, lt=1,
targstyle='-'):
"""
Plot voltage and ion concentrations, potentially compared to a target model
Args:
ts(ndarray of dimension [time]): time steps of the measurements
i_inj(ndarray of dimension [time]): input current
states(ndarray of dimension [time, state_var, nb_neuron]):
y_states(list of ndarray [time, nb_neuron], optional): list of values for the target model, each element is an
ndarray containing the recordings of one state variable (Default value = None)
suffix(str): suffix for the name of the saved file (Default value = "")
show(bool): If True, show the figure (Default value = True)
save(bool): If True, save the figure (Default value = False)
l(float): width of the main lines (Default value = 1)
lt(float): width of the target lines (Default value = 1)
targstyle(str): style of the target lines (Default value = '-')
"""
plt.figure()
nb_plots = len(cls._ions) + 2
custom_cycler = None
if (states.ndim > 3): # circuit in parallel
states = np.reshape(np.swapaxes(states,-2,-1), (states.shape[0], states.shape[1], -1))
custom_cycler = cycler('color', utils.COLORS.repeat(y_states[cls.V_pos].shape[1]))
y_states = [np.reshape(y, (y.shape[0], -1)) if y is not None else None for y in y_states]
# Plot voltage
p = plt.subplot(nb_plots, 1, 1)
if custom_cycler is not None:
p.set_prop_cycle(custom_cycler)
plt.plot(ts, states[:, cls.V_pos], linewidth=l)
if y_states is not None:
if y_states[cls.V_pos] is not None:
plt.plot(ts, y_states[cls.V_pos], 'r', linestyle=targstyle, linewidth=lt, label='target model')
plt.legend()
plt.ylabel('Voltage (mV)')
for i, (ion, pos) in enumerate(cls._ions.items()):
p = plt.subplot(nb_plots, 1, 2+i)
if custom_cycler is not None:
p.set_prop_cycle(custom_cycler)
plt.plot(ts, states[:, pos], linewidth=l)
if y_states is not None:
if y_states[pos] is not None:
plt.plot(ts, y_states[pos], 'r', linestyle=targstyle, linewidth=lt, label='target model')
plt.legend()
plt.ylabel('[{}]'.format(ion))
plt.subplot(nb_plots, 1, nb_plots)
plt.plot(ts, i_inj, 'b')
plt.xlabel('t (ms)')
plt.ylabel('$I_{inj}$ ($\\mu{A}/cm^2$)')
utils.save_show(show, save, utils.IMG_DIR + 'output_%s' % suffix)
[docs] @abstractmethod
def calculate(self, i):
"""Iterate over i (current) and return the state variables obtained after each step
Args:
i(ndarray): input current, dimension [time, (batch, (self.num))]
Returns:
ndarray: state vectors concatenated [i.shape[0], len(self.init_state)(, i.shape[1], (i.shape[2]))]
"""
pass
[docs]class BioNeuron(Neuron):
"""Abstract class to implement for using a new biological model
All methods and class variables have to be implemented in order to have the expected behavior
"""
default_params = None
"""dict, Default set of parameters for the model, of the form {<param_name> : value}"""
parameter_names = None
"""names of parameters from the model"""
_constraints_dic = None
"""dict, Constraints to be applied during optimization
Should be of the form : {<variable_name> : [lower_bound, upper_bound]}
"""
def __new__(cls, *args, **kwargs):
obj = Neuron.__new__(cls)
obj._init_names()
return obj
[docs] def __init__(self, init_p=None, tensors=False, dt=0.1):
"""
Reshape the initial state and parameters for parallelization in case init_p is a list
Args:
init_p(dict or list of dict): initial parameters of the neuron(s). If init_p is a list, then this object
will model n = len(init_p) neurons
tensors(bool): used in the step function in order to use tensorflow or numpy
dt(float): time step
"""
Neuron.__init__(self, dt=dt)
if(init_p is None):
init_p = self.default_params
self._num = 1
elif(init_p == 'random'):
init_p = self.get_random()
self._num = 1
elif isinstance(init_p, list):
self._num = len(init_p)
if self._num == 1:
init_p = init_p[0]
else:
init_p = {var: np.array([p[var] for p in init_p], dtype=np.float32) for var in init_p[0].keys()}
elif hasattr(init_p[self.parameter_names[0]], '__len__'):
self._num = len(init_p[self.parameter_names[0]])
init_p = {var: np.array(val, dtype=np.float32) for var, val in init_p.items()}
else:
self._num = 1
if self._num > 1:
self._init_state = np.stack([self._init_state for _ in range(self._num)], axis=-1)
self._tensors = tensors
self._init_p = init_p
self._param = self._init_p.copy()
self.dt = dt
def _inf(self, V, rate):
"""Compute the steady state value of a gate activation rate"""
mdp = self._param['%s__mdp' % rate]
scale = self._param['%s__scale' % rate]
if self._tensors:
return tf.sigmoid((V - mdp) / scale)
else:
return 1 / (1 + sp.exp((mdp - V) / scale))
def _update_gate(self, rate, name, V):
tau = self._param['%s__tau'%name]
return ((tau * self.dt) / (tau + self.dt)) * ((rate / self.dt) + (self._inf(V, name) / tau))
[docs] def calculate(self, i_inj):
"""
Simulate the neuron with input current `i_inj` and return the state vectors
Args:
i_inj: input currents of shape [time, batch]
Returns:
ndarray: series of state vectors of shape [time, state, batch]
"""
X = [self._init_state]
for i in i_inj:
X.append(self.step(X[-1], i))
return np.array(X[1:])
@classmethod
def _init_names(cls):
cls.parameter_names = list(cls.default_params.keys())
[docs] @staticmethod
def get_random():
"""Return a dictionnary with the same keys as default_params and random values"""
pass
[docs] @staticmethod
def plot_results(*args, **kwargs):
"""Function for plotting detailed results of some experiment"""
pass
[docs] def parallelize(self, n):
"""Add a dimension of size n in the initial parameters and initial state
Args:
n(int): size of the new dimension
"""
if self._num > 1 and list(self._init_p.values())[0].ndim == 1:
self._init_p = {var: np.stack([val for _ in range(n)], axis=val.ndim) for var, val in self._init_p.items()}
elif not hasattr(list(self._init_p.values())[0], '__len__'):
self._init_p = {var: np.stack([val for _ in range(n)], axis=-1) for var, val in self._init_p.items()}
self._init_state = np.stack([self._init_state for _ in range(n)], axis=-1)
self._param = self._init_p.copy()