diff --git a/MANIFEST.in b/MANIFEST.in index 9c783c6..e6aa0db 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -1,6 +1,6 @@ include README.md -include PointNICE/lookups/*/*.pkl -include PointNICE/templates/*.xlsx +include PySONIC/lookups/*/*.pkl +include PySONIC/templates/*.xlsx include /sim/*.py include /plot/plot_comp.py include /plot/plot_batch.py \ No newline at end of file diff --git a/PointNICE.sublime-project b/PySONIC.sublime-project similarity index 100% rename from PointNICE.sublime-project rename to PySONIC.sublime-project diff --git a/PointNICE/__init__.py b/PySONIC/__init__.py similarity index 100% rename from PointNICE/__init__.py rename to PySONIC/__init__.py diff --git a/PointNICE/bls.py b/PySONIC/bls.py similarity index 99% rename from PointNICE/bls.py rename to PySONIC/bls.py index e35a346..61c9b06 100644 --- a/PointNICE/bls.py +++ b/PySONIC/bls.py @@ -1,685 +1,685 @@ #!/usr/bin/env python # -*- coding: utf-8 -*- # @Author: Theo Lemaire # @Date: 2016-09-29 16:16:19 # @Email: theo.lemaire@epfl.ch # @Last Modified by: Theo Lemaire -# @Last Modified time: 2018-08-21 14:27:37 +# @Last Modified time: 2018-08-21 16:10:35 import inspect import logging import warnings import numpy as np import scipy.integrate as integrate from scipy.optimize import brentq, curve_fit from .utils import * from .constants import * # Get package logger -logger = logging.getLogger('PointNICE') +logger = logging.getLogger('PySONIC') class BilayerSonophore: """ This class contains the geometric and mechanical parameters of the Bilayer Sonophore Model, as well as all the core functions needed to compute the dynamics (kinetics and kinematics) of the bilayer membrane cavitation, and run dynamic BLS simulations. """ # BIOMECHANICAL PARAMETERS T = 309.15 # Temperature (K) Rg = 8.314 # Universal gas constant (Pa.m^3.mol^-1.K^-1) delta0 = 2.0e-9 # Thickness of the leaflet (m) Delta_ = 1.4e-9 # Initial gap between the two leaflets on a non-charged membrane at equil. (m) pDelta = 1.0e5 # Attraction/repulsion pressure coefficient (Pa) m = 5.0 # Exponent in the repulsion term (dimensionless) n = 3.3 # Exponent in the attraction term (dimensionless) rhoL = 1075.0 # Density of the surrounding fluid (kg/m^3) muL = 7.0e-4 # Dynamic viscosity of the surrounding fluid (Pa.s) muS = 0.035 # Dynamic viscosity of the leaflet (Pa.s) kA = 0.24 # Area compression modulus of the leaflet (N/m) alpha = 7.56 # Tissue shear loss modulus frequency coefficient (Pa.s) C0 = 0.62 # Initial gas molar concentration in the surrounding fluid (mol/m^3) kH = 1.613e5 # Henry's constant (Pa.m^3/mol) P0 = 1.0e5 # Static pressure in the surrounding fluid (Pa) Dgl = 3.68e-9 # Diffusion coefficient of gas in the fluid (m^2/s) xi = 0.5e-9 # Boundary layer thickness for gas transport across leaflet (m) c = 1515.0 # Speed of sound in medium (m/s) # BIOPHYSICAL PARAMETERS epsilon0 = 8.854e-12 # Vacuum permittivity (F/m) epsilonR = 1.0 # Relative permittivity of intramembrane cavity (dimensionless) def __init__(self, diameter, Fdrive, Cm0, Qm0, embedding_depth=0.0): """ Constructor of the class. :param diameter: in-plane diameter of the sonophore structure within the membrane (m) :param Fdrive: frequency of acoustic perturbation (Hz) :param Cm0: membrane resting capacitance (F/m2) :param Qm0: membrane resting charge density (C/m2) :param embedding_depth: depth of the embedding tissue around the membrane (m) """ logger.debug('%.1f nm BLS initialization at %.2f kHz, %.2f nC/cm2', diameter * 1e9, Fdrive * 1e-3, Qm0 * 1e5) # Extract resting constants and geometry self.Cm0 = Cm0 self.Qm0 = Qm0 self.a = diameter self.d = embedding_depth self.S0 = np.pi * self.a**2 # Derive frequency-dependent tissue elastic modulus G_tissue = self.alpha * Fdrive # G'' (Pa) self.kA_tissue = 2 * G_tissue * self.d # kA of the tissue layer (N/m) # Check existence of lookups for derived parameters lookups = get_BLS_lookups(self.a) Qkey = '{:.2f}'.format(Qm0 * 1e5) # If no lookup, compute parameters and store them in lookup if not lookups or Qkey not in lookups: # Find Delta that cancels out Pm + Pec at Z = 0 (m) if self.Qm0 == 0.0: D_eq = self.Delta_ else: (D_eq, Pnet_eq) = self.findDeltaEq(self.Qm0) assert Pnet_eq < PNET_EQ_MAX, 'High Pnet at Z = 0 with ∆ = %.2f nm' % (D_eq * 1e9) self.Delta = D_eq # Find optimal Lennard-Jones parameters to approximate PMavg (LJ_approx, std_err, _) = self.LJfitPMavg() assert std_err < PMAVG_STD_ERR_MAX, 'High error in PmAvg nonlinear fit:'\ ' std_err = %.2f Pa' % std_err self.LJ_approx = LJ_approx lookups[Qkey] = {'LJ_approx': LJ_approx, 'Delta_eq': D_eq} logger.debug('Saving BLS derived parameters to lookup file') save_BLS_lookups(self.a, lookups) # If lookup exists, load parameters from it else: logger.debug('Loading BLS derived parameters from lookup file') self.LJ_approx = lookups[Qkey]['LJ_approx'] self.Delta = lookups[Qkey]['Delta_eq'] # Compute initial volume and gas content self.V0 = np.pi * self.Delta * self.a**2 self.ng0 = self.gasPa2mol(self.P0, self.V0) def __str__(self): s = '-------- Bilayer Sonophore --------\n' s += 'class attributes:\n' class_attrs = inspect.getmembers(self.__class__, lambda a: not(inspect.isroutine(a))) class_attrs = [a for a in class_attrs if not(a[0].startswith('__') and a[0].endswith('__'))] for ca in class_attrs: s += '{} = {}\n'.format(ca[0], ca[1]) s += 'instance attributes:\n' inst_attrs = inspect.getmembers(self, lambda a: not(inspect.isroutine(a))) inst_attrs = [a for a in inst_attrs if not(a[0].startswith('__') and a[0].endswith('__')) and a not in class_attrs] for ia in inst_attrs: s += '{} = {}\n'.format(ia[0], ia[1]) return s def reinit(self): logger.debug('Re-initializing BLS object') # Find Delta that cancels out Pm + Pec at Z = 0 (m) if self.Qm0 == 0.0: D_eq = self.Delta_ else: (D_eq, Pnet_eq) = self.findDeltaEq(self.Qm0) assert Pnet_eq < PNET_EQ_MAX, 'High Pnet at Z = 0 with ∆ = %.2f nm' % (D_eq * 1e9) self.Delta = D_eq # Compute initial volume and gas content self.V0 = np.pi * self.Delta * self.a**2 self.ng0 = self.gasPa2mol(self.P0, self.V0) def curvrad(self, Z): """ Return the (signed) instantaneous curvature radius of the leaflet. :param Z: leaflet apex outward deflection value (m) :return: leaflet curvature radius (m) """ if Z == 0.0: return np.inf else: return (self.a**2 + Z**2) / (2 * Z) def v_curvrad(self, Z): ''' Vectorized curvrad function ''' return np.array(list(map(self.curvrad, Z))) def surface(self, Z): """ Return the surface area of the stretched leaflet (spherical cap). :param Z: leaflet apex outward deflection value (m) :return: surface of the stretched leaflet (m^2) """ return np.pi * (self.a**2 + Z**2) def volume(self, Z): """ Return the total volume of the inter-leaflet space (cylinder +/- spherical cap). :param Z: leaflet apex outward deflection value (m) :return: inner volume of the bilayer sonophore structure (m^3) """ return np.pi * self.a**2 * self.Delta\ * (1 + (Z / (3 * self.Delta) * (3 + Z**2 / self.a**2))) def arealstrain(self, Z): """ Compute the areal strain of the stretched leaflet. epsilon = (S - S0)/S0 = (Z/a)^2 :param Z: leaflet apex outward deflection value (m) :return: areal strain (dimensionless) """ return (Z / self.a)**2 def Capct(self, Z): """ Compute the membrane capacitance per unit area, under the assumption of parallel-plate capacitor with average inter-layer distance. :param Z: leaflet apex outward deflection value (m) :return: capacitance per unit area (F/m2) """ if Z == 0.0: return self.Cm0 else: return ((self.Cm0 * self.Delta / self.a**2) * (Z + (self.a**2 - Z**2 - Z * self.Delta) / (2 * Z) * np.log((2 * Z + self.Delta) / self.Delta))) def v_Capct(self, Z): ''' Vectorized Capct function ''' return np.array(list(map(self.Capct, Z))) def derCapct(self, Z, U): """ Compute the derivative of the membrane capacitance per unit area with respect to time, under the assumption of parallel-plate capacitor. :param Z: leaflet apex outward deflection value (m) :param U: leaflet apex outward deflection velocity (m/s) :return: derivative of capacitance per unit area (F/m2.s) """ dCmdZ = ((self.Cm0 * self.Delta / self.a**2) * ((Z**2 + self.a**2) / (Z * (2 * Z + self.Delta)) - ((Z**2 + self.a**2) * np.log((2 * Z + self.Delta) / self.Delta)) / (2 * Z**2))) return dCmdZ * U def localdef(self, r, Z, R): """ Compute the (signed) local transverse leaflet deviation at a distance r from the center of the dome. :param r: in-plane distance from center of the sonophore (m) :param Z: leaflet apex outward deflection value (m) :param R: leaflet curvature radius (m) :return: local transverse leaflet deviation (m) """ if np.abs(Z) == 0.0: return 0.0 else: return np.sign(Z) * (np.sqrt(R**2 - r**2) - np.abs(R) + np.abs(Z)) def Pacoustic(self, t, Adrive, Fdrive, phi=np.pi): """ Compute the acoustic pressure at a specific time, given the amplitude, frequency and phase of the acoustic stimulus. :param t: time of interest :param Adrive: acoustic drive amplitude (Pa) :param Fdrive: acoustic drive frequency (Hz) :param phi: acoustic drive phase (rad) """ return Adrive * np.sin(2 * np.pi * Fdrive * t - phi) def PMlocal(self, r, Z, R): """ Compute the local intermolecular pressure. :param r: in-plane distance from center of the sonophore (m) :param Z: leaflet apex outward deflection value (m) :param R: leaflet curvature radius (m) :return: local intermolecular pressure (Pa) """ z = self.localdef(r, Z, R) relgap = (2 * z + self.Delta) / self.Delta_ return self.pDelta * ((1 / relgap)**self.m - (1 / relgap)**self.n) def PMavg(self, Z, R, S): """ Compute the average intermolecular pressure felt across the leaflet by quadratic integration. :param Z: leaflet apex outward deflection value (m) :param R: leaflet curvature radius (m) :param S: surface of the stretched leaflet (m^2) :return: averaged intermolecular resultant pressure across the leaflet (Pa) .. warning:: quadratic integration is computationally expensive. """ # Intermolecular force over an infinitely thin ring of radius r fMring = lambda r, Z, R: 2 * np.pi * r * self.PMlocal(r, Z, R) # Integrate from 0 to a fTotal, _ = integrate.quad(fMring, 0, self.a, args=(Z, R)) return fTotal / S def v_PMavg(self, Z, R, S): ''' Vectorized PMavg function ''' return np.array(list(map(self.PMavg, Z, R, S))) def LJfitPMavg(self): """ Determine optimal parameters of a Lennard-Jones expression approximating the average intermolecular pressure. These parameters are obtained by a nonlinear fit of the Lennard-Jones function for a range of deflection values between predetermined Zmin and Zmax. :return: 3-tuple with optimized LJ parameters for PmAvg prediction (Map) and the standard and max errors of the prediction in the fitting range (in Pascals) """ # Determine lower bound of deflection range: when Pm = Pmmax PMmax = LJFIT_PM_MAX # Pa Zminlb = -0.49 * self.Delta Zminub = 0.0 f = lambda Z, Pmmax: self.PMavg(Z, self.curvrad(Z), self.surface(Z)) - PMmax Zmin = brentq(f, Zminlb, Zminub, args=(PMmax), xtol=1e-16) # Create vectors for geometric variables Zmax = 2 * self.a Z = np.arange(Zmin, Zmax, 1e-11) Pmavg = self.v_PMavg(Z, self.v_curvrad(Z), self.surface(Z)) # Compute optimal nonlinear fit of custom LJ function with initial guess x0_guess = self.delta0 C_guess = 0.1 * self.pDelta nrep_guess = self.m nattr_guess = self.n pguess = (x0_guess, C_guess, nrep_guess, nattr_guess) popt, _ = curve_fit(lambda x, x0, C, nrep, nattr: LennardJones(x, self.Delta, x0, C, nrep, nattr), Z, Pmavg, p0=pguess, maxfev=10000) (x0_opt, C_opt, nrep_opt, nattr_opt) = popt Pmavg_fit = LennardJones(Z, self.Delta, x0_opt, C_opt, nrep_opt, nattr_opt) # Compute prediction error residuals = Pmavg - Pmavg_fit ss_res = np.sum(residuals**2) N = residuals.size std_err = np.sqrt(ss_res / N) max_err = max(np.abs(residuals)) logger.debug('LJ approx: x0 = %.2f nm, C = %.2f kPa, m = %.2f, n = %.2f', x0_opt * 1e9, C_opt * 1e-3, nrep_opt, nattr_opt) LJ_approx = {"x0": x0_opt, "C": C_opt, "nrep": nrep_opt, "nattr": nattr_opt} return (LJ_approx, std_err, max_err) def PMavgpred(self, Z): """ Return the predicted intermolecular pressure based on a specific Lennard-Jones function fitted on the deflection physiological range. :param Z: leaflet apex outward deflection value (m) :return: predicted average intermolecular pressure (Pa) """ return LennardJones(Z, self.Delta, self.LJ_approx['x0'], self.LJ_approx['C'], self.LJ_approx['nrep'], self.LJ_approx['nattr']) def Pelec(self, Z, Qm): """ Compute the electric equivalent pressure term. :param Z: leaflet apex outward deflection value (m) :param Qm: membrane charge density (C/m2) :return: electric equivalent pressure (Pa) """ relS = self.S0 / self.surface(Z) abs_perm = self.epsilon0 * self.epsilonR # F/m return -relS * Qm**2 / (2 * abs_perm) # Pa def findDeltaEq(self, Qm): """ Compute the Delta that cancels out the (Pm + Pec) equation at Z = 0 for a given membrane charge density, using the Brent method to refine the pressure root iteratively. :param Qm: membrane charge density (C/m2) :return: equilibrium value (m) and associated pressure (Pa) """ f = lambda Delta: (self.pDelta * ((self.Delta_ / Delta)**self.m - (self.Delta_ / Delta)**self.n) + self.Pelec(0.0, Qm)) Delta_lb = 0.1 * self.Delta_ Delta_ub = 2.0 * self.Delta_ Delta_eq = brentq(f, Delta_lb, Delta_ub, xtol=1e-16) logger.debug('∆eq = %.2f nm', Delta_eq * 1e9) return (Delta_eq, f(Delta_eq)) def gasflux(self, Z, P): """ Compute the gas molar flux through the BLS boundary layer for an unsteady system. :param Z: leaflet apex outward deflection value (m) :param P: internal gas pressure in the inter-leaflet space (Pa) :return: gas molar flux (mol/s) """ dC = self.C0 - P / self.kH return 2 * self.surface(Z) * self.Dgl * dC / self.xi def gasmol2Pa(self, ng, V): """ Compute the gas pressure in the inter-leaflet space for an unsteady system, from the value of gas molar content. :param ng: internal molar content (mol) :param V: inner volume of the bilayer sonophore structure (m^3) :return: internal gas pressure (Pa) """ return ng * self.Rg * self.T / V def gasPa2mol(self, P, V): """ Compute the gas molar content in the inter-leaflet space for an unsteady system, from the value of internal gas pressure. :param P: internal gas pressure in the inter-leaflet space (Pa) :param V: inner volume of the bilayer sonophore structure (m^3) :return: internal gas molar content (mol) """ return P * V / (self.Rg * self.T) def PtotQS(self, Z, ng, Qm, Pac, Pm_comp_method): """ Compute the balance pressure of the quasi-steady system, upon application of an external perturbation on a charged membrane: Ptot = Pm + Pg + Pec - P0 - Pac. :param Z: leaflet apex outward deflection value (m) :param ng: internal molar content (mol) :param Qm: membrane charge density (C/m2) :param Pac: external acoustic perturbation (Pa) :param Pm_comp_method: type of method used to compute average intermolecular pressure :return: total balance pressure (Pa) """ if Pm_comp_method is PmCompMethod.direct: Pm = self.PMavg(Z, self.curvrad(Z), self.surface(Z)) elif Pm_comp_method is PmCompMethod.predict: Pm = self.PMavgpred(Z) return Pm + self.gasmol2Pa(ng, self.volume(Z)) - self.P0 - Pac + self.Pelec(Z, Qm) def balancedefQS(self, ng, Qm, Pac=0.0, Pm_comp_method=PmCompMethod.predict): """ Compute the leaflet deflection upon application of an external perturbation to a quasi-steady system with a charged membrane. This function uses the Brent method (progressive approximation of function root) to solve the following transcendental equation for Z: Pm + Pg + Pec - P0 - Pac = 0. :param ng: internal molar content (mol) :param Qm: membrane charge density (C/m2) :param Pac: external acoustic perturbation (Pa) :param Pm_comp_method: type of method used to compute average intermolecular pressure :return: leaflet deflection (Z) canceling out the balance equation """ lb = -0.49 * self.Delta ub = self.a Plb = self.PtotQS(lb, ng, Qm, Pac, Pm_comp_method) Pub = self.PtotQS(ub, ng, Qm, Pac, Pm_comp_method) assert (Plb > 0 > Pub), '[%d, %d] is not a sign changing interval for PtotQS' % (lb, ub) return brentq(self.PtotQS, lb, ub, args=(ng, Qm, Pac, Pm_comp_method), xtol=1e-16) def TEleaflet(self, Z): """ Compute the circumferential elastic tension felt across the entire leaflet upon stretching. :param Z: leaflet apex outward deflection value (m) :return: circumferential elastic tension (N/m) """ return self.kA * self.arealstrain(Z) def TEtissue(self, Z): """ Compute the circumferential elastic tension felt across the embedding viscoelastic tissue layer upon stretching. :param Z: leaflet apex outward deflection value (m) :return: circumferential elastic tension (N/m) """ return self.kA_tissue * self.arealstrain(Z) def TEtot(self, Z): """ Compute the total circumferential elastic tension (leaflet and embedding tissue) felt upon stretching. :param Z: leaflet apex outward deflection value (m) :return: circumferential elastic tension (N/m) """ return self.TEleaflet(Z) + self.TEtissue(Z) def PEtot(self, Z, R): """ Compute the total elastic tension pressure (leaflet + embedding tissue) felt upon stretching. :param Z: leaflet apex outward deflection value (m) :param R: leaflet curvature radius (m) :return: elastic tension pressure (Pa) """ return - self.TEtot(Z) / R def PVleaflet(self, U, R): """ Compute the viscous stress felt across the entire leaflet upon stretching. :param U: leaflet apex outward deflection velocity (m/s) :param R: leaflet curvature radius (m) :return: leaflet viscous stress (Pa) """ return - 12 * U * self.delta0 * self.muS / R**2 def PVfluid(self, U, R): """ Compute the viscous stress felt across the entire fluid upon stretching. :param U: leaflet apex outward deflection velocity (m/s) :param R: leaflet curvature radius (m) :return: fluid viscous stress (Pa) """ return - 4 * U * self.muL / np.abs(R) def accP(self, Pres, R): """ Compute the pressure-driven acceleration of the leaflet in the unsteady system, upon application of an external perturbation. :param Pres: net resultant pressure (Pa) :param R: leaflet curvature radius (m) :return: pressure-driven acceleration (m/s^2) """ return Pres / (self.rhoL * np.abs(R)) def accNL(self, U, R): """ Compute the non-linear term of the leaflet acceleration in the unsteady system, upon application of an external perturbation. :param U: leaflet apex outward deflection velocity (m/s) :param R: leaflet curvature radius (m) :return: nonlinear acceleration (m/s^2) .. note:: A simplified version of nonlinear acceleration (neglecting dR/dH) is used here. """ # return - (3/2 - 2*R/H) * U**2 / R return -(3 * U**2) / (2 * R) def eqMech(self, y, t, Adrive, Fdrive, Qm, phi, Pm_comp_method=PmCompMethod.predict): """ Compute the derivatives of the 3-ODE mechanical system variables, with an imposed constant charge density. :param y: vector of HH system variables at time t :param t: specific instant in time (s) :param Adrive: acoustic drive amplitude (Pa) :param Fdrive: acoustic drive frequency (Hz) :param Qm: membrane charge density (F/m2) :param phi: acoustic drive phase (rad) :param Pm_comp_method: type of method used to compute average intermolecular pressure :return: vector of mechanical system derivatives at time t """ # Split input vector explicitly (U, Z, ng) = y # Correct deflection value is below critical compression if Z < -0.5 * self.Delta: logger.warning('Deflection out of range: Z = %.2f nm', Z * 1e9) Z = -0.49 * self.Delta # Compute curvature radius R = self.curvrad(Z) # Compute total pressure Pg = self.gasmol2Pa(ng, self.volume(Z)) if Pm_comp_method is PmCompMethod.direct: Pm = self.PMavg(Z, self.curvrad(Z), self.surface(Z)) elif Pm_comp_method is PmCompMethod.predict: Pm = self.PMavgpred(Z) Ptot = (Pm + Pg - self.P0 - self.Pacoustic(t, Adrive, Fdrive, phi) + self.PEtot(Z, R) + self.PVleaflet(U, R) + self.PVfluid(U, R) + self.Pelec(Z, Qm)) # Compute derivatives dUdt = self.accP(Ptot, R) + self.accNL(U, R) dZdt = U dngdt = self.gasflux(Z, Pg) # Return derivatives vector return [dUdt, dZdt, dngdt] def run(self, Fdrive, Adrive, Qm, phi=np.pi, Pm_comp_method=PmCompMethod.predict): """ Compute short solutions of the mechanical system for specific US stimulation parameters and with an imposed membrane charge density. :param Fdrive: acoustic drive frequency (Hz) :param Adrive: acoustic drive amplitude (Pa) :param phi: acoustic drive phase (rad) :param Qm: imposed membrane charge density (C/m2) :param Pm_comp_method: type of method used to compute average intermolecular pressure :return: 3-tuple with the time profile, the solution matrix and a state vector """ # Check validity of stimulation parameters if not all(isinstance(param, float) for param in [Fdrive, Adrive, Qm, phi]): raise InputError('Invalid stimulation parameters (must be float typed)') if Fdrive <= 0: raise InputError('Invalid US driving frequency: {} kHz (must be strictly positive)' .format(Fdrive * 1e-3)) if Adrive < 0: raise InputError('Invalid US pressure amplitude: {} kPa (must be positive or null)' .format(Adrive * 1e-3)) if Qm < CHARGE_RANGE[0] or Qm > CHARGE_RANGE[1]: raise InputError('Invalid applied charge: {} nC/cm2 (must be within [{}, {}] interval' .format(Qm * 1e5, CHARGE_RANGE[0] * 1e5, CHARGE_RANGE[1] * 1e5)) if phi < 0 or phi >= 2 * np.pi: raise InputError('Invalid US pressure phase: {:.2f} rad (must be within [0, 2 PI[ rad' .format(phi)) # Raise warnings as error warnings.filterwarnings('error') # Determine mechanical system time step Tdrive = 1 / Fdrive dt_mech = Tdrive / NPC_FULL t_mech_cycle = np.linspace(0, Tdrive - dt_mech, NPC_FULL) # Initialize system variables t0 = 0.0 Z0 = 0.0 U0 = 0.0 ng0 = self.ng0 # Solve quasi-steady equation to compute first deflection value Pac1 = self.Pacoustic(t0 + dt_mech, Adrive, Fdrive, phi) Z1 = self.balancedefQS(ng0, Qm, Pac1, Pm_comp_method) U1 = (Z1 - Z0) / dt_mech # Construct arrays to hold system variables states = np.array([1, 1]) t = np.array([t0, t0 + dt_mech]) y = np.array([[U0, U1], [Z0, Z1], [ng0, ng0]]) # Integrate mechanical system for a few acoustic cycles until stabilization j = 0 ng_last = None Z_last = None periodic_conv = False while not periodic_conv and j < NCYCLES_MAX: t_mech = t_mech_cycle + t[-1] + dt_mech y_mech = integrate.odeint(self.eqMech, y[:, -1], t_mech, args=(Adrive, Fdrive, Qm, phi, Pm_comp_method)).T # Compare Z and ng signals over the last 2 acoustic periods if j > 0: Z_rmse = rmse(Z_last, y_mech[1, :]) ng_rmse = rmse(ng_last, y_mech[2, :]) logger.debug('step %u: Z_rmse = %.2e m, ng_rmse = %.2e mol', j, Z_rmse, ng_rmse) if Z_rmse < Z_ERR_MAX and ng_rmse < NG_ERR_MAX: periodic_conv = True # Update last vectors for next comparison Z_last = y_mech[1, :] ng_last = y_mech[2, :] # Concatenate time and solutions to global vectors states = np.concatenate([states, np.ones(NPC_FULL)], axis=0) t = np.concatenate([t, t_mech], axis=0) y = np.concatenate([y, y_mech], axis=1) # Increment loop index j += 1 if j == NCYCLES_MAX: logger.warning('No convergence: stopping after %u cycles', j) else: logger.debug('Periodic convergence after %u cycles', j) states[-1] = 0 # return output variables return (t, y[1:, :], states) diff --git a/PointNICE/constants.py b/PySONIC/constants.py similarity index 100% rename from PointNICE/constants.py rename to PySONIC/constants.py diff --git a/PointNICE/lookups/BLS_lookups_a100.0nm.json b/PySONIC/lookups/BLS_lookups_a100.0nm.json similarity index 100% rename from PointNICE/lookups/BLS_lookups_a100.0nm.json rename to PySONIC/lookups/BLS_lookups_a100.0nm.json diff --git a/PointNICE/lookups/BLS_lookups_a105.2nm.json b/PySONIC/lookups/BLS_lookups_a105.2nm.json similarity index 100% rename from PointNICE/lookups/BLS_lookups_a105.2nm.json rename to PySONIC/lookups/BLS_lookups_a105.2nm.json diff --git a/PointNICE/lookups/BLS_lookups_a116.1nm.json b/PySONIC/lookups/BLS_lookups_a116.1nm.json similarity index 100% rename from PointNICE/lookups/BLS_lookups_a116.1nm.json rename to PySONIC/lookups/BLS_lookups_a116.1nm.json diff --git a/PointNICE/lookups/BLS_lookups_a15.0nm.json b/PySONIC/lookups/BLS_lookups_a15.0nm.json similarity index 100% rename from PointNICE/lookups/BLS_lookups_a15.0nm.json rename to PySONIC/lookups/BLS_lookups_a15.0nm.json diff --git a/PointNICE/lookups/BLS_lookups_a150.0nm.json b/PySONIC/lookups/BLS_lookups_a150.0nm.json similarity index 100% rename from PointNICE/lookups/BLS_lookups_a150.0nm.json rename to PySONIC/lookups/BLS_lookups_a150.0nm.json diff --git a/PointNICE/lookups/BLS_lookups_a155.4nm.json b/PySONIC/lookups/BLS_lookups_a155.4nm.json similarity index 100% rename from PointNICE/lookups/BLS_lookups_a155.4nm.json rename to PySONIC/lookups/BLS_lookups_a155.4nm.json diff --git a/PointNICE/lookups/BLS_lookups_a19.4nm.json b/PySONIC/lookups/BLS_lookups_a19.4nm.json similarity index 100% rename from PointNICE/lookups/BLS_lookups_a19.4nm.json rename to PySONIC/lookups/BLS_lookups_a19.4nm.json diff --git a/PointNICE/lookups/BLS_lookups_a20.0nm.json b/PySONIC/lookups/BLS_lookups_a20.0nm.json similarity index 100% rename from PointNICE/lookups/BLS_lookups_a20.0nm.json rename to PySONIC/lookups/BLS_lookups_a20.0nm.json diff --git a/PointNICE/lookups/BLS_lookups_a22.1nm.json b/PySONIC/lookups/BLS_lookups_a22.1nm.json similarity index 100% rename from PointNICE/lookups/BLS_lookups_a22.1nm.json rename to PySONIC/lookups/BLS_lookups_a22.1nm.json diff --git a/PointNICE/lookups/BLS_lookups_a229.4nm.json b/PySONIC/lookups/BLS_lookups_a229.4nm.json similarity index 100% rename from PointNICE/lookups/BLS_lookups_a229.4nm.json rename to PySONIC/lookups/BLS_lookups_a229.4nm.json diff --git a/PointNICE/lookups/BLS_lookups_a25.0nm.json b/PySONIC/lookups/BLS_lookups_a25.0nm.json similarity index 100% rename from PointNICE/lookups/BLS_lookups_a25.0nm.json rename to PySONIC/lookups/BLS_lookups_a25.0nm.json diff --git a/PointNICE/lookups/BLS_lookups_a31.0nm.json b/PySONIC/lookups/BLS_lookups_a31.0nm.json similarity index 100% rename from PointNICE/lookups/BLS_lookups_a31.0nm.json rename to PySONIC/lookups/BLS_lookups_a31.0nm.json diff --git a/PointNICE/lookups/BLS_lookups_a32.0nm.json b/PySONIC/lookups/BLS_lookups_a32.0nm.json similarity index 100% rename from PointNICE/lookups/BLS_lookups_a32.0nm.json rename to PySONIC/lookups/BLS_lookups_a32.0nm.json diff --git a/PointNICE/lookups/BLS_lookups_a32.3nm.json b/PySONIC/lookups/BLS_lookups_a32.3nm.json similarity index 100% rename from PointNICE/lookups/BLS_lookups_a32.3nm.json rename to PySONIC/lookups/BLS_lookups_a32.3nm.json diff --git a/PointNICE/lookups/BLS_lookups_a32.7nm.json b/PySONIC/lookups/BLS_lookups_a32.7nm.json similarity index 100% rename from PointNICE/lookups/BLS_lookups_a32.7nm.json rename to PySONIC/lookups/BLS_lookups_a32.7nm.json diff --git a/PointNICE/lookups/BLS_lookups_a33.0nm.json b/PySONIC/lookups/BLS_lookups_a33.0nm.json similarity index 100% rename from PointNICE/lookups/BLS_lookups_a33.0nm.json rename to PySONIC/lookups/BLS_lookups_a33.0nm.json diff --git a/PointNICE/lookups/BLS_lookups_a338.7nm.json b/PySONIC/lookups/BLS_lookups_a338.7nm.json similarity index 100% rename from PointNICE/lookups/BLS_lookups_a338.7nm.json rename to PySONIC/lookups/BLS_lookups_a338.7nm.json diff --git a/PointNICE/lookups/BLS_lookups_a41.7nm.json b/PySONIC/lookups/BLS_lookups_a41.7nm.json similarity index 100% rename from PointNICE/lookups/BLS_lookups_a41.7nm.json rename to PySONIC/lookups/BLS_lookups_a41.7nm.json diff --git a/PointNICE/lookups/BLS_lookups_a48.3nm.json b/PySONIC/lookups/BLS_lookups_a48.3nm.json similarity index 100% rename from PointNICE/lookups/BLS_lookups_a48.3nm.json rename to PySONIC/lookups/BLS_lookups_a48.3nm.json diff --git a/PointNICE/lookups/BLS_lookups_a50.0nm.json b/PySONIC/lookups/BLS_lookups_a50.0nm.json similarity index 100% rename from PointNICE/lookups/BLS_lookups_a50.0nm.json rename to PySONIC/lookups/BLS_lookups_a50.0nm.json diff --git a/PointNICE/lookups/BLS_lookups_a500.0nm.json b/PySONIC/lookups/BLS_lookups_a500.0nm.json similarity index 100% rename from PointNICE/lookups/BLS_lookups_a500.0nm.json rename to PySONIC/lookups/BLS_lookups_a500.0nm.json diff --git a/PointNICE/lookups/BLS_lookups_a53.9nm.json b/PySONIC/lookups/BLS_lookups_a53.9nm.json similarity index 100% rename from PointNICE/lookups/BLS_lookups_a53.9nm.json rename to PySONIC/lookups/BLS_lookups_a53.9nm.json diff --git a/PointNICE/lookups/BLS_lookups_a69.6nm.json b/PySONIC/lookups/BLS_lookups_a69.6nm.json similarity index 100% rename from PointNICE/lookups/BLS_lookups_a69.6nm.json rename to PySONIC/lookups/BLS_lookups_a69.6nm.json diff --git a/PointNICE/lookups/BLS_lookups_a71.3nm.json b/PySONIC/lookups/BLS_lookups_a71.3nm.json similarity index 100% rename from PointNICE/lookups/BLS_lookups_a71.3nm.json rename to PySONIC/lookups/BLS_lookups_a71.3nm.json diff --git a/PointNICE/lookups/BLS_lookups_a89.9nm.json b/PySONIC/lookups/BLS_lookups_a89.9nm.json similarity index 100% rename from PointNICE/lookups/BLS_lookups_a89.9nm.json rename to PySONIC/lookups/BLS_lookups_a89.9nm.json diff --git a/PointNICE/lookups/FS_lookups_a32.0nm.pkl b/PySONIC/lookups/FS_lookups_a32.0nm.pkl similarity index 100% rename from PointNICE/lookups/FS_lookups_a32.0nm.pkl rename to PySONIC/lookups/FS_lookups_a32.0nm.pkl diff --git a/PointNICE/lookups/IB_lookups_a32.0nm.pkl b/PySONIC/lookups/IB_lookups_a32.0nm.pkl similarity index 100% rename from PointNICE/lookups/IB_lookups_a32.0nm.pkl rename to PySONIC/lookups/IB_lookups_a32.0nm.pkl diff --git a/PointNICE/lookups/LTS_lookups_a32.0nm.pkl b/PySONIC/lookups/LTS_lookups_a32.0nm.pkl similarity index 100% rename from PointNICE/lookups/LTS_lookups_a32.0nm.pkl rename to PySONIC/lookups/LTS_lookups_a32.0nm.pkl diff --git a/PointNICE/lookups/LeechP_lookups_a32.0nm.pkl b/PySONIC/lookups/LeechP_lookups_a32.0nm.pkl similarity index 100% rename from PointNICE/lookups/LeechP_lookups_a32.0nm.pkl rename to PySONIC/lookups/LeechP_lookups_a32.0nm.pkl diff --git a/PointNICE/lookups/LeechT_lookups_a32.0nm.pkl b/PySONIC/lookups/LeechT_lookups_a32.0nm.pkl similarity index 100% rename from PointNICE/lookups/LeechT_lookups_a32.0nm.pkl rename to PySONIC/lookups/LeechT_lookups_a32.0nm.pkl diff --git a/PointNICE/lookups/RE_lookups_a32.0nm.pkl b/PySONIC/lookups/RE_lookups_a32.0nm.pkl similarity index 100% rename from PointNICE/lookups/RE_lookups_a32.0nm.pkl rename to PySONIC/lookups/RE_lookups_a32.0nm.pkl diff --git a/PointNICE/lookups/RS_lookups_a32.0nm.pkl b/PySONIC/lookups/RS_lookups_a32.0nm.pkl similarity index 100% rename from PointNICE/lookups/RS_lookups_a32.0nm.pkl rename to PySONIC/lookups/RS_lookups_a32.0nm.pkl diff --git a/PointNICE/lookups/RS_lookups_a32.0nm_mpi.pkl b/PySONIC/lookups/RS_lookups_a32.0nm_mpi.pkl similarity index 100% rename from PointNICE/lookups/RS_lookups_a32.0nm_mpi.pkl rename to PySONIC/lookups/RS_lookups_a32.0nm_mpi.pkl diff --git a/PointNICE/lookups/TC_lookups_a32.0nm.pkl b/PySONIC/lookups/TC_lookups_a32.0nm.pkl similarity index 100% rename from PointNICE/lookups/TC_lookups_a32.0nm.pkl rename to PySONIC/lookups/TC_lookups_a32.0nm.pkl diff --git a/PointNICE/neurons/TC.mod b/PySONIC/neurons/TC.mod similarity index 100% rename from PointNICE/neurons/TC.mod rename to PySONIC/neurons/TC.mod diff --git a/PointNICE/neurons/__init__.py b/PySONIC/neurons/__init__.py similarity index 100% rename from PointNICE/neurons/__init__.py rename to PySONIC/neurons/__init__.py diff --git a/PointNICE/neurons/base.py b/PySONIC/neurons/base.py similarity index 97% rename from PointNICE/neurons/base.py rename to PySONIC/neurons/base.py index 98e8244..b02c91c 100644 --- a/PointNICE/neurons/base.py +++ b/PySONIC/neurons/base.py @@ -1,160 +1,160 @@ #!/usr/bin/env python # -*- coding: utf-8 -*- # @Author: Theo Lemaire # @Date: 2017-08-03 11:53:04 # @Email: theo.lemaire@epfl.ch # @Last Modified by: Theo Lemaire -# @Last Modified time: 2018-05-08 14:23:44 +# @Last Modified time: 2018-08-21 15:27:49 ''' Module standard API for all neuron mechanisms. Each mechanism class can use different methods to define the membrane dynamics of a specific neuron type. However, they must contain some mandatory attributes and methods - in order to be properly imported in other PointNICE modules and used in NICE simulations. + in order to be properly imported in other sonic modules and used in NICE simulations. ''' import abc class BaseMech(metaclass=abc.ABCMeta): ''' Abstract class defining the common API (i.e. mandatory attributes and methods) of all subclasses implementing the channels mechanisms of specific neurons. The mandatory attributes are: - **name**: a string defining the name of the mechanism. - **Cm0**: a float defining the membrane resting capacitance (in F/m2) - **Vm0**: a float defining the membrane resting potential (in mV) - **states_names**: a list of strings defining the names of the different state probabilities governing the channels behaviour (i.e. the differential HH variables). - **states0**: a 1D array of floats (NOT integers !!!) defining the initial values of the different state probabilities. - **coeff_names**: a list of strings defining the names of the different coefficients to be used in effective simulations. The mandatory methods are: - **currNet**: compute the net ionic current density (in mA/m2) across the membrane, given a specific membrane potential (in mV) and channel states. - **steadyStates**: compute the channels steady-state values for a specific membrane potential value (in mV). - **derStates**: compute the derivatives of channel states, given a specific membrane potential (in mV) and channel states. This method must return a list of derivatives ordered identically as in the states0 attribute. - **getEffRates**: get the effective rate constants of ion channels to be used in effective simulations. This method must return an array of effective rates ordered identically as in the coeff_names attribute. - **derStatesEff**: compute the effective derivatives of channel states, based on 1-dimensional linear interpolators of "effective" coefficients. This method must return a list of derivatives ordered identically as in the states0 attribute. ''' @property @abc.abstractmethod def name(self): return 'Should never reach here' @property @abc.abstractmethod def Cm0(self): return 'Should never reach here' @property @abc.abstractmethod def Vm0(self): return 'Should never reach here' # @property # @abc.abstractmethod # def states_names(self): # return 'Should never reach here' # @property # @abc.abstractmethod # def states0(self): # return 'Should never reach here' # @property # @abc.abstractmethod # def coeff_names(self): # return 'Should never reach here' @abc.abstractmethod def currNet(self, Vm, states): ''' Compute the net ionic current per unit area. :param Vm: membrane potential (mV) :states: state probabilities of the ion channels :return: current per unit area (mA/m2) ''' @abc.abstractmethod def steadyStates(self, Vm): ''' Compute the channels steady-state values for a specific membrane potential value. :param Vm: membrane potential (mV) :return: array of steady-states ''' @abc.abstractmethod def derStates(self, Vm, states): ''' Compute the derivatives of channel states. :param Vm: membrane potential (mV) :states: state probabilities of the ion channels :return: current per unit area (mA/m2) ''' @abc.abstractmethod def getEffRates(self, Vm): ''' Get the effective rate constants of ion channels, averaged along an acoustic cycle, for future use in effective simulations. :param Vm: array of membrane potential values for an acoustic cycle (mV) :return: an array of rate average constants (s-1) ''' @abc.abstractmethod def derStatesEff(self, Qm, states, interp_data): ''' Compute the effective derivatives of channel states, based on 1-dimensional linear interpolation of "effective" coefficients that summarize the system's behaviour over an acoustic cycle. :param Qm: membrane charge density (C/m2) :states: state probabilities of the ion channels :param interp_data: dictionary of 1D vectors of "effective" coefficients over the charge domain, for specific frequency and amplitude values. ''' def getGates(self): ''' Retrieve the names of the neuron's states that match an ion channel gating. ''' gates = [] for x in self.states_names: if 'alpha{}'.format(x.lower()) in self.coeff_names: gates.append(x) return gates def getRates(self, Vm): ''' Compute the ion channels rate constants for a given membrane potential. :param Vm: membrane potential (mV) :return: a dictionary of rate constants and their values at the given potential. ''' rates = {} for x in self.getGates(): x = x.lower() alpha_str, beta_str = ['{}{}'.format(s, x.lower()) for s in ['alpha', 'beta']] inf_str, tau_str = ['{}inf'.format(x.lower()), 'tau{}'.format(x.lower())] if hasattr(self, 'alpha{}'.format(x)): alphax = getattr(self, alpha_str)(Vm) betax = getattr(self, beta_str)(Vm) elif hasattr(self, '{}inf'.format(x)): xinf = getattr(self, inf_str)(Vm) taux = getattr(self, tau_str)(Vm) alphax = xinf / taux betax = 1 / taux - alphax rates[alpha_str] = alphax rates[beta_str] = betax return rates diff --git a/PointNICE/neurons/cortical.py b/PySONIC/neurons/cortical.py similarity index 99% rename from PointNICE/neurons/cortical.py rename to PySONIC/neurons/cortical.py index 3a4d5c2..4bd0e71 100644 --- a/PointNICE/neurons/cortical.py +++ b/PySONIC/neurons/cortical.py @@ -1,817 +1,817 @@ #!/usr/bin/env python # -*- coding: utf-8 -*- # @Author: Theo Lemaire # @Date: 2017-07-31 15:19:51 # @Email: theo.lemaire@epfl.ch # @Last Modified by: Theo Lemaire -# @Last Modified time: 2018-07-18 14:23:20 +# @Last Modified time: 2018-08-21 16:10:36 ''' Channels mechanisms for thalamic neurons. ''' import logging import numpy as np from .base import BaseMech # Get package logger -logger = logging.getLogger('PointNICE') +logger = logging.getLogger('PySONIC') class Cortical(BaseMech): ''' Class defining the generic membrane channel dynamics of a cortical neuron with 4 different current types: - Inward Sodium current - Outward, delayed-rectifier Potassium current - Outward, slow non.inactivating Potassium current - Non-specific leakage current This generic class cannot be used directly as it does not contain any specific parameters. Reference: *Pospischil, M., Toledo-Rodriguez, M., Monier, C., Piwkowska, Z., Bal, T., Frégnac, Y., Markram, H., and Destexhe, A. (2008). Minimal Hodgkin-Huxley type models for different classes of cortical and thalamic neurons. Biol Cybern 99, 427–441.* ''' # Generic biophysical parameters of cortical cells Cm0 = 1e-2 # Cell membrane resting capacitance (F/m2) Vm0 = 0.0 # Dummy value for membrane potential (mV) VNa = 50.0 # Sodium Nernst potential (mV) VK = -90.0 # Potassium Nernst potential (mV) VCa = 120.0 # # Calcium Nernst potential (mV) def __init__(self): ''' Constructor of the class ''' # Names and initial states of the channels state probabilities self.states_names = ['m', 'h', 'n', 'p'] self.states0 = np.array([]) # Names of the different coefficients to be averaged in a lookup table. self.coeff_names = ['alpham', 'betam', 'alphah', 'betah', 'alphan', 'betan', 'alphap', 'betap'] # Charge interval bounds for lookup creation self.Qbounds = (np.round(self.Vm0 - 25.0) * 1e-5, 50.0e-5) def alpham(self, Vm): ''' Compute the alpha rate for the open-probability of Sodium channels. :param Vm: membrane potential (mV) :return: rate constant (s-1) ''' Vdiff = Vm - self.VT alpha = (-0.32 * (Vdiff - 13) / (np.exp(- (Vdiff - 13) / 4) - 1)) # ms-1 return alpha * 1e3 # s-1 def betam(self, Vm): ''' Compute the beta rate for the open-probability of Sodium channels. :param Vm: membrane potential (mV) :return: rate constant (s-1) ''' Vdiff = Vm - self.VT beta = (0.28 * (Vdiff - 40) / (np.exp((Vdiff - 40) / 5) - 1)) # ms-1 return beta * 1e3 # s-1 def alphah(self, Vm): ''' Compute the alpha rate for the inactivation-probability of Sodium channels. :param Vm: membrane potential (mV) :return: rate constant (s-1) ''' Vdiff = Vm - self.VT alpha = (0.128 * np.exp(-(Vdiff - 17) / 18)) # ms-1 return alpha * 1e3 # s-1 def betah(self, Vm): ''' Compute the beta rate for the inactivation-probability of Sodium channels. :param Vm: membrane potential (mV) :return: rate constant (s-1) ''' Vdiff = Vm - self.VT beta = (4 / (1 + np.exp(-(Vdiff - 40) / 5))) # ms-1 return beta * 1e3 # s-1 def alphan(self, Vm): ''' Compute the alpha rate for the open-probability of delayed-rectifier Potassium channels. :param Vm: membrane potential (mV) :return: rate constant (s-1) ''' Vdiff = Vm - self.VT alpha = (-0.032 * (Vdiff - 15) / (np.exp(-(Vdiff - 15) / 5) - 1)) # ms-1 return alpha * 1e3 # s-1 def betan(self, Vm): ''' Compute the beta rate for the open-probability of delayed-rectifier Potassium channels. :param Vm: membrane potential (mV) :return: rate constant (s-1) ''' Vdiff = Vm - self.VT beta = (0.5 * np.exp(-(Vdiff - 10) / 40)) # ms-1 return beta * 1e3 # s-1 def pinf(self, Vm): ''' Compute the asymptotic value of the open-probability of slow non-inactivating Potassium channels. :param Vm: membrane potential (mV) :return: asymptotic probability (-) ''' return 1.0 / (1 + np.exp(-(Vm + 35) / 10)) # prob def taup(self, Vm): ''' Compute the decay time constant for adaptation of slow non-inactivating Potassium channels. :param Vm: membrane potential (mV) :return: decayed time constant (s) ''' return self.TauMax / (3.3 * np.exp((Vm + 35) / 20) + np.exp(-(Vm + 35) / 20)) # s def derM(self, Vm, m): ''' Compute the evolution of the open-probability of Sodium channels. :param Vm: membrane potential (mV) :param m: open-probability of Sodium channels (prob) :return: derivative of open-probability w.r.t. time (prob/s) ''' return self.alpham(Vm) * (1 - m) - self.betam(Vm) * m def derH(self, Vm, h): ''' Compute the evolution of the inactivation-probability of Sodium channels. :param Vm: membrane potential (mV) :param h: inactivation-probability of Sodium channels (prob) :return: derivative of open-probability w.r.t. time (prob/s) ''' return self.alphah(Vm) * (1 - h) - self.betah(Vm) * h def derN(self, Vm, n): ''' Compute the evolution of the open-probability of delayed-rectifier Potassium channels. :param Vm: membrane potential (mV) :param n: open-probability of delayed-rectifier Potassium channels (prob) :return: derivative of open-probability w.r.t. time (prob/s) ''' return self.alphan(Vm) * (1 - n) - self.betan(Vm) * n def derP(self, Vm, p): ''' Compute the evolution of the open-probability of slow non-inactivating Potassium channels. :param Vm: membrane potential (mV) :param p: open-probability of slow non-inactivating Potassium channels (prob) :return: derivative of open-probability w.r.t. time (prob/s) ''' return (self.pinf(Vm) - p) / self.taup(Vm) def currNa(self, m, h, Vm): ''' Compute the inward Sodium current per unit area. :param m: open-probability of Sodium channels :param h: inactivation-probability of Sodium channels :param Vm: membrane potential (mV) :return: current per unit area (mA/m2) ''' GNa = self.GNaMax * m**3 * h return GNa * (Vm - self.VNa) def currK(self, n, Vm): ''' Compute the outward, delayed-rectifier Potassium current per unit area. :param n: open-probability of delayed-rectifier Potassium channels :param Vm: membrane potential (mV) :return: current per unit area (mA/m2) ''' GK = self.GKMax * n**4 return GK * (Vm - self.VK) def currM(self, p, Vm): ''' Compute the outward, slow non-inactivating Potassium current per unit area. :param p: open-probability of the slow non-inactivating Potassium channels :param Vm: membrane potential (mV) :return: current per unit area (mA/m2) ''' GM = self.GMMax * p return GM * (Vm - self.VK) def currL(self, Vm): ''' Compute the non-specific leakage current per unit area. :param Vm: membrane potential (mV) :return: current per unit area (mA/m2) ''' return self.GL * (Vm - self.VL) def currNet(self, Vm, states): ''' Concrete implementation of the abstract API method. ''' m, h, n, p = states return (self.currNa(m, h, Vm) + self.currK(n, Vm) + self.currM(p, Vm) + self.currL(Vm)) # mA/m2 def steadyStates(self, Vm): ''' Concrete implementation of the abstract API method. ''' # Solve the equation dx/dt = 0 at Vm for each x-state meq = self.alpham(Vm) / (self.alpham(Vm) + self.betam(Vm)) heq = self.alphah(Vm) / (self.alphah(Vm) + self.betah(Vm)) neq = self.alphan(Vm) / (self.alphan(Vm) + self.betan(Vm)) peq = self.pinf(Vm) return np.array([meq, heq, neq, peq]) def derStates(self, Vm, states): ''' Concrete implementation of the abstract API method. ''' m, h, n, p = states dmdt = self.derM(Vm, m) dhdt = self.derH(Vm, h) dndt = self.derN(Vm, n) dpdt = self.derP(Vm, p) return [dmdt, dhdt, dndt, dpdt] def getEffRates(self, Vm): ''' Concrete implementation of the abstract API method. ''' # Compute average cycle value for rate constants am_avg = np.mean(self.alpham(Vm)) bm_avg = np.mean(self.betam(Vm)) ah_avg = np.mean(self.alphah(Vm)) bh_avg = np.mean(self.betah(Vm)) an_avg = np.mean(self.alphan(Vm)) bn_avg = np.mean(self.betan(Vm)) Tp = self.taup(Vm) pinf = self.pinf(Vm) ap_avg = np.mean(pinf / Tp) bp_avg = np.mean(1 / Tp) - ap_avg # Return array of coefficients return np.array([am_avg, bm_avg, ah_avg, bh_avg, an_avg, bn_avg, ap_avg, bp_avg]) def derStatesEff(self, Qm, states, interp_data): ''' Concrete implementation of the abstract API method. ''' rates = np.array([np.interp(Qm, interp_data['Q'], interp_data[rn]) for rn in self.coeff_names]) m, h, n, p = states dmdt = rates[0] * (1 - m) - rates[1] * m dhdt = rates[2] * (1 - h) - rates[3] * h dndt = rates[4] * (1 - n) - rates[5] * n dpdt = rates[6] * (1 - p) - rates[7] * p return [dmdt, dhdt, dndt, dpdt] class CorticalRS(Cortical): ''' Specific membrane channel dynamics of a cortical regular spiking, excitatory pyramidal neuron. Reference: *Pospischil, M., Toledo-Rodriguez, M., Monier, C., Piwkowska, Z., Bal, T., Frégnac, Y., Markram, H., and Destexhe, A. (2008). Minimal Hodgkin-Huxley type models for different classes of cortical and thalamic neurons. Biol Cybern 99, 427–441.* ''' # Name of channel mechanism name = 'RS' # Cell-specific biophysical parameters Vm0 = -71.9 # Cell membrane resting potential (mV) GNaMax = 560.0 # Max. conductance of Sodium current (S/m^2) GKMax = 60.0 # Max. conductance of delayed Potassium current (S/m^2) GMMax = 0.75 # Max. conductance of slow non-inactivating Potassium current (S/m^2) GL = 0.205 # Conductance of non-specific leakage current (S/m^2) VL = -70.3 # Non-specific leakage Nernst potential (mV) VT = -56.2 # Spike threshold adjustment parameter (mV) TauMax = 0.608 # Max. adaptation decay of slow non-inactivating Potassium current (s) # Default plotting scheme pltvars_scheme = { 'i_{Na}\ kin.': ['m', 'h'], 'i_K\ kin.': ['n'], 'i_M\ kin.': ['p'], 'I': ['iNa', 'iK', 'iM', 'iL', 'iNet'] } def __init__(self): ''' Constructor of the class. ''' # Instantiate parent class super().__init__() # Define initial channel probabilities (solving dx/dt = 0 at resting potential) self.states0 = self.steadyStates(self.Vm0) class CorticalFS(Cortical): ''' Specific membrane channel dynamics of a cortical fast-spiking, inhibitory neuron. Reference: *Pospischil, M., Toledo-Rodriguez, M., Monier, C., Piwkowska, Z., Bal, T., Frégnac, Y., Markram, H., and Destexhe, A. (2008). Minimal Hodgkin-Huxley type models for different classes of cortical and thalamic neurons. Biol Cybern 99, 427–441.* ''' # Name of channel mechanism name = 'FS' # Cell-specific biophysical parameters Vm0 = -71.4 # Cell membrane resting potential (mV) GNaMax = 580.0 # Max. conductance of Sodium current (S/m^2) GKMax = 39.0 # Max. conductance of delayed Potassium current (S/m^2) GMMax = 0.787 # Max. conductance of slow non-inactivating Potassium current (S/m^2) GL = 0.38 # Conductance of non-specific leakage current (S/m^2) VL = -70.4 # Non-specific leakage Nernst potential (mV) VT = -57.9 # Spike threshold adjustment parameter (mV) TauMax = 0.502 # Max. adaptation decay of slow non-inactivating Potassium current (s) # Default plotting scheme pltvars_scheme = { 'i_{Na}\ kin.': ['m', 'h'], 'i_K\ kin.': ['n'], 'i_M\ kin.': ['p'], 'I': ['iNa', 'iK', 'iM', 'iL', 'iNet'] } def __init__(self): ''' Constructor of the class. ''' # Instantiate parent class super().__init__() # Define initial channel probabilities (solving dx/dt = 0 at resting potential) self.states0 = self.steadyStates(self.Vm0) class CorticalLTS(Cortical): ''' Specific membrane channel dynamics of a cortical low-threshold spiking, inhibitory neuron with an additional inward Calcium current due to the presence of a T-type channel. References: *Pospischil, M., Toledo-Rodriguez, M., Monier, C., Piwkowska, Z., Bal, T., Frégnac, Y., Markram, H., and Destexhe, A. (2008). Minimal Hodgkin-Huxley type models for different classes of cortical and thalamic neurons. Biol Cybern 99, 427–441.* *Huguenard, J.R., and McCormick, D.A. (1992). Simulation of the currents involved in rhythmic oscillations in thalamic relay neurons. J. Neurophysiol. 68, 1373–1383.* ''' # Name of channel mechanism name = 'LTS' # Cell-specific biophysical parameters Vm0 = -54.0 # Cell membrane resting potential (mV) GNaMax = 500.0 # Max. conductance of Sodium current (S/m^2) GKMax = 40.0 # Max. conductance of delayed Potassium current (S/m^2) GMMax = 0.28 # Max. conductance of slow non-inactivating Potassium current (S/m^2) GTMax = 4.0 # Max. conductance of low-threshold Calcium current (S/m^2) GL = 0.19 # Conductance of non-specific leakage current (S/m^2) VL = -50.0 # Non-specific leakage Nernst potential (mV) VT = -50.0 # Spike threshold adjustment parameter (mV) TauMax = 4.0 # Max. adaptation decay of slow non-inactivating Potassium current (s) Vx = -7.0 # Voltage-dependence uniform shift factor at 36°C (mV) # Default plotting scheme pltvars_scheme = { 'i_{Na}\ kin.': ['m', 'h'], 'i_K\ kin.': ['n'], 'i_M\ kin.': ['p'], 'i_T\ kin.': ['s', 'u'], 'I': ['iNa', 'iK', 'iM', 'iT', 'iL', 'iNet'] } def __init__(self): ''' Constructor of the class. ''' # Instantiate parent class super().__init__() # Add names of cell-specific Calcium channel probabilities self.states_names += ['s', 'u'] # Define initial channel probabilities (solving dx/dt = 0 at resting potential) self.states0 = self.steadyStates(self.Vm0) # Define the names of the different coefficients to be averaged in a lookup table. self.coeff_names += ['alphas', 'betas', 'alphau', 'betau'] def sinf(self, Vm): ''' Compute the asymptotic value of the open-probability of the S-type, activation gate of Calcium channels. :param Vm: membrane potential (mV) :return: asymptotic probability (-) ''' return 1.0 / (1.0 + np.exp(-(Vm + self.Vx + 57.0) / 6.2)) # prob def taus(self, Vm): ''' Compute the decay time constant for adaptation of S-type, activation gate of Calcium channels. :param Vm: membrane potential (mV) :return: decayed time constant (s) ''' tmp = np.exp(-(Vm + self.Vx + 132.0) / 16.7) + np.exp((Vm + self.Vx + 16.8) / 18.2) return 1.0 / 3.7 * (0.612 + 1.0 / tmp) * 1e-3 # s def uinf(self, Vm): ''' Compute the asymptotic value of the open-probability of the U-type, inactivation gate of Calcium channels. :param Vm: membrane potential (mV) :return: asymptotic probability (-) ''' return 1.0 / (1.0 + np.exp((Vm + self.Vx + 81.0) / 4.0)) # prob def tauu(self, Vm): ''' Compute the decay time constant for adaptation of U-type, inactivation gate of Calcium channels. :param Vm: membrane potential (mV) :return: decayed time constant (s) ''' if Vm + self.Vx < -80.0: return 1.0 / 3.7 * np.exp((Vm + self.Vx + 467.0) / 66.6) * 1e-3 # s else: return 1.0 / 3.7 * (np.exp(-(Vm + self.Vx + 22) / 10.5) + 28.0) * 1e-3 # s def derS(self, Vm, s): ''' Compute the evolution of the open-probability of the S-type, activation gate of Calcium channels. :param Vm: membrane potential (mV) :param s: open-probability of S-type Calcium activation gates (prob) :return: derivative of open-probability w.r.t. time (prob/s) ''' return (self.sinf(Vm) - s) / self.taus(Vm) def derU(self, Vm, u): ''' Compute the evolution of the open-probability of the U-type, inactivation gate of Calcium channels. :param Vm: membrane potential (mV) :param u: open-probability of U-type Calcium inactivation gates (prob) :return: derivative of open-probability w.r.t. time (prob/s) ''' return (self.uinf(Vm) - u) / self.tauu(Vm) def currCa(self, s, u, Vm): ''' Compute the inward, low-threshold Calcium current per unit area. :param s: open-probability of the S-type activation gate of Calcium channels :param u: open-probability of the U-type inactivation gate of Calcium channels :param Vm: membrane potential (mV) :return: current per unit area (mA/m2) ''' GT = self.GTMax * s**2 * u return GT * (Vm - self.VCa) def currNet(self, Vm, states): ''' Concrete implementation of the abstract API method. ''' m, h, n, p, s, u = states return (self.currNa(m, h, Vm) + self.currK(n, Vm) + self.currM(p, Vm) + self.currCa(s, u, Vm) + self.currL(Vm)) # mA/m2 def steadyStates(self, Vm): ''' Concrete implementation of the abstract API method. ''' # Call parent method to compute Sodium and Potassium channels gates steady-states NaK_eqstates = super().steadyStates(Vm) # Compute Calcium channel gates steady-states seq = self.sinf(Vm) ueq = self.uinf(Vm) Ca_eqstates = np.array([seq, ueq]) # Merge all steady-states and return return np.concatenate((NaK_eqstates, Ca_eqstates)) def derStates(self, Vm, states): ''' Concrete implementation of the abstract API method. ''' # Unpack input states *NaK_states, s, u = states # Call parent method to compute Sodium and Potassium channels states derivatives NaK_derstates = super().derStates(Vm, NaK_states) # Compute Calcium channels states derivatives dsdt = self.derS(Vm, s) dudt = self.derU(Vm, u) # Merge all states derivatives and return return NaK_derstates + [dsdt, dudt] def getEffRates(self, Vm): ''' Concrete implementation of the abstract API method. ''' # Call parent method to compute Sodium and Potassium effective rate constants NaK_rates = super().getEffRates(Vm) # Compute Calcium effective rate constants Ts = self.taus(Vm) as_avg = np.mean(self.sinf(Vm) / Ts) bs_avg = np.mean(1 / Ts) - as_avg Tu = np.array([self.tauu(v) for v in Vm]) au_avg = np.mean(self.uinf(Vm) / Tu) bu_avg = np.mean(1 / Tu) - au_avg Ca_rates = np.array([as_avg, bs_avg, au_avg, bu_avg]) # Merge all rates and return return np.concatenate((NaK_rates, Ca_rates)) def derStatesEff(self, Qm, states, interp_data): ''' Concrete implementation of the abstract API method. ''' # Unpack input states *NaK_states, s, u = states # Call parent method to compute Sodium and Potassium channels states derivatives NaK_dstates = super().derStatesEff(Qm, NaK_states, interp_data) # Compute Calcium channels states derivatives Ca_rates = np.array([np.interp(Qm, interp_data['Q'], interp_data[rn]) for rn in self.coeff_names[8:]]) dsdt = Ca_rates[0] * (1 - s) - Ca_rates[1] * s dudt = Ca_rates[2] * (1 - u) - Ca_rates[3] * u # Merge all states derivatives and return return NaK_dstates + [dsdt, dudt] class CorticalIB(Cortical): ''' Specific membrane channel dynamics of a cortical intrinsically bursting neuron with an additional inward Calcium current due to the presence of a L-type channel. References: *Pospischil, M., Toledo-Rodriguez, M., Monier, C., Piwkowska, Z., Bal, T., Frégnac, Y., Markram, H., and Destexhe, A. (2008). Minimal Hodgkin-Huxley type models for different classes of cortical and thalamic neurons. Biol Cybern 99, 427–441.* *Reuveni, I., Friedman, A., Amitai, Y., and Gutnick, M.J. (1993). Stepwise repolarization from Ca2+ plateaus in neocortical pyramidal cells: evidence for nonhomogeneous distribution of HVA Ca2+ channels in dendrites. J. Neurosci. 13, 4609–4621.* ''' # Name of channel mechanism name = 'IB' # Cell-specific biophysical parameters Vm0 = -71.4 # Cell membrane resting potential (mV) GNaMax = 500 # Max. conductance of Sodium current (S/m^2) GKMax = 50 # Max. conductance of delayed Potassium current (S/m^2) GMMax = 0.3 # Max. conductance of slow non-inactivating Potassium current (S/m^2) GCaLMax = 1.0 # Max. conductance of L-type Calcium current (S/m^2) GL = 0.1 # Conductance of non-specific leakage current (S/m^2) VL = -70 # Non-specific leakage Nernst potential (mV) VT = -56.2 # Spike threshold adjustment parameter (mV) TauMax = 0.608 # Max. adaptation decay of slow non-inactivating Potassium current (s) # Default plotting scheme pltvars_scheme = { 'i_{Na}\ kin.': ['m', 'h'], 'i_K\ kin.': ['n'], 'i_M\ kin.': ['p'], 'i_{CaL}\ kin.': ['q', 'r', 'q2r'], 'I': ['iNa', 'iK', 'iM', 'iCaL', 'iL', 'iNet'] } def __init__(self): ''' Constructor of the class. ''' # Instantiate parent class super().__init__() # Add names of cell-specific Calcium channel probabilities self.states_names += ['q', 'r'] # Define initial channel probabilities (solving dx/dt = 0 at resting potential) self.states0 = self.steadyStates(self.Vm0) # Define the names of the different coefficients to be averaged in a lookup table. self.coeff_names += ['alphaq', 'betaq', 'alphar', 'betar'] def alphaq(self, Vm): ''' Compute the alpha rate for the open-probability of L-type Calcium channels. :param Vm: membrane potential (mV) :return: rate constant (s-1) ''' alpha = - 0.055 * (Vm + 27) / (np.exp(-(Vm + 27) / 3.8) - 1) # ms-1 return alpha * 1e3 # s-1 def betaq(self, Vm): ''' Compute the beta rate for the open-probability of L-type Calcium channels. :param Vm: membrane potential (mV) :return: rate constant (s-1) ''' beta = 0.94 * np.exp(-(Vm + 75) / 17) # ms-1 return beta * 1e3 # s-1 def alphar(self, Vm): ''' Compute the alpha rate for the inactivation-probability of L-type Calcium channels. :param Vm: membrane potential (mV) :return: rate constant (s-1) ''' alpha = 0.000457 * np.exp(-(Vm + 13) / 50) # ms-1 return alpha * 1e3 # s-1 def betar(self, Vm): ''' Compute the beta rate for the inactivation-probability of L-type Calcium channels. :param Vm: membrane potential (mV) :return: rate constant (s-1) ''' beta = 0.0065 / (np.exp(-(Vm + 15) / 28) + 1) # ms-1 return beta * 1e3 # s-1 def derQ(self, Vm, q): ''' Compute the evolution of the open-probability of the Q (activation) gate of L-type Calcium channels. :param Vm: membrane potential (mV) :param q: open-probability of Q gate (prob) :return: derivative of open-probability w.r.t. time (prob/s) ''' return self.alphaq(Vm) * (1 - q) - self.betaq(Vm) * q def derR(self, Vm, r): ''' Compute the evolution of the open-probability of the R (inactivation) gate of L-type Calcium channels. :param Vm: membrane potential (mV) :param r: open-probability of R gate (prob) :return: derivative of open-probability w.r.t. time (prob/s) ''' return self.alphar(Vm) * (1 - r) - self.betar(Vm) * r def currCaL(self, q, r, Vm): ''' Compute the inward L-type Calcium current per unit area. :param q: open-probability of Q gate (prob) :param r: open-probability of R gate (prob) :param Vm: membrane potential (mV) :return: current per unit area (mA/m2) ''' GCaL = self.GCaLMax * q**2 * r return GCaL * (Vm - self.VCa) def currNet(self, Vm, states): ''' Concrete implementation of the abstract API method. ''' m, h, n, p, q, r = states return (self.currNa(m, h, Vm) + self.currK(n, Vm) + self.currM(p, Vm) + self.currCaL(q, r, Vm) + self.currL(Vm)) # mA/m2 def steadyStates(self, Vm): ''' Concrete implementation of the abstract API method. ''' # Call parent method to compute Sodium and Potassium channels gates steady-states NaK_eqstates = super().steadyStates(Vm) # Compute L-type Calcium channel gates steady-states qeq = self.alphaq(Vm) / (self.alphaq(Vm) + self.betaq(Vm)) req = self.alphar(Vm) / (self.alphar(Vm) + self.betar(Vm)) CaL_eqstates = np.array([qeq, req]) # Merge all steady-states and return return np.concatenate((NaK_eqstates, CaL_eqstates)) def derStates(self, Vm, states): ''' Concrete implementation of the abstract API method. ''' # Unpack input states *NaK_states, q, r = states # Call parent method to compute Sodium and Potassium channels states derivatives NaK_derstates = super().derStates(Vm, NaK_states) # Compute L-type Calcium channels states derivatives dqdt = self.derQ(Vm, q) drdt = self.derR(Vm, r) # Merge all states derivatives and return return NaK_derstates + [dqdt, drdt] def getEffRates(self, Vm): ''' Concrete implementation of the abstract API method. ''' # Call parent method to compute Sodium and Potassium effective rate constants NaK_rates = super().getEffRates(Vm) # Compute Calcium effective rate constants aq_avg = np.mean(self.alphaq(Vm)) bq_avg = np.mean(self.betaq(Vm)) ar_avg = np.mean(self.alphar(Vm)) br_avg = np.mean(self.betar(Vm)) CaL_rates = np.array([aq_avg, bq_avg, ar_avg, br_avg]) # Merge all rates and return return np.concatenate((NaK_rates, CaL_rates)) def derStatesEff(self, Qm, states, interp_data): ''' Concrete implementation of the abstract API method. ''' # Unpack input states *NaK_states, q, r = states # Call parent method to compute Sodium and Potassium channels states derivatives NaK_dstates = super().derStatesEff(Qm, NaK_states, interp_data) # Compute Calcium channels states derivatives CaL_rates = np.array([np.interp(Qm, interp_data['Q'], interp_data[rn]) for rn in self.coeff_names[8:]]) dqdt = CaL_rates[0] * (1 - q) - CaL_rates[1] * q drdt = CaL_rates[2] * (1 - r) - CaL_rates[3] * r # Merge all states derivatives and return return NaK_dstates + [dqdt, drdt] diff --git a/PointNICE/neurons/leech.py b/PySONIC/neurons/leech.py similarity index 99% rename from PointNICE/neurons/leech.py rename to PySONIC/neurons/leech.py index 9592c93..569d188 100644 --- a/PointNICE/neurons/leech.py +++ b/PySONIC/neurons/leech.py @@ -1,1080 +1,1080 @@ #!/usr/bin/env python # -*- coding: utf-8 -*- # @Author: Theo Lemaire # @Date: 2017-07-31 15:20:54 # @Email: theo.lemaire@epfl.ch # @Last Modified by: Theo Lemaire -# @Last Modified time: 2018-03-13 15:04:00 +# @Last Modified time: 2018-08-21 16:10:36 ''' Channels mechanisms for leech ganglion neurons. ''' import logging from functools import partialmethod import numpy as np from .base import BaseMech # Get package logger -logger = logging.getLogger('PointNICE') +logger = logging.getLogger('PySONIC') class LeechTouch(BaseMech): ''' Class defining the membrane channel dynamics of a leech touch sensory neuron. with 4 different current types: - Inward Sodium current - Outward Potassium current - Inward Calcium current - Non-specific leakage current - Calcium-dependent, outward Potassium current - Outward, Sodium pumping current Reference: *Cataldo, E., Brunelli, M., Byrne, J.H., Av-Ron, E., Cai, Y., and Baxter, D.A. (2005). Computational model of touch sensory cells (T Cells) of the leech: role of the afterhyperpolarization (AHP) in activity-dependent conduction failure. J Comput Neurosci 18, 5–24.* ''' # Name of channel mechanism name = 'LeechT' # Cell-specific biophysical parameters Cm0 = 1e-2 # Cell membrane resting capacitance (F/m2) Vm0 = -53.58 # Cell membrane resting potential (mV) VNa = 45.0 # Sodium Nernst potential (mV) VK = -62.0 # Potassium Nernst potential (mV) VCa = 60.0 # Calcium Nernst potential (mV) VL = -48.0 # Non-specific leakage Nernst potential (mV) VPumpNa = -300.0 # Sodium pump current reversal potential (mV) GNaMax = 3500.0 # Max. conductance of Sodium current (S/m^2) GKMax = 900.0 # Max. conductance of Potassium current (S/m^2) GCaMax = 20.0 # Max. conductance of Calcium current (S/m^2) GKCaMax = 236.0 # Max. conductance of Calcium-dependent Potassium current (S/m^2) GL = 1.0 # Conductance of non-specific leakage current (S/m^2) GPumpNa = 20.0 # Max. conductance of Sodium pump current (S/m^2) taum = 0.1e-3 # Sodium activation time constant (s) taus = 0.6e-3 # Calcium activation time constant (s) # Original conversion constants from inward ion current (nA) to build-up of # intracellular ion concentration (arb.) K_Na_original = 0.016 # iNa to intracellular [Na+] K_Ca_original = 0.1 # iCa to intracellular [Ca2+] # Constants needed to convert K from original model (soma compartment) # to current model (point-neuron) surface = 6434.0e-12 # surface of cell assumed as a single soma (m2) curr_factor = 1e6 # mA to nA # Time constants for the removal of ions from intracellular pools (s) tau_Na_removal = 16.0 # Na+ removal tau_Ca_removal = 1.25 # Ca2+ removal # Time constants for the iPumpNa and iKCa currents activation # from specific intracellular ions (s) tau_PumpNa_act = 0.1 # iPumpNa activation from intracellular Na+ tau_KCa_act = 0.01 # iKCa activation from intracellular Ca2+ # Default plotting scheme pltvars_scheme = { 'i_{Na}\ kin.': ['m', 'h', 'm3h'], 'i_K\ kin.': ['n'], 'i_{Ca}\ kin.': ['s'], 'pools': ['C_Na_arb', 'C_Na_arb_activation', 'C_Ca_arb', 'C_Ca_arb_activation'], 'I': ['iNa', 'iK', 'iCa', 'iKCa', 'iPumpNa', 'iL', 'iNet'] } def __init__(self): ''' Constructor of the class. ''' # Names and initial states of the channels state probabilities self.states_names = ['m', 'h', 'n', 's', 'C_Na', 'A_Na', 'C_Ca', 'A_Ca'] self.states0 = np.array([]) # Names of the channels effective coefficients self.coeff_names = ['alpham', 'betam', 'alphah', 'betah', 'alphan', 'betan', 'alphas', 'betas'] self.K_Na = self.K_Na_original * self.surface * self.curr_factor self.K_Ca = self.K_Ca_original * self.surface * self.curr_factor # Define initial channel probabilities (solving dx/dt = 0 at resting potential) self.states0 = self.steadyStates(self.Vm0) # Charge interval bounds for lookup creation self.Qbounds = (np.round(self.Vm0 - 10.0) * 1e-5, 50.0e-5) # ----------------- Generic ----------------- def _xinf(self, Vm, halfmax, slope, power): ''' Generic function computing the steady-state activation/inactivation of a particular ion channel at a given voltage. :param Vm: membrane potential (mV) :param halfmax: half-(in)activation voltage (mV) :param slope: slope parameter of (in)activation function (mV) :param power: power exponent multiplying the exponential expression (integer) :return: steady-state (in)activation (-) ''' return 1 / (1 + np.exp((Vm - halfmax) / slope))**power def _taux(self, Vm, halfmax, slope, tauMax, tauMin): ''' Generic function computing the voltage-dependent, activation/inactivation time constant of a particular ion channel at a given voltage. :param Vm: membrane potential (mV) :param halfmax: voltage at which (in)activation time constant is half-maximal (mV) :param slope: slope parameter of (in)activation time constant function (mV) :return: steady-state (in)activation (-) ''' return (tauMax - tauMin) / (1 + np.exp((Vm - halfmax) / slope)) + tauMin def _derC_ion(self, Cion, Iion, Kion, tau): ''' Generic function computing the time derivative of the concentration of a specific ion in its intracellular pool. :param Cion: ion concentration in the pool (arbitrary unit) :param Iion: ionic current (mA/m2) :param Kion: scaling factor for current contribution to pool (arb. unit / nA???) :param tau: time constant for removal of ions from the pool (s) :return: variation of ionic concentration in the pool (arbitrary unit /s) ''' return (Kion * (-Iion) - Cion) / tau def _derA_ion(self, Aion, Cion, tau): ''' Generic function computing the time derivative of the concentration and time dependent activation function, for a specific pool-dependent ionic current. :param Aion: concentration and time dependent activation function (arbitrary unit) :param Cion: ion concentration in the pool (arbitrary unit) :param tau: time constant for activation function variation (s) :return: variation of activation function (arbitrary unit / s) ''' return (Cion - Aion) / tau # ------------------ Na ------------------- minf = partialmethod(_xinf, halfmax=-35.0, slope=-5.0, power=1) hinf = partialmethod(_xinf, halfmax=-50.0, slope=9.0, power=2) tauh = partialmethod(_taux, halfmax=-36.0, slope=3.5, tauMax=14.0e-3, tauMin=0.2e-3) def derM(self, Vm, m): ''' Instantaneous derivative of Sodium activation. ''' return (self.minf(Vm) - m) / self.taum # s-1 def derH(self, Vm, h): ''' Instantaneous derivative of Sodium inactivation. ''' return (self.hinf(Vm) - h) / self.tauh(Vm) # s-1 # ------------------ K ------------------- ninf = partialmethod(_xinf, halfmax=-22.0, slope=-9.0, power=1) taun = partialmethod(_taux, halfmax=-10.0, slope=10.0, tauMax=6.0e-3, tauMin=1.0e-3) def derN(self, Vm, n): ''' Instantaneous derivative of Potassium activation. ''' return (self.ninf(Vm) - n) / self.taun(Vm) # s-1 # ------------------ Ca ------------------- sinf = partialmethod(_xinf, halfmax=-10.0, slope=-2.8, power=1) def derS(self, Vm, s): ''' Instantaneous derivative of Calcium activation. ''' return (self.sinf(Vm) - s) / self.taus # s-1 # ------------------ Pools ------------------- def derC_Na(self, C_Na, I_Na): ''' Derivative of Sodium concentration in intracellular pool. ''' return self._derC_ion(C_Na, I_Na, self.K_Na, self.tau_Na_removal) def derA_Na(self, A_Na, C_Na): ''' Derivative of Sodium pool-dependent activation function for iPumpNa. ''' return self._derA_ion(A_Na, C_Na, self.tau_PumpNa_act) def derC_Ca(self, C_Ca, I_Ca): ''' Derivative of Calcium concentration in intracellular pool. ''' return self._derC_ion(C_Ca, I_Ca, self.K_Ca, self.tau_Ca_removal) def derA_Ca(self, A_Ca, C_Ca): ''' Derivative of Calcium pool-dependent activation function for iKCa. ''' return self._derA_ion(A_Ca, C_Ca, self.tau_KCa_act) # ------------------ Currents ------------------- def currNa(self, m, h, Vm): ''' Sodium inward current. ''' return self.GNaMax * m**3 * h * (Vm - self.VNa) def currK(self, n, Vm): ''' Potassium outward current. ''' return self.GKMax * n**2 * (Vm - self.VK) def currCa(self, s, Vm): ''' Calcium inward current. ''' return self.GCaMax * s * (Vm - self.VCa) def currKCa(self, A_Ca, Vm): ''' Calcium-activated Potassium outward current. ''' return self.GKCaMax * A_Ca * (Vm - self.VK) def currPumpNa(self, A_Na, Vm): ''' Outward current mimicking the activity of the NaK-ATPase pump. ''' return self.GPumpNa * A_Na * (Vm - self.VPumpNa) def currL(self, Vm): ''' Leakage current. ''' return self.GL * (Vm - self.VL) def currNet(self, Vm, states): ''' Concrete implementation of the abstract API method. ''' m, h, n, s, _, A_Na, _, A_Ca = states return (self.currNa(m, h, Vm) + self.currK(n, Vm) + self.currCa(s, Vm) + self.currL(Vm) + self.currPumpNa(A_Na, Vm) + self.currKCa(A_Ca, Vm)) # mA/m2 def steadyStates(self, Vm): ''' Concrete implementation of the abstract API method. ''' # Standard gating dynamics: Solve the equation dx/dt = 0 at Vm for each x-state meq = self.minf(Vm) heq = self.hinf(Vm) neq = self.ninf(Vm) seq = self.sinf(Vm) # PumpNa pool concentration and activation steady-state INa_eq = self.currNa(meq, heq, Vm) CNa_eq = self.K_Na * (-INa_eq) ANa_eq = CNa_eq # KCa current pool concentration and activation steady-state ICa_eq = self.currCa(seq, Vm) CCa_eq = self.K_Ca * (-ICa_eq) ACa_eq = CCa_eq return np.array([meq, heq, neq, seq, CNa_eq, ANa_eq, CCa_eq, ACa_eq]) def derStates(self, Vm, states): ''' Concrete implementation of the abstract API method. ''' # Unpack states m, h, n, s, C_Na, A_Na, C_Ca, A_Ca = states # Standard gating states derivatives dmdt = self.derM(Vm, m) dhdt = self.derH(Vm, h) dndt = self.derN(Vm, n) dsdt = self.derS(Vm, s) # PumpNa current pool concentration and activation state I_Na = self.currNa(m, h, Vm) dCNa_dt = self.derC_Na(C_Na, I_Na) dANa_dt = self.derA_Na(A_Na, C_Na) # KCa current pool concentration and activation state I_Ca = self.currCa(s, Vm) dCCa_dt = self.derC_Ca(C_Ca, I_Ca) dACa_dt = self.derA_Ca(A_Ca, C_Ca) # Pack derivatives and return return [dmdt, dhdt, dndt, dsdt, dCNa_dt, dANa_dt, dCCa_dt, dACa_dt] def getEffRates(self, Vm): ''' Concrete implementation of the abstract API method. ''' # Compute average cycle value for rate constants Tm = self.taum minf = self.minf(Vm) am_avg = np.mean(minf / Tm) bm_avg = np.mean(1 / Tm) - am_avg Th = self.tauh(Vm) hinf = self.hinf(Vm) ah_avg = np.mean(hinf / Th) bh_avg = np.mean(1 / Th) - ah_avg Tn = self.taun(Vm) ninf = self.ninf(Vm) an_avg = np.mean(ninf / Tn) bn_avg = np.mean(1 / Tn) - an_avg Ts = self.taus sinf = self.sinf(Vm) as_avg = np.mean(sinf / Ts) bs_avg = np.mean(1 / Ts) - as_avg # Return array of coefficients return np.array([am_avg, bm_avg, ah_avg, bh_avg, an_avg, bn_avg, as_avg, bs_avg]) def derStatesEff(self, Qm, states, interp_data): ''' Concrete implementation of the abstract API method. ''' rates = np.array([np.interp(Qm, interp_data['Q'], interp_data[rn]) for rn in self.coeff_names]) Vmeff = np.interp(Qm, interp_data['Q'], interp_data['V']) # Unpack states m, h, n, s, C_Na, A_Na, C_Ca, A_Ca = states # Standard gating states derivatives dmdt = rates[0] * (1 - m) - rates[1] * m dhdt = rates[2] * (1 - h) - rates[3] * h dndt = rates[4] * (1 - n) - rates[5] * n dsdt = rates[6] * (1 - s) - rates[7] * s # PumpNa current pool concentration and activation state I_Na = self.currNa(m, h, Vmeff) dCNa_dt = self.derC_Na(C_Na, I_Na) dANa_dt = self.derA_Na(A_Na, C_Na) # KCa current pool concentration and activation state I_Ca_eff = self.currCa(s, Vmeff) dCCa_dt = self.derC_Ca(C_Ca, I_Ca_eff) dACa_dt = self.derA_Ca(A_Ca, C_Ca) # Pack derivatives and return return [dmdt, dhdt, dndt, dsdt, dCNa_dt, dANa_dt, dCCa_dt, dACa_dt] class LeechMech(BaseMech): ''' Class defining the basic dynamics of Sodium, Potassium and Calcium channels for several neurons of the leech. Reference: *Baccus, S.A. (1998). Synaptic facilitation by reflected action potentials: enhancement of transmission when nerve impulses reverse direction at axon branch points. Proc. Natl. Acad. Sci. U.S.A. 95, 8345–8350.* ''' alphaC_sf = 1e-5 # Calcium activation rate constant scaling factor (M) betaC = 0.1e3 # beta rate for the open-probability of Ca2+-dependent Potassium channels (s-1) T = 293.15 # Room temperature (K) Rg = 8.314 # Universal gas constant (J.mol^-1.K^-1) Faraday = 9.6485e4 # Faraday constant (C/mol) def alpham(self, Vm): ''' Compute the alpha rate for the open-probability of Sodium channels. :param Vm: membrane potential (mV) :return: rate constant (s-1) ''' alpha = -0.03 * (Vm + 28) / (np.exp(- (Vm + 28) / 15) - 1) # ms-1 return alpha * 1e3 # s-1 def betam(self, Vm): ''' Compute the beta rate for the open-probability of Sodium channels. :param Vm: membrane potential (mV) :return: rate constant (s-1) ''' beta = 2.7 * np.exp(-(Vm + 53) / 18) # ms-1 return beta * 1e3 # s-1 def alphah(self, Vm): ''' Compute the alpha rate for the inactivation-probability of Sodium channels. :param Vm: membrane potential (mV) :return: rate constant (s-1) ''' alpha = 0.045 * np.exp(-(Vm + 58) / 18) # ms-1 return alpha * 1e3 # s-1 def betah(self, Vm): ''' Compute the beta rate for the inactivation-probability of Sodium channels. :param Vm: membrane potential (mV) :return: rate constant (s-1) .. warning:: the original paper contains an error (multiplication) in the expression of this rate constant, corrected in the mod file on ModelDB (division). ''' beta = 0.72 / (np.exp(-(Vm + 23) / 14) + 1) # ms-1 return beta * 1e3 # s-1 def alphan(self, Vm): ''' Compute the alpha rate for the open-probability of delayed-rectifier Potassium channels. :param Vm: membrane potential (mV) :return: rate constant (s-1) ''' alpha = -0.024 * (Vm - 17) / (np.exp(-(Vm - 17) / 8) - 1) # ms-1 return alpha * 1e3 # s-1 def betan(self, Vm): ''' Compute the beta rate for the open-probability of delayed-rectifier Potassium channels. :param Vm: membrane potential (mV) :return: rate constant (s-1) ''' beta = 0.2 * np.exp(-(Vm + 48) / 35) # ms-1 return beta * 1e3 # s-1 def alphas(self, Vm): ''' Compute the alpha rate for the open-probability of Calcium channels. :param Vm: membrane potential (mV) :return: rate constant (s-1) ''' alpha = -1.5 * (Vm - 20) / (np.exp(-(Vm - 20) / 5) - 1) # ms-1 return alpha * 1e3 # s-1 def betas(self, Vm): ''' Compute the beta rate for the open-probability of Calcium channels. :param Vm: membrane potential (mV) :return: rate constant (s-1) ''' beta = 1.5 * np.exp(-(Vm + 25) / 10) # ms-1 return beta * 1e3 # s-1 def alphaC(self, C_Ca_in): ''' Compute the alpha rate for the open-probability of Calcium-dependent Potassium channels. :param C_Ca_in: intracellular Calcium concentration (M) :return: rate constant (s-1) ''' alpha = 0.1 * C_Ca_in / self.alphaC_sf # ms-1 return alpha * 1e3 # s-1 def derM(self, Vm, m): ''' Compute the evolution of the open-probability of Sodium channels. :param Vm: membrane potential (mV) :param m: open-probability of Sodium channels (prob) :return: derivative of open-probability w.r.t. time (prob/s) ''' return self.alpham(Vm) * (1 - m) - self.betam(Vm) * m def derH(self, Vm, h): ''' Compute the evolution of the inactivation-probability of Sodium channels. :param Vm: membrane potential (mV) :param h: inactivation-probability of Sodium channels (prob) :return: derivative of open-probability w.r.t. time (prob/s) ''' return self.alphah(Vm) * (1 - h) - self.betah(Vm) * h def derN(self, Vm, n): ''' Compute the evolution of the open-probability of delayed-rectifier Potassium channels. :param Vm: membrane potential (mV) :param n: open-probability of delayed-rectifier Potassium channels (prob) :return: derivative of open-probability w.r.t. time (prob/s) ''' return self.alphan(Vm) * (1 - n) - self.betan(Vm) * n def derS(self, Vm, s): ''' Compute the evolution of the open-probability of Calcium channels. :param Vm: membrane potential (mV) :param s: open-probability of Calcium channels (prob) :return: derivative of open-probability w.r.t. time (prob/s) ''' return self.alphas(Vm) * (1 - s) - self.betas(Vm) * s def derC(self, c, C_Ca_in): ''' Compute the evolution of the open-probability of Calcium-dependent Potassium channels. :param c: open-probability of Calcium-dependent Potassium channels (prob) :param C_Ca_in: intracellular Calcium concentration (M) :return: derivative of open-probability w.r.t. time (prob/s) ''' return self.alphaC(C_Ca_in) * (1 - c) - self.betaC * c def currNa(self, m, h, Vm, C_Na_in): ''' Compute the inward Sodium current per unit area. :param m: open-probability of Sodium channels :param h: inactivation-probability of Sodium channels :param Vm: membrane potential (mV) :param C_Na_in: intracellular Sodium concentration (M) :return: current per unit area (mA/m2) ''' GNa = self.GNaMax * m**4 * h VNa = self.nernst(self.Z_Na, C_Na_in, self.C_Na_out) # Sodium Nernst potential return GNa * (Vm - VNa) def currK(self, n, Vm): ''' Compute the outward, delayed-rectifier Potassium current per unit area. :param n: open-probability of delayed-rectifier Potassium channels :param Vm: membrane potential (mV) :return: current per unit area (mA/m2) ''' GK = self.GKMax * n**2 return GK * (Vm - self.VK) def currCa(self, s, Vm, C_Ca_in): ''' Compute the inward Calcium current per unit area. :param s: open-probability of Calcium channels :param Vm: membrane potential (mV) :param C_Ca_in: intracellular Calcium concentration (M) :return: current per unit area (mA/m2) ''' GCa = self.GCaMax * s VCa = self.nernst(self.Z_Ca, C_Ca_in, self.C_Ca_out) # Calcium Nernst potential return GCa * (Vm - VCa) def currKCa(self, c, Vm): ''' Compute the outward Calcium-dependent Potassium current per unit area. :param c: open-probability of Calcium-dependent Potassium channels :param Vm: membrane potential (mV) :return: current per unit area (mA/m2) ''' GKCa = self.GKCaMax * c return GKCa * (Vm - self.VK) def currL(self, Vm): ''' Compute the non-specific leakage current per unit area. :param Vm: membrane potential (mV) :return: current per unit area (mA/m2) ''' return self.GL * (Vm - self.VL) class LeechPressure(LeechMech): ''' Class defining the membrane channel dynamics of a leech pressure sensory neuron. with 7 different current types: - Inward Sodium current - Outward Potassium current - Inward high-voltage-activated Calcium current - Non-specific leakage current - Calcium-dependent, outward Potassium current - Sodium pump current - Calcium pump current Reference: *Baccus, S.A. (1998). Synaptic facilitation by reflected action potentials: enhancement of transmission when nerve impulses reverse direction at axon branch points. Proc. Natl. Acad. Sci. U.S.A. 95, 8345–8350.* ''' # Name of channel mechanism name = 'LeechP' # Cell-specific biophysical parameters Cm0 = 1e-2 # Cell membrane resting capacitance (F/m2) Vm0 = -48.865 # Cell membrane resting potential (mV) C_Na_out = 0.11 # Sodium extracellular concentration (M) C_Ca_out = 1.8e-3 # Calcium extracellular concentration (M) C_Na_in0 = 0.01 # Initial Sodium intracellular concentration (M) C_Ca_in0 = 1e-7 # Initial Calcium intracellular concentration (M) # VNa = 60 # Sodium Nernst potential, from MOD file on ModelDB (mV) # VCa = 125 # Calcium Nernst potential, from MOD file on ModelDB (mV) VK = -68.0 # Potassium Nernst potential (mV) VL = -49.0 # Non-specific leakage Nernst potential (mV) INaPmax = 70.0 # Maximum pump rate of the NaK-ATPase (mA/m2) khalf_Na = 0.012 # Sodium concentration at which NaK-ATPase is at half its maximum rate (M) ksteep_Na = 1e-3 # Sensitivity of NaK-ATPase to varying Sodium concentrations (M) iCaS = 0.1 # Calcium pump current parameter (mA/m2) GNaMax = 3500.0 # Max. conductance of Sodium current (S/m^2) GKMax = 60.0 # Max. conductance of Potassium current (S/m^2) GCaMax = 0.02 # Max. conductance of Calcium current (S/m^2) GKCaMax = 8.0 # Max. conductance of Calcium-dependent Potassium current (S/m^2) GL = 5.0 # Conductance of non-specific leakage current (S/m^2) diam = 50e-6 # Cell soma diameter (m) Z_Na = 1 # Sodium valence Z_Ca = 2 # Calcium valence # Default plotting scheme pltvars_scheme = { 'i_{Na}\ kin.': ['m', 'h', 'm4h'], 'i_K\ kin.': ['n'], 'i_{Ca}\ kin.': ['s'], 'i_{KCa}\ kin.': ['c'], 'pools': ['C_Na', 'C_Ca'], 'I': ['iNa2', 'iK', 'iCa2', 'iKCa2', 'iPumpNa2', 'iPumpCa2', 'iL', 'iNet'] } def __init__(self): ''' Constructor of the class. ''' SV_ratio = 6 / self.diam # surface to volume ratio of the (spherical) cell soma # Conversion constant from membrane ionic currents into # change rate of intracellular ionic concentrations self.K_Na = SV_ratio / (self.Z_Na * self.Faraday) * 1e-6 # Sodium (M/s) self.K_Ca = SV_ratio / (self.Z_Ca * self.Faraday) * 1e-6 # Calcium (M/s) # Names and initial states of the channels state probabilities self.states_names = ['m', 'h', 'n', 's', 'c', 'C_Na', 'C_Ca'] self.states0 = np.array([]) # Names of the channels effective coefficients self.coeff_names = ['alpham', 'betam', 'alphah', 'betah', 'alphan', 'betan', 'alphas', 'betas'] # Define initial channel probabilities (solving dx/dt = 0 at resting potential) self.states0 = self.steadyStates(self.Vm0) # Charge interval bounds for lookup creation self.Qbounds = (np.round(self.Vm0 - 10.0) * 1e-5, 60.0e-5) def nernst(self, z_ion, C_ion_in, C_ion_out): ''' Return the Nernst potential of a specific ion given its intra and extracellular concentrations. :param z_ion: ion valence :param C_ion_in: intracellular ion concentration (M) :param C_ion_out: extracellular ion concentration (M) :return: ion Nernst potential (mV) ''' return (self.Rg * self.T) / (z_ion * self.Faraday) * np.log(C_ion_out / C_ion_in) * 1e3 def currPumpNa(self, C_Na_in): ''' Outward current mimicking the activity of the NaK-ATPase pump. :param C_Na_in: intracellular Sodium concentration (M) :return: current per unit area (mA/m2) ''' return self.INaPmax / (1 + np.exp((self.khalf_Na - C_Na_in) / self.ksteep_Na)) def currPumpCa(self, C_Ca_in): ''' Outward current representing the activity of a Calcium pump. :param C_Ca_in: intracellular Calcium concentration (M) :return: current per unit area (mA/m2) ''' return self.iCaS * (C_Ca_in - self.C_Ca_in0) / 1.5 def currNet(self, Vm, states): ''' Concrete implementation of the abstract API method. ''' m, h, n, s, c, C_Na_in, C_Ca_in = states return (self.currNa(m, h, Vm, C_Na_in) + self.currK(n, Vm) + self.currCa(s, Vm, C_Ca_in) + self.currKCa(c, Vm) + self.currL(Vm) + (self.currPumpNa(C_Na_in) / 3.) + self.currPumpCa(C_Ca_in)) # mA/m2 def steadyStates(self, Vm): ''' Concrete implementation of the abstract API method. ''' # Intracellular concentrations C_Na_eq = self.C_Na_in0 C_Ca_eq = self.C_Ca_in0 # Standard gating dynamics: Solve the equation dx/dt = 0 at Vm for each x-state meq = self.alpham(Vm) / (self.alpham(Vm) + self.betam(Vm)) heq = self.alphah(Vm) / (self.alphah(Vm) + self.betah(Vm)) neq = self.alphan(Vm) / (self.alphan(Vm) + self.betan(Vm)) seq = self.alphas(Vm) / (self.alphas(Vm) + self.betas(Vm)) ceq = self.alphaC(C_Ca_eq) / (self.alphaC(C_Ca_eq) + self.betaC) return np.array([meq, heq, neq, seq, ceq, C_Na_eq, C_Ca_eq]) def derStates(self, Vm, states): ''' Concrete implementation of the abstract API method. ''' # Unpack states m, h, n, s, c, C_Na_in, C_Ca_in = states # Standard gating states derivatives dmdt = self.derM(Vm, m) dhdt = self.derH(Vm, h) dndt = self.derN(Vm, n) dsdt = self.derS(Vm, s) dcdt = self.derC(c, C_Ca_in) # Intracellular concentrations dCNa_dt = - (self.currNa(m, h, Vm, C_Na_in) + self.currPumpNa(C_Na_in)) * self.K_Na # M/s dCCa_dt = -(self.currCa(s, Vm, C_Ca_in) + self.currPumpCa(C_Ca_in)) * self.K_Ca # M/s # Pack derivatives and return return [dmdt, dhdt, dndt, dsdt, dcdt, dCNa_dt, dCCa_dt] def getEffRates(self, Vm): ''' Concrete implementation of the abstract API method. ''' # Compute average cycle value for rate constants am_avg = np.mean(self.alpham(Vm)) bm_avg = np.mean(self.betam(Vm)) ah_avg = np.mean(self.alphah(Vm)) bh_avg = np.mean(self.betah(Vm)) an_avg = np.mean(self.alphan(Vm)) bn_avg = np.mean(self.betan(Vm)) as_avg = np.mean(self.alphas(Vm)) bs_avg = np.mean(self.betas(Vm)) # Return array of coefficients return np.array([am_avg, bm_avg, ah_avg, bh_avg, an_avg, bn_avg, as_avg, bs_avg]) def derStatesEff(self, Qm, states, interp_data): ''' Concrete implementation of the abstract API method. ''' rates = np.array([np.interp(Qm, interp_data['Q'], interp_data[rn]) for rn in self.coeff_names]) Vmeff = np.interp(Qm, interp_data['Q'], interp_data['V']) # Unpack states m, h, n, s, c, C_Na_in, C_Ca_in = states # Standard gating states derivatives dmdt = rates[0] * (1 - m) - rates[1] * m dhdt = rates[2] * (1 - h) - rates[3] * h dndt = rates[4] * (1 - n) - rates[5] * n dsdt = rates[6] * (1 - s) - rates[7] * s # KCa current gating state derivative dcdt = self.derC(c, C_Ca_in) # Intracellular concentrations dCNa_dt = - (self.currNa(m, h, Vmeff, C_Na_in) + self.currPumpNa(C_Na_in)) * self.K_Na # M/s dCCa_dt = -(self.currCa(s, Vmeff, C_Ca_in) + self.currPumpCa(C_Ca_in)) * self.K_Ca # M/s # Pack derivatives and return return [dmdt, dhdt, dndt, dsdt, dcdt, dCNa_dt, dCCa_dt] class LeechRetzius(LeechMech): ''' Class defining the membrane channel dynamics of a leech Retzius neuron. with 5 different current types: - Inward Sodium current - Outward Potassium current - Inward high-voltage-activated Calcium current - Non-specific leakage current - Calcium-dependent, outward Potassium current References: *Vazquez, Y., Mendez, B., Trueta, C., and De-Miguel, F.F. (2009). Summation of excitatory postsynaptic potentials in electrically-coupled neurones. Neuroscience 163, 202–212.* *ModelDB link: https://senselab.med.yale.edu/modeldb/ShowModel.cshtml?model=120910* ''' # Name of channel mechanism # name = 'LeechR' # Cell-specific biophysical parameters Cm0 = 5e-2 # Cell membrane resting capacitance (F/m2) Vm0 = -44.45 # Cell membrane resting potential (mV) VNa = 50.0 # Sodium Nernst potential, from retztemp.ses file on ModelDB (mV) VCa = 125.0 # Calcium Nernst potential, from cachdend.mod file on ModelDB (mV) VK = -79.0 # Potassium Nernst potential, from retztemp.ses file on ModelDB (mV) VL = -30.0 # Non-specific leakage Nernst potential, from leakdend.mod file on ModelDB (mV) GNaMax = 1250.0 # Max. conductance of Sodium current (S/m^2) GKMax = 10.0 # Max. conductance of Potassium current (S/m^2) GAMax = 100.0 # Max. conductance of transient Potassium current (S/m^2) GCaMax = 4.0 # Max. conductance of Calcium current (S/m^2) GKCaMax = 130.0 # Max. conductance of Calcium-dependent Potassium current (S/m^2) GL = 1.25 # Conductance of non-specific leakage current (S/m^2) Vhalf = -73.1 # mV C_Ca_in = 5e-8 # Calcium intracellular concentration, from retztemp.ses file (M) # Default plotting scheme pltvars_scheme = { 'i_{Na}\ kin.': ['m', 'h', 'm4h'], 'i_K\ kin.': ['n'], 'i_A\ kin.': ['a', 'b', 'ab'], 'i_{Ca}\ kin.': ['s'], 'i_{KCa}\ kin.': ['c'], 'I': ['iNa', 'iK', 'iCa', 'iKCa2', 'iA', 'iL', 'iNet'] } def __init__(self): ''' Constructor of the class. ''' # Names and initial states of the channels state probabilities self.states_names = ['m', 'h', 'n', 's', 'c', 'a', 'b'] self.states0 = np.array([]) # Names of the channels effective coefficients self.coeff_names = ['alpham', 'betam', 'alphah', 'betah', 'alphan', 'betan', 'alphas', 'betas', 'alphac', 'betac', 'alphaa', 'betaa' 'alphab', 'betab'] # Define initial channel probabilities (solving dx/dt = 0 at resting potential) self.states0 = self.steadyStates(self.Vm0) def ainf(self, Vm): ''' Steady-state activation probability of transient Potassium channels. Source: *Beck, H., Ficker, E., and Heinemann, U. (1992). Properties of two voltage-activated potassium currents in acutely isolated juvenile rat dentate gyrus granule cells. J. Neurophysiol. 68, 2086–2099.* :param Vm: membrane potential (mV) :return: time constant (s) ''' Vth = -55.0 # mV return 0 if Vm <= Vth else min(1, 2 * (Vm - Vth)**3 / ((11 - Vth)**3 + (Vm - Vth)**3)) def taua(self, Vm): ''' Activation time constant of transient Potassium channels. (assuming T = 20°C). Source: *Beck, H., Ficker, E., and Heinemann, U. (1992). Properties of two voltage-activated potassium currents in acutely isolated juvenile rat dentate gyrus granule cells. J. Neurophysiol. 68, 2086–2099.* :param Vm: membrane potential (mV) :return: time constant (s) ''' x = -1.5 * (Vm - self.Vhalf) * 1e-3 * self.Faraday / (self.Rg * self.T) # [-] alpha = np.exp(x) # ms-1 beta = np.exp(0.7 * x) # ms-1 return max(0.5, beta / (0.3 * (1 + alpha))) * 1e-3 # s def binf(self, Vm): ''' Steady-state inactivation probability of transient Potassium channels. Source: *Beck, H., Ficker, E., and Heinemann, U. (1992). Properties of two voltage-activated potassium currents in acutely isolated juvenile rat dentate gyrus granule cells. J. Neurophysiol. 68, 2086–2099.* :param Vm: membrane potential (mV) :return: time constant (s) ''' return 1. / (1 + np.exp((self.Vhalf - Vm) / -6.3)) def taub(self, Vm): ''' Inactivation time constant of transient Potassium channels. (assuming T = 20°C). Source: *Beck, H., Ficker, E., and Heinemann, U. (1992). Properties of two voltage-activated potassium currents in acutely isolated juvenile rat dentate gyrus granule cells. J. Neurophysiol. 68, 2086–2099.* :param Vm: membrane potential (mV) :return: time constant (s) ''' x = 2 * (Vm - self.Vhalf) * 1e-3 * self.Faraday / (self.Rg * self.T) alpha = np.exp(x) beta = np.exp(0.65 * x) return max(7.5, beta / (0.02 * (1 + alpha))) * 1e-3 # s def derA(self, Vm, a): ''' Compute the evolution of the activation-probability of transient Potassium channels. :param Vm: membrane potential (mV) :param a: activation-probability of transient Potassium channels (prob) :return: derivative of open-probability w.r.t. time (prob/s) ''' return (self.ainf(Vm) - a) / self.taua(Vm) def derB(self, Vm, b): ''' Compute the evolution of the inactivation-probability of transient Potassium channels. :param Vm: membrane potential (mV) :param b: inactivation-probability of transient Potassium channels (prob) :return: derivative of open-probability w.r.t. time (prob/s) ''' return (self.binf(Vm) - b) / self.taub(Vm) def currA(self, a, b, Vm): ''' Compute the outward, transient Potassium current per unit area. :param a: open-probability of transient Potassium channels :param b: inactivation-probability of transient Potassium channels :param Vm: membrane potential (mV) :return: current per unit area (mA/m2) ''' GK = self.GAMax * a * b return GK * (Vm - self.VK) def currNet(self, Vm, states): ''' Concrete implementation of the abstract API method. ''' m, h, n, s, c, a, b = states return (self.currNa(m, h, Vm) + self.currK(n, Vm) + self.currCa(s, Vm) + self.currL(Vm) + self.currKCa(c, Vm) + self.currA(a, b, Vm)) # mA/m2 def steadyStates(self, Vm): ''' Concrete implementation of the abstract API method. ''' # Standard gating dynamics: Solve the equation dx/dt = 0 at Vm for each x-state meq = self.alpham(Vm) / (self.alpham(Vm) + self.betam(Vm)) heq = self.alphah(Vm) / (self.alphah(Vm) + self.betah(Vm)) neq = self.alphan(Vm) / (self.alphan(Vm) + self.betan(Vm)) seq = self.alphas(Vm) / (self.alphas(Vm) + self.betas(Vm)) ceq = self.alphaC(self.C_Ca_in) / (self.alphaC(self.C_Ca_in) + self.betaC) aeq = self.ainf(Vm) beq = self.binf(Vm) return np.array([meq, heq, neq, seq, ceq, aeq, beq]) def derStates(self, Vm, states): ''' Concrete implementation of the abstract API method. ''' # Unpack states m, h, n, s, c, a, b = states # Standard gating states derivatives dmdt = self.derM(Vm, m) dhdt = self.derH(Vm, h) dndt = self.derN(Vm, n) dsdt = self.derS(Vm, s) dcdt = self.derC(c, self.C_Ca_in) dadt = self.derA(Vm, a) dbdt = self.derB(Vm, b) # Pack derivatives and return return [dmdt, dhdt, dndt, dsdt, dcdt, dadt, dbdt] def getEffRates(self, Vm): ''' Concrete implementation of the abstract API method. ''' # Compute average cycle value for rate constants am_avg = np.mean(self.alpham(Vm)) bm_avg = np.mean(self.betam(Vm)) ah_avg = np.mean(self.alphah(Vm)) bh_avg = np.mean(self.betah(Vm)) an_avg = np.mean(self.alphan(Vm)) bn_avg = np.mean(self.betan(Vm)) as_avg = np.mean(self.alphas(Vm)) bs_avg = np.mean(self.betas(Vm)) Ta = self.taua(Vm) ainf = self.ainf(Vm) aa_avg = np.mean(ainf / Ta) ba_avg = np.mean(1 / Ta) - aa_avg Tb = self.taub(Vm) binf = self.binf(Vm) ab_avg = np.mean(binf / Tb) bb_avg = np.mean(1 / Tb) - ab_avg # Return array of coefficients return np.array([am_avg, bm_avg, ah_avg, bh_avg, an_avg, bn_avg, as_avg, bs_avg, aa_avg, ba_avg, ab_avg, bb_avg]) def derStatesEff(self, Qm, states, interp_data): ''' Concrete implementation of the abstract API method. ''' rates = np.array([np.interp(Qm, interp_data['Q'], interp_data[rn]) for rn in self.coeff_names]) # Unpack states m, h, n, s, c, a, b = states # Standard gating states derivatives dmdt = rates[0] * (1 - m) - rates[1] * m dhdt = rates[2] * (1 - h) - rates[3] * h dndt = rates[4] * (1 - n) - rates[5] * n dsdt = rates[6] * (1 - s) - rates[7] * s dadt = rates[8] * (1 - a) - rates[9] * a dbdt = rates[10] * (1 - b) - rates[11] * b # KCa current gating state derivative dcdt = self.derC(c, self.C_Ca_in) # Pack derivatives and return return [dmdt, dhdt, dndt, dsdt, dcdt, dadt, dbdt] diff --git a/PointNICE/neurons/thalamic.py b/PySONIC/neurons/thalamic.py similarity index 99% rename from PointNICE/neurons/thalamic.py rename to PySONIC/neurons/thalamic.py index a58e86d..3cf7495 100644 --- a/PointNICE/neurons/thalamic.py +++ b/PySONIC/neurons/thalamic.py @@ -1,792 +1,792 @@ #!/usr/bin/env python # -*- coding: utf-8 -*- # @Author: Theo Lemaire # @Date: 2017-07-31 15:20:54 # @Email: theo.lemaire@epfl.ch # @Last Modified by: Theo Lemaire -# @Last Modified time: 2018-07-18 14:23:08 +# @Last Modified time: 2018-08-21 16:10:36 ''' Channels mechanisms for thalamic neurons. ''' import logging import numpy as np from .base import BaseMech # Get package logger -logger = logging.getLogger('PointNICE') +logger = logging.getLogger('PySONIC') class Thalamic(BaseMech): ''' Class defining the generic membrane channel dynamics of a thalamic neuron with 4 different current types: - Inward Sodium current - Outward Potassium current - Inward Calcium current - Non-specific leakage current This generic class cannot be used directly as it does not contain any specific parameters. Reference: *Plaksin, M., Kimmel, E., and Shoham, S. (2016). Cell-Type-Selective Effects of Intramembrane Cavitation as a Unifying Theoretical Framework for Ultrasonic Neuromodulation. eNeuro 3.* ''' # Generic biophysical parameters of thalamic cells Cm0 = 1e-2 # Cell membrane resting capacitance (F/m2) Vm0 = 0.0 # Dummy value for membrane potential (mV) VNa = 50.0 # Sodium Nernst potential (mV) VK = -90.0 # Potassium Nernst potential (mV) VCa = 120.0 # Calcium Nernst potential (mV) def __init__(self): ''' Constructor of the class ''' # Names and initial states of the channels state probabilities self.states_names = ['m', 'h', 'n', 's', 'u'] self.states0 = np.array([]) # Names of the different coefficients to be averaged in a lookup table. self.coeff_names = ['alpham', 'betam', 'alphah', 'betah', 'alphan', 'betan', 'alphas', 'betas', 'alphau', 'betau'] # Charge interval bounds for lookup creation self.Qbounds = (np.round(self.Vm0 - 25.0) * 1e-5, 50.0e-5) def alpham(self, Vm): ''' Compute the alpha rate for the open-probability of Sodium channels. :param Vm: membrane potential (mV) :return: rate constant (s-1) ''' Vdiff = Vm - self.VT alpha = (-0.32 * (Vdiff - 13) / (np.exp(- (Vdiff - 13) / 4) - 1)) # ms-1 return alpha * 1e3 # s-1 def betam(self, Vm): ''' Compute the beta rate for the open-probability of Sodium channels. :param Vm: membrane potential (mV) :return: rate constant (s-1) ''' Vdiff = Vm - self.VT beta = (0.28 * (Vdiff - 40) / (np.exp((Vdiff - 40) / 5) - 1)) # ms-1 return beta * 1e3 # s-1 def alphah(self, Vm): ''' Compute the alpha rate for the inactivation-probability of Sodium channels. :param Vm: membrane potential (mV) :return: rate constant (s-1) ''' Vdiff = Vm - self.VT alpha = (0.128 * np.exp(-(Vdiff - 17) / 18)) # ms-1 return alpha * 1e3 # s-1 def betah(self, Vm): ''' Compute the beta rate for the inactivation-probability of Sodium channels. :param Vm: membrane potential (mV) :return: rate constant (s-1) ''' Vdiff = Vm - self.VT beta = (4 / (1 + np.exp(-(Vdiff - 40) / 5))) # ms-1 return beta * 1e3 # s-1 def alphan(self, Vm): ''' Compute the alpha rate for the open-probability of delayed-rectifier Potassium channels. :param Vm: membrane potential (mV) :return: rate constant (s-1) ''' Vdiff = Vm - self.VT alpha = (-0.032 * (Vdiff - 15) / (np.exp(-(Vdiff - 15) / 5) - 1)) # ms-1 return alpha * 1e3 # s-1 def betan(self, Vm): ''' Compute the beta rate for the open-probability of delayed-rectifier Potassium channels. :param Vm: membrane potential (mV) :return: rate constant (s-1) ''' Vdiff = Vm - self.VT beta = (0.5 * np.exp(-(Vdiff - 10) / 40)) # ms-1 return beta * 1e3 # s-1 def derM(self, Vm, m): ''' Compute the evolution of the open-probability of Sodium channels. :param Vm: membrane potential (mV) :param m: open-probability of Sodium channels (prob) :return: derivative of open-probability w.r.t. time (prob/s) ''' return self.alpham(Vm) * (1 - m) - self.betam(Vm) * m def derH(self, Vm, h): ''' Compute the evolution of the inactivation-probability of Sodium channels. :param Vm: membrane potential (mV) :param h: inactivation-probability of Sodium channels (prob) :return: derivative of open-probability w.r.t. time (prob/s) ''' return self.alphah(Vm) * (1 - h) - self.betah(Vm) * h def derN(self, Vm, n): ''' Compute the evolution of the open-probability of delayed-rectifier Potassium channels. :param Vm: membrane potential (mV) :param n: open-probability of delayed-rectifier Potassium channels (prob) :return: derivative of open-probability w.r.t. time (prob/s) ''' return self.alphan(Vm) * (1 - n) - self.betan(Vm) * n def derS(self, Vm, s): ''' Compute the evolution of the open-probability of the S-type, activation gate of Calcium channels. :param Vm: membrane potential (mV) :param s: open-probability of S-type Calcium activation gates (prob) :return: derivative of open-probability w.r.t. time (prob/s) ''' return (self.sinf(Vm) - s) / self.taus(Vm) def derU(self, Vm, u): ''' Compute the evolution of the open-probability of the U-type, inactivation gate of Calcium channels. :param Vm: membrane potential (mV) :param u: open-probability of U-type Calcium inactivation gates (prob) :return: derivative of open-probability w.r.t. time (prob/s) ''' return (self.uinf(Vm) - u) / self.tauu(Vm) def currNa(self, m, h, Vm): ''' Compute the inward Sodium current per unit area. :param m: open-probability of Sodium channels :param h: inactivation-probability of Sodium channels :param Vm: membrane potential (mV) :return: current per unit area (mA/m2) ''' GNa = self.GNaMax * m**3 * h return GNa * (Vm - self.VNa) def currK(self, n, Vm): ''' Compute the outward delayed-rectifier Potassium current per unit area. :param n: open-probability of delayed-rectifier Potassium channels :param Vm: membrane potential (mV) :return: current per unit area (mA/m2) ''' GK = self.GKMax * n**4 return GK * (Vm - self.VK) def currCa(self, s, u, Vm): ''' Compute the inward Calcium current per unit area. :param s: open-probability of the S-type activation gate of Calcium channels :param u: open-probability of the U-type inactivation gate of Calcium channels :param Vm: membrane potential (mV) :return: current per unit area (mA/m2) ''' GT = self.GTMax * s**2 * u return GT * (Vm - self.VCa) def currL(self, Vm): ''' Compute the non-specific leakage current per unit area. :param Vm: membrane potential (mV) :return: current per unit area (mA/m2) ''' return self.GL * (Vm - self.VL) def currNet(self, Vm, states): ''' Concrete implementation of the abstract API method. ''' m, h, n, s, u = states return (self.currNa(m, h, Vm) + self.currK(n, Vm) + self.currCa(s, u, Vm) + self.currL(Vm)) # mA/m2 def steadyStates(self, Vm): ''' Concrete implementation of the abstract API method. ''' # Solve the equation dx/dt = 0 at Vm for each x-state meq = self.alpham(Vm) / (self.alpham(Vm) + self.betam(Vm)) heq = self.alphah(Vm) / (self.alphah(Vm) + self.betah(Vm)) neq = self.alphan(Vm) / (self.alphan(Vm) + self.betan(Vm)) seq = self.sinf(Vm) ueq = self.uinf(Vm) return np.array([meq, heq, neq, seq, ueq]) def derStates(self, Vm, states): ''' Concrete implementation of the abstract API method. ''' m, h, n, s, u = states dmdt = self.derM(Vm, m) dhdt = self.derH(Vm, h) dndt = self.derN(Vm, n) dsdt = self.derS(Vm, s) dudt = self.derU(Vm, u) return [dmdt, dhdt, dndt, dsdt, dudt] def getEffRates(self, Vm): ''' Concrete implementation of the abstract API method. ''' # Compute average cycle value for rate constants am_avg = np.mean(self.alpham(Vm)) bm_avg = np.mean(self.betam(Vm)) ah_avg = np.mean(self.alphah(Vm)) bh_avg = np.mean(self.betah(Vm)) an_avg = np.mean(self.alphan(Vm)) bn_avg = np.mean(self.betan(Vm)) Ts = self.taus(Vm) as_avg = np.mean(self.sinf(Vm) / Ts) bs_avg = np.mean(1 / Ts) - as_avg Tu = np.array([self.tauu(v) for v in Vm]) au_avg = np.mean(self.uinf(Vm) / Tu) bu_avg = np.mean(1 / Tu) - au_avg # Return array of coefficients return np.array([am_avg, bm_avg, ah_avg, bh_avg, an_avg, bn_avg, as_avg, bs_avg, au_avg, bu_avg]) def derStatesEff(self, Qm, states, interp_data): ''' Concrete implementation of the abstract API method. ''' rates = np.array([np.interp(Qm, interp_data['Q'], interp_data[rn]) for rn in self.coeff_names]) m, h, n, s, u = states dmdt = rates[0] * (1 - m) - rates[1] * m dhdt = rates[2] * (1 - h) - rates[3] * h dndt = rates[4] * (1 - n) - rates[5] * n dsdt = rates[6] * (1 - s) - rates[7] * s dudt = rates[8] * (1 - u) - rates[9] * u return [dmdt, dhdt, dndt, dsdt, dudt] class ThalamicRE(Thalamic): ''' Specific membrane channel dynamics of a thalamic reticular neuron. References: *Destexhe, A., Contreras, D., Steriade, M., Sejnowski, T.J., and Huguenard, J.R. (1996). In vivo, in vitro, and computational analysis of dendritic calcium currents in thalamic reticular neurons. J. Neurosci. 16, 169–185.* *Huguenard, J.R., and Prince, D.A. (1992). A novel T-type current underlies prolonged Ca(2+)-dependent burst firing in GABAergic neurons of rat thalamic reticular nucleus. J. Neurosci. 12, 3804–3817.* ''' # Name of channel mechanism name = 'RE' # Cell-specific biophysical parameters Vm0 = -89.5 # Cell membrane resting potential (mV) GNaMax = 2000.0 # Max. conductance of Sodium current (S/m^2) GKMax = 200.0 # Max. conductance of Potassium current (S/m^2) GTMax = 30.0 # Max. conductance of low-threshold Calcium current (S/m^2) GL = 0.5 # Conductance of non-specific leakage current (S/m^2) VL = -90.0 # Non-specific leakage Nernst potential (mV) VT = -67.0 # Spike threshold adjustment parameter (mV) # Default plotting scheme pltvars_scheme = { 'i_{Na}\ kin.': ['m', 'h', 'm3h'], 'i_K\ kin.': ['n'], 'i_{TS}\ kin.': ['s', 'u', 's2u'], 'I': ['iNa', 'iK', 'iTs', 'iL', 'iNet'] } def __init__(self): ''' Constructor of the class. ''' # Instantiate parent class super().__init__() # Define initial channel probabilities (solving dx/dt = 0 at resting potential) self.states0 = self.steadyStates(self.Vm0) def sinf(self, Vm): ''' Compute the asymptotic value of the open-probability of the S-type, activation gate of Calcium channels. :param Vm: membrane potential (mV) :return: asymptotic probability (-) ''' return 1.0 / (1.0 + np.exp(-(Vm + 52.0) / 7.4)) # prob def taus(self, Vm): ''' Compute the decay time constant for adaptation of S-type, activation gate of Calcium channels. :param Vm: membrane potential (mV) :return: decayed time constant (s) ''' return (1 + 0.33 / (np.exp((Vm + 27.0) / 10.0) + np.exp(-(Vm + 102.0) / 15.0))) * 1e-3 # s def uinf(self, Vm): ''' Compute the asymptotic value of the open-probability of the U-type, inactivation gate of Calcium channels. :param Vm: membrane potential (mV) :return: asymptotic probability (-) ''' return 1.0 / (1.0 + np.exp((Vm + 80.0) / 5.0)) # prob def tauu(self, Vm): ''' Compute the decay time constant for adaptation of U-type, inactivation gate of Calcium channels. :param Vm: membrane potential (mV) :return: decayed time constant (s) ''' return (28.3 + 0.33 / (np.exp((Vm + 48.0) / 4.0) + np.exp(-(Vm + 407.0) / 50.0))) * 1e-3 # s class ThalamoCortical(Thalamic): ''' Specific membrane channel dynamics of a thalamo-cortical neuron, with a specific hyperpolarization-activated, mixed cationic current and a leakage Potassium current. References: *Pospischil, M., Toledo-Rodriguez, M., Monier, C., Piwkowska, Z., Bal, T., Frégnac, Y., Markram, H., and Destexhe, A. (2008). Minimal Hodgkin-Huxley type models for different classes of cortical and thalamic neurons. Biol Cybern 99, 427–441.* *Destexhe, A., Bal, T., McCormick, D.A., and Sejnowski, T.J. (1996). Ionic mechanisms underlying synchronized oscillations and propagating waves in a model of ferret thalamic slices. J. Neurophysiol. 76, 2049–2070.* *McCormick, D.A., and Huguenard, J.R. (1992). A model of the electrophysiological properties of thalamocortical relay neurons. J. Neurophysiol. 68, 1384–1400.* ''' # Name of channel mechanism name = 'TC' # Cell-specific biophysical parameters # Vm0 = -63.4 # Cell membrane resting potential (mV) Vm0 = -61.93 # Cell membrane resting potential (mV) GNaMax = 900.0 # Max. conductance of Sodium current (S/m^2) GKMax = 100.0 # Max. conductance of Potassium current (S/m^2) GTMax = 20.0 # Max. conductance of low-threshold Calcium current (S/m^2) GKL = 0.138 # Conductance of leakage Potassium current (S/m^2) GhMax = 0.175 # Max. conductance of mixed cationic current (S/m^2) GL = 0.1 # Conductance of non-specific leakage current (S/m^2) Vh = -40.0 # Mixed cationic current reversal potential (mV) VL = -70.0 # Non-specific leakage Nernst potential (mV) VT = -52.0 # Spike threshold adjustment parameter (mV) Vx = 0.0 # Voltage-dependence uniform shift factor at 36°C (mV) tau_Ca_removal = 5e-3 # decay time constant for intracellular Ca2+ dissolution (s) CCa_min = 50e-9 # minimal intracellular Calcium concentration (M) deff = 100e-9 # effective depth beneath membrane for intracellular [Ca2+] calculation F_Ca = 1.92988e5 # Faraday constant for bivalent ion (Coulomb / mole) nCa = 4 # number of Calcium binding sites on regulating factor k1 = 2.5e22 # intracellular Ca2+ regulation factor (M-4 s-1) k2 = 0.4 # intracellular Ca2+ regulation factor (s-1) k3 = 100.0 # intracellular Ca2+ regulation factor (s-1) k4 = 1.0 # intracellular Ca2+ regulation factor (s-1) # Default plotting scheme pltvars_scheme = { 'i_{Na}\ kin.': ['m', 'h'], 'i_K\ kin.': ['n'], 'i_{T}\ kin.': ['s', 'u'], 'i_{H}\ kin.': ['O', 'OL', 'O + 2OL'], 'I': ['iNa', 'iK', 'iT', 'iH', 'iKL', 'iL', 'iNet'] } def __init__(self): ''' Constructor of the class. ''' # Instantiate parent class super().__init__() # Compute current to concentration conversion constant self.iT_2_CCa = 1e-6 / (self.deff * self.F_Ca) # Define names of the channels state probabilities self.states_names += ['O', 'C', 'P0', 'C_Ca'] # Define the names of the different coefficients to be averaged in a lookup table. self.coeff_names += ['alphao', 'betao'] # Define initial channel probabilities (solving dx/dt = 0 at resting potential) self.states0 = self.steadyStates(self.Vm0) def sinf(self, Vm): ''' Compute the asymptotic value of the open-probability of the S-type, activation gate of Calcium channels. Reference: *Pospischil, M., Toledo-Rodriguez, M., Monier, C., Piwkowska, Z., Bal, T., Frégnac, Y., Markram, H., and Destexhe, A. (2008). Minimal Hodgkin-Huxley type models for different classes of cortical and thalamic neurons. Biol Cybern 99, 427–441.* :param Vm: membrane potential (mV) :return: asymptotic probability (-) ''' return 1.0 / (1.0 + np.exp(-(Vm + self.Vx + 57.0) / 6.2)) # prob def taus(self, Vm): ''' Compute the decay time constant for adaptation of S-type, activation gate of Calcium channels. Reference: *Pospischil, M., Toledo-Rodriguez, M., Monier, C., Piwkowska, Z., Bal, T., Frégnac, Y., Markram, H., and Destexhe, A. (2008). Minimal Hodgkin-Huxley type models for different classes of cortical and thalamic neurons. Biol Cybern 99, 427–441.* :param Vm: membrane potential (mV) :return: decayed time constant (s) ''' tmp = np.exp(-(Vm + self.Vx + 132.0) / 16.7) + np.exp((Vm + self.Vx + 16.8) / 18.2) return 1.0 / 3.7 * (0.612 + 1.0 / tmp) * 1e-3 # s def uinf(self, Vm): ''' Compute the asymptotic value of the open-probability of the U-type, inactivation gate of Calcium channels. Reference: *Pospischil, M., Toledo-Rodriguez, M., Monier, C., Piwkowska, Z., Bal, T., Frégnac, Y., Markram, H., and Destexhe, A. (2008). Minimal Hodgkin-Huxley type models for different classes of cortical and thalamic neurons. Biol Cybern 99, 427–441.* :param Vm: membrane potential (mV) :return: asymptotic probability (-) ''' return 1.0 / (1.0 + np.exp((Vm + self.Vx + 81.0) / 4.0)) # prob def tauu(self, Vm): ''' Compute the decay time constant for adaptation of U-type, inactivation gate of Calcium channels. Reference: *Pospischil, M., Toledo-Rodriguez, M., Monier, C., Piwkowska, Z., Bal, T., Frégnac, Y., Markram, H., and Destexhe, A. (2008). Minimal Hodgkin-Huxley type models for different classes of cortical and thalamic neurons. Biol Cybern 99, 427–441.* :param Vm: membrane potential (mV) :return: decayed time constant (s) ''' if Vm + self.Vx < -80.0: return 1.0 / 3.7 * np.exp((Vm + self.Vx + 467.0) / 66.6) * 1e-3 # s else: return 1 / 3.7 * (np.exp(-(Vm + self.Vx + 22) / 10.5) + 28.0) * 1e-3 # s def derS(self, Vm, s): ''' Compute the evolution of the open-probability of the S-type, activation gate of Calcium channels. :param Vm: membrane potential (mV) :param s: open-probability of S-type Calcium activation gates (prob) :return: derivative of open-probability w.r.t. time (prob/s) ''' return (self.sinf(Vm) - s) / self.taus(Vm) def derU(self, Vm, u): ''' Compute the evolution of the open-probability of the U-type, inactivation gate of Calcium channels. :param Vm: membrane potential (mV) :param u: open-probability of U-type Calcium inactivation gates (prob) :return: derivative of open-probability w.r.t. time (prob/s) ''' return (self.uinf(Vm) - u) / self.tauu(Vm) def oinf(self, Vm): ''' Voltage-dependent steady-state activation of hyperpolarization-activated cation current channels. Reference: *Huguenard, J.R., and McCormick, D.A. (1992). Simulation of the currents involved in rhythmic oscillations in thalamic relay neurons. J. Neurophysiol. 68, 1373–1383.* :param Vm: membrane potential (mV) :return: steady-state activation (-) ''' return 1.0 / (1.0 + np.exp((Vm + 75.0) / 5.5)) def tauo(self, Vm): ''' Time constant for activation of hyperpolarization-activated cation current channels. Reference: *Huguenard, J.R., and McCormick, D.A. (1992). Simulation of the currents involved in rhythmic oscillations in thalamic relay neurons. J. Neurophysiol. 68, 1373–1383.* :param Vm: membrane potential (mV) :return: time constant (s) ''' return 1 / (np.exp(-14.59 - 0.086 * Vm) + np.exp(-1.87 + 0.0701 * Vm)) * 1e-3 def alphao(self, Vm): ''' Transition rate between closed and open form of hyperpolarization-activated cation current channels. :param Vm: membrane potential (mV) :return: transition rate (s-1) ''' return self.oinf(Vm) / self.tauo(Vm) def betao(self, Vm): ''' Transition rate between open and closed form of hyperpolarization-activated cation current channels. :param Vm: membrane potential (mV) :return: transition rate (s-1) ''' return (1 - self.oinf(Vm)) / self.tauo(Vm) def derC(self, C, O, Vm): ''' Compute the evolution of the proportion of hyperpolarization-activated cation current channels in closed state. Kinetics scheme of Calcium dependent activation derived from: *Destexhe, A., Bal, T., McCormick, D.A., and Sejnowski, T.J. (1996). Ionic mechanisms underlying synchronized oscillations and propagating waves in a model of ferret thalamic slices. J. Neurophysiol. 76, 2049–2070.* :param Vm: membrane potential (mV) :param C: proportion of Ih channels in closed state (-) :param O: proportion of Ih channels in open state (-) :return: derivative of proportion w.r.t. time (s-1) ''' return self.betao(Vm) * O - self.alphao(Vm) * C def derO(self, C, O, P0, Vm): ''' Compute the evolution of the proportion of hyperpolarization-activated cation current channels in open state. Kinetics scheme of Calcium dependent activation derived from: *Destexhe, A., Bal, T., McCormick, D.A., and Sejnowski, T.J. (1996). Ionic mechanisms underlying synchronized oscillations and propagating waves in a model of ferret thalamic slices. J. Neurophysiol. 76, 2049–2070.* :param Vm: membrane potential (mV) :param C: proportion of Ih channels in closed state (-) :param O: proportion of Ih channels in open state (-) :param P0: proportion of Ih channels regulating factor in unbound state (-) :return: derivative of proportion w.r.t. time (s-1) ''' return - self.derC(C, O, Vm) - self.k3 * O * (1 - P0) + self.k4 * (1 - O - C) def derP0(self, P0, C_Ca): ''' Compute the evolution of the proportion of Ih channels regulating factor in unbound state. Kinetics scheme of Calcium dependent activation derived from: *Destexhe, A., Bal, T., McCormick, D.A., and Sejnowski, T.J. (1996). Ionic mechanisms underlying synchronized oscillations and propagating waves in a model of ferret thalamic slices. J. Neurophysiol. 76, 2049–2070.* :param Vm: membrane potential (mV) :param P0: proportion of Ih channels regulating factor in unbound state (-) :param C_Ca: Calcium concentration in effective submembranal space (M) :return: derivative of proportion w.r.t. time (s-1) ''' return self.k2 * (1 - P0) - self.k1 * P0 * C_Ca**self.nCa def derC_Ca(self, C_Ca, ICa): ''' Compute the evolution of the Calcium concentration in submembranal space. Model of Ca2+ buffering and contribution from iCa derived from: *McCormick, D.A., and Huguenard, J.R. (1992). A model of the electrophysiological properties of thalamocortical relay neurons. J. Neurophysiol. 68, 1384–1400.* :param Vm: membrane potential (mV) :param C_Ca: Calcium concentration in submembranal space (M) :param ICa: inward Calcium current filling up the submembranal space with Ca2+ (mA/m2) :return: derivative of Calcium concentration in submembranal space w.r.t. time (s-1) ''' return (self.CCa_min - C_Ca) / self.tau_Ca_removal - self.iT_2_CCa * ICa def currKL(self, Vm): ''' Compute the voltage-dependent leak Potassium current per unit area. :param Vm: membrane potential (mV) :return: current per unit area (mA/m2) ''' return self.GKL * (Vm - self.VK) def currH(self, O, C, Vm): ''' Compute the outward mixed cationic current per unit area. :param O: proportion of the channels in open form :param OL: proportion of the channels in locked-open form :param Vm: membrane potential (mV) :return: current per unit area (mA/m2) ''' OL = 1 - O - C return self.GhMax * (O + 2 * OL) * (Vm - self.Vh) def currNet(self, Vm, states): ''' Concrete implementation of the abstract API method. ''' m, h, n, s, u, O, C, _, _ = states return (self.currNa(m, h, Vm) + self.currK(n, Vm) + self.currCa(s, u, Vm) + self.currKL(Vm) + self.currH(O, C, Vm) + self.currL(Vm)) # mA/m2 def steadyStates(self, Vm): ''' Concrete implementation of the abstract API method. ''' # Call parent method to compute Sodium, Potassium and Calcium channels gates steady-states NaKCa_eqstates = super().steadyStates(Vm) # Compute steady-state Calcium current seq = NaKCa_eqstates[3] ueq = NaKCa_eqstates[4] iTeq = self.currCa(seq, ueq, Vm) # Compute steady-state variables for the kinetics system of Ih CCa_eq = self.CCa_min - self.tau_Ca_removal * self.iT_2_CCa * iTeq P0_eq = self.k2 / (self.k2 + self.k1 * CCa_eq**self.nCa) BA = self.betao(Vm) / self.alphao(Vm) O_eq = self.k4 / (self.k3 * (1 - P0_eq) + self.k4 * (1 + BA)) C_eq = BA * O_eq kin_eqstates = np.array([O_eq, C_eq, P0_eq, CCa_eq]) # Merge all steady-states and return return np.concatenate((NaKCa_eqstates, kin_eqstates)) def derStates(self, Vm, states): ''' Concrete implementation of the abstract API method. ''' m, h, n, s, u, O, C, P0, C_Ca = states NaKCa_states = [m, h, n, s, u] NaKCa_derstates = super().derStates(Vm, NaKCa_states) dO_dt = self.derO(C, O, P0, Vm) dC_dt = self.derC(C, O, Vm) dP0_dt = self.derP0(P0, C_Ca) ICa = self.currCa(s, u, Vm) dCCa_dt = self.derC_Ca(C_Ca, ICa) return NaKCa_derstates + [dO_dt, dC_dt, dP0_dt, dCCa_dt] def getEffRates(self, Vm): ''' Concrete implementation of the abstract API method. ''' # Compute effective coefficients for Sodium, Potassium and Calcium conductances NaKCa_effrates = super().getEffRates(Vm) # Compute effective coefficients for Ih conductance ao_avg = np.mean(self.alphao(Vm)) bo_avg = np.mean(self.betao(Vm)) iH_effrates = np.array([ao_avg, bo_avg]) # Return array of coefficients return np.concatenate((NaKCa_effrates, iH_effrates)) def derStatesEff(self, Qm, states, interp_data): ''' Concrete implementation of the abstract API method. ''' rates = np.array([np.interp(Qm, interp_data['Q'], interp_data[rn]) for rn in self.coeff_names]) Vmeff = np.interp(Qm, interp_data['Q'], interp_data['V']) # Unpack states m, h, n, s, u, O, C, P0, C_Ca = states # INa, IK, ICa effective states derivatives dmdt = rates[0] * (1 - m) - rates[1] * m dhdt = rates[2] * (1 - h) - rates[3] * h dndt = rates[4] * (1 - n) - rates[5] * n dsdt = rates[6] * (1 - s) - rates[7] * s dudt = rates[8] * (1 - u) - rates[9] * u # Ih effective states derivatives dC_dt = rates[11] * O - rates[10] * C dO_dt = - dC_dt - self.k3 * O * (1 - P0) + self.k4 * (1 - O - C) dP0_dt = self.derP0(P0, C_Ca) ICa_eff = self.currCa(s, u, Vmeff) dCCa_dt = self.derC_Ca(C_Ca, ICa_eff) # Merge derivatives and return return [dmdt, dhdt, dndt, dsdt, dudt, dO_dt, dC_dt, dP0_dt, dCCa_dt] diff --git a/PointNICE/plt/__init__.py b/PySONIC/plt/__init__.py similarity index 100% rename from PointNICE/plt/__init__.py rename to PySONIC/plt/__init__.py diff --git a/PointNICE/plt/pltutils.py b/PySONIC/plt/pltutils.py similarity index 99% rename from PointNICE/plt/pltutils.py rename to PySONIC/plt/pltutils.py index d00e2da..1d55dee 100644 --- a/PointNICE/plt/pltutils.py +++ b/PySONIC/plt/pltutils.py @@ -1,1235 +1,1235 @@ # -*- coding: utf-8 -*- # @Author: Theo Lemaire # @Date: 2017-08-23 14:55:37 # @Last Modified by: Theo Lemaire -# @Last Modified time: 2018-07-23 14:29:15 +# @Last Modified time: 2018-08-21 16:10:36 ''' Plotting utilities ''' import sys import os import pickle import ntpath import re import logging import tkinter as tk from tkinter import filedialog import numpy as np from scipy.interpolate import interp2d # from scipy.optimize import brentq import matplotlib import matplotlib.pyplot as plt from matplotlib.patches import Rectangle import matplotlib.cm as cm from matplotlib.ticker import FormatStrFormatter import pandas as pd from .. import neurons from ..constants import DT_EFF from ..utils import getNeuronsDict, getLookupDir, rescale, InputError, computeMeshEdges, si_format, itrpLookupsFreq from ..bls import BilayerSonophore from .pltvars import pltvars matplotlib.rcParams['pdf.fonttype'] = 42 matplotlib.rcParams['ps.fonttype'] = 42 matplotlib.rcParams['font.family'] = 'arial' # Get package logger -logger = logging.getLogger('PointNICE') +logger = logging.getLogger('PySONIC') # Define global variables neuron = None bls = None timeunits = {'ASTIM': 't_ms', 'ESTIM': 't_ms', 'MECH': 't_us'} # Regular expression for input files rgxp = re.compile('(ESTIM|ASTIM)_([A-Za-z]*)_(.*).pkl') rgxp_mech = re.compile('(MECH)_(.*).pkl') # nb = '[0-9]*[.]?[0-9]+' # rgxp_ASTIM = re.compile('(ASTIM)_(\w+)_(PW|CW)_({0})nm_({0})kHz_({0})kPa_({0})ms(.*)_(\w+).pkl'.format(nb)) # rgxp_ESTIM = re.compile('(ESTIM)_(\w+)_(PW|CW)_({0})mA_per_m2_({0})ms(.*).pkl'.format(nb)) # rgxp_PW = re.compile('_PRF({0})kHz_DC({0})_(PW|CW)_(\d+)kHz_(\d+)kPa_(\d+)ms_(.*).pkl'.format(nb)) # Figure naming conventions ESTIM_CW_title = '{} neuron: CW E-STIM {:.2f}mA/m2, {:.0f}ms' ESTIM_PW_title = '{} neuron: PW E-STIM {:.2f}mA/m2, {:.0f}ms, {:.2f}Hz PRF, {:.0f}% DC' ASTIM_CW_title = '{} neuron: CW A-STIM {:.0f}kHz, {:.0f}kPa, {:.0f}ms' ASTIM_PW_title = '{} neuron: PW A-STIM {:.0f}kHz, {:.0f}kPa, {:.0f}ms, {:.2f}Hz PRF, {:.2f}% DC' MECH_title = '{:.0f}nm BLS structure: MECH-STIM {:.0f}kHz, {:.0f}kPa' def cm2inch(*tupl): inch = 2.54 if isinstance(tupl[0], tuple): return tuple(i / inch for i in tupl[0]) else: return tuple(i / inch for i in tupl) class InteractiveLegend(object): """ Class defining an interactive matplotlib legend, where lines visibility can be toggled by simply clicking on the corresponding legend label. Other graphic objects can also be associated to the toggle of a specific line Adapted from: http://stackoverflow.com/questions/31410043/hiding-lines-after-showing-a-pyplot-figure """ def __init__(self, legend, aliases): self.legend = legend self.fig = legend.axes.figure self.lookup_artist, self.lookup_handle = self._build_lookups(legend) self._setup_connections() self.handles_aliases = aliases self.update() def _setup_connections(self): for artist in self.legend.texts + self.legend.legendHandles: artist.set_picker(10) # 10 points tolerance self.fig.canvas.mpl_connect('pick_event', self.on_pick) def _build_lookups(self, legend): ''' Method of the InteractiveLegend class building the legend lookups. ''' labels = [t.get_text() for t in legend.texts] handles = legend.legendHandles label2handle = dict(zip(labels, handles)) handle2text = dict(zip(handles, legend.texts)) lookup_artist = {} lookup_handle = {} for artist in legend.axes.get_children(): if artist.get_label() in labels: handle = label2handle[artist.get_label()] lookup_handle[artist] = handle lookup_artist[handle] = artist lookup_artist[handle2text[handle]] = artist lookup_handle.update(zip(handles, handles)) lookup_handle.update(zip(legend.texts, handles)) return lookup_artist, lookup_handle def on_pick(self, event): handle = event.artist if handle in self.lookup_artist: artist = self.lookup_artist[handle] artist.set_visible(not artist.get_visible()) self.update() def update(self): for artist in self.lookup_artist.values(): handle = self.lookup_handle[artist] if artist.get_visible(): handle.set_visible(True) if artist in self.handles_aliases: for al in self.handles_aliases[artist]: al.set_visible(True) else: handle.set_visible(False) if artist in self.handles_aliases: for al in self.handles_aliases[artist]: al.set_visible(False) self.fig.canvas.draw() def show(self): ''' showing the interactive legend ''' plt.show() def getPatchesLoc(t, states): ''' Determine the location of stimulus patches. :param t: simulation time vector (s). :param states: a vector of stimulation state (ON/OFF) at each instant in time. :return: 3-tuple with number of patches, timing of STIM-ON an STIM-OFF instants. ''' # Compute states derivatives and identify bounds indexes of pulses dstates = np.diff(states) ipatch_on = np.insert(np.where(dstates > 0.0)[0] + 1, 0, 0) ipatch_off = np.where(dstates < 0.0)[0] if ipatch_off.size < ipatch_on.size: ioff = t.size - 1 if ipatch_off.size == 0: ipatch_off = np.array([ioff]) else: ipatch_off = np.insert(ipatch_off, ipatch_off.size - 1, ioff) # Get time instants for pulses ON and OFF npatches = ipatch_on.size tpatch_on = t[ipatch_on] tpatch_off = t[ipatch_off] # return 3-tuple with #patches, pulse ON and pulse OFF instants return (npatches, tpatch_on, tpatch_off) def SaveFigDialog(dirname, filename): """ Open a FileSaveDialogBox to set the directory and name of the figure to be saved. The default directory and filename are given, and the default extension is ".pdf" :param dirname: default directory :param filename: default filename :return: full path to the chosen filename """ root = tk.Tk() root.withdraw() filename_out = filedialog.asksaveasfilename(defaultextension=".pdf", initialdir=dirname, initialfile=filename) return filename_out def plotComp(varname, filepaths, labels=None, fs=15, lw=2, colors=None, lines=None, patches='one', xticks=None, yticks=None, blacklegend=False, straightlegend=False, showfig=True, inset=None, figsize=(11, 4)): ''' Compare profiles of several specific output variables of NICE simulations. :param varname: name of variable to extract and compare :param filepaths: list of full paths to output data files to be compared :param labels: list of labels to use in the legend :param fs: labels fontsize :param patches: string indicating whether to indicate periods of stimulation with colored rectangular patches ''' # Input check 1: variable name if varname not in pltvars: raise InputError('Unknown plot variable: "{}"'.format(varname)) pltvar = pltvars[varname] # Input check 2: labels if labels is not None: if len(labels) != len(filepaths): raise InputError('Invalid labels ({}): not matching number of compared files ({})' .format(len(labels), len(filepaths))) if not all(isinstance(x, str) for x in labels): raise InputError('Invalid labels: must be string typed') # Input check 3: line styles and colors if colors is None: colors = ['C{}'.format(j) for j in range(len(filepaths))] if lines is None: lines = ['-'] * len(filepaths) # Input check 4: STIM-ON patches greypatch = False if patches == 'none': patches = [False] * len(filepaths) elif patches == 'all': patches = [True] * len(filepaths) elif patches == 'one': patches = [True] + [False] * (len(filepaths) - 1) greypatch = True elif isinstance(patches, list): if len(patches) != len(filepaths): raise InputError('Invalid patches ({}): not matching number of compared files ({})' .format(len(patches), len(filepaths))) if not all(isinstance(p, bool) for p in patches): raise InputError('Invalid patch sequence: all list items must be boolean typed') else: raise InputError('Invalid patches: must be either "none", all", "one", or a boolean list') # Initialize figure and axis fig, ax = plt.subplots(figsize=figsize) ax.set_zorder(0) for item in ['top', 'right']: ax.spines[item].set_visible(False) if 'min' in pltvar and 'max' in pltvar: # optional min and max on y-axis ax.set_ylim(pltvar['min'], pltvar['max']) if pltvar['unit']: # y-label with optional unit ax.set_ylabel('$\\rm {}\ ({})$'.format(pltvar['label'], pltvar['unit']), fontsize=fs) else: ax.set_ylabel('$\\rm {}$'.format(pltvar['label']), fontsize=fs) if xticks is not None: # optional x-ticks ax.set_xticks(xticks) if yticks is not None: # optional y-ticks ax.set_yticks(yticks) else: ax.locator_params(axis='y', nbins=2) if any(ax.get_yticks() < 0): ax.yaxis.set_major_formatter(FormatStrFormatter('%+.0f')) for tick in ax.xaxis.get_major_ticks() + ax.yaxis.get_major_ticks(): tick.label.set_fontsize(fs) # Optional inset axis if inset is not None: inset_ax = fig.add_axes(ax.get_position()) inset_ax.set_xlim(inset['xlims'][0], inset['xlims'][1]) inset_ax.set_ylim(inset['ylims'][0], inset['ylims'][1]) inset_ax.set_xticks([]) inset_ax.set_yticks([]) # inset_ax.patch.set_alpha(1.0) inset_ax.set_zorder(1) inset_ax.add_patch(Rectangle((inset['xlims'][0], inset['ylims'][0]), inset['xlims'][1] - inset['xlims'][0], inset['ylims'][1] - inset['ylims'][0], color='w')) # Retrieve neurons dictionary neurons_dict = getNeuronsDict() # Loop through data files aliases = {} for j, filepath in enumerate(filepaths): # Retrieve sim type pkl_filename = ntpath.basename(filepath) mo1 = rgxp.fullmatch(pkl_filename) mo2 = rgxp_mech.fullmatch(pkl_filename) if mo1: mo = mo1 elif mo2: mo = mo2 else: logger.error('Error: "%s" file does not match regexp pattern', pkl_filename) sys.exit(1) sim_type = mo.group(1) if sim_type not in ('MECH', 'ASTIM', 'ESTIM'): raise InputError('Invalid simulation type: {}'.format(sim_type)) if j == 0: sim_type_ref = sim_type t_plt = pltvars[timeunits[sim_type]] elif sim_type != sim_type_ref: raise InputError('Invalid comparison: different simulation types') # Load data logger.info('Loading data from "%s"', pkl_filename) with open(filepath, 'rb') as fh: frame = pickle.load(fh) df = frame['data'] meta = frame['meta'] # Extract variables t = df['t'].values states = df['states'].values nsamples = t.size # Initialize neuron object if ESTIM or ASTIM sim type if sim_type in ['ASTIM', 'ESTIM']: neuron_name = mo.group(2) global neuron neuron = neurons_dict[neuron_name]() Cm0 = neuron.Cm0 Qm0 = Cm0 * neuron.Vm0 * 1e-3 # Extract neuron states if needed if 'alias' in pltvar and 'neuron_states' in pltvar['alias']: neuron_states = [df[sn].values for sn in neuron.states_names] else: Cm0 = meta['Cm0'] Qm0 = meta['Qm0'] # Initialize BLS if needed if sim_type in ['MECH', 'ASTIM'] and 'alias' in pltvar and 'bls' in pltvar['alias']: global bls bls = BilayerSonophore(meta['a'], meta['Fdrive'], Cm0, Qm0) # Determine patches location npatches, tpatch_on, tpatch_off = getPatchesLoc(t, states) # Add onset to time vectors if t_plt['onset'] > 0.0: tonset = np.array([-t_plt['onset'], -t[0] - t[1]]) t = np.hstack((tonset, t)) states = np.hstack((states, np.zeros(2))) # Set x-axis label ax.set_xlabel('$\\rm {}\ ({})$'.format(t_plt['label'], t_plt['unit']), fontsize=fs) # Extract variable to plot if 'alias' in pltvar: var = eval(pltvar['alias']) elif 'key' in pltvar: var = df[pltvar['key']].values elif 'constant' in pltvar: var = eval(pltvar['constant']) * np.ones(nsamples) else: var = df[varname].values if var.size == t.size - 2: if varname is 'Vm': var = np.hstack((np.array([neuron.Vm0] * 2), var)) else: var = np.hstack((np.array([var[0]] * 2), var)) # var = np.insert(var, 0, var[0]) # Determine legend label if labels is not None: label = labels[j] else: if sim_type == 'ESTIM': if meta['DC'] == 1.0: label = ESTIM_CW_title.format(neuron_name, meta['Astim'], meta['tstim'] * 1e3) else: label = ESTIM_PW_title.format(neuron_name, meta['Astim'], meta['tstim'] * 1e3, meta['PRF'], meta['DC'] * 1e2) elif sim_type == 'ASTIM': if meta['DC'] == 1.0: label = ASTIM_CW_title.format(neuron_name, meta['Fdrive'] * 1e-3, meta['Adrive'] * 1e-3, meta['tstim'] * 1e3) else: label = ASTIM_PW_title.format(neuron_name, meta['Fdrive'] * 1e-3, meta['Adrive'] * 1e-3, meta['tstim'] * 1e3, meta['PRF'], meta['DC'] * 1e2) elif sim_type == 'MECH': label = MECH_title.format(meta['a'] * 1e9, meta['Fdrive'] * 1e-3, meta['Adrive'] * 1e-3) # Plot trace handle = ax.plot(t * t_plt['factor'], var * pltvar['factor'], linewidth=lw, linestyle=lines[j], color=colors[j], label=label) if inset is not None: inset_window = np.logical_and(t > (inset['xlims'][0] / t_plt['factor']), t < (inset['xlims'][1] / t_plt['factor'])) inset_ax.plot(t[inset_window] * t_plt['factor'], var[inset_window] * pltvar['factor'], linewidth=lw, linestyle=lines[j], color=colors[j]) # Add optional STIM-ON patches if patches[j]: (ybottom, ytop) = ax.get_ylim() la = [] color = '#8A8A8A' if greypatch else handle[0].get_color() for i in range(npatches): la.append(ax.axvspan(tpatch_on[i] * t_plt['factor'], tpatch_off[i] * t_plt['factor'], edgecolor='none', facecolor=color, alpha=0.2)) aliases[handle[0]] = la if inset is not None: cond_on = np.logical_and(tpatch_on > (inset['xlims'][0] / t_plt['factor']), tpatch_on < (inset['xlims'][1] / t_plt['factor'])) cond_off = np.logical_and(tpatch_off > (inset['xlims'][0] / t_plt['factor']), tpatch_off < (inset['xlims'][1] / t_plt['factor'])) cond_glob = np.logical_and(tpatch_on < (inset['xlims'][0] / t_plt['factor']), tpatch_off > (inset['xlims'][1] / t_plt['factor'])) cond_onoff = np.logical_or(cond_on, cond_off) cond = np.logical_or(cond_onoff, cond_glob) npatches_inset = np.sum(cond) for i in range(npatches_inset): inset_ax.add_patch(Rectangle((tpatch_on[cond][i] * t_plt['factor'], ybottom), (tpatch_off[cond][i] - tpatch_on[cond][i]) * t_plt['factor'], ytop - ybottom, color=color, alpha=0.1)) fig.tight_layout() # Optional operations on inset: if inset is not None: # Re-position inset axis axpos = ax.get_position() left, right, = rescale(inset['xcoords'], ax.get_xlim()[0], ax.get_xlim()[1], axpos.x0, axpos.x0 + axpos.width) bottom, top, = rescale(inset['ycoords'], ax.get_ylim()[0], ax.get_ylim()[1], axpos.y0, axpos.y0 + axpos.height) inset_ax.set_position([left, bottom, right - left, top - bottom]) for i in inset_ax.spines.values(): i.set_linewidth(2) # Materialize inset target region with contour frame ax.plot(inset['xlims'], [inset['ylims'][0]] * 2, linestyle='-', color='k') ax.plot(inset['xlims'], [inset['ylims'][1]] * 2, linestyle='-', color='k') ax.plot([inset['xlims'][0]] * 2, inset['ylims'], linestyle='-', color='k') ax.plot([inset['xlims'][1]] * 2, inset['ylims'], linestyle='-', color='k') # Link target and inset with dashed lines if possible if inset['xcoords'][1] < inset['xlims'][0]: ax.plot([inset['xcoords'][1], inset['xlims'][0]], [inset['ycoords'][0], inset['ylims'][0]], linestyle='--', color='k') ax.plot([inset['xcoords'][1], inset['xlims'][0]], [inset['ycoords'][1], inset['ylims'][1]], linestyle='--', color='k') elif inset['xcoords'][0] > inset['xlims'][1]: ax.plot([inset['xcoords'][0], inset['xlims'][1]], [inset['ycoords'][0], inset['ylims'][0]], linestyle='--', color='k') ax.plot([inset['xcoords'][0], inset['xlims'][1]], [inset['ycoords'][1], inset['ylims'][1]], linestyle='--', color='k') else: logger.warning('Inset x-coordinates intersect with those of target region') # Create interactive legend leg = ax.legend(loc=1, fontsize=fs, frameon=False) if blacklegend: for l in leg.get_lines(): l.set_color('k') if straightlegend: for l in leg.get_lines(): l.set_linestyle('-') interactive_legend = InteractiveLegend(ax.legend_, aliases) if showfig: plt.show() return fig def plotBatch(directory, filepaths, vars_dict=None, plt_show=True, plt_save=False, ask_before_save=True, fig_ext='png', tag='fig', fs=15, lw=2, title=True, show_patches=True): ''' Plot a figure with profiles of several specific NICE output variables, for several NICE simulations. :param positions: subplot indexes of each variable :param filepaths: list of full paths to output data files to be compared :param vars_dict: dict of lists of variables names to extract and plot together :param plt_show: boolean stating whether to show the created figures :param plt_save: boolean stating whether to save the created figures :param ask_before_save: boolean stating whether to show the created figures :param fig_ext: file extension for the saved figures :param tag: suffix added to the end of the figures name :param fs: labels font size :param lw: curves line width :param title: boolean stating whether to display a general title on the figures :param show_patches: boolean indicating whether to indicate periods of stimulation with colored rectangular patches ''' # Check validity of plot variables if vars_dict: yvars = list(sum(list(vars_dict.values()), [])) for key in yvars: if key not in pltvars: raise InputError('Unknown plot variable: "{}"'.format(key)) # Dictionary of neurons neurons_dict = getNeuronsDict() # Loop through data files for filepath in filepaths: # Get code from file name pkl_filename = ntpath.basename(filepath) filecode = pkl_filename[0:-4] # Retrieve sim type mo1 = rgxp.fullmatch(pkl_filename) mo2 = rgxp_mech.fullmatch(pkl_filename) if mo1: mo = mo1 elif mo2: mo = mo2 else: logger.error('Error: "%s" file does not match regexp pattern', pkl_filename) sys.exit(1) sim_type = mo.group(1) if sim_type not in ('MECH', 'ASTIM', 'ESTIM'): raise InputError('Invalid simulation type: {}'.format(sim_type)) # Load data logger.info('Loading data from "%s"', pkl_filename) with open(filepath, 'rb') as fh: frame = pickle.load(fh) df = frame['data'] meta = frame['meta'] # Extract variables logger.info('Extracting variables') t = df['t'].values states = df['states'].values nsamples = t.size # Initialize channel mechanism if sim_type in ['ASTIM', 'ESTIM']: neuron_name = mo.group(2) global neuron neuron = neurons_dict[neuron_name]() neuron_states = [df[sn].values for sn in neuron.states_names] Cm0 = neuron.Cm0 Qm0 = Cm0 * neuron.Vm0 * 1e-3 t_plt = pltvars['t_ms'] else: Cm0 = meta['Cm0'] Qm0 = meta['Qm0'] t_plt = pltvars['t_us'] # Initialize BLS if sim_type in ['MECH', 'ASTIM']: global bls Fdrive = meta['Fdrive'] a = meta['a'] bls = BilayerSonophore(a, Fdrive, Cm0, Qm0) # Determine patches location npatches, tpatch_on, tpatch_off = getPatchesLoc(t, states) # Adding onset to time vector if t_plt['onset'] > 0.0: tonset = np.array([-t_plt['onset'], -t[0] - t[1]]) t = np.hstack((tonset, t)) states = np.hstack((states, np.zeros(2))) # Determine variables to plot if not provided if not vars_dict: if sim_type == 'ASTIM': vars_dict = {'Z': ['Z'], 'Q_m': ['Qm']} elif sim_type == 'ESTIM': vars_dict = {'V_m': ['Vm']} elif sim_type == 'MECH': vars_dict = {'P_{AC}': ['Pac'], 'Z': ['Z'], 'n_g': ['ng']} if sim_type in ['ASTIM', 'ESTIM'] and hasattr(neuron, 'pltvars_scheme'): vars_dict.update(neuron.pltvars_scheme) labels = list(vars_dict.keys()) naxes = len(vars_dict) # Plotting if naxes == 1: _, ax = plt.subplots(figsize=(11, 4)) axes = [ax] else: _, axes = plt.subplots(naxes, 1, figsize=(11, min(3 * naxes, 9))) for i in range(naxes): ax = axes[i] for item in ['top', 'right']: ax.spines[item].set_visible(False) ax_pltvars = [pltvars[j] for j in vars_dict[labels[i]]] nvars = len(ax_pltvars) # X-axis if i < naxes - 1: ax.get_xaxis().set_ticklabels([]) else: ax.set_xlabel('${}\ ({})$'.format(t_plt['label'], t_plt['unit']), fontsize=fs) for tick in ax.xaxis.get_major_ticks(): tick.label.set_fontsize(fs) # Y-axis if ax_pltvars[0]['unit']: ax.set_ylabel('${}\ ({})$'.format(labels[i], ax_pltvars[0]['unit']), fontsize=fs) else: ax.set_ylabel('${}$'.format(labels[i]), fontsize=fs) if 'min' in ax_pltvars[0] and 'max' in ax_pltvars[0]: ax_min = min([ap['min'] for ap in ax_pltvars]) ax_max = max([ap['max'] for ap in ax_pltvars]) ax.set_ylim(ax_min, ax_max) ax.locator_params(axis='y', nbins=2) for tick in ax.yaxis.get_major_ticks(): tick.label.set_fontsize(fs) # Time series icolor = 0 for j in range(nvars): # Extract variable pltvar = ax_pltvars[j] if 'alias' in pltvar: var = eval(pltvar['alias']) elif 'key' in pltvar: var = df[pltvar['key']].values elif 'constant' in pltvar: var = eval(pltvar['constant']) * np.ones(nsamples) else: var = df[vars_dict[labels[i]][j]].values if var.size == t.size - 2: if pltvar['desc'] == 'membrane potential': var = np.hstack((np.array([neuron.Vm0] * 2), var)) else: var = np.hstack((np.array([var[0]] * 2), var)) # var = np.insert(var, 0, var[0]) # Plot variable if 'constant' in pltvar or pltvar['desc'] in ['net current']: ax.plot(t * t_plt['factor'], var * pltvar['factor'], '--', c='black', lw=lw, label='${}$'.format(pltvar['label'])) else: ax.plot(t * t_plt['factor'], var * pltvar['factor'], c='C{}'.format(icolor), lw=lw, label='${}$'.format(pltvar['label'])) icolor += 1 # Patches if show_patches == 1: (ybottom, ytop) = ax.get_ylim() for j in range(npatches): ax.axvspan(tpatch_on[j] * t_plt['factor'], tpatch_off[j] * t_plt['factor'], edgecolor='none', facecolor='#8A8A8A', alpha=0.2) # Legend if nvars > 1: ax.legend(fontsize=fs, loc=7, ncol=nvars // 4 + 1) # Title if title: if sim_type == 'ESTIM': if meta['DC'] == 1.0: fig_title = ESTIM_CW_title.format(neuron.name, meta['Astim'], meta['tstim'] * 1e3) else: fig_title = ESTIM_PW_title.format(neuron.name, meta['Astim'], meta['tstim'] * 1e3, meta['PRF'], meta['DC'] * 1e2) elif sim_type == 'ASTIM': if meta['DC'] == 1.0: fig_title = ASTIM_CW_title.format(neuron.name, Fdrive * 1e-3, meta['Adrive'] * 1e-3, meta['tstim'] * 1e3) else: fig_title = ASTIM_PW_title.format(neuron.name, Fdrive * 1e-3, meta['Adrive'] * 1e-3, meta['tstim'] * 1e3, meta['PRF'], meta['DC'] * 1e2) elif sim_type == 'MECH': fig_title = MECH_title.format(a * 1e9, Fdrive * 1e-3, meta['Adrive'] * 1e-3) axes[0].set_title(fig_title, fontsize=fs) plt.tight_layout() # Save figure if needed (automatic or checked) if plt_save: if ask_before_save: plt_filename = SaveFigDialog(directory, '{}_{}.{}'.format(filecode, tag, fig_ext)) else: plt_filename = '{}/{}_{}.{}'.format(directory, filecode, tag, fig_ext) if plt_filename: plt.savefig(plt_filename) logger.info('Saving figure as "{}"'.format(plt_filename)) plt.close() # Show all plots if needed if plt_show: plt.show() def plotGatingKinetics(neuron, fs=15): ''' Plot the voltage-dependent steady-states and time constants of activation and inactivation gates of the different ionic currents involved in a specific neuron's membrane. :param neuron: specific channel mechanism object :param fs: labels and title font size ''' # Input membrane potential vector Vm = np.linspace(-100, 50, 300) xinf_dict = {} taux_dict = {} logger.info('Computing %s neuron gating kinetics', neuron.name) names = neuron.states_names print(names) for xname in names: Vm_state = True # Names of functions of interest xinf_func_str = xname.lower() + 'inf' taux_func_str = 'tau' + xname.lower() alphax_func_str = 'alpha' + xname.lower() betax_func_str = 'beta' + xname.lower() # derx_func_str = 'der' + xname.upper() # 1st choice: use xinf and taux function if hasattr(neuron, xinf_func_str) and hasattr(neuron, taux_func_str): xinf_func = getattr(neuron, xinf_func_str) taux_func = getattr(neuron, taux_func_str) xinf = np.array([xinf_func(v) for v in Vm]) if isinstance(taux_func, float): taux = taux_func * np.ones(len(Vm)) else: taux = np.array([taux_func(v) for v in Vm]) # 2nd choice: use alphax and betax functions elif hasattr(neuron, alphax_func_str) and hasattr(neuron, betax_func_str): alphax_func = getattr(neuron, alphax_func_str) betax_func = getattr(neuron, betax_func_str) alphax = np.array([alphax_func(v) for v in Vm]) if isinstance(betax_func, float): betax = betax_func * np.ones(len(Vm)) else: betax = np.array([betax_func(v) for v in Vm]) taux = 1.0 / (alphax + betax) xinf = taux * alphax # # 3rd choice: use derX choice # elif hasattr(neuron, derx_func_str): # derx_func = getattr(neuron, derx_func_str) # xinf = brentq(lambda x: derx_func(neuron.Vm, x), 0, 1) else: Vm_state = False if not Vm_state: logger.error('no function to compute %s-state gating kinetics', xname) else: xinf_dict[xname] = xinf taux_dict[xname] = taux fig, axes = plt.subplots(2) fig.suptitle('{} neuron: gating dynamics'.format(neuron.name)) ax = axes[0] ax.get_xaxis().set_ticklabels([]) ax.set_ylabel('$X_{\infty}$', fontsize=fs) for xname in names: if xname in xinf_dict: ax.plot(Vm, xinf_dict[xname], lw=2, label='$' + xname + '_{\infty}$') ax.legend(fontsize=fs, loc=7) ax = axes[1] ax.set_xlabel('$V_m\ (mV)$', fontsize=fs) ax.set_ylabel('$\\tau_X\ (ms)$', fontsize=fs) for xname in names: if xname in taux_dict: ax.plot(Vm, taux_dict[xname] * 1e3, lw=2, label='$\\tau_{' + xname + '}$') ax.legend(fontsize=fs, loc=7) plt.show() def plotRateConstants(neuron, fs=15): ''' Plot the voltage-dependent activation and inactivation rate constants for each gate of all ionic currents involved in a specific neuron's membrane. :param neuron: specific channel mechanism object :param fs: labels and title font size ''' # Input membrane potential vector Vm = np.linspace(neuron.Vm0 - 10, 50, 100) alphax_dict = {} betax_dict = {} logger.info('Computing %s neuron gating kinetics', neuron.name) names = neuron.states_names for xname in names: Vm_state = True # Names of functions of interest xinf_func_str = xname.lower() + 'inf' taux_func_str = 'tau' + xname.lower() alphax_func_str = 'alpha' + xname.lower() betax_func_str = 'beta' + xname.lower() # 1st choice: use alphax and betax functions if hasattr(neuron, alphax_func_str) and hasattr(neuron, betax_func_str): alphax_func = getattr(neuron, alphax_func_str) betax_func = getattr(neuron, betax_func_str) alphax = np.array([alphax_func(v) for v in Vm]) betax = np.array([betax_func(v) for v in Vm]) # 2nd choice: use xinf and taux function elif hasattr(neuron, xinf_func_str) and hasattr(neuron, taux_func_str): xinf_func = getattr(neuron, xinf_func_str) taux_func = getattr(neuron, taux_func_str) xinf = np.array([xinf_func(v) for v in Vm]) taux = np.array([taux_func(v) for v in Vm]) alphax = xinf / taux betax = 1.0 / taux - alphax else: Vm_state = False if not Vm_state: logger.error('no function to compute %s-state gating kinetics', xname) else: alphax_dict[xname] = alphax betax_dict[xname] = betax naxes = len(alphax_dict) _, axes = plt.subplots(naxes, figsize=(11, min(3 * naxes, 9))) for i, xname in enumerate(alphax_dict.keys()): ax1 = axes[i] if i == 0: ax1.set_title('{} neuron: rate constants'.format(neuron.name)) if i == naxes - 1: ax1.set_xlabel('$V_m\ (mV)$', fontsize=fs) else: ax1.get_xaxis().set_ticklabels([]) ax1.set_ylabel('$\\alpha_{' + xname + '}\ (ms^{-1})$', fontsize=fs, color='C0') for label in ax1.get_yticklabels(): label.set_color('C0') ax1.plot(Vm, alphax_dict[xname] * 1e-3, lw=2) ax2 = ax1.twinx() ax2.set_ylabel('$\\beta_{' + xname + '}\ (ms^{-1})$', fontsize=fs, color='C1') for label in ax2.get_yticklabels(): label.set_color('C1') ax2.plot(Vm, betax_dict[xname] * 1e-3, lw=2, color='C1') plt.tight_layout() plt.show() def setGrid(n, ncolmax=3): ''' Determine number of rows and columns in figure grid, based on number of variables to plot. ''' if n <= ncolmax: return (1, n) else: return ((n - 1) // ncolmax + 1, ncolmax) def plotEffVars(neuron, Fdrive, a=32e-9, amps=None, charges=None, keys=None, fs=12, ncolmax=2): ''' Plot the profiles of effective variables of a specific neuron for a given frequency. For each variable, one line chart per amplitude is plotted, using charge as the input variable on the abscissa and a linear color code for the amplitude value. :param neuron: channel mechanism object :param Fdrive: acoustic drive frequency (Hz) :param a: sonophore diameter (m) :param amps: vector of amplitudes at which variables must be plotted (Pa) :param charges: vector of charges at which variables must be plotted (C/m2) :param keys: list of variables to plot :param fs: figure fontsize :param ncolmax: max number of columns on the figure :return: handle to the created figure ''' # Check lookup file existence lookup_file = '{}_lookups_a{:.1f}nm.pkl'.format(neuron.name, a * 1e9) lookup_path = '{}/{}'.format(getLookupDir(), lookup_file) if not os.path.isfile(lookup_path): raise InputError('Missing lookup file: "{}"'.format(lookup_file)) # Load coefficients with open(lookup_path, 'rb') as fh: lookups3D = pickle.load(fh) # Retrieve 1D inputs from lookup dictionary freqs = lookups3D.pop('f') amps_ref = lookups3D.pop('A') charges_ref = lookups3D.pop('Q') # Filter lookups keys if provided if keys is not None: lookups3D = {key: lookups3D[key] for key in keys} # Interpolate 3D lookups at US frequency lookups2D = itrpLookupsFreq(lookups3D, freqs, Fdrive) if 'V' in lookups2D: lookups2D['Vm'] = lookups2D.pop('V') keys[keys.index('V')] = 'Vm' # Define log-amplitude color code if amps is None: amps = amps_ref mymap = cm.get_cmap('Oranges') norm = matplotlib.colors.LogNorm(amps.min(), amps.max()) sm = cm.ScalarMappable(norm=norm, cmap=mymap) sm._A = [] # Plot logger.info('plotting') nrows, ncols = setGrid(len(lookups2D), ncolmax=ncolmax) xvar = pltvars['Qm'] if charges is None: charges = charges_ref Qbounds = np.array([charges.min(), charges.max()]) * xvar['factor'] fig, _ = plt.subplots(figsize=(3 * ncols, 1 * nrows), squeeze=False) for j, key in enumerate(keys): ax = plt.subplot2grid((nrows, ncols), (j // ncols, j % ncols)) for s in ['right', 'top']: ax.spines[s].set_visible(False) yvar = pltvars[key] if j // ncols == nrows - 1: ax.set_xlabel('$\\rm {}\ ({})$'.format(xvar['label'], xvar['unit']), fontsize=fs) ax.set_xticks(Qbounds) else: ax.set_xticks([]) ax.spines['bottom'].set_visible(False) ax.xaxis.set_label_coords(0.5, -0.1) ax.yaxis.set_label_coords(-0.02, 0.5) for item in ax.get_xticklabels() + ax.get_yticklabels(): item.set_fontsize(fs) ymin = np.inf ymax = -np.inf y0 = np.squeeze(interp2d(amps_ref, charges_ref, lookups2D[key].T)(0, charges)) # Plot effective variable for each selected amplitude for Adrive in amps: y = np.squeeze(interp2d(amps_ref, charges_ref, lookups2D[key].T)(Adrive, charges)) if 'alpha' in key or 'beta' in key: y[y > y0.max() * 2] = np.nan ax.plot(charges * xvar['factor'], y * yvar['factor'], c=sm.to_rgba(Adrive)) ymin = min(ymin, y.min()) ymax = max(ymax, y.max()) # Plot reference variable ax.plot(charges * xvar['factor'], y0 * yvar['factor'], '--', c='k') ymax = max(ymax, y0.max()) ymin = min(ymin, y0.min()) # Set axis y-limits if 'alpha' in key or 'beta' in key: ymax = y0.max() * 2 ylim = [ymin * yvar['factor'], ymax * yvar['factor']] if key == 'ng': ylim = [np.floor(ylim[0] * 1e2) / 1e2, np.ceil(ylim[1] * 1e2) / 1e2] else: factor = 1 / np.power(10, np.floor(np.log10(ylim[1]))) print(key, ylim[1], factor) ylim = [np.floor(ylim[0] * factor) / factor, np.ceil(ylim[1] * factor) / factor] dy = ylim[1] - ylim[0] ax.set_yticks(ylim) ax.set_ylim(ylim) # ax.set_ylim([ylim[0] - 0.05 * dy, ylim[1] + 0.05 * dy]) # Annotate variable and unit xlim = ax.get_xlim() if np.argmax(y0) < np.argmin(y0): xtext = xlim[0] + 0.6 * (xlim[1] - xlim[0]) else: xtext = xlim[0] + 0.01 * (xlim[1] - xlim[0]) if key in ['Vm', 'ng']: ytext = ylim[0] + 0.85 * dy else: ytext = ylim[0] + 0.15 * dy ax.text(xtext, ytext, '$\\rm {}\ ({})$'.format(yvar['label'], yvar['unit']), fontsize=fs) fig.suptitle('{} neuron: original vs. effective variables @ {:.0f} kHz'.format( neuron.name, Fdrive * 1e-3)) # Plot colorbar fig.subplots_adjust(left=0.10, bottom=0.05, top=0.9, right=0.85) cbarax = fig.add_axes([0.87, 0.05, 0.04, 0.85]) fig.colorbar(sm, cax=cbarax) cbarax.set_ylabel('amplitude (Pa)', fontsize=fs) for item in cbarax.get_yticklabels(): item.set_fontsize(fs) return fig def plotActivationMap(DCs, amps, actmap, FRlims, title=None, Ascale='log', FRscale='log', fs=8): ''' Plot a neuron's activation map over the amplitude x duty cycle 2D space. :param DCs: duty cycle vector :param amps: amplitude vector :param actmap: 2D activation matrix :param FRlims: lower and upper bounds of firing rate color-scale :param title: figure title :param Ascale: scale to use for the amplitude dimension ('lin' or 'log') :param FRscale: scale to use for the firing rate coloring ('lin' or 'log') :param fs: fontsize to use for the title and labels :return: 3-tuple with the handle to the generated figure and the mesh x and y coordinates ''' # Check firing rate bounding minFR, maxFR = (actmap[actmap > 0].min(), actmap.max()) logger.info('FR range: %.0f - %.0f Hz', minFR, maxFR) if minFR < FRlims[0]: logger.warning('Minimal firing rate (%.0f Hz) is below defined lower bound (%.0f Hz)', minFR, FRlims[0]) if maxFR > FRlims[1]: logger.warning('Maximal firing rate (%.0f Hz) is above defined upper bound (%.0f Hz)', maxFR, FRlims[1]) # Plot activation map if FRscale == 'lin': norm = matplotlib.colors.Normalize(*FRlims) elif FRscale == 'log': norm = matplotlib.colors.LogNorm(*FRlims) fig, ax = plt.subplots(figsize=cm2inch(8, 5.8)) fig.subplots_adjust(left=0.15, bottom=0.15, right=0.8, top=0.92) if title is not None: ax.set_title(title, fontsize=fs) if Ascale == 'log': ax.set_yscale('log') ax.set_xlabel('Duty cycle (%)', fontsize=fs, labelpad=-0.5) ax.set_ylabel('Amplitude (kPa)', fontsize=fs) ax.set_xlim(np.array([DCs.min(), DCs.max()]) * 1e2) for item in ax.get_xticklabels() + ax.get_yticklabels(): item.set_fontsize(fs) xedges = computeMeshEdges(DCs) yedges = computeMeshEdges(amps, scale='log') actmap[actmap == -1] = np.nan actmap[actmap == 0] = 1e-3 cmap = plt.get_cmap('viridis') cmap.set_bad('silver') cmap.set_under('k') ax.pcolormesh(xedges * 1e2, yedges * 1e-3, actmap, cmap=cmap, norm=norm) # Plot firing rate colorbar sm = plt.cm.ScalarMappable(cmap=cmap, norm=norm) sm._A = [] pos1 = ax.get_position() # get the map axis position cbarax = fig.add_axes([pos1.x1 + 0.02, pos1.y0, 0.03, pos1.height]) fig.colorbar(sm, cax=cbarax) cbarax.set_ylabel('Firing rate (Hz)', fontsize=fs) for item in cbarax.get_yticklabels(): item.set_fontsize(fs) return (fig, xedges, yedges) def plotDualMaxMap(DCs, amps, maxmap, factor, actmap, title, lbl='xy', Ascale='log', fs=8): ''' Plot a variable maximum map over the amplitude x duty cycle 2D space, with different color codes for the sub and supra-threshold regions. :param DCs: duty cycle vector :param amps: amplitude vector :param maxmap: 2D variable maximum matrix :param factor: unit factor to use for the colorbars labels :param actmap: 2D activation matrix :param title: figure title :param lbl: indicates whether to label the x and y axes :param Ascale: scale to use for the amplitude dimension ('lin' or 'log') :param fs: fontsize to use for the title and labels :return: a handle to the generated figure ''' # Split variable max map into sub-threshold and supra-threshold max maps maxmap_sub, maxmap_supra = maxmap.copy(), maxmap.copy() maxmap_sub[actmap >= 0] = np.nan maxmap_supra[actmap < 0] = np.nan # Plot dual max map fig, ax = plt.subplots(figsize=cm2inch(8, 5.8)) fig.subplots_adjust(left=0.15, bottom=0.15, right=0.8, top=0.92) ax.set_title(title, fontsize=fs) if Ascale == 'log': ax.set_yscale('log') if 'x' in lbl: ax.set_xlabel('Duty cycle (%)', fontsize=fs) else: ax.set_xticklabels([]) if 'y' in lbl: ax.set_ylabel('Amplitude (kPa)', fontsize=fs) else: ax.set_yticklabels([]) for item in ax.get_xticklabels() + ax.get_yticklabels(): item.set_fontsize(fs) xedges = computeMeshEdges(DCs) yedges = computeMeshEdges(amps, scale=Ascale) # Plot the 2 corresponding colorbars sm_sub = ax.pcolormesh(xedges * 1e2, yedges * 1e-3, maxmap_sub * factor, cmap='Blues') sm_supra = ax.pcolormesh(xedges * 1e2, yedges * 1e-3, maxmap_supra * factor, cmap='Reds') pos1 = ax.get_position() # get the map axis position height = (pos1.height - 0.05) / 2.0 cbarax_sub = fig.add_axes([pos1.x1 + 0.02, pos1.y0, 0.03, height]) cbar_sub = fig.colorbar(sm_sub, cax=cbarax_sub, format='%.1f') cbar_sub.set_ticks(np.array([np.nanmin(maxmap_sub), np.nanmax(maxmap_sub)]) * factor) cbarax_supra = fig.add_axes([pos1.x1 + 0.02, pos1.y1 - height, 0.03, height]) cbar_supra = fig.colorbar(sm_supra, cax=cbarax_supra, format='%.1f') cbar_supra.set_ticks(np.array([np.nanmin(maxmap_supra), np.nanmax(maxmap_supra)]) * factor) for item in cbarax_sub.get_yticklabels() + cbarax_supra.get_yticklabels(): item.set_fontsize(fs) return fig def plotRawTrace(fpath, key, ybounds): ''' Plot the raw signal of a given variable within specified bounds. :param foath: full path to the data file :param key: key to the target variable :param ybounds: y-axis bounds :return: handle to the generated figure ''' # Check file existence fname = ntpath.basename(fpath) if not os.path.isfile(fpath): raise InputError('Error: "{}" file does not exist'.format(fname)) # Load data logger.debug('Loading data from "%s"', fname) with open(fpath, 'rb') as fh: frame = pickle.load(fh) df = frame['data'] t = df['t'].values y = df[key].values * pltvars[key]['factor'] Δy = y.max() - y.min() logger.info('d%s = %.1f %s', key, Δy, pltvars[key]['unit']) # Plot trace fig, ax = plt.subplots(figsize=cm2inch(12.5, 5.8)) fig.canvas.set_window_title(fname) for s in ['top', 'bottom', 'left', 'right']: ax.spines[s].set_visible(False) ax.set_xticks([]) ax.set_yticks([]) ax.set_ylim(ybounds) ax.plot(t, y, color='k', linewidth=1) fig.tight_layout() return fig def plotTraces(fpath, keys, tbounds): ''' Plot the raw signal of sevral variables within specified bounds. :param foath: full path to the data file :param key: key to the target variable :param tbounds: x-axis bounds :return: handle to the generated figure ''' # Check file existence fname = ntpath.basename(fpath) if not os.path.isfile(fpath): raise InputError('Error: "{}" file does not exist'.format(fname)) # Load data logger.debug('Loading data from "%s"', fname) with open(fpath, 'rb') as fh: frame = pickle.load(fh) df = frame['data'] t = df['t'].values * 1e3 # Plot trace fs = 8 fig, ax = plt.subplots(figsize=cm2inch(7, 3)) fig.canvas.set_window_title(fname) plt.subplots_adjust(left=0.2, bottom=0.2, right=0.95, top=0.95) for s in ['top', 'right']: ax.spines[s].set_visible(False) for s in ['bottom', 'left']: ax.spines[s].set_position(('axes', -0.03)) ax.spines[s].set_linewidth(2) ax.yaxis.set_tick_params(width=2) # ax.spines['bottom'].set_linewidth(2) ax.set_xlim(tbounds) ax.set_xticks([]) ymin = np.nan ymax = np.nan dt = tbounds[1] - tbounds[0] ax.set_xlabel('{}s'.format(si_format(dt * 1e-3, space=' ')), fontsize=fs) ax.set_ylabel('mV - $\\rm nC/cm^2$', fontsize=fs, labelpad=-15) colors = {'Vm': 'darkgrey', 'Qm': 'k'} for key in keys: y = df[key].values * pltvars[key]['factor'] ymin = np.nanmin([ymin, y.min()]) ymax = np.nanmax([ymax, y.max()]) # if key == 'Qm': # y0 = y[0] # ax.plot(t, y0 * np.ones(t.size), '--', color='k', linewidth=1) Δy = y.max() - y.min() logger.info('d%s = %.1f %s', key, Δy, pltvars[key]['unit']) ax.plot(t, y, color=colors[key], linewidth=1) ax.yaxis.set_major_formatter(FormatStrFormatter('%.0f')) # ax.set_yticks([ymin, ymax]) ax.set_ylim([-200, 100]) ax.set_yticks([-200, 100]) for item in ax.get_yticklabels(): item.set_fontsize(fs) # fig.tight_layout() return fig diff --git a/PointNICE/plt/pltvars.py b/PySONIC/plt/pltvars.py similarity index 100% rename from PointNICE/plt/pltvars.py rename to PySONIC/plt/pltvars.py diff --git a/PointNICE/solvers/SolverElec.py b/PySONIC/solvers/SolverElec.py similarity index 98% rename from PointNICE/solvers/SolverElec.py rename to PySONIC/solvers/SolverElec.py index 448916d..4642407 100644 --- a/PointNICE/solvers/SolverElec.py +++ b/PySONIC/solvers/SolverElec.py @@ -1,160 +1,160 @@ #!/usr/bin/env python # -*- coding: utf-8 -*- # @Author: Theo Lemaire # @Date: 2016-09-29 16:16:19 # @Email: theo.lemaire@epfl.ch # @Last Modified by: Theo Lemaire -# @Last Modified time: 2018-05-03 12:27:21 +# @Last Modified time: 2018-08-21 16:10:37 import warnings import logging import numpy as np import scipy.integrate as integrate from ..constants import * from ..neurons import BaseMech from ..utils import InputError # Get package logger -logger = logging.getLogger('PointNICE') +logger = logging.getLogger('PySONIC') class SolverElec: def __init__(self): # Do nothing pass def eqHH(self, y, _, neuron, Iinj): ''' Compute the derivatives of a HH system variables for a specific value of injected current. :param y: vector of HH system variables at time t :param t: time value (s, unused) :param neuron: neuron object :param Iinj: injected current (mA/m2) :return: vector of HH system derivatives at time t ''' Vm, *states = y Iionic = neuron.currNet(Vm, states) # mA/m2 dVmdt = (- Iionic + Iinj) / neuron.Cm0 # mV/s dstates = neuron.derStates(Vm, states) return [dVmdt, *dstates] def run(self, neuron, Astim, tstim, toffset, PRF=None, DC=1.0): ''' Compute solutions of a neuron's HH system for a specific set of electrical stimulation parameters, using a classic integration scheme. :param neuron: neuron object :param Astim: pulse amplitude (mA/m2) :param tstim: pulse duration (s) :param toffset: offset duration (s) :param PRF: pulse repetition frequency (Hz) :param DC: pulse duty cycle (-) :return: 3-tuple with the time profile and solution matrix and a state vector ''' # Check validity of stimulation parameters if not isinstance(neuron, BaseMech): raise InputError('Invalid neuron type: "{}" (must inherit from BaseMech class)' .format(neuron.name)) if not all(isinstance(param, float) for param in [Astim, tstim, toffset, DC]): raise InputError('Invalid stimulation parameters (must be float typed)') if tstim <= 0: raise InputError('Invalid stimulus duration: {} ms (must be strictly positive)' .format(tstim * 1e3)) if toffset < 0: raise InputError('Invalid stimulus offset: {} ms (must be positive or null)' .format(toffset * 1e3)) if DC <= 0.0 or DC > 1.0: raise InputError('Invalid duty cycle: {} (must be within ]0; 1])'.format(DC)) if DC < 1.0: if not isinstance(PRF, float): raise InputError('Invalid PRF value (must be float typed)') if PRF is None: raise InputError('Missing PRF value (must be provided when DC < 1)') if PRF < 1 / tstim: raise InputError('Invalid PRF: {} Hz (PR interval exceeds stimulus duration)' .format(PRF)) # Raise warnings as error warnings.filterwarnings('error') # Determine system time step dt = DT_ESTIM # if CW stimulus: divide integration during stimulus into single interval if DC == 1.0: PRF = 1 / tstim # Compute vector sizes npulses = int(np.round(PRF * tstim)) Tpulse_on = DC / PRF Tpulse_off = (1 - DC) / PRF # For high-PRF pulsed protocols: adapt time step to ensure minimal # number of samples during TON or TOFF dt_warning_msg = 'high-PRF protocol: lowering time step to %.2e s to properly integrate %s' for key, Tpulse in {'TON': Tpulse_on, 'TOFF': Tpulse_off}.items(): if Tpulse > 0 and Tpulse / dt < MIN_SAMPLES_PER_PULSE_INT: dt = Tpulse / MIN_SAMPLES_PER_PULSE_INT logger.warning(dt_warning_msg, dt, key) n_pulse_on = int(np.round(Tpulse_on / dt)) n_pulse_off = int(np.round(Tpulse_off / dt)) # Compute offset size n_off = int(np.round(toffset / dt)) # Set initial conditions y0 = [neuron.Vm0, *neuron.states0] nvar = len(y0) # Initialize global arrays t = np.array([0.]) states = np.array([1]) y = np.array([y0]).T # Initialize pulse time and states vectors t_pulse0 = np.linspace(0, Tpulse_on + Tpulse_off, n_pulse_on + n_pulse_off) states_pulse = np.concatenate((np.ones(n_pulse_on), np.zeros(n_pulse_off))) # Loop through all pulse (ON and OFF) intervals for i in range(npulses): # Construct and initialize arrays t_pulse = t_pulse0 + t[-1] y_pulse = np.empty((nvar, n_pulse_on + n_pulse_off)) # Integrate ON system y_pulse[:, :n_pulse_on] = integrate.odeint(self.eqHH, y[:, -1], t_pulse[:n_pulse_on], args=(neuron, Astim)).T # Integrate OFF system if n_pulse_off > 0: y_pulse[:, n_pulse_on:] = integrate.odeint(self.eqHH, y_pulse[:, n_pulse_on - 1], t_pulse[n_pulse_on:], args=(neuron, 0.0)).T # Append pulse arrays to global arrays states = np.concatenate([states, states_pulse[1:]]) t = np.concatenate([t, t_pulse[1:]]) y = np.concatenate([y, y_pulse[:, 1:]], axis=1) # Integrate offset interval if n_off > 0: t_off = np.linspace(0, toffset, n_off) + t[-1] states_off = np.zeros(n_off) y_off = integrate.odeint(self.eqHH, y[:, -1], t_off, args=(neuron, 0.0)).T # Concatenate offset arrays to global arrays states = np.concatenate([states, states_off[1:]]) t = np.concatenate([t, t_off[1:]]) y = np.concatenate([y, y_off[:, 1:]], axis=1) # Return output variables return (t, y, states) diff --git a/PointNICE/solvers/SolverUS.py b/PySONIC/solvers/SolverUS.py similarity index 99% rename from PointNICE/solvers/SolverUS.py rename to PySONIC/solvers/SolverUS.py index 5ba6031..28c0317 100644 --- a/PointNICE/solvers/SolverUS.py +++ b/PySONIC/solvers/SolverUS.py @@ -1,747 +1,747 @@ #!/usr/bin/env python # -*- coding: utf-8 -*- # @Author: Theo Lemaire # @Date: 2016-09-29 16:16:19 # @Email: theo.lemaire@epfl.ch # @Last Modified by: Theo Lemaire -# @Last Modified time: 2018-08-21 14:19:43 +# @Last Modified time: 2018-08-21 16:10:37 import os import warnings import pickle import logging import progressbar as pb import numpy as np import scipy.integrate as integrate from scipy.interpolate import interp2d from ..bls import BilayerSonophore from ..utils import * from ..constants import * from ..neurons import BaseMech # Get package logger -logger = logging.getLogger('PointNICE') +logger = logging.getLogger('PySONIC') class SolverUS(BilayerSonophore): """ This class extends the BilayerSonophore class by adding a biophysical Hodgkin-Huxley model on top of the mechanical BLS model. """ def __init__(self, diameter, neuron, Fdrive, embedding_depth=0.0): """ Constructor of the class. :param diameter: in-plane diameter of the sonophore structure within the membrane (m) :param neuron: neuron object :param Fdrive: frequency of acoustic perturbation (Hz) :param embedding_depth: depth of the embedding tissue around the membrane (m) """ # Check validity of input parameters if not isinstance(neuron, BaseMech): raise InputError('Invalid neuron type: "{}" (must inherit from BaseMech class)' .format(neuron.name)) if not isinstance(Fdrive, float): raise InputError('Invalid US driving frequency (must be float typed)') if Fdrive < 0: raise InputError('Invalid US driving frequency: {} kHz (must be positive or null)' .format(Fdrive * 1e-3)) # TODO: check parameters dictionary (float type, mandatory members) # Initialize BLS object Cm0 = neuron.Cm0 Vm0 = neuron.Vm0 BilayerSonophore.__init__(self, diameter, Fdrive, Cm0, Cm0 * Vm0 * 1e-3, embedding_depth) logger.debug('US solver initialization with %s neuron', neuron.name) def eqHH(self, y, t, neuron, Cm): """ Compute the derivatives of the n-ODE HH system variables, based on a value of membrane capacitance. :param y: vector of HH system variables at time t :param t: specific instant in time (s) :param neuron: neuron object :param Cm: membrane capacitance (F/m2) :return: vector of HH system derivatives at time t """ # Split input vector explicitly Qm, *states = y # Compute membrane potential Vm = Qm / Cm * 1e3 # mV # Compute derivatives dQm = - neuron.currNet(Vm, states) * 1e-3 # A/m2 dstates = neuron.derStates(Vm, states) # Return derivatives vector return [dQm, *dstates] def eqHH2(self, t, y, neuron, Cm): return self.eqHH(y, t, neuron, Cm) def eqFull(self, y, t, neuron, Adrive, Fdrive, phi): """ Compute the derivatives of the (n+3) ODE full NBLS system variables. :param y: vector of state variables :param t: specific instant in time (s) :param neuron: neuron object :param Adrive: acoustic drive amplitude (Pa) :param Fdrive: acoustic drive frequency (Hz) :param phi: acoustic drive phase (rad) :return: vector of derivatives """ # Compute derivatives of mechanical and electrical systems dydt_mech = self.eqMech(y[:3], t, Adrive, Fdrive, y[3], phi) dydt_elec = self.eqHH(y[3:], t, neuron, self.Capct(y[1])) # return concatenated output return dydt_mech + dydt_elec def eqFull2(self, t, y, neuron, Adrive, Fdrive, phi): return self.eqFull(y, t, neuron, Adrive, Fdrive, phi) def eqHHeff(self, t, y, neuron, interp_data): """ Compute the derivatives of the n-ODE effective HH system variables, based on 1-dimensional linear interpolation of "effective" coefficients that summarize the system's behaviour over an acoustic cycle. :param t: specific instant in time (s) :param y: vector of HH system variables at time t :param neuron: neuron object :param interp_data: dictionary of 1D data points of "effective" coefficients over the charge domain, for specific frequency and amplitude values. :return: vector of effective system derivatives at time t """ # Split input vector explicitly Qm, *states = y # Compute charge and channel states variation Vm = np.interp(Qm, interp_data['Q'], interp_data['V']) # mV dQmdt = - neuron.currNet(Vm, states) * 1e-3 dstates = neuron.derStatesEff(Qm, states, interp_data) # Return derivatives vector return [dQmdt, *dstates] def __runClassic(self, neuron, Fdrive, Adrive, tstim, toffset, PRF, DC, phi=np.pi): """ Compute solutions of the system for a specific set of US stimulation parameters, using a classic integration scheme. The first iteration uses the quasi-steady simplification to compute the initiation of motion from a flat leaflet configuration. Afterwards, the ODE system is solved iteratively until completion. :param neuron: neuron object :param Fdrive: acoustic drive frequency (Hz) :param Adrive: acoustic drive amplitude (Pa) :param tstim: duration of US stimulation (s) :param toffset: duration of the offset (s) :param PRF: pulse repetition frequency (Hz) :param DC: pulse duty cycle (-) :param phi: acoustic drive phase (rad) :return: 3-tuple with the time profile, the effective solution matrix and a state vector """ # Raise warnings as error warnings.filterwarnings('error') # Determine system time step Tdrive = 1 / Fdrive dt = Tdrive / NPC_FULL # if CW stimulus: divide integration during stimulus into 100 intervals if DC == 1.0: PRF = 100 / tstim # Compute vector sizes npulses = int(np.round(PRF * tstim)) Tpulse_on = DC / PRF Tpulse_off = (1 - DC) / PRF n_pulse_on = int(np.round(Tpulse_on / dt)) n_pulse_off = int(np.round(Tpulse_off / dt)) n_off = int(np.round(toffset / dt)) # Solve quasi-steady equation to compute first deflection value Z0 = 0.0 ng0 = self.ng0 Qm0 = self.Qm0 Pac1 = self.Pacoustic(dt, Adrive, Fdrive, phi) Z1 = self.balancedefQS(ng0, Qm0, Pac1) # Initialize global arrays states = np.array([1, 1]) t = np.array([0., dt]) y_membrane = np.array([[0., (Z1 - Z0) / dt], [Z0, Z1], [ng0, ng0], [Qm0, Qm0]]) y_channels = np.tile(neuron.states0, (2, 1)).T y = np.vstack((y_membrane, y_channels)) nvar = y.shape[0] # Initialize pulse time and states vectors t_pulse0 = np.linspace(0, Tpulse_on + Tpulse_off, n_pulse_on + n_pulse_off) states_pulse = np.concatenate((np.ones(n_pulse_on), np.zeros(n_pulse_off))) # Initialize progress bar if logger.getEffectiveLevel() <= logging.INFO: widgets = ['Running: ', pb.Percentage(), ' ', pb.Bar(), ' ', pb.ETA()] pbar = pb.ProgressBar(widgets=widgets, max_value=int(npulses * (toffset + tstim) / tstim)) pbar.start() # Loop through all pulse (ON and OFF) intervals for i in range(npulses): # Construct and initialize arrays t_pulse = t_pulse0 + t[-1] y_pulse = np.empty((nvar, n_pulse_on + n_pulse_off)) # Integrate ON system y_pulse[:, :n_pulse_on] = integrate.odeint(self.eqFull, y[:, -1], t_pulse[:n_pulse_on], args=(neuron, Adrive, Fdrive, phi)).T # Integrate OFF system if n_pulse_off > 0: y_pulse[:, n_pulse_on:] = integrate.odeint(self.eqFull, y_pulse[:, n_pulse_on - 1], t_pulse[n_pulse_on:], args=(neuron, 0.0, 0.0, 0.0)).T # Append pulse arrays to global arrays states = np.concatenate([states, states_pulse[1:]]) t = np.concatenate([t, t_pulse[1:]]) y = np.concatenate([y, y_pulse[:, 1:]], axis=1) # Update progress bar if logger.getEffectiveLevel() <= logging.INFO: pbar.update(i) # Integrate offset interval if n_off > 0: t_off = np.linspace(0, toffset, n_off) + t[-1] states_off = np.zeros(n_off) y_off = integrate.odeint(self.eqFull, y[:, -1], t_off, args=(neuron, 0.0, 0.0, 0.0)).T # Concatenate offset arrays to global arrays states = np.concatenate([states, states_off[1:]]) t = np.concatenate([t, t_off[1:]]) y = np.concatenate([y, y_off[:, 1:]], axis=1) # Terminate progress bar if logger.getEffectiveLevel() <= logging.INFO: pbar.finish() # Downsample arrays in time-domain accordgin to target temporal resolution ds_factor = int(np.round(CLASSIC_TARGET_DT / dt)) if ds_factor > 1: Fs = 1 / (dt * ds_factor) logger.info('Downsampling output arrays by factor %u (Fs = %.2f MHz)', ds_factor, Fs * 1e-6) t = t[::ds_factor] y = y[:, ::ds_factor] states = states[::ds_factor] # Compute membrane potential vector (in mV) Vm = y[3, :] / self.v_Capct(y[1, :]) * 1e3 # mV # Return output variables with Vm # return (t, y[1:, :], states) return (t, np.vstack([y[1:4, :], Vm, y[4:, :]]), states) def __runEffective(self, neuron, Fdrive, Adrive, tstim, toffset, PRF, DC, dt=DT_EFF): """ Compute solutions of the system for a specific set of US stimulation parameters, using charge-predicted "effective" coefficients to solve the HH equations at each step. :param neuron: neuron object :param Fdrive: acoustic drive frequency (Hz) :param Adrive: acoustic drive amplitude (Pa) :param tstim: duration of US stimulation (s) :param toffset: duration of the offset (s) :param PRF: pulse repetition frequency (Hz) :param DC: pulse duty cycle (-) :param dt: integration time step (s) :return: 3-tuple with the time profile, the effective solution matrix and a state vector """ # Raise warnings as error warnings.filterwarnings('error') # Load appropriate 2D lookups Aref, Qref, lookups2D = getLookups2D(neuron.name, self.a, Fdrive) # Check that acoustic amplitude is within lookup range margin = 1e-9 # adding margin to compensate for eventual round error Arange = (Aref.min() - margin, Aref.max() + margin) if Adrive < Arange[0] or Adrive > Arange[1]: raise InputError('Invalid amplitude: {}Pa (must be within {}Pa - {} Pa lookup interval)' .format(*si_format([Adrive, *Arange], precision=2, space=' '))) # Interpolate 2D lookups at US amplitude (along with "ng" at zero amplitude) lookups1D = {key: np.squeeze(interp2d(Aref, Qref, lookups2D[key].T)(Adrive, Qref)) for key in lookups2D.keys()} lookups1D['ng0'] = np.squeeze(interp2d(Aref, Qref, lookups2D['ng'].T)(0.0, Qref)) # Add reference charge vector to 1D lookup dictionary lookups1D['Q'] = Qref # Initialize system solvers solver_on = integrate.ode(self.eqHHeff) solver_on.set_integrator('lsoda', nsteps=SOLVER_NSTEPS) solver_on.set_f_params(neuron, lookups1D) solver_off = integrate.ode(self.eqHH2) solver_off.set_integrator('lsoda', nsteps=SOLVER_NSTEPS) # if CW stimulus: change PRF to have exactly one integration interval during stimulus if DC == 1.0: PRF = 1 / tstim # Compute vector sizes npulses = int(np.round(PRF * tstim)) Tpulse_on = DC / PRF Tpulse_off = (1 - DC) / PRF # For high-PRF pulsed protocols: adapt time step to ensure minimal # number of samples during TON or TOFF dt_warning_msg = 'high-PRF protocol: lowering time step to %.2e s to properly integrate %s' for key, Tpulse in {'TON': Tpulse_on, 'TOFF': Tpulse_off}.items(): if Tpulse > 0 and Tpulse / dt < MIN_SAMPLES_PER_PULSE_INT: dt = Tpulse / MIN_SAMPLES_PER_PULSE_INT logger.warning(dt_warning_msg, dt, key) n_pulse_on = int(np.round(Tpulse_on / dt)) + 1 n_pulse_off = int(np.round(Tpulse_off / dt)) # Compute ofset size n_off = int(np.round(toffset / dt)) # Initialize global arrays states = np.array([1]) t = np.array([0.0]) y = np.atleast_2d(np.insert(neuron.states0, 0, self.Qm0)).T nvar = y.shape[0] Zeff = np.array([0.0]) ngeff = np.array([self.ng0]) # Initializing accurate pulse time vector t_pulse_on = np.linspace(0, Tpulse_on, n_pulse_on) t_pulse_off = np.linspace(dt, Tpulse_off, n_pulse_off) + Tpulse_on t_pulse0 = np.concatenate([t_pulse_on, t_pulse_off]) states_pulse = np.concatenate((np.ones(n_pulse_on), np.zeros(n_pulse_off))) # Loop through all pulse (ON and OFF) intervals for i in range(npulses): # Construct and initialize arrays t_pulse = t_pulse0 + t[-1] y_pulse = np.empty((nvar, n_pulse_on + n_pulse_off)) ngeff_pulse = np.empty(n_pulse_on + n_pulse_off) Zeff_pulse = np.empty(n_pulse_on + n_pulse_off) y_pulse[:, 0] = y[:, -1] ngeff_pulse[0] = ngeff[-1] Zeff_pulse[0] = Zeff[-1] # Initialize iterator k = 0 # Integrate ON system solver_on.set_initial_value(y_pulse[:, k], t_pulse[k]) while solver_on.successful() and k < n_pulse_on - 1: k += 1 solver_on.integrate(t_pulse[k]) y_pulse[:, k] = solver_on.y ngeff_pulse[k] = np.interp(y_pulse[0, k], lookups1D['Q'], lookups1D['ng']) # mole Zeff_pulse[k] = self.balancedefQS(ngeff_pulse[k], y_pulse[0, k]) # m # Integrate OFF system if n_pulse_off > 0: solver_off.set_initial_value(y_pulse[:, k], t_pulse[k]) solver_off.set_f_params(neuron, self.Capct(Zeff_pulse[k])) while solver_off.successful() and k < n_pulse_on + n_pulse_off - 1: k += 1 solver_off.integrate(t_pulse[k]) y_pulse[:, k] = solver_off.y ngeff_pulse[k] = np.interp(y_pulse[0, k], lookups1D['Q'], lookups1D['ng0']) # mole Zeff_pulse[k] = self.balancedefQS(ngeff_pulse[k], y_pulse[0, k]) # m solver_off.set_f_params(neuron, self.Capct(Zeff_pulse[k])) # Append pulse arrays to global arrays states = np.concatenate([states[:-1], states_pulse]) t = np.concatenate([t, t_pulse[1:]]) y = np.concatenate([y, y_pulse[:, 1:]], axis=1) Zeff = np.concatenate([Zeff, Zeff_pulse[1:]]) ngeff = np.concatenate([ngeff, ngeff_pulse[1:]]) # Integrate offset interval if n_off > 0: t_off = np.linspace(0, toffset, n_off) + t[-1] states_off = np.zeros(n_off) y_off = np.empty((nvar, n_off)) ngeff_off = np.empty(n_off) Zeff_off = np.empty(n_off) y_off[:, 0] = y[:, -1] ngeff_off[0] = ngeff[-1] Zeff_off[0] = Zeff[-1] solver_off.set_initial_value(y_off[:, 0], t_off[0]) solver_off.set_f_params(neuron, self.Capct(Zeff_pulse[-1])) k = 0 while solver_off.successful() and k < n_off - 1: k += 1 solver_off.integrate(t_off[k]) y_off[:, k] = solver_off.y ngeff_off[k] = np.interp(y_off[0, k], lookups1D['Q'], lookups1D['ng0']) # mole Zeff_off[k] = self.balancedefQS(ngeff_off[k], y_off[0, k]) # m solver_off.set_f_params(neuron, self.Capct(Zeff_off[k])) # Concatenate offset arrays to global arrays states = np.concatenate([states, states_off[1:]]) t = np.concatenate([t, t_off[1:]]) y = np.concatenate([y, y_off[:, 1:]], axis=1) Zeff = np.concatenate([Zeff, Zeff_off[1:]]) ngeff = np.concatenate([ngeff, ngeff_off[1:]]) # Compute membrane potential vector (in mV) Vm = np.zeros(states.size) Vm[states == 0] = y[0, states == 0] / self.v_Capct(Zeff[states == 0]) * 1e3 # mV Vm[states == 1] = np.interp(y[0, states == 1], lookups1D['Q'], lookups1D['V']) # mV # Add Zeff, ngeff and Vm to solution matrix y = np.vstack([Zeff, ngeff, y[0, :], Vm, y[1:, :]]) # return output variables return (t, y, states) def __runHybrid(self, neuron, Fdrive, Adrive, tstim, toffset, phi=np.pi): """ Compute solutions of the system for a specific set of US stimulation parameters, using a hybrid integration scheme. The first iteration uses the quasi-steady simplification to compute the initiation of motion from a flat leaflet configuration. Afterwards, the NBLS ODE system is solved iteratively for "slices" of N-microseconds, in a 2-steps scheme: - First, the full (n+3) ODE system is integrated for a few acoustic cycles until Z and ng reach a stable periodic solution (limit cycle) - Second, the signals of the 3 mechanical variables over the last acoustic period are selected and resampled to a far lower sampling rate - Third, the HH n-ODE system is integrated for the remaining time of the slice, using periodic expansion of the mechanical signals to precompute the values of capacitance. :param neuron: neuron object :param Fdrive: acoustic drive frequency (Hz) :param Adrive: acoustic drive amplitude (Pa) :param tstim: duration of US stimulation (s) :param toffset: duration of the offset (s) :param phi: acoustic drive phase (rad) :return: 3-tuple with the time profile, the solution matrix and a state vector .. warning:: This method cannot handle pulsed stimuli """ # Raise warnings as error warnings.filterwarnings('error') # Initialize full and HH systems solvers solver_full = integrate.ode(self.eqFull2) solver_full.set_f_params(neuron, Adrive, Fdrive, phi) solver_full.set_integrator('lsoda', nsteps=SOLVER_NSTEPS) solver_hh = integrate.ode(self.eqHH2) solver_hh.set_integrator('dop853', nsteps=SOLVER_NSTEPS, atol=1e-12) # Determine full and HH systems time steps Tdrive = 1 / Fdrive dt_full = Tdrive / NPC_FULL dt_hh = Tdrive / NPC_HH n_full_per_hh = int(NPC_FULL / NPC_HH) t_full_cycle = np.linspace(0, Tdrive - dt_full, NPC_FULL) t_hh_cycle = np.linspace(0, Tdrive - dt_hh, NPC_HH) # Determine number of samples in prediction vectors npc_pred = NPC_FULL - n_full_per_hh + 1 # Solve quasi-steady equation to compute first deflection value Z0 = 0.0 ng0 = self.ng0 Qm0 = self.Qm0 Pac1 = self.Pacoustic(dt_full, Adrive, Fdrive, phi) Z1 = self.balancedefQS(ng0, Qm0, Pac1) # Initialize global arrays states = np.array([1, 1]) t = np.array([0., dt_full]) y_membrane = np.array([[0., (Z1 - Z0) / dt_full], [Z0, Z1], [ng0, ng0], [Qm0, Qm0]]) y_channels = np.tile(neuron.states0, (2, 1)).T y = np.vstack((y_membrane, y_channels)) nvar = y.shape[0] # Initialize progress bar if logger.getEffectiveLevel() == logging.DEBUG: widgets = ['Running: ', pb.Percentage(), ' ', pb.Bar(), ' ', pb.ETA()] pbar = pb.ProgressBar(widgets=widgets, max_value=1000) pbar.start() # For each hybrid integration interval irep = 0 sim_error = False while not sim_error and t[-1] < tstim + toffset: # Integrate full system for a few acoustic cycles until stabilization periodic_conv = False j = 0 ng_last = None Z_last = None while not sim_error and not periodic_conv: if t[-1] > tstim: solver_full.set_f_params(neuron, 0.0, 0.0, 0.0) t_full = t_full_cycle + t[-1] + dt_full y_full = np.empty((nvar, NPC_FULL)) y0_full = y[:, -1] solver_full.set_initial_value(y0_full, t[-1]) k = 0 while solver_full.successful() and k <= NPC_FULL - 1: solver_full.integrate(t_full[k]) y_full[:, k] = solver_full.y k += 1 # Compare Z and ng signals over the last 2 acoustic periods if j > 0 and rmse(Z_last, y_full[1, :]) < Z_ERR_MAX \ and rmse(ng_last, y_full[2, :]) < NG_ERR_MAX: periodic_conv = True # Update last vectors for next comparison Z_last = y_full[1, :] ng_last = y_full[2, :] # Concatenate time and solutions to global vectors states = np.concatenate([states, np.ones(NPC_FULL)], axis=0) t = np.concatenate([t, t_full], axis=0) y = np.concatenate([y, y_full], axis=1) # Increment loop index j += 1 # Retrieve last period of the 3 mechanical variables to propagate in HH system t_last = t[-npc_pred:] mech_last = y[0:3, -npc_pred:] # Downsample signals to specified HH system time step (_, mech_pred) = DownSample(t_last, mech_last, NPC_HH) # Integrate HH system until certain dQ or dT is reached Q0 = y[3, -1] dQ = 0.0 t0_interval = t[-1] dt_interval = 0.0 j = 0 if t[-1] < tstim: tlim = tstim else: tlim = tstim + toffset while (not sim_error and t[-1] < tlim and (np.abs(dQ) < DQ_UPDATE or dt_interval < DT_UPDATE)): t_hh = t_hh_cycle + t[-1] + dt_hh y_hh = np.empty((nvar - 3, NPC_HH)) y0_hh = y[3:, -1] solver_hh.set_initial_value(y0_hh, t[-1]) k = 0 while solver_hh.successful() and k <= NPC_HH - 1: solver_hh.set_f_params(neuron, self.Capct(mech_pred[1, k])) solver_hh.integrate(t_hh[k]) y_hh[:, k] = solver_hh.y k += 1 # Concatenate time and solutions to global vectors states = np.concatenate([states, np.zeros(NPC_HH)], axis=0) t = np.concatenate([t, t_hh], axis=0) y = np.concatenate([y, np.concatenate([mech_pred, y_hh], axis=0)], axis=1) # Compute charge variation from interval beginning dQ = y[3, -1] - Q0 dt_interval = t[-1] - t0_interval # Increment loop index j += 1 # Update progress bar if logger.getEffectiveLevel() == logging.DEBUG: pbar.update(int(1000 * (t[-1] / (tstim + toffset)))) irep += 1 # Terminate progress bar if logger.getEffectiveLevel() == logging.DEBUG: pbar.finish() # Compute membrane potential vector (in mV) Vm = y[3, :] / self.v_Capct(y[1, :]) * 1e3 # mV # Return output variables with Vm # return (t, y[1:, :], states) return (t, np.vstack([y[1:4, :], Vm, y[4:, :]]), states) def run(self, neuron, Fdrive, Adrive, tstim, toffset, PRF=None, DC=1.0, sim_type='effective'): """ Run simulation of the system for a specific set of US stimulation parameters. :param neuron: neuron object :param Fdrive: acoustic drive frequency (Hz) :param Adrive: acoustic drive amplitude (Pa) :param tstim: duration of US stimulation (s) :param toffset: duration of the offset (s) :param PRF: pulse repetition frequency (Hz) :param DC: pulse duty cycle (-) :param sim_type: selected integration method :return: 3-tuple with the time profile, the solution matrix and a state vector """ # Check validity of simulation type if sim_type not in ('classic', 'effective', 'NEURON', 'hybrid'): raise InputError('Invalid integration method: "{}"'.format(sim_type)) # Check validity of stimulation parameters if not isinstance(neuron, BaseMech): raise InputError('Invalid neuron type: "{}" (must inherit from BaseMech class)' .format(neuron.name)) if not all(isinstance(param, float) for param in [Fdrive, Adrive, tstim, toffset, DC]): raise InputError('Invalid stimulation parameters (must be float typed)') if Fdrive <= 0: raise InputError('Invalid US driving frequency: {} kHz (must be strictly positive)' .format(Fdrive * 1e-3)) if Adrive < 0: raise InputError('Invalid US pressure amplitude: {} kPa (must be positive or null)' .format(Adrive * 1e-3)) if tstim <= 0: raise InputError('Invalid stimulus duration: {} ms (must be strictly positive)' .format(tstim * 1e3)) if toffset < 0: raise InputError('Invalid stimulus offset: {} ms (must be positive or null)' .format(toffset * 1e3)) if DC <= 0.0 or DC > 1.0: raise InputError('Invalid duty cycle: {} (must be within ]0; 1])'.format(DC)) if DC < 1.0: if not isinstance(PRF, float): raise InputError('Invalid PRF value (must be float typed)') if PRF is None: raise InputError('Missing PRF value (must be provided when DC < 1)') if PRF < 1 / tstim: raise InputError('Invalid PRF: {} Hz (PR interval exceeds stimulus duration' .format(PRF)) if PRF >= Fdrive: raise InputError('Invalid PRF: {} Hz (must be smaller than driving frequency)' .format(PRF)) # Call appropriate simulation function if sim_type == 'classic': return self.__runClassic(neuron, Fdrive, Adrive, tstim, toffset, PRF, DC) elif sim_type == 'effective': return self.__runEffective(neuron, Fdrive, Adrive, tstim, toffset, PRF, DC) elif sim_type == 'NEURON': return self.__runNEURON(neuron, Fdrive, Adrive, tstim, toffset, PRF, DC) elif sim_type == 'hybrid': if DC < 1.0: raise InputError('Pulsed protocol incompatible with hybrid integration method') return self.__runHybrid(neuron, Fdrive, Adrive, tstim, toffset) def findRheobaseAmps(self, neuron, Fdrive, DCs, Vthr, curr='net'): ''' Find the rheobase amplitudes (i.e. threshold acoustic amplitudes of infinite duration that would result in excitation) of a specific neuron for various stimulation duty cycles. :param neuron: neuron object :param Fdrive: acoustic drive frequency (Hz) :param DCs: duty cycles vector (-) :param Vthr: threshold membrane potential above which the neuron necessarily fires (mV) :return: rheobase amplitudes vector (Pa) ''' # Check lookup file existence lookup_file = '{}_lookups_a{:.1f}nm.pkl'.format(neuron.name, self.a * 1e9) lookup_path = '{}/{}'.format(getLookupDir(), lookup_file) if not os.path.isfile(lookup_path): raise InputError('Missing lookup file: "{}"'.format(lookup_file)) # Load lookups dictionary with open(lookup_path, 'rb') as fh: lookups3D = pickle.load(fh) # Retrieve 1D inputs from lookups dictionary freqs = lookups3D.pop('f') amps = lookups3D.pop('A') charges = lookups3D.pop('Q') # Check that stimulation parameters are within lookup range margin = 1e-9 # adding margin to compensate for eventual round error frange = (freqs.min() - margin, freqs.max() + margin) if Fdrive < frange[0] or Fdrive > frange[1]: raise InputError(('Invalid frequency: {:.2f} kHz (must be within ' + '{:.1f} kHz - {:.1f} MHz lookup interval)') .format(Fdrive * 1e-3, frange[0] * 1e-3, frange[1] * 1e-6)) # Interpolate 3D lookpus at given frequency and threshold charge lookups2D = itrpLookupsFreq(lookups3D, freqs, Fdrive) Qthr = neuron.Cm0 * Vthr * 1e-3 # C/m2 lookups1D = {key: np.squeeze(interp2d(amps, charges, lookups2D[key].T)(amps, Qthr)) for key in lookups2D.keys()} # Remove unnecessary items ot get ON rates and effective potential at threshold charge rates_on = lookups1D rates_on.pop('ng') Vm_on = rates_on.pop('V') # Compute neuron OFF rates at threshold potential rates_off = neuron.getRates(Vthr) # Compute rheobase amplitudes rheboase_amps = np.empty(DCs.size) for i, DC in enumerate(DCs): sstates_pulse = np.empty((len(neuron.states_names), amps.size)) for j, x in enumerate(neuron.states_names): # If channel state, compute pulse-average steady-state values if x in neuron.getGates(): x = x.lower() alpha_str, beta_str = ['{}{}'.format(s, x) for s in ['alpha', 'beta']] alphax_pulse = rates_on[alpha_str] * DC + rates_off[alpha_str] * (1 - DC) betax_pulse = rates_on[beta_str] * DC + rates_off[beta_str] * (1 - DC) sstates_pulse[j, :] = alphax_pulse / (alphax_pulse + betax_pulse) # Otherwise assume the state has reached a steady-state value for Vthr else: sstates_pulse[j, :] = np.ones(amps.size) * neuron.steadyStates(Vthr)[j] # Compute the pulse average net (or leakage) current along the amplitude space if curr == 'net': iNet_on = neuron.currNet(Vm_on, sstates_pulse) iNet_off = neuron.currNet(Vthr, sstates_pulse) elif curr == 'leak': iNet_on = neuron.currL(Vm_on) iNet_off = neuron.currL(Vthr) iNet_avg = iNet_on * DC + iNet_off * (1 - DC) # Find the threshold amplitude that cancels the pulse average net current rheboase_amps[i] = np.interp(0, -iNet_avg, amps, left=0., right=np.nan) inan = np.where(np.isnan(rheboase_amps))[0] if inan.size > 0: if inan.size == rheboase_amps.size: logger.error('No rheobase amplitudes within [%s - %sPa] for the provided duty cycles', *si_format((amps.min(), amps.max()))) else: minDC = DCs[inan.max() + 1] logger.warning('No rheobase amplitudes within [%s - %sPa] below %.1f%% duty cycle', *si_format((amps.min(), amps.max())), minDC * 1e2) return rheboase_amps diff --git a/PointNICE/solvers/__init__.py b/PySONIC/solvers/__init__.py similarity index 100% rename from PointNICE/solvers/__init__.py rename to PySONIC/solvers/__init__.py diff --git a/PointNICE/solvers/simutils.py b/PySONIC/solvers/simutils.py similarity index 99% rename from PointNICE/solvers/simutils.py rename to PySONIC/solvers/simutils.py index 208c129..d5124d0 100644 --- a/PointNICE/solvers/simutils.py +++ b/PySONIC/solvers/simutils.py @@ -1,2318 +1,2318 @@ # -*- coding: utf-8 -*- # @Author: Theo Lemaire # @Date: 2017-08-22 14:33:04 # @Last Modified by: Theo Lemaire -# @Last Modified time: 2018-07-24 12:01:13 +# @Last Modified time: 2018-08-21 16:10:36 """ Utility functions used in simulations """ import os import time import logging import pickle import shutil import tkinter as tk from tkinter import filedialog import numpy as np import pandas as pd from openpyxl import load_workbook import lockfile import multiprocessing as mp from ..bls import BilayerSonophore from .SolverUS import SolverUS from .SolverElec import SolverElec from ..constants import * from ..neurons import * from ..utils import getNeuronsDict, InputError, PmCompMethod, si_format, getCycleAverage, nDindexes # Get package logger -logger = logging.getLogger('PointNICE') +logger = logging.getLogger('PySONIC') class Consumer(mp.Process): ''' Generic consumer process, taking tasks from a queue and outputing results in another queue. ''' def __init__(self, task_queue, result_queue): mp.Process.__init__(self) self.task_queue = task_queue self.result_queue = result_queue print('Starting {}'.format(self.name)) def run(self): while True: nextTask = self.task_queue.get() if nextTask is None: print('Exiting {}'.format(self.name)) self.task_queue.task_done() break print('{}: {}'.format(self.name, nextTask)) answer = nextTask() self.task_queue.task_done() self.result_queue.put(answer) return class LookupWorker(): ''' Worker class that computes "effective" coefficients of the HH system for a specific combination of stimulus frequency, stimulus amplitude and charge density. A short mechanical simulation is run while imposing the specific charge density, until periodic stabilization. The HH coefficients are then averaged over the last acoustic cycle to yield "effective" coefficients. ''' def __init__(self, wid, bls, neuron, Fdrive, Adrive, Qm, phi, nsims): ''' Class constructor. :param wid: worker ID :param bls: BilayerSonophore object :param neuron: neuron object :param Fdrive: acoustic drive frequency (Hz) :param Adrive: acoustic drive amplitude (Pa) :param Qm: imposed charge density (C/m2) :param phi: acoustic drive phase (rad) :param nsims: total number or simulations ''' self.id = wid self.bls = bls self.neuron = neuron self.Fdrive = Fdrive self.Adrive = Adrive self.Qm = Qm self.phi = phi self.nsims = nsims def __call__(self): ''' Method that computes effective coefficients. ''' try: # Run simulation and retrieve deflection and gas content vectors from last cycle _, [Z, ng], _ = self.bls.run(self.Fdrive, self.Adrive, self.Qm, self.phi) Z_last = Z[-NPC_FULL:] # m # Compute membrane potential vector Vm = self.Qm / self.bls.v_Capct(Z_last) * 1e3 # mV # Compute average cycle value for membrane potential and rate constants Vm_eff = np.mean(Vm) # mV rates_eff = self.neuron.getEffRates(Vm) # Take final cycle value for gas content ng_eff = ng[-1] # mole return (self.id, [Vm_eff, ng_eff, *rates_eff]) except (Warning, AssertionError) as inst: logger.warning('Integration error: %s. Continue batch? (y/n)', extra={inst}) user_str = input() if user_str not in ['y', 'Y']: return -1 def __str__(self): return 'simulation {}/{} (f = {}Hz, A = {}Pa, Q = {:.2f} nC/cm2)'\ .format(self.id + 1, self.nsims, *si_format([self.Fdrive, self.Adrive], space=' '), self.Qm * 1e5) class AStimWorker(): ''' Worker class that runs a single A-STIM simulation a given neuron for specific stimulation parameters, and save the results in a PKL file. ''' def __init__(self, wid, batch_dir, log_filepath, solver, neuron, Fdrive, Adrive, tstim, toffset, PRF, DC, int_method, nsims): ''' Class constructor. :param wid: worker ID :param solver: SolverUS object :param neuron: neuron object :param Fdrive: acoustic drive frequency (Hz) :param Adrive: acoustic drive amplitude (Pa) :param tstim: duration of US stimulation (s) :param toffset: duration of the offset (s) :param PRF: pulse repetition frequency (Hz) :param DC: pulse duty cycle (-) :param int_method: selected integration method :param nsims: total number or simulations ''' self.id = wid self.batch_dir = batch_dir self.log_filepath = log_filepath self.solver = solver self.neuron = neuron self.Fdrive = Fdrive self.Adrive = Adrive self.tstim = tstim self.toffset = toffset self.PRF = PRF self.DC = DC self.int_method = int_method self.nsims = nsims def __call__(self): ''' Method that runs the simulation. ''' simcode = 'ASTIM_{}_{}_{:.0f}nm_{:.0f}kHz_{:.1f}kPa_{:.0f}ms_{}{}'\ .format(self.neuron.name, 'CW' if self.DC == 1 else 'PW', self.solver.a * 1e9, self.Fdrive * 1e-3, self.Adrive * 1e-3, self.tstim * 1e3, 'PRF{:.2f}Hz_DC{:.2f}%_'.format(self.PRF, self.DC * 1e2) if self.DC < 1. else '', self.int_method) try: # Get date and time info date_str = time.strftime("%Y.%m.%d") daytime_str = time.strftime("%H:%M:%S") # Run simulation tstart = time.time() (t, y, states) = self.solver.run(self.neuron, self.Fdrive, self.Adrive, self.tstim, self.toffset, self.PRF, self.DC, self.int_method) Z, ng, Qm, Vm, *channels = y U = np.insert(np.diff(Z) / np.diff(t), 0, 0.0) tcomp = time.time() - tstart logger.debug('completed in %ss', si_format(tcomp, 2)) # Store dataframe and metadata df = pd.DataFrame({'t': t, 'states': states, 'U': U, 'Z': Z, 'ng': ng, 'Qm': Qm, 'Vm': Vm}) for j in range(len(self.neuron.states_names)): df[self.neuron.states_names[j]] = channels[j] meta = {'neuron': self.neuron.name, 'a': self.solver.a, 'd': self.solver.d, 'Fdrive': self.Fdrive, 'Adrive': self.Adrive, 'phi': np.pi, 'tstim': self.tstim, 'toffset': self.toffset, 'PRF': self.PRF, 'DC': self.DC, 'tcomp': tcomp} # Export into to PKL file output_filepath = '{}/{}.pkl'.format(self.batch_dir, simcode) with open(output_filepath, 'wb') as fh: pickle.dump({'meta': meta, 'data': df}, fh) logger.debug('simulation data exported to "%s"', output_filepath) # Detect spikes on Qm signal dt = t[1] - t[0] ipeaks, *_ = findPeaks(Qm, SPIKE_MIN_QAMP, int(np.ceil(SPIKE_MIN_DT / dt)), SPIKE_MIN_QPROM) n_spikes = ipeaks.size lat = t[ipeaks[0]] if n_spikes > 0 else 'N/A' sr = np.mean(1 / np.diff(t[ipeaks])) if n_spikes > 1 else 'N/A' logger.debug('%u spike%s detected', n_spikes, "s" if n_spikes > 1 else "") # Export key metrics to log file log = { 'A': date_str, 'B': daytime_str, 'C': self.neuron.name, 'D': self.solver.a * 1e9, 'E': self.solver.d * 1e6, 'F': self.Fdrive * 1e-3, 'G': self.Adrive * 1e-3, 'H': self.tstim * 1e3, 'I': self.PRF * 1e-3 if self.DC < 1 else 'N/A', 'J': self.DC, 'K': self.int_method, 'L': t.size, 'M': round(tcomp, 2), 'N': n_spikes, 'O': lat * 1e3 if isinstance(lat, float) else 'N/A', 'P': sr * 1e-3 if isinstance(sr, float) else 'N/A' } if xlslog(self.log_filepath, 'Data', log) == 1: logger.debug('log exported to "%s"', self.log_filepath) else: logger.error('log export to "%s" aborted', self.log_filepath) return output_filepath except (Warning, AssertionError) as inst: logger.warning('Integration error: %s. Continue batch? (y/n)', extra={inst}) user_str = input() if user_str not in ['y', 'Y']: return -1 def __str__(self): worker_str = 'A-STIM {} simulation {}/{}: {} neuron, a = {}m, f = {}Hz, A = {}Pa, t = {}s'\ .format(self.int_method, self.id, self.nsims, self.neuron.name, *si_format([self.solver.a, self.Fdrive], 1, space=' '), si_format(self.Adrive, 2, space=' '), si_format(self.tstim, 1, space=' ')) if self.DC < 1.0: worker_str += ', PRF = {}Hz, DC = {:.2f}%'\ .format(si_format(self.PRF, 2, space=' '), self.DC * 1e2) return worker_str class EStimWorker(): ''' Worker class that runs a single E-STIM simulation a given neuron for specific stimulation parameters, and save the results in a PKL file. ''' def __init__(self, wid, batch_dir, log_filepath, solver, neuron, Astim, tstim, toffset, PRF, DC, nsims): ''' Class constructor. :param wid: worker ID :param solver: SolverElec object :param neuron: neuron object :param Astim: stimulus amplitude (mA/m2) :param tstim: duration of US stimulation (s) :param toffset: duration of the offset (s) :param PRF: pulse repetition frequency (Hz) :param DC: pulse duty cycle (-) :param nsims: total number or simulations ''' self.id = wid self.batch_dir = batch_dir self.log_filepath = log_filepath self.solver = solver self.neuron = neuron self.Astim = Astim self.tstim = tstim self.toffset = toffset self.PRF = PRF self.DC = DC self.nsims = nsims def __call__(self): ''' Method that runs the simulation. ''' simcode = 'ESTIM_{}_{}_{:.1f}mA_per_m2_{:.0f}ms{}'\ .format(self.neuron.name, 'CW' if self.DC == 1 else 'PW', self.Astim, self.tstim * 1e3, '_PRF{:.2f}Hz_DC{:.2f}%'.format(self.PRF, self.DC * 1e2) if self.DC < 1. else '') try: # Get date and time info date_str = time.strftime("%Y.%m.%d") daytime_str = time.strftime("%H:%M:%S") # Run simulation tstart = time.time() (t, y, states) = self.solver.run(self.neuron, self.Astim, self.tstim, self.toffset, self.PRF, self.DC) Vm, *channels = y tcomp = time.time() - tstart logger.debug('completed in %ss', si_format(tcomp, 1)) # Store dataframe and metadata df = pd.DataFrame({'t': t, 'states': states, 'Vm': Vm, 'Qm': Vm * self.neuron.Cm0 * 1e-3}) for j in range(len(self.neuron.states_names)): df[self.neuron.states_names[j]] = channels[j] meta = {'neuron': self.neuron.name, 'Astim': self.Astim, 'tstim': self.tstim, 'toffset': self.toffset, 'PRF': self.PRF, 'DC': self.DC, 'tcomp': tcomp} # Export into to PKL file output_filepath = '{}/{}.pkl'.format(self.batch_dir, simcode) with open(output_filepath, 'wb') as fh: pickle.dump({'meta': meta, 'data': df}, fh) logger.debug('simulation data exported to "%s"', output_filepath) # Detect spikes on Vm signal dt = t[1] - t[0] ipeaks, *_ = findPeaks(Vm, SPIKE_MIN_VAMP, int(np.ceil(SPIKE_MIN_DT / dt)), SPIKE_MIN_VPROM) n_spikes = ipeaks.size lat = t[ipeaks[0]] if n_spikes > 0 else 'N/A' sr = np.mean(1 / np.diff(t[ipeaks])) if n_spikes > 1 else 'N/A' logger.debug('%u spike%s detected', n_spikes, "s" if n_spikes > 1 else "") # Export key metrics to log file log = { 'A': date_str, 'B': daytime_str, 'C': self.neuron.name, 'D': self.Astim, 'E': self.tstim * 1e3, 'F': self.PRF * 1e-3 if self.DC < 1 else 'N/A', 'G': self.DC, 'H': t.size, 'I': round(tcomp, 2), 'J': n_spikes, 'K': lat * 1e3 if isinstance(lat, float) else 'N/A', 'L': sr * 1e-3 if isinstance(sr, float) else 'N/A' } if xlslog(self.log_filepath, 'Data', log) == 1: logger.debug('log exported to "%s"', self.log_filepath) else: logger.error('log export to "%s" aborted', self.log_filepath) return output_filepath except (Warning, AssertionError) as inst: logger.warning('Integration error: %s. Continue batch? (y/n)', extra={inst}) user_str = input() if user_str not in ['y', 'Y']: return -1 def __str__(self): worker_str = 'E-STIM simulation {}/{}: {} neuron, A = {}A/m2, t = {}s'\ .format(self.id, self.nsims, self.neuron.name, si_format(self.Astim * 1e-3, 2, space=' '), si_format(self.tstim, 1, space=' ')) if self.DC < 1.0: worker_str += ', PRF = {}Hz, DC = {:.2f}%'\ .format(si_format(self.PRF, 2, space=' '), self.DC * 1e2) return worker_str class MechWorker(): ''' Worker class that runs a single simulation of the mechanical system with specific parameters and an imposed value of charge density, and save the results in a PKL file. ''' def __init__(self, wid, batch_dir, log_filepath, bls, Fdrive, Adrive, Qm, nsims): ''' Class constructor. :param wid: worker ID :param batch_dir: full path to output directory of batch :param log_filepath: full path log file of batch :param bls: BilayerSonophore instance :param Fdrive: acoustic drive frequency (Hz) :param Adrive: acoustic drive amplitude (Pa) :param Qm: applided membrane charge density (C/m2) :param nsims: total number or simulations ''' self.id = wid self.batch_dir = batch_dir self.log_filepath = log_filepath self.bls = bls self.Fdrive = Fdrive self.Adrive = Adrive self.Qm = Qm self.nsims = nsims def __call__(self): ''' Method that runs the simulation. ''' simcode = 'MECH_{:.0f}nm_{:.0f}kHz_{:.1f}kPa_{:.1f}nCcm2'\ .format(self.bls.a * 1e9, self.Fdrive * 1e-3, self.Adrive * 1e-3, self.Qm * 1e5) try: # Get date and time info date_str = time.strftime("%Y.%m.%d") daytime_str = time.strftime("%H:%M:%S") # Run simulation tstart = time.time() (t, y, states) = self.bls.run(self.Fdrive, self.Adrive, self.Qm) (Z, ng) = y U = np.insert(np.diff(Z) / np.diff(t), 0, 0.0) tcomp = time.time() - tstart logger.debug('completed in %ss', si_format(tcomp, 1)) # Store dataframe and metadata df = pd.DataFrame({'t': t, 'states': states, 'U': U, 'Z': Z, 'ng': ng}) meta = {'a': self.bls.a, 'd': self.bls.d, 'Cm0': self.bls.Cm0, 'Qm0': self.bls.Qm0, 'Fdrive': self.Fdrive, 'Adrive': self.Adrive, 'phi': np.pi, 'Qm': self.Qm, 'tcomp': tcomp} # Export into to PKL file output_filepath = '{}/{}.pkl'.format(self.batch_dir, simcode) with open(output_filepath, 'wb') as fh: pickle.dump({'meta': meta, 'data': df}, fh) logger.debug('simulation data exported to "%s"', output_filepath) # Compute key output metrics Zmax = np.amax(Z) Zmin = np.amin(Z) Zabs_max = np.amax(np.abs([Zmin, Zmax])) eAmax = self.bls.arealstrain(Zabs_max) Tmax = self.bls.TEtot(Zabs_max) Pmmax = self.bls.PMavgpred(Zmin) ngmax = np.amax(ng) dUdtmax = np.amax(np.abs(np.diff(U) / np.diff(t)**2)) # Export key metrics to log file log = { 'A': date_str, 'B': daytime_str, 'C': self.bls.a * 1e9, 'D': self.bls.d * 1e6, 'E': self.Fdrive * 1e-3, 'F': self.Adrive * 1e-3, 'G': self.Qm * 1e5, 'H': t.size, 'I': tcomp, 'J': self.bls.kA + self.bls.kA_tissue, 'K': Zmax * 1e9, 'L': eAmax, 'M': Tmax * 1e3, 'N': (ngmax - self.bls.ng0) / self.bls.ng0, 'O': Pmmax * 1e-3, 'P': dUdtmax } if xlslog(self.log_filepath, 'Data', log) == 1: logger.info('log exported to "%s"', self.log_filepath) else: logger.error('log export to "%s" aborted', self.log_filepath) return output_filepath except (Warning, AssertionError) as inst: logger.warning('Integration error: %s. Continue batch? (y/n)', extra={inst}) user_str = input() if user_str not in ['y', 'Y']: return -1 def __str__(self): return 'Mechanical simulation {}/{}: a = {}m, d = {}m, f = {}Hz, A = {}Pa, Q = {}C/cm2'\ .format(self.id, self.nsims, *si_format([self.bls.a, self.bls.d, self.Fdrive], 1, space=' '), *si_format([self.Adrive, self.Qm * 1e-4], 2, space=' ')) class EStimTitrator(): ''' Worker class that uses a dichotomic recursive search to determine the threshold value of a specific electric stimulation parameter needed to obtain neural excitation, keeping all other parameters fixed. The titration parameter can be stimulation amplitude, duration or any variable for which the number of spikes is a monotonically increasing function. ''' def __init__(self, wid, solver, neuron, Astim, tstim, toffset, PRF, DC, nsims=1): ''' Class constructor. :param wid: worker ID :param solver: SolverElec object :param neuron: neuron object :param Astim: injected current density amplitude (mA/m2) :param tstim: duration of US stimulation (s) :param toffset: duration of the offset (s) :param PRF: pulse repetition frequency (Hz) :param DC: pulse duty cycle (-) :param nsims: total number or simulations ''' self.id = wid self.solver = solver self.neuron = neuron self.Astim = Astim self.tstim = tstim self.toffset = toffset self.PRF = PRF self.DC = DC self.nsims = nsims # Determine titration type if Astim is None: self.t_type = 'A' self.unit = 'A/m2' self.factor = 1e-3 self.interval = (0., 2 * TITRATION_ESTIM_A_MAX) self.thr = TITRATION_ESTIM_DA_MAX self.maxval = TITRATION_ESTIM_A_MAX elif tstim is None: self.t_type = 't' self.unit = 's' self.factor = 1 self.interval = (0., 2 * TITRATION_T_MAX) self.thr = TITRATION_DT_THR self.maxval = TITRATION_T_MAX elif DC is None: self.t_type = 'DC' self.unit = '%' self.factor = 1e2 self.interval = (0., 2 * TITRATION_DC_MAX) self.thr = TITRATION_DDC_THR self.maxval = TITRATION_DC_MAX else: print('Error: Invalid titration type') self.t0 = None def __call__(self): ''' Method running the titration, called recursively until an accurate threshold is found. :return: 5-tuple with the determined threshold, time profile, solution matrix, state vector and response latency ''' if self.t0 is None: self.t0 = time.time() # Define current value value = (self.interval[0] + self.interval[1]) / 2 # Define stimulation parameters if self.t_type == 'A': stim_params = [value, self.tstim, self.toffset, self.PRF, self.DC] elif self.t_type == 't': stim_params = [self.Astim, value, self.toffset, self.PRF, self.DC] elif self.t_type == 'DC': stim_params = [self.Astim, self.tstim, self.toffset, self.PRF, value] # Run simulation and detect spikes (t, y, states) = self.solver.run(self.neuron, *stim_params) dt = t[1] - t[0] ipeaks, *_ = findPeaks(y[0, :], SPIKE_MIN_VAMP, int(np.ceil(SPIKE_MIN_DT / dt)), SPIKE_MIN_VPROM) n_spikes = ipeaks.size latency = t[ipeaks[0]] if n_spikes > 0 else None print('{}{} ---> {} spike{} detected'.format(si_format(value * self.factor, 2, space=' '), self.unit, n_spikes, "s" if n_spikes > 1 else "")) # If accurate threshold is found, return simulation results if (self.interval[1] - self.interval[0]) <= self.thr and n_spikes == 1: tcomp = time.time() - self.t0 print('completed in {:.2f} s, threshold = {}{}' .format(tcomp, si_format(value * self.factor, 2, space=' '), self.unit)) return (value, t, y, states, latency, tcomp) # Otherwise, refine titration interval and iterate recursively else: if n_spikes == 0: # if value too close to max then stop if (self.maxval - value) <= self.thr: logger.warning('no spikes detected within titration interval') return (np.nan, t, y, states, latency) self.interval = (value, self.interval[1]) else: self.interval = (self.interval[0], value) return self.__call__() def __str__(self): params = [self.Astim, self.tstim, self.PRF, self.DC] punits = {'A': 'A/m2', 't': 's', 'PRF': 'Hz', 'DC': '%'} if self.Astim is not None: params[0] *= 1e-3 if self.DC is not None: params[3] *= 1e2 pnames = list(punits.keys()) ittr = params.index(None) del params[ittr] del pnames[ittr] log_str = ', '.join(['{} = {}{}'.format(pname, si_format(param, 2, space=' '), punits[pname]) for pname, param in zip(pnames, params)]) return '{} neuron - E-STIM titration {}/{} ({})'\ .format(self.neuron.name, self.id, self.nsims, log_str) class AStimTitrator(): ''' Worker class that uses a dichotomic recursive search to determine the threshold value of a specific acoustic stimulation parameter needed to obtain neural excitation, keeping all other parameters fixed. The titration parameter can be stimulation amplitude, duration or any variable for which the number of spikes is a monotonically increasing function. ''' def __init__(self, wid, solver, neuron, Fdrive, Adrive, tstim, toffset, PRF, DC, int_method='effective', nsims=1): ''' Class constructor. :param wid: worker ID :param solver: SolverUS object :param neuron: neuron object :param Fdrive: acoustic drive frequency (Hz) :param Adrive: acoustic drive amplitude (Pa) :param tstim: duration of US stimulation (s) :param toffset: duration of the offset (s) :param PRF: pulse repetition frequency (Hz) :param DC: pulse duty cycle (-) :param int_method: selected integration method :param nsims: total number or simulations ''' self.id = wid self.solver = solver self.neuron = neuron self.Fdrive = Fdrive self.Adrive = Adrive self.tstim = tstim self.toffset = toffset self.PRF = PRF self.DC = DC self.int_method = int_method self.nsims = nsims # Determine titration type if Adrive is None: self.t_type = 'A' self.unit = 'Pa' self.factor = 1 self.interval = (0., 2 * TITRATION_ASTIM_A_MAX) self.thr = TITRATION_ASTIM_DA_MAX self.maxval = TITRATION_ASTIM_A_MAX elif tstim is None: self.t_type = 't' self.unit = 's' self.factor = 1 self.interval = (0., 2 * TITRATION_T_MAX) self.thr = TITRATION_DT_THR self.maxval = TITRATION_T_MAX elif DC is None: self.t_type = 'DC' self.unit = '%' self.factor = 1e2 self.interval = (0., 2 * TITRATION_DC_MAX) self.thr = TITRATION_DDC_THR self.maxval = TITRATION_DC_MAX else: print('Error: Invalid titration type') self.t0 = None def __call__(self): ''' Method running the titration, called recursively until an accurate threshold is found. :return: 5-tuple with the determined threshold, time profile, solution matrix, state vector and response latency ''' if self.t0 is None: self.t0 = time.time() # Define current value value = (self.interval[0] + self.interval[1]) / 2 # Define stimulation parameters if self.t_type == 'A': stim_params = [self.Fdrive, value, self.tstim, self.toffset, self.PRF, self.DC] elif self.t_type == 't': stim_params = [self.Fdrive, self.Adrive, value, self.toffset, self.PRF, self.DC] elif self.t_type == 'DC': stim_params = [self.Fdrive, self.Adrive, self.tstim, self.toffset, self.PRF, value] # Run simulation and detect spikes (t, y, states) = self.solver.run(self.neuron, *stim_params, self.int_method) dt = t[1] - t[0] ipeaks, *_ = findPeaks(y[2, :], SPIKE_MIN_QAMP, int(np.ceil(SPIKE_MIN_DT / dt)), SPIKE_MIN_QPROM) n_spikes = ipeaks.size latency = t[ipeaks[0]] if n_spikes > 0 else None print('{}{} ---> {} spike{} detected'.format(si_format(value * self.factor, 2, space=' '), self.unit, n_spikes, "s" if n_spikes > 1 else "")) # If accurate threshold is found, return simulation results if (self.interval[1] - self.interval[0]) <= self.thr and n_spikes == 1: tcomp = time.time() - self.t0 print('completed in {:.2f} s, threshold = {}{}' .format(tcomp, si_format(value * self.factor, 2, space=' '), self.unit)) return (value, t, y, states, latency, tcomp) # Otherwise, refine titration interval and iterate recursively else: if n_spikes == 0: # if value too close to max then stop if (self.maxval - value) <= self.thr: logger.warning('no spikes detected within titration interval') return (np.nan, t, y, states, latency) self.interval = (value, self.interval[1]) else: self.interval = (self.interval[0], value) return self.__call__() def __str__(self): params = [self.Fdrive, self.Adrive, self.tstim, self.PRF, self.DC] punits = {'f': 'Hz', 'A': 'A/m2', 't': 's', 'PRF': 'Hz', 'DC': '%'} if self.Adrive is not None: params[1] *= 1e-3 if self.DC is not None: params[4] *= 1e2 pnames = list(punits.keys()) ittr = params.index(None) del params[ittr] del pnames[ittr] log_str = ', '.join(['{} = {}{}'.format(pname, si_format(param, 2, space=' '), punits[pname]) for pname, param in zip(pnames, params)]) return '{} neuron - A-STIM titration {}/{} ({})'\ .format(self.neuron.name, self.id, self.nsims, log_str) def setBatchDir(): ''' Select batch directory for output files.α :return: full path to batch directory ''' root = tk.Tk() root.withdraw() batch_dir = filedialog.askdirectory() if not batch_dir: raise InputError('No output directory chosen') return batch_dir def checkBatchLog(batch_dir, batch_type): ''' Check for appropriate log file in batch directory, and create one if it is absent. :param batch_dir: full path to batch directory :param batch_type: type of simulation batch :return: 2 tuple with full path to log file and boolean stating if log file was created ''' # Check for directory existence if not os.path.isdir(batch_dir): raise InputError('"{}" output directory does not exist'.format(batch_dir)) # Determine log template from batch type if batch_type == 'MECH': logfile = 'log_MECH.xlsx' elif batch_type == 'A-STIM': logfile = 'log_ASTIM.xlsx' elif batch_type == 'E-STIM': logfile = 'log_ESTIM.xlsx' else: raise InputError('Unknown batch type', batch_type) # Get template in package subdirectory this_dir, _ = os.path.split(__file__) parent_dir = os.path.abspath(os.path.join(this_dir, os.pardir)) logsrc = parent_dir + '/templates/' + logfile assert os.path.isfile(logsrc), 'template log file "{}" not found'.format(logsrc) # Copy template in batch directory if no appropriate log file logdst = batch_dir + '/' + logfile is_log = os.path.isfile(logdst) if not is_log: shutil.copy2(logsrc, logdst) return (logdst, not is_log) def createSimQueue(amps, durations, offsets, PRFs, DCs): ''' Create a serialized 2D array of all parameter combinations for a series of individual parameter sweeps, while avoiding repetition of CW protocols for a given PRF sweep. :param amps: list (or 1D-array) of acoustic amplitudes :param durations: list (or 1D-array) of stimulus durations :param offsets: list (or 1D-array) of stimulus offsets (paired with durations array) :param PRFs: list (or 1D-array) of pulse-repetition frequencies :param DCs: list (or 1D-array) of duty cycle values :return: 2D-array with (amplitude, duration, offset, PRF, DC) for each stimulation protocol ''' # Convert input to 1D-arrays amps = np.array(amps) durations = np.array(durations) offsets = np.array(offsets) PRFs = np.array(PRFs) DCs = np.array(DCs) # Create index arrays iamps = range(len(amps)) idurs = range(len(durations)) # Create empty output matrix queue = np.empty((1, 5)) # Continuous protocols if 1.0 in DCs: nCW = len(amps) * len(durations) arr1 = np.ones(nCW) iCW_queue = np.array(np.meshgrid(iamps, idurs)).T.reshape(nCW, 2) CW_queue = np.vstack((amps[iCW_queue[:, 0]], durations[iCW_queue[:, 1]], offsets[iCW_queue[:, 1]], PRFs.min() * arr1, arr1)).T queue = np.vstack((queue, CW_queue)) # Pulsed protocols if np.any(DCs != 1.0): pulsed_DCs = DCs[DCs != 1.0] iPRFs = range(len(PRFs)) ipulsed_DCs = range(len(pulsed_DCs)) nPW = len(amps) * len(durations) * len(PRFs) * len(pulsed_DCs) iPW_queue = np.array(np.meshgrid(iamps, idurs, iPRFs, ipulsed_DCs)).T.reshape(nPW, 4) PW_queue = np.vstack((amps[iPW_queue[:, 0]], durations[iPW_queue[:, 1]], offsets[iPW_queue[:, 1]], PRFs[iPW_queue[:, 2]], pulsed_DCs[iPW_queue[:, 3]])).T queue = np.vstack((queue, PW_queue)) # Return return queue[1:, :] def xlslog(filename, sheetname, data): """ Append log data on a new row to specific sheet of excel workbook, using a lockfile to avoid read/write errors between concurrent processes. :param filename: absolute or relative path to the Excel workbook :param sheetname: name of the Excel spreadsheet to which data is appended :param data: data structure to be added to specific columns on a new row :return: boolean indicating success (1) or failure (0) of operation """ try: lock = lockfile.FileLock(filename) lock.acquire() wb = load_workbook(filename) ws = wb[sheetname] keys = data.keys() i = 1 row_data = {} for k in keys: row_data[k] = data[k] i += 1 ws.append(row_data) wb.save(filename) lock.release() return 1 except PermissionError: # If file cannot be accessed for writing because already opened logger.warning('Cannot write to "%s". Close the file and type "Y"', filename) user_str = input() if user_str in ['y', 'Y']: return xlslog(filename, sheetname, data) else: return 0 def detectPeaks(x, mph=None, mpd=1, threshold=0, edge='rising', kpsh=False, valley=False, ax=None): ''' Detect peaks in data based on their amplitude and other features. Adapted from Marco Duarte: http://nbviewer.jupyter.org/github/demotu/BMC/blob/master/notebooks/DetectPeaks.ipynb :param x: 1D array_like data. :param mph: minimum peak height (default = None). :param mpd: minimum peak distance in indexes (default = 1) :param threshold : minimum peak prominence (default = 0) :param edge : for a flat peak, keep only the rising edge ('rising'), only the falling edge ('falling'), both edges ('both'), or don't detect a flat peak (None). (default = 'rising') :param kpsh: keep peaks with same height even if they are closer than `mpd` (default = False). :param valley: detect valleys (local minima) instead of peaks (default = False). :param show: plot data in matplotlib figure (default = False). :param ax: a matplotlib.axes.Axes instance, optional (default = None). :return: 1D array with the indices of the peaks ''' print('min peak height:', mph, ', min peak distance:', mpd, ', min peak prominence:', threshold) # Convert input to numpy array x = np.atleast_1d(x).astype('float64') # Revert signal sign for valley detection if valley: x = -x # Differentiate signal dx = np.diff(x) # Find indices of all peaks with edge criterion ine, ire, ife = np.array([[], [], []], dtype=int) if not edge: ine = np.where((np.hstack((dx, 0)) < 0) & (np.hstack((0, dx)) > 0))[0] else: if edge.lower() in ['rising', 'both']: ire = np.where((np.hstack((dx, 0)) <= 0) & (np.hstack((0, dx)) > 0))[0] if edge.lower() in ['falling', 'both']: ife = np.where((np.hstack((dx, 0)) < 0) & (np.hstack((0, dx)) >= 0))[0] ind = np.unique(np.hstack((ine, ire, ife))) # Remove first and last values of x if they are detected as peaks if ind.size and ind[0] == 0: ind = ind[1:] if ind.size and ind[-1] == x.size - 1: ind = ind[:-1] print('{} raw peaks'.format(ind.size)) # Remove peaks < minimum peak height if ind.size and mph is not None: ind = ind[x[ind] >= mph] print('{} height-filtered peaks'.format(ind.size)) # Remove peaks - neighbors < threshold if ind.size and threshold > 0: dx = np.min(np.vstack([x[ind] - x[ind - 1], x[ind] - x[ind + 1]]), axis=0) ind = np.delete(ind, np.where(dx < threshold)[0]) print('{} prominence-filtered peaks'.format(ind.size)) # Detect small peaks closer than minimum peak distance if ind.size and mpd > 1: ind = ind[np.argsort(x[ind])][::-1] # sort ind by peak height idel = np.zeros(ind.size, dtype=bool) for i in range(ind.size): if not idel[i]: # keep peaks with the same height if kpsh is True idel = idel | (ind >= ind[i] - mpd) & (ind <= ind[i] + mpd) \ & (x[ind[i]] > x[ind] if kpsh else True) idel[i] = 0 # Keep current peak # remove the small peaks and sort back the indices by their occurrence ind = np.sort(ind[~idel]) print('{} distance-filtered peaks'.format(ind.size)) return ind def detectPeaksTime(t, y, mph, mtd, mpp=0): """ Extension of the detectPeaks function to detect peaks in data based on their amplitude and time difference, with a non-uniform time vector. :param t: time vector (not necessarily uniform) :param y: signal :param mph: minimal peak height :param mtd: minimal time difference :mpp: minmal peak prominence :return: array of peak indexes """ # Determine whether time vector is uniform (threshold in time step variation) dt = np.diff(t) if (dt.max() - dt.min()) / dt.min() < 1e-2: isuniform = True else: isuniform = False if isuniform: print('uniform time vector') dt = t[1] - t[0] mpd = int(np.ceil(mtd / dt)) ipeaks = detectPeaks(y, mph, mpd=mpd, threshold=mpp) else: print('non-uniform time vector') # Detect peaks on signal with no restriction on inter-peak distance irawpeaks = detectPeaks(y, mph, mpd=1, threshold=mpp) npeaks = irawpeaks.size if npeaks > 0: # Filter relevant peaks with temporal distance ipeaks = [irawpeaks[0]] for i in range(1, npeaks): i1 = ipeaks[-1] i2 = irawpeaks[i] if t[i2] - t[i1] < mtd: if y[i2] > y[i1]: ipeaks[-1] = i2 else: ipeaks.append(i2) else: ipeaks = [] ipeaks = np.array(ipeaks) return ipeaks def detectSpikes(t, Qm, min_amp, min_dt): ''' Detect spikes on a charge density signal, and return their number, latency and rate. :param t: time vector (s) :param Qm: charge density vector (C/m2) :param min_amp: minimal charge amplitude to detect spikes (C/m2) :param min_dt: minimal time interval between 2 spikes (s) :return: 3-tuple with number of spikes, latency (s) and spike rate (sp/s) ''' i_spikes = detectPeaksTime(t, Qm, min_amp, min_dt) if len(i_spikes) > 0: latency = t[i_spikes[0]] # s n_spikes = i_spikes.size if n_spikes > 1: first_to_last_spike = t[i_spikes[-1]] - t[i_spikes[0]] # s spike_rate = (n_spikes - 1) / first_to_last_spike # spikes/s else: spike_rate = 'N/A' else: latency = 'N/A' spike_rate = 'N/A' n_spikes = 0 return (n_spikes, latency, spike_rate) def findPeaks(y, mph=None, mpd=None, mpp=None): ''' Detect peaks in a signal based on their height, prominence and/or separating distance. :param y: signal vector :param mph: minimum peak height (in signal units, default = None). :param mpd: minimum inter-peak distance (in indexes, default = None) :param mpp: minimum peak prominence (in signal units, default = None) :return: 4-tuple of arrays with the indexes of peaks occurence, peaks prominence, peaks width at half-prominence and peaks half-prominence bounds (left and right) Adapted from: - Marco Duarte's detect_peaks function (http://nbviewer.jupyter.org/github/demotu/BMC/blob/master/notebooks/DetectPeaks.ipynb) - MATLAB findpeaks function (https://ch.mathworks.com/help/signal/ref/findpeaks.html) ''' # Define empty output empty = (np.array([]),) * 4 # Find all peaks and valleys dy = np.diff(y) s = np.sign(dy) ipeaks = np.where(np.diff(s) < 0)[0] + 1 ivalleys = np.where(np.diff(s) > 0)[0] + 1 # Return empty output if no peak detected if ipeaks.size == 0: return empty # Ensure each peak is bounded by two valleys, adding signal boundaries as valleys if necessary if ivalleys.size == 0 or ipeaks[0] < ivalleys[0]: ivalleys = np.insert(ivalleys, 0, 0) if ipeaks[-1] > ivalleys[-1]: ivalleys = np.insert(ivalleys, ivalleys.size, y.size - 1) # assert ivalleys.size - ipeaks.size == 1, 'Number of peaks and valleys not matching' if ivalleys.size - ipeaks.size != 1: logger.warning('detection incongruity: %u peaks vs. %u valleys detected', ipeaks.size, ivalleys.size) return empty # Remove peaks < minimum peak height if mph is not None: ipeaks = ipeaks[y[ipeaks] >= mph] if ipeaks.size == 0: return empty # Detect small peaks closer than minimum peak distance if mpd is not None: ipeaks = ipeaks[np.argsort(y[ipeaks])][::-1] # sort ipeaks by descending peak height idel = np.zeros(ipeaks.size, dtype=bool) # initialize boolean deletion array (all false) for i in range(ipeaks.size): # for each peak if not idel[i]: # if not marked for deletion closepeaks = (ipeaks >= ipeaks[i] - mpd) & (ipeaks <= ipeaks[i] + mpd) # close peaks idel = idel | closepeaks # mark for deletion along with previously marked peaks # idel = idel | (ipeaks >= ipeaks[i] - mpd) & (ipeaks <= ipeaks[i] + mpd) idel[i] = 0 # keep current peak # remove the small peaks and sort back the indices by their occurrence ipeaks = np.sort(ipeaks[~idel]) # Detect smallest valleys between consecutive relevant peaks ibottomvalleys = [] if ipeaks[0] > ivalleys[0]: itrappedvalleys = ivalleys[ivalleys < ipeaks[0]] ibottomvalleys.append(itrappedvalleys[np.argmin(y[itrappedvalleys])]) for i, j in zip(ipeaks[:-1], ipeaks[1:]): itrappedvalleys = ivalleys[np.logical_and(ivalleys > i, ivalleys < j)] ibottomvalleys.append(itrappedvalleys[np.argmin(y[itrappedvalleys])]) if ipeaks[-1] < ivalleys[-1]: itrappedvalleys = ivalleys[ivalleys > ipeaks[-1]] ibottomvalleys.append(itrappedvalleys[np.argmin(y[itrappedvalleys])]) ipeaks = ipeaks ivalleys = np.array(ibottomvalleys, dtype=int) # Ensure each peak is bounded by two valleys, adding signal boundaries as valleys if necessary if ipeaks[0] < ivalleys[0]: ivalleys = np.insert(ivalleys, 0, 0) if ipeaks[-1] > ivalleys[-1]: ivalleys = np.insert(ivalleys, ivalleys.size, y.size - 1) # Remove peaks < minimum peak prominence if mpp is not None: # Compute peaks prominences as difference between peaks and their closest valley prominences = y[ipeaks] - np.amax((y[ivalleys[:-1]], y[ivalleys[1:]]), axis=0) # initialize peaks and valleys deletion tables idelp = np.zeros(ipeaks.size, dtype=bool) idelv = np.zeros(ivalleys.size, dtype=bool) # for each peak (sorted by ascending prominence order) for ind in np.argsort(prominences): ipeak = ipeaks[ind] # get peak index # get peak bases as first valleys on either side not marked for deletion indleftbase = ind indrightbase = ind + 1 while idelv[indleftbase]: indleftbase -= 1 while idelv[indrightbase]: indrightbase += 1 ileftbase = ivalleys[indleftbase] irightbase = ivalleys[indrightbase] # Compute peak prominence and mark for deletion if < mpp indmaxbase = indleftbase if y[ileftbase] > y[irightbase] else indrightbase if y[ipeak] - y[ivalleys[indmaxbase]] < mpp: idelp[ind] = True # mark peak for deletion idelv[indmaxbase] = True # mark highest surrouding valley for deletion # remove irrelevant peaks and valleys, and sort back the indices by their occurrence ipeaks = np.sort(ipeaks[~idelp]) ivalleys = np.sort(ivalleys[~idelv]) if ipeaks.size == 0: return empty # Compute peaks prominences and reference half-prominence levels prominences = y[ipeaks] - np.amax((y[ivalleys[:-1]], y[ivalleys[1:]]), axis=0) refheights = y[ipeaks] - prominences / 2 # Compute half-prominence bounds ibounds = np.empty((ipeaks.size, 2)) for i in range(ipeaks.size): # compute the index of the left-intercept at half max ileft = ipeaks[i] while ileft >= ivalleys[i] and y[ileft] > refheights[i]: ileft -= 1 if ileft < ivalleys[i]: # intercept exactly on valley ibounds[i, 0] = ivalleys[i] else: # interpolate intercept linearly between signal boundary points a = (y[ileft + 1] - y[ileft]) / 1 b = y[ileft] - a * ileft ibounds[i, 0] = (refheights[i] - b) / a # compute the index of the right-intercept at half max iright = ipeaks[i] while iright <= ivalleys[i + 1] and y[iright] > refheights[i]: iright += 1 if iright > ivalleys[i + 1]: # intercept exactly on valley ibounds[i, 1] = ivalleys[i + 1] else: # interpolate intercept linearly between signal boundary points if iright == y.size - 1: # special case: if end of signal is reached, decrement iright iright -= 1 a = (y[iright + 1] - y[iright]) / 1 b = y[iright] - a * iright ibounds[i, 1] = (refheights[i] - b) / a # Compute peaks widths at half-prominence widths = np.diff(ibounds, axis=1) return (ipeaks, prominences, widths, ibounds) def runMechBatch(batch_dir, log_filepath, Cm0, Qm0, stim_params, a, d=0.0, multiprocess=False): ''' Run batch simulations of the mechanical system with imposed values of charge density, for various sonophore spans and stimulation parameters. :param batch_dir: full path to output directory of batch :param log_filepath: full path log file of batch :param Cm0: membrane resting capacitance (F/m2) :param Qm0: membrane resting charge density (C/m2) :param stim_params: dictionary containing sweeps for all stimulation parameters :param a: BLS in-plane diameter (m) :param d: depth of embedding tissue around plasma membrane (m) :param multiprocess: boolean statting wether or not to use multiprocessing ''' # Checking validity of stimulation parameters mandatory_params = ['freqs', 'amps', 'charges'] for mparam in mandatory_params: if mparam not in stim_params: raise InputError('Missing stimulation parameter field: "{}"'.format(mparam)) logger.info("Starting mechanical simulation batch") # Unpack stimulation parameters amps = np.array(stim_params['amps']) charges = np.array(stim_params['charges']) # Generate simulations queue nA = len(amps) nQ = len(charges) sim_queue = np.array(np.meshgrid(amps, charges)).T.reshape(nA * nQ, 2) nqueue = sim_queue.shape[0] nsims = len(stim_params['freqs']) * nqueue # Initiate multiple processing objects if needed if multiprocess: mp.freeze_support() # Create tasks and results queues tasks = mp.JoinableQueue() results = mp.Queue() # Create and start consumer processes nconsumers = min(mp.cpu_count(), nsims) consumers = [Consumer(tasks, results) for i in range(nconsumers)] for w in consumers: w.start() # Run simulations wid = 0 filepaths = [] for Fdrive in stim_params['freqs']: # Create BilayerSonophore instance (modulus of embedding tissue depends on frequency) bls = BilayerSonophore(a, Fdrive, Cm0, Qm0, d) for i in range(nqueue): wid += 1 Adrive, Qm = sim_queue[i, :] worker = MechWorker(wid, batch_dir, log_filepath, bls, Fdrive, Adrive, Qm, nsims) if multiprocess: tasks.put(worker, block=False) else: logger.info('%s', worker) output_filepath = worker.__call__() filepaths.append(output_filepath) if multiprocess: # Stop processes for i in range(nconsumers): tasks.put(None, block=False) tasks.join() # Retrieve workers output for x in range(nsims): output_filepath = results.get() filepaths.append(output_filepath) # Close tasks and results queues tasks.close() results.close() return filepaths def runEStimBatch(batch_dir, log_filepath, neurons, stim_params, multiprocess=False): ''' Run batch E-STIM simulations of the system for various neuron types and stimulation parameters. :param batch_dir: full path to output directory of batch :param log_filepath: full path log file of batch :param neurons: list of neurons names :param stim_params: dictionary containing sweeps for all stimulation parameters :param multiprocess: boolean statting wether or not to use multiprocessing :return: list of full paths to the output files ''' mandatory_params = ['amps', 'durations', 'offsets', 'PRFs', 'DCs'] for mparam in mandatory_params: if mparam not in stim_params: raise InputError('Missing stimulation parameter field: "{}"'.format(mparam)) logger.info("Starting E-STIM simulation batch") # Generate simulations queue sim_queue = createSimQueue(stim_params['amps'], stim_params['durations'], stim_params['offsets'], stim_params['PRFs'], stim_params['DCs']) nqueue = sim_queue.shape[0] nsims = len(neurons) * nqueue # Initialize solver solver = SolverElec() # Initiate multiple processing objects if needed if multiprocess: mp.freeze_support() # Create tasks and results queues tasks = mp.JoinableQueue() results = mp.Queue() # Create and start consumer processes nconsumers = min(mp.cpu_count(), nsims) consumers = [Consumer(tasks, results) for i in range(nconsumers)] for w in consumers: w.start() # Run simulations wid = 0 filepaths = [] for nname in neurons: neuron = getNeuronsDict()[nname]() # Run simulations in queue for i in range(nqueue): wid += 1 Astim, tstim, toffset, PRF, DC = sim_queue[i, :] worker = EStimWorker(wid, batch_dir, log_filepath, solver, neuron, Astim, tstim, toffset, PRF, DC, nsims) if multiprocess: tasks.put(worker, block=False) else: logger.info('%s', worker) output_filepath = worker.__call__() filepaths.append(output_filepath) if multiprocess: # Stop processes for i in range(nconsumers): tasks.put(None, block=False) tasks.join() # Retrieve workers output for x in range(nsims): output_filepath = results.get() filepaths.append(output_filepath) # Close tasks and results queues tasks.close() results.close() return filepaths def titrateEStimBatch(batch_dir, log_filepath, neurons, stim_params, multiprocess=False): ''' Run batch electrical titrations of the system for various neuron types and stimulation parameters, to determine the threshold of a specific stimulus parameter for neural excitation. :param batch_dir: full path to output directory of batch :param log_filepath: full path log file of batch :param neurons: list of neurons names :param stim_params: dictionary containing sweeps for all stimulation parameters :param multiprocess: boolean statting wether or not to use multiprocessing :return: list of full paths to the output files ''' logger.info("Starting E-STIM titration batch") # Determine titration parameter and titrations list if 'durations' not in stim_params: t_type = 't' sim_queue = createSimQueue(stim_params['amps'], [None], [TITRATION_T_OFFSET], stim_params['PRFs'], stim_params['DCs']) elif 'amps' not in stim_params: t_type = 'A' sim_queue = createSimQueue([None], stim_params['durations'], [TITRATION_T_OFFSET] * len(stim_params['durations']), stim_params['PRFs'], stim_params['DCs']) elif 'DC' not in stim_params: t_type = 'DC' sim_queue = createSimQueue(stim_params['amps'], stim_params['durations'], [TITRATION_T_OFFSET] * len(stim_params['durations']), stim_params['PRFs'], [None]) nqueue = sim_queue.shape[0] # Create SolverElec instance solver = SolverElec() # Initiate multiple processing objects if needed nsims = len(neurons) * nqueue if multiprocess: mp.freeze_support() # Create tasks and results queues tasks = mp.JoinableQueue() results = mp.Queue() # Create and start consumer processes nconsumers = min(mp.cpu_count(), nsims) consumers = [Consumer(tasks, results) for i in range(nconsumers)] for w in consumers: w.start() # Run titrations wid = 0 filepaths = [] for nname in neurons: neuron = getNeuronsDict()[nname]() for i in range(nqueue): wid += 1 # Extract parameters Astim, tstim, toffset, PRF, DC = sim_queue[i, :] # Get date and time info date_str = time.strftime("%Y.%m.%d") daytime_str = time.strftime("%H:%M:%S") try: # Run titration titrator = EStimTitrator(wid, solver, neuron, *sim_queue[i, :], nsims) if multiprocess: tasks.put(titrator, block=False) else: logger.info('%s', titrator) (output_thr, t, y, states, lat, tcomp) = titrator.__call__() Vm, *channels = y # Determine output variable if t_type == 'A': Astim = output_thr elif t_type == 't': tstim = output_thr elif t_type == 'DC': DC = output_thr # Define output naming simcode = 'ESTIM_{}_{}_{:.1f}mA_per_m2_{:.0f}ms{}'\ .format(neuron.name, 'CW' if DC == 1. else 'PW', Astim, tstim * 1e3, '_PRF{:.2f}Hz_DC{:.2f}%'.format(PRF, DC * 1e2) if DC < 1. else '') # Store dataframe and metadata df = pd.DataFrame({'t': t, 'states': states, 'Vm': Vm}) for j in range(len(neuron.states_names)): df[neuron.states_names[j]] = channels[j] meta = {'neuron': neuron.name, 'Astim': Astim, 'tstim': tstim, 'toffset': toffset, 'PRF': PRF, 'DC': DC, 'tcomp': tcomp} # Export into to PKL file output_filepath = '{}/{}.pkl'.format(batch_dir, simcode) with open(output_filepath, 'wb') as fh: pickle.dump({'meta': meta, 'data': df}, fh) logger.info('simulation data exported to "%s"', output_filepath) filepaths.append(output_filepath) # Detect spikes on Qm signal dt = t[1] - t[0] ipeaks, *_ = findPeaks(Vm, SPIKE_MIN_VAMP, int(np.ceil(SPIKE_MIN_DT / dt)), SPIKE_MIN_VPROM) n_spikes = ipeaks.size lat = t[ipeaks[0]] if n_spikes > 0 else 'N/A' sr = np.mean(1 / np.diff(t[ipeaks])) if n_spikes > 1 else 'N/A' logger.info('%u spike%s detected', n_spikes, "s" if n_spikes > 1 else "") # Export key metrics to log file log = { 'A': date_str, 'B': daytime_str, 'C': neuron.name, 'D': Astim, 'E': tstim * 1e3, 'F': PRF * 1e-3 if DC < 1 else 'N/A', 'G': DC, 'H': t.size, 'I': round(tcomp, 2), 'J': n_spikes, 'K': lat * 1e3 if isinstance(lat, float) else 'N/A', 'L': sr * 1e-3 if isinstance(sr, float) else 'N/A' } if xlslog(log_filepath, 'Data', log) == 1: logger.info('log exported to "%s"', log_filepath) else: logger.error('log export to "%s" aborted', log_filepath) except (Warning, AssertionError) as inst: logger.warning('Integration error: %s. Continue batch? (y/n)', extra={inst}) user_str = input() if user_str not in ['y', 'Y']: return filepaths if multiprocess: # Stop processes for i in range(nconsumers): tasks.put(None, block=False) tasks.join() # Retrieve workers output for x in range(nsims): (output_thr, t, y, states, lat, tcomp) = results.get() Vm, *channels = y # Determine output variable if t_type == 'A': Astim = output_thr elif t_type == 't': tstim = output_thr elif t_type == 'DC': DC = output_thr # Define output naming simcode = 'ESTIM_{}_{}_{:.1f}mA_per_m2_{:.0f}ms{}'\ .format(neuron.name, 'CW' if DC == 1. else 'PW', Astim, tstim * 1e3, '_PRF{:.2f}Hz_DC{:.2f}%'.format(PRF, DC * 1e2) if DC < 1. else '') # Store dataframe and metadata df = pd.DataFrame({'t': t, 'states': states, 'Vm': Vm}) for j in range(len(neuron.states_names)): df[neuron.states_names[j]] = channels[j] meta = {'neuron': neuron.name, 'Astim': Astim, 'tstim': tstim, 'toffset': toffset, 'PRF': PRF, 'DC': DC, 'tcomp': tcomp} # Export into to PKL file output_filepath = '{}/{}.pkl'.format(batch_dir, simcode) with open(output_filepath, 'wb') as fh: pickle.dump({'meta': meta, 'data': df}, fh) logger.info('simulation data exported to "%s"', output_filepath) filepaths.append(output_filepath) # Detect spikes on Qm signal dt = t[1] - t[0] ipeaks, *_ = findPeaks(Vm, SPIKE_MIN_VAMP, int(np.ceil(SPIKE_MIN_DT / dt)), SPIKE_MIN_VPROM) n_spikes = ipeaks.size lat = t[ipeaks[0]] if n_spikes > 0 else 'N/A' sr = np.mean(1 / np.diff(t[ipeaks])) if n_spikes > 1 else 'N/A' logger.info('%u spike%s detected', n_spikes, "s" if n_spikes > 1 else "") # Export key metrics to log file log = { 'A': date_str, 'B': daytime_str, 'C': neuron.name, 'D': Astim, 'E': tstim * 1e3, 'F': PRF * 1e-3 if DC < 1 else 'N/A', 'G': DC, 'H': t.size, 'I': round(tcomp, 2), 'J': n_spikes, 'K': lat * 1e3 if isinstance(lat, float) else 'N/A', 'L': sr * 1e-3 if isinstance(sr, float) else 'N/A' } if xlslog(log_filepath, 'Data', log) == 1: logger.info('log exported to "%s"', log_filepath) else: logger.error('log export to "%s" aborted', log_filepath) # Close tasks and results queues tasks.close() results.close() return filepaths def runAStimBatch(batch_dir, log_filepath, neurons, stim_params, a, int_method='effective', multiprocess=False): ''' Run batch simulations of the system for various neuron types, sonophore and stimulation parameters. :param batch_dir: full path to output directory of batch :param log_filepath: full path log file of batch :param neurons: list of neurons names :param stim_params: dictionary containing sweeps for all stimulation parameters :param a: BLS structure diameter (m) :param int_method: selected integration method :param multiprocess: boolean statting wether or not to use multiprocessing :return: list of full paths to the output files ''' mandatory_params = ['freqs', 'amps', 'durations', 'offsets', 'PRFs', 'DCs'] for mparam in mandatory_params: if mparam not in stim_params: raise InputError('Missing stimulation parameter field: "{}"'.format(mparam)) logger.info("Starting A-STIM simulation batch") # Generate simulations queue sim_queue = createSimQueue(stim_params['amps'], stim_params['durations'], stim_params['offsets'], stim_params['PRFs'], stim_params['DCs']) nqueue = sim_queue.shape[0] nsims = len(neurons) * len(stim_params['freqs']) * nqueue # Initiate multiple processing objects if needed if multiprocess: mp.freeze_support() # Create tasks and results queues tasks = mp.JoinableQueue() results = mp.Queue() # Create and start consumer processes nconsumers = min(mp.cpu_count(), nsims) consumers = [Consumer(tasks, results) for i in range(nconsumers)] for w in consumers: w.start() # Run simulations wid = 0 filepaths = [] for nname in neurons: neuron = getNeuronsDict()[nname]() for Fdrive in stim_params['freqs']: # Initialize SolverUS solver = SolverUS(a, neuron, Fdrive) # Run simulations in queue for i in range(nqueue): wid += 1 Adrive, tstim, toffset, PRF, DC = sim_queue[i, :] worker = AStimWorker(wid, batch_dir, log_filepath, solver, neuron, Fdrive, Adrive, tstim, toffset, PRF, DC, int_method, nsims) if multiprocess: tasks.put(worker, block=False) else: logger.info('%s', worker) output_filepath = worker.__call__() filepaths.append(output_filepath) if multiprocess: # Stop processes for i in range(nconsumers): tasks.put(None, block=False) tasks.join() # Retrieve workers output for x in range(nsims): output_filepath = results.get() filepaths.append(output_filepath) # Close tasks and results queues tasks.close() results.close() return filepaths def titrateAStimBatch(batch_dir, log_filepath, neurons, stim_params, a, int_method='effective', multiprocess=False): ''' Run batch acoustic titrations of the system for various neuron types, sonophore and stimulation parameters, to determine the threshold of a specific stimulus parameter for neural excitation. :param batch_dir: full path to output directory of batch :param log_filepath: full path log file of batch :param neurons: list of neurons names :param stim_params: dictionary containing sweeps for all stimulation parameters :param a: BLS structure diameter (m) :param int_method: selected integration method :param multiprocess: boolean statting wether or not to use multiprocessing :return: list of full paths to the output files ''' logger.info("Starting A-STIM titration batch") # Determine titration parameter and titrations list if 'durations' not in stim_params: t_type = 't' sim_queue = createSimQueue(stim_params['amps'], [None], [TITRATION_T_OFFSET], stim_params['PRFs'], stim_params['DCs']) elif 'amps' not in stim_params: t_type = 'A' sim_queue = createSimQueue([None], stim_params['durations'], [TITRATION_T_OFFSET] * len(stim_params['durations']), stim_params['PRFs'], stim_params['DCs']) elif 'DC' not in stim_params: t_type = 'DC' sim_queue = createSimQueue(stim_params['amps'], stim_params['durations'], [TITRATION_T_OFFSET] * len(stim_params['durations']), stim_params['PRFs'], [None]) print(sim_queue) nqueue = sim_queue.shape[0] nsims = len(neurons) * len(stim_params['freqs']) * nqueue print(nsims) # Initialize multiprocessing pobjects if needed if multiprocess: mp.freeze_support() # Create tasks and results queues tasks = mp.JoinableQueue() results = mp.Queue() # Create and start consumer processes nconsumers = min(mp.cpu_count(), nsims) consumers = [Consumer(tasks, results) for i in range(nconsumers)] for w in consumers: w.start() # Run titrations wid = 0 filepaths = [] for nname in neurons: neuron = getNeuronsDict()[nname]() for Fdrive in stim_params['freqs']: # Create SolverUS instance (modulus of embedding tissue depends on frequency) solver = SolverUS(a, neuron, Fdrive) for i in range(nqueue): wid += 1 # Extract parameters Adrive, tstim, toffset, PRF, DC = sim_queue[i, :] # Get date and time info date_str = time.strftime("%Y.%m.%d") daytime_str = time.strftime("%H:%M:%S") # Run titration try: titrator = AStimTitrator(wid, solver, neuron, Fdrive, *sim_queue[i, :], int_method, nsims) if multiprocess: tasks.put(titrator, block=False) else: logger.info('%s', titrator) (output_thr, t, y, states, lat, tcomp) = titrator.__call__() Z, ng, Qm, Vm, *channels = y U = np.insert(np.diff(Z) / np.diff(t), 0, 0.0) # Determine output variable if t_type == 'A': Adrive = output_thr elif t_type == 't': tstim = output_thr elif t_type == 'DC': DC = output_thr # Define output naming simcode = 'ASTIM_{}_{}_{:.0f}nm_{:.0f}kHz_{:.1f}kPa_{:.0f}ms_{}{}'\ .format(neuron.name, 'CW' if DC == 1 else 'PW', solver.a * 1e9, Fdrive * 1e-3, Adrive * 1e-3, tstim * 1e3, 'PRF{:.2f}Hz_DC{:.2f}%_'.format(PRF, DC * 1e2) if DC < 1. else '', int_method) # Store dataframe and metadata df = pd.DataFrame({'t': t, 'states': states, 'U': U, 'Z': Z, 'ng': ng, 'Qm': Qm, 'Vm': Vm}) for j in range(len(neuron.states_names)): df[neuron.states_names[j]] = channels[j] meta = {'neuron': neuron.name, 'a': solver.a, 'd': solver.d, 'Fdrive': Fdrive, 'Adrive': Adrive, 'phi': np.pi, 'tstim': tstim, 'toffset': toffset, 'PRF': PRF, 'DC': DC, 'tcomp': tcomp} # Export into to PKL file output_filepath = '{}/{}.pkl'.format(batch_dir, simcode) with open(output_filepath, 'wb') as fh: pickle.dump({'meta': meta, 'data': df}, fh) logger.debug('simulation data exported to "%s"', output_filepath) filepaths.append(output_filepath) # Detect spikes on Qm signal dt = t[1] - t[0] ipeaks, *_ = findPeaks(Qm, SPIKE_MIN_QAMP, int(np.ceil(SPIKE_MIN_DT / dt)), SPIKE_MIN_QPROM) n_spikes = ipeaks.size lat = t[ipeaks[0]] if n_spikes > 0 else 'N/A' sr = np.mean(1 / np.diff(t[ipeaks])) if n_spikes > 1 else 'N/A' logger.info('%u spike%s detected', n_spikes, "s" if n_spikes > 1 else "") # Export key metrics to log file log = { 'A': date_str, 'B': daytime_str, 'C': neuron.name, 'D': solver.a * 1e9, 'E': solver.d * 1e6, 'F': Fdrive * 1e-3, 'G': Adrive * 1e-3, 'H': tstim * 1e3, 'I': PRF * 1e-3 if DC < 1 else 'N/A', 'J': DC, 'K': int_method, 'L': t.size, 'M': round(tcomp, 2), 'N': n_spikes, 'O': lat * 1e3 if isinstance(lat, float) else 'N/A', 'P': sr * 1e-3 if isinstance(sr, float) else 'N/A' } if xlslog(log_filepath, 'Data', log) == 1: logger.info('log exported to "%s"', log_filepath) else: logger.error('log export to "%s" aborted', log_filepath) except (Warning, AssertionError) as inst: logger.warning('Integration error: %s. Continue batch? (y/n)', extra={inst}) user_str = input() if user_str not in ['y', 'Y']: return filepaths if multiprocess: # Stop processes for i in range(nconsumers): tasks.put(None, block=False) tasks.join() # Retrieve workers output for x in range(nsims): (output_thr, t, y, states, lat, tcomp) = results.get() Z, ng, Qm, Vm, *channels = y U = np.insert(np.diff(Z) / np.diff(t), 0, 0.0) # Determine output variable if t_type == 'A': Adrive = output_thr elif t_type == 't': tstim = output_thr elif t_type == 'DC': DC = output_thr # Define output naming simcode = 'ASTIM_{}_{}_{:.0f}nm_{:.0f}kHz_{:.1f}kPa_{:.0f}ms_{}{}'\ .format(neuron.name, 'CW' if DC == 1 else 'PW', solver.a * 1e9, Fdrive * 1e-3, Adrive * 1e-3, tstim * 1e3, 'PRF{:.2f}Hz_DC{:.2f}%_'.format(PRF, DC * 1e2) if DC < 1. else '', int_method) # Store dataframe and metadata df = pd.DataFrame({'t': t, 'states': states, 'U': U, 'Z': Z, 'ng': ng, 'Qm': Qm, 'Vm': Vm}) for j in range(len(neuron.states_names)): df[neuron.states_names[j]] = channels[j] meta = {'neuron': neuron.name, 'a': solver.a, 'd': solver.d, 'Fdrive': Fdrive, 'Adrive': Adrive, 'phi': np.pi, 'tstim': tstim, 'toffset': toffset, 'PRF': PRF, 'DC': DC, 'tcomp': tcomp} # Export into to PKL file output_filepath = '{}/{}.pkl'.format(batch_dir, simcode) with open(output_filepath, 'wb') as fh: pickle.dump({'meta': meta, 'data': df}, fh) logger.debug('simulation data exported to "%s"', output_filepath) filepaths.append(output_filepath) # Detect spikes on Qm signal dt = t[1] - t[0] ipeaks, *_ = findPeaks(Qm, SPIKE_MIN_QAMP, int(np.ceil(SPIKE_MIN_DT / dt)), SPIKE_MIN_QPROM) n_spikes = ipeaks.size lat = t[ipeaks[0]] if n_spikes > 0 else 'N/A' sr = np.mean(1 / np.diff(t[ipeaks])) if n_spikes > 1 else 'N/A' logger.info('%u spike%s detected', n_spikes, "s" if n_spikes > 1 else "") # Export key metrics to log file log = { 'A': date_str, 'B': daytime_str, 'C': neuron.name, 'D': solver.a * 1e9, 'E': solver.d * 1e6, 'F': Fdrive * 1e-3, 'G': Adrive * 1e-3, 'H': tstim * 1e3, 'I': PRF * 1e-3 if DC < 1 else 'N/A', 'J': DC, 'K': int_method, 'L': t.size, 'M': round(tcomp, 2), 'N': n_spikes, 'O': lat * 1e3 if isinstance(lat, float) else 'N/A', 'P': sr * 1e-3 if isinstance(sr, float) else 'N/A' } if xlslog(log_filepath, 'Data', log) == 1: logger.info('log exported to "%s"', log_filepath) else: logger.error('log export to "%s" aborted', log_filepath) return filepaths def computeSpikeMetrics(filenames): ''' Analyze the charge density profile from a list of files and compute for each one of them the following spiking metrics: - latency (ms) - firing rate mean and standard deviation (Hz) - spike amplitude mean and standard deviation (nC/cm2) - spike width mean and standard deviation (ms) :param filenames: list of files to analyze :return: a dataframe with the computed metrics ''' # Initialize metrics dictionaries keys = [ 'latencies (ms)', 'mean firing rates (Hz)', 'std firing rates (Hz)', 'mean spike amplitudes (nC/cm2)', 'std spike amplitudes (nC/cm2)', 'mean spike widths (ms)', 'std spike widths (ms)' ] metrics = {k: [] for k in keys} # Compute spiking metrics for fname in filenames: # Load data from file logger.debug('loading data from file "{}"'.format(fname)) with open(fname, 'rb') as fh: frame = pickle.load(fh) df = frame['data'] meta = frame['meta'] tstim = meta['tstim'] t = df['t'].values Qm = df['Qm'].values dt = t[1] - t[0] # Detect spikes on charge profile mpd = int(np.ceil(SPIKE_MIN_DT / dt)) ispikes, prominences, widths, _ = findPeaks(Qm, SPIKE_MIN_QAMP, mpd, SPIKE_MIN_QPROM) widths *= dt if ispikes.size > 0: # Compute latency latency = t[ispikes[0]] # Select prior-offset spikes ispikes_prior = ispikes[t[ispikes] < tstim] else: latency = np.nan ispikes_prior = np.array([]) # Compute spikes widths and amplitude if ispikes_prior.size > 0: widths_prior = widths[:ispikes_prior.size] prominences_prior = prominences[:ispikes_prior.size] else: widths_prior = np.array([np.nan]) prominences_prior = np.array([np.nan]) # Compute inter-spike intervals and firing rates if ispikes_prior.size > 1: ISIs_prior = np.diff(t[ispikes_prior]) FRs_prior = 1 / ISIs_prior else: ISIs_prior = np.array([np.nan]) FRs_prior = np.array([np.nan]) # Log spiking metrics logger.debug('%u spikes detected (%u prior to offset)', ispikes.size, ispikes_prior.size) logger.debug('latency: %.2f ms', latency * 1e3) logger.debug('average spike width within stimulus: %.2f +/- %.2f ms', np.nanmean(widths_prior) * 1e3, np.nanstd(widths_prior) * 1e3) logger.debug('average spike amplitude within stimulus: %.2f +/- %.2f nC/cm2', np.nanmean(prominences_prior) * 1e5, np.nanstd(prominences_prior) * 1e5) logger.debug('average ISI within stimulus: %.2f +/- %.2f ms', np.nanmean(ISIs_prior) * 1e3, np.nanstd(ISIs_prior) * 1e3) logger.debug('average FR within stimulus: %.2f +/- %.2f Hz', np.nanmean(FRs_prior), np.nanstd(FRs_prior)) # Complete metrics dictionaries metrics['latencies (ms)'].append(latency * 1e3) metrics['mean firing rates (Hz)'].append(np.mean(FRs_prior)) metrics['std firing rates (Hz)'].append(np.std(FRs_prior)) metrics['mean spike amplitudes (nC/cm2)'].append(np.mean(prominences_prior) * 1e5) metrics['std spike amplitudes (nC/cm2)'].append(np.std(prominences_prior) * 1e5) metrics['mean spike widths (ms)'].append(np.mean(widths_prior) * 1e3) metrics['std spike widths (ms)'].append(np.std(widths_prior) * 1e3) # Return dataframe with metrics return pd.DataFrame(metrics, columns=metrics.keys()) def getCycleProfiles(a, f, A, Cm0, Qm0, Qm): ''' Run a mechanical simulation until periodic stabilization, and compute pressure profiles over the last acoustic cycle. :param a: in-plane diameter of the sonophore structure within the membrane (m) :param f: acoustic drive frequency (Hz) :param A: acoustic drive amplitude (Pa) :param Cm0: membrane resting capacitance (F/m2) :param Qm0: membrane resting charge density (C/m2) :param Qm: imposed membrane charge density (C/m2) :return: a dataframe with the time, kinematic and pressure profiles over the last cycle. ''' # Create sonophore object bls = BilayerSonophore(a, f, Cm0, Qm0) # Run default simulation and compute relevant profiles logger.info('Running mechanical simulation (a = %sm, f = %sHz, A = %sPa)', si_format(a, 1), si_format(f, 1), si_format(A, 1)) t, y, _ = bls.run(f, A, Qm, Pm_comp_method=PmCompMethod.direct) dt = (t[-1] - t[0]) / (t.size - 1) Z, ng = y[:, -NPC_FULL:] t = t[-NPC_FULL:] t -= t[0] logger.info('Computing pressure cyclic profiles') R = bls.v_curvrad(Z) U = np.diff(Z) / dt U = np.hstack((U, U[-1])) data = { 't': t, 'Z': Z, 'Cm': bls.v_Capct(Z), 'P_M': bls.v_PMavg(Z, R, bls.surface(Z)), 'P_Q': bls.Pelec(Z, Qm), 'P_{VE}': bls.PEtot(Z, R) + bls.PVleaflet(U, R), 'P_V': bls.PVfluid(U, R), 'P_G': bls.gasmol2Pa(ng, bls.volume(Z)), 'P_0': - np.ones(Z.size) * bls.P0 } return pd.DataFrame(data, columns=data.keys()) def runSweepSA(bls, f, A, Qm, params, rel_sweep): ''' Run mechanical simulations while varying multiple model parameters around their default value, and compute the relative changes in cycle-averaged sonophore membrane potential over the last acoustic period upon periodic stabilization. :param bls: BilayerSonophore object :param f: acoustic drive frequency (Hz) :param A: acoustic drive amplitude (Pa) :param Qm: imposed membrane charge density (C/m2) :param params: list of model parameters to explore :param rel_sweep: array of relative parameter changes :return: a dataframe with the cycle-averaged sonophore membrane potentials for the parameter variations, for each parameter. ''' nsweep = len(rel_sweep) logger.info('Starting sensitivity analysis (%u parameters, sweep size = %u)', len(params), nsweep) t0 = time.time() # Run default simulation and compute cycle-averaged membrane potential _, y, _ = bls.run(f, A, Qm, Pm_comp_method=PmCompMethod.direct) Z = y[0, -NPC_FULL:] Cm = bls.v_Capct(Z) # F/m2 Vmavg_default = np.mean(Qm / Cm) * 1e3 # mV # Create data dictionary for computed output changes data = {'relative input change': rel_sweep - 1} nsims = len(params) * nsweep for j, p in enumerate(params): default = getattr(bls, p) sweep = rel_sweep * default Vmavg = np.empty(nsweep) logger.info('Computing system\'s sentitivty to %s (default = %.2e)', p, default) for i, val in enumerate(sweep): # Re-initialize BLS object with modififed attribute setattr(bls, p, val) bls.reinit() # Run simulation and compute cycle-averaged membrane potential _, y, _ = bls.run(f, A, Qm, Pm_comp_method=PmCompMethod.direct) Z = y[0, -NPC_FULL:] Cm = bls.v_Capct(Z) # F/m2 Vmavg[i] = np.mean(Qm / Cm) * 1e3 # mV logger.info('simulation %u/%u: %s = %.2e (%+.1f %%) --> |Vm| = %.1f mV (%+.3f %%)', j * nsweep + i + 1, nsims, p, val, (val - default) / default * 1e2, Vmavg[i], (Vmavg[i] - Vmavg_default) / Vmavg_default * 1e2) # Fill in data dictionary data[p] = Vmavg # Set parameter back to default setattr(bls, p, default) tcomp = time.time() - t0 logger.info('Sensitivity analysis susccessfully completed in %.0f s', tcomp) # return pandas dataframe return pd.DataFrame(data, columns=data.keys()) def getActivationMap(root, neuron, a, f, tstim, toffset, PRF, amps, DCs): ''' Compute the activation map of a neuron at a given frequency and PRF, by computing the spiking metrics of simulation results over a 2D space (amplitude x duty cycle). :param root: directory containing the input data files :param neuron: neuron name :param a: sonophore diameter :param f: acoustic drive frequency (Hz) :param tstim: duration of US stimulation (s) :param toffset: duration of the offset (s) :param PRF: pulse repetition frequency (Hz) :param amps: vector of acoustic amplitudes (Pa) :param DCs: vector of duty cycles (-) :return the activation matrix ''' # Initialize activation map actmap = np.empty((amps.size, DCs.size)) # Loop through amplitudes and duty cycles nfiles = DCs.size * amps.size for i, A in enumerate(amps): for j, DC in enumerate(DCs): # Define filename PW_str = '_PRF{:.2f}Hz_DC{:.2f}%'.format(PRF, DC * 1e2) if DC < 1 else '' fname = ('ASTIM_{}_{}W_{:.0f}nm_{:.0f}kHz_{:.1f}kPa_{:.0f}ms{}_effective.pkl' .format(neuron, 'P' if DC < 1 else 'C', a * 1e9, f * 1e-3, A * 1e-3, tstim * 1e3, PW_str)) # Extract charge profile from data file fpath = os.path.join(root, fname) if os.path.isfile(fpath): logger.debug('Loading file {}/{}: "{}"'.format(i * amps.size + j + 1, nfiles, fname)) with open(fpath, 'rb') as fh: frame = pickle.load(fh) df = frame['data'] meta = frame['meta'] tstim = meta['tstim'] t = df['t'].values Qm = df['Qm'].values dt = t[1] - t[0] # Detect spikes on charge profile during stimulus mpd = int(np.ceil(SPIKE_MIN_DT / dt)) ispikes, *_ = findPeaks(Qm[t <= tstim], SPIKE_MIN_QAMP, mpd, SPIKE_MIN_QPROM) # Compute firing metrics if ispikes.size == 0: # if no spike, assign -1 actmap[i, j] = -1 elif ispikes.size == 1: # if only 1 spike, assign 0 actmap[i, j] = 0 else: # if more than 1 spike, assign firing rate FRs = 1 / np.diff(t[ispikes]) actmap[i, j] = np.mean(FRs) else: logger.error('"{}" file not found'.format(fname)) actmap[i, j] = np.nan return actmap def getMaxMap(key, root, neuron, a, f, tstim, toffset, PRF, amps, DCs, mode='max', cavg=False): ''' Compute the max. value map of a neuron's specific variable at a given frequency and PRF over a 2D space (amplitude x duty cycle). :param key: the variable name to find in the simulations dataframes :param root: directory containing the input data files :param neuron: neuron name :param a: sonophore diameter :param f: acoustic drive frequency (Hz) :param tstim: duration of US stimulation (s) :param toffset: duration of the offset (s) :param PRF: pulse repetition frequency (Hz) :param amps: vector of acoustic amplitudes (Pa) :param DCs: vector of duty cycles (-) :param mode: string indicating whether to search for maximum, minimum or absolute maximum :return the maximum matrix ''' # Initialize max map maxmap = np.empty((amps.size, DCs.size)) # Loop through amplitudes and duty cycles nfiles = DCs.size * amps.size for i, A in enumerate(amps): for j, DC in enumerate(DCs): # Define filename PW_str = '_PRF{:.2f}Hz_DC{:.2f}%'.format(PRF, DC * 1e2) if DC < 1 else '' fname = ('ASTIM_{}_{}W_{:.0f}nm_{:.0f}kHz_{:.1f}kPa_{:.0f}ms{}_effective.pkl' .format(neuron, 'P' if DC < 1 else 'C', a * 1e9, f * 1e-3, A * 1e-3, tstim * 1e3, PW_str)) # Extract charge profile from data file fpath = os.path.join(root, fname) if os.path.isfile(fpath): logger.debug('Loading file {}/{}: "{}"'.format(i * amps.size + j + 1, nfiles, fname)) with open(fpath, 'rb') as fh: frame = pickle.load(fh) df = frame['data'] t = df['t'].values if key in df: x = df[key].values else: x = eval(key) if cavg: x = getCycleAverage(t, x, 1 / PRF) if mode == 'min': maxmap[i, j] = x.min() elif mode == 'max': maxmap[i, j] = x.max() elif mode == 'absmax': maxmap[i, j] = np.abs(x).max() else: maxmap[i, j] = np.nan return maxmap def computeAStimLookups(neuron, a, freqs, amps, phi=np.pi, multiprocess=False): ''' Run simulations of the mechanical system for a multiple combinations of imposed US frequencies, acoustic amplitudes and charge densities, compute effective coefficients and store them in a dictionary of 3D arrays. :param neuron: neuron object :param freqs: array of acoustic drive frequencies (Hz) :param amps: array of acoustic drive amplitudes (Pa) :param phi: acoustic drive phase (rad) :param multiprocess: boolean statting wether or not to use multiprocessing :return: lookups dictionary ''' # Check validity of input parameters if not isinstance(neuron, BaseMech): raise InputError('Invalid neuron type: "{}" (must inherit from BaseMech class)' .format(neuron.name)) if not isinstance(freqs, np.ndarray): if isinstance(freqs, list): if not all(isinstance(x, float) for x in freqs): raise InputError('Invalid frequencies (must all be float typed)') freqs = np.array(freqs) else: raise InputError('Invalid frequencies (must be provided as list or numpy array)') if not isinstance(amps, np.ndarray): if isinstance(amps, list): if not all(isinstance(x, float) for x in amps): raise InputError('Invalid amplitudes (must all be float typed)') amps = np.array(amps) else: raise InputError('Invalid amplitudes (must be provided as list or numpy array)') nf = freqs.size nA = amps.size if nf == 0: raise InputError('Empty frequencies array') if nA == 0: raise InputError('Empty amplitudes array') if freqs.min() <= 0: raise InputError('Invalid US driving frequencies (must all be strictly positive)') if amps.min() < 0: raise InputError('Invalid US pressure amplitudes (must all be positive or null)') logger.info('Starting batch lookup creation for %s neuron', neuron.name) t0 = time.time() # Initialize BLS object bls = BilayerSonophore(a, 0.0, neuron.Cm0, neuron.Cm0 * neuron.Vm0 * 1e-3) # Create neuron-specific charge vector charges = np.arange(neuron.Qbounds[0], neuron.Qbounds[1] + 1e-5, 1e-5) # C/m2 nQ = charges.size dims = (nf, nA, nQ) # Initialize lookup dictionary of 3D array to store effective coefficients coeffs_names = ['V', 'ng', *neuron.coeff_names] ncoeffs = len(coeffs_names) lookup_dict = {cn: np.empty(dims) for cn in coeffs_names} # Initiate multipleprocessing objects if needed if multiprocess: mp.freeze_support() # Create tasks and results queues tasks = mp.JoinableQueue() results = mp.Queue() # Create and start consumer processes nconsumers = mp.cpu_count() consumers = [Consumer(tasks, results) for i in range(nconsumers)] for w in consumers: w.start() # Loop through all (f, A, Q) combinations nsims = np.prod(np.array(dims)) for i in range(nf): for j in range(nA): for k in range(nQ): wid = i * (nA * nQ) + j * nQ + k worker = LookupWorker(wid, bls, neuron, freqs[i], amps[j], charges[k], phi, nsims) if multiprocess: tasks.put(worker, block=False) else: logger.info('%s', worker) _, Qcoeffs = worker.__call__() for icoeff in range(ncoeffs): lookup_dict[coeffs_names[icoeff]][i, j, k] = Qcoeffs[icoeff] if multiprocess: # Stop processes for i in range(nconsumers): tasks.put(None, block=False) tasks.join() # Retrieve workers output for x in range(nsims): wid, Qcoeffs = results.get() i, j, k = nDindexes(dims, wid) for icoeff in range(ncoeffs): lookup_dict[coeffs_names[icoeff]][i, j, k] = Qcoeffs[icoeff] # Close tasks and results queues tasks.close() results.close() # Add input frequency, amplitude and charge arrays to lookup dictionary lookup_dict['f'] = freqs # Hz lookup_dict['A'] = amps # Pa lookup_dict['Q'] = charges # C/m2 logger.info('%s lookups computed in %.0f s', neuron.name, time.time() - t0) return lookup_dict diff --git a/PointNICE/templates/log_ASTIM.xlsx b/PySONIC/templates/log_ASTIM.xlsx similarity index 100% rename from PointNICE/templates/log_ASTIM.xlsx rename to PySONIC/templates/log_ASTIM.xlsx diff --git a/PointNICE/templates/log_ESTIM.xlsx b/PySONIC/templates/log_ESTIM.xlsx similarity index 100% rename from PointNICE/templates/log_ESTIM.xlsx rename to PySONIC/templates/log_ESTIM.xlsx diff --git a/PointNICE/templates/log_MECH.xlsx b/PySONIC/templates/log_MECH.xlsx similarity index 100% rename from PointNICE/templates/log_MECH.xlsx rename to PySONIC/templates/log_MECH.xlsx diff --git a/PointNICE/utils.py b/PySONIC/utils.py similarity index 99% rename from PointNICE/utils.py rename to PySONIC/utils.py index 36bc89b..ca5f94f 100644 --- a/PointNICE/utils.py +++ b/PySONIC/utils.py @@ -1,511 +1,511 @@ #!/usr/bin/env python # -*- coding: utf-8 -*- # @Author: Theo Lemaire # @Date: 2016-09-19 22:30:46 # @Email: theo.lemaire@epfl.ch # @Last Modified by: Theo Lemaire -# @Last Modified time: 2018-08-21 14:08:06 +# @Last Modified time: 2018-08-21 16:10:36 """ Definition of generic utility functions used in other modules """ from enum import Enum import operator import os import pickle import tkinter as tk from tkinter import filedialog import inspect import json import yaml from openpyxl import load_workbook import numpy as np import colorlog from . import neurons def setLogger(): log_formatter = colorlog.ColoredFormatter( '%(log_color)s %(asctime)s %(message)s', datefmt='%d/%m/%Y %H:%M:%S:', reset=True, log_colors={ 'DEBUG': 'green', 'INFO': 'white', 'WARNING': 'yellow', 'ERROR': 'red', 'CRITICAL': 'red,bg_white', }, style='%' ) log_handler = colorlog.StreamHandler() log_handler.setFormatter(log_formatter) - color_logger = colorlog.getLogger('PointNICE') + color_logger = colorlog.getLogger('PySONIC') color_logger.addHandler(log_handler) return color_logger # Get package logger logger = setLogger() class InputError(Exception): ''' Exception raised for errors in the input. ''' pass class PmCompMethod(Enum): """ Enum: types of computation method for the intermolecular pressure """ direct = 1 predict = 2 def loadYAML(filepath): """ Load a dictionary of parameters from an external YAML file. :param filepath: full path to the YAML file :return: parameters dictionary """ _, filename = os.path.split(filepath) logger.debug('Loading parameters from "%s"', filename) with open(filepath, 'r') as f: stream = f.read() params = yaml.load(stream) return ParseNestedDict(params) def getLookupDir(): """ Return the location of the directory holding lookups files. :return: absolute path to the directory """ this_dir, _ = os.path.split(__file__) return os.path.join(this_dir, 'lookups') def ParseNestedDict(dict_in): """ Loop through a nested dictionary object and convert all string fields to floats. """ for key, value in dict_in.items(): if isinstance(value, dict): # If value itself is dictionary dict_in[key] = ParseNestedDict(value) elif isinstance(dict_in[key], str): dict_in[key] = float(dict_in[key]) return dict_in def OpenFilesDialog(filetype, dirname=''): """ Open a FileOpenDialogBox to select one or multiple file. The default directory and file type are given. :param dirname: default directory :param filetype: default file type :return: tuple of full paths to the chosen filenames """ root = tk.Tk() root.withdraw() filenames = filedialog.askopenfilenames(filetypes=[(filetype + " files", '.' + filetype)], initialdir=dirname) if filenames: par_dir = os.path.abspath(os.path.join(filenames[0], os.pardir)) else: par_dir = None return (filenames, par_dir) def ImportExcelCol(filename, sheetname, colstr, startrow): """ Load a specific column of an excel workbook as a numpy array. :param filename: absolute or relative path to the Excel workbook :param sheetname: name of the Excel spreadsheet to which data is appended :param colstr: string of the column to import :param startrow: index of the first row to consider :return: 1D numpy array with the column data """ wb = load_workbook(filename, read_only=True) ws = wb[sheetname] range_start_str = colstr + str(startrow) range_stop_str = colstr + str(ws.max_row) tmp = np.array([[i.value for i in j] for j in ws[range_start_str:range_stop_str]]) return tmp[:, 0] def ConstructMatrix(serialized_inputA, serialized_inputB, serialized_output): """ Construct a 2D output matrix from serialized input. :param serialized_inputA: serialized input variable A :param serialized_inputB: serialized input variable B :param serialized_output: serialized output variable :return: 4-tuple with vectors of unique values of A (m) and B (n), output variable 2D matrix (m,n) and number of holes in the matrix """ As = np.unique(serialized_inputA) Bs = np.unique(serialized_inputB) nA = As.size nB = Bs.size output = np.zeros((nA, nB)) output[:] = np.NAN nholes = 0 for i in range(nA): iA = np.where(serialized_inputA == As[i]) for j in range(nB): iB = np.where(serialized_inputB == Bs[j]) iMatch = np.intersect1d(iA, iB) if iMatch.size == 0: nholes += 1 elif iMatch.size > 1: logger.warning('Identical serialized inputs with values (%f, %f)', As[i], Bs[j]) else: iMatch = iMatch[0] output[i, j] = serialized_output[iMatch] return (As, Bs, output, nholes) def rmse(x1, x2): """ Compute the root mean square error between two 1D arrays """ return np.sqrt(((x1 - x2) ** 2).mean()) def rsquared(x1, x2): ''' compute the R-squared coefficient between two 1D arrays ''' residuals = x1 - x2 ss_res = np.sum(residuals**2) ss_tot = np.sum((x1 - np.mean(x1))**2) return 1 - (ss_res / ss_tot) def DownSample(t_dense, y, nsparse): """ Decimate periodic signals to a specified number of samples.""" if(y.ndim) > 1: nsignals = y.shape[0] else: nsignals = 1 y = np.array([y]) # determine time step and period of input signal T = t_dense[-1] - t_dense[0] dt_dense = t_dense[1] - t_dense[0] # resample time vector linearly t_ds = np.linspace(t_dense[0], t_dense[-1], nsparse) # create MAV window nmav = int(0.03 * T / dt_dense) if nmav % 2 == 0: nmav += 1 mav = np.ones(nmav) / nmav # determine signals padding npad = int((nmav - 1) / 2) # determine indexes of sampling on convolved signals ids = np.round(np.linspace(0, t_dense.size - 1, nsparse)).astype(int) y_ds = np.empty((nsignals, nsparse)) # loop through signals for i in range(nsignals): # pad, convolve and resample pad_left = y[i, -(npad + 2):-2] pad_right = y[i, 1:npad + 1] y_ext = np.concatenate((pad_left, y[i, :], pad_right), axis=0) y_mav = np.convolve(y_ext, mav, mode='valid') y_ds[i, :] = y_mav[ids] if nsignals == 1: y_ds = y_ds[0, :] return (t_ds, y_ds) def Pressure2Intensity(p, rho=1075.0, c=1515.0): """ Return the spatial peak, pulse average acoustic intensity (ISPPA) associated with the specified pressure amplitude. :param p: pressure amplitude (Pa) :param rho: medium density (kg/m3) :param c: speed of sound in medium (m/s) :return: spatial peak, pulse average acoustic intensity (W/m2) """ return p**2 / (2 * rho * c) def Intensity2Pressure(I, rho=1075.0, c=1515.0): """ Return the pressure amplitude associated with the specified spatial peak, pulse average acoustic intensity (ISPPA). :param I: spatial peak, pulse average acoustic intensity (W/m2) :param rho: medium density (kg/m3) :param c: speed of sound in medium (m/s) :return: pressure amplitude (Pa) """ return np.sqrt(2 * rho * c * I) def find_nearest(array, value): ''' Find nearest element in 1D array. ''' idx = (np.abs(array - value)).argmin() return (idx, array[idx]) def rescale(x, lb=None, ub=None, lb_new=0, ub_new=1): ''' Rescale a value to a specific interval by linear transformation. ''' if lb is None: lb = x.min() if ub is None: ub = x.max() xnorm = (x - lb) / (ub - lb) return xnorm * (ub_new - lb_new) + lb_new def LennardJones(x, beta, alpha, C, m, n): """ Generic expression of a Lennard-Jones function, adapted for the context of symmetric deflection (distance = 2x). :param x: deflection (i.e. half-distance) :param beta: x-shifting factor :param alpha: x-scaling factor :param C: y-scaling factor :param m: exponent of the repulsion term :param n: exponent of the attraction term :return: Lennard-Jones potential at given distance (2x) """ return C * (np.power((alpha / (2 * x + beta)), m) - np.power((alpha / (2 * x + beta)), n)) def getNeuronsDict(): ''' Return dictionary of neurons classes that can be instantiated. ''' neurons_dict = {} for _, obj in inspect.getmembers(neurons): if inspect.isclass(obj) and isinstance(obj.name, str): neurons_dict[obj.name] = obj return neurons_dict def get_BLS_lookups(a): lookup_path = getLookupDir() + '/BLS_lookups_a{:.1f}nm.json'.format(a * 1e9) try: with open(lookup_path) as fh: sample = json.load(fh) return sample except FileNotFoundError: return {} def save_BLS_lookups(a, lookups): """ Save BLS parameter into specific lookup file :return: absolute path to the directory """ lookup_path = getLookupDir() + '/BLS_lookups_a{:.1f}nm.json'.format(a * 1e9) with open(lookup_path, 'w') as fh: json.dump(lookups, fh) def extractCompTimes(filenames): ''' Extract computation times from a list of simulation files. ''' tcomps = np.empty(len(filenames)) for i, fn in enumerate(filenames): logger.info('Loading data from "%s"', fn) with open(fn, 'rb') as fh: frame = pickle.load(fh) meta = frame['meta'] tcomps[i] = meta['tcomp'] return tcomps def computeMeshEdges(x, scale='lin'): ''' Compute the appropriate edges of a mesh that quads a linear or logarihtmic distribution. :param x: the input vector :param scale: the type of distribution ('lin' for linear, 'log' for logarihtmic) :return: the edges vector ''' if scale is 'log': x = np.log10(x) dx = x[1] - x[0] if scale is 'lin': y = np.linspace(x[0] - dx / 2, x[-1] + dx / 2, x.size + 1) elif scale is 'log': y = np.logspace(x[0] - dx / 2, x[-1] + dx / 2, x.size + 1) return y si_prefixes = { 'y': 1e-24, # yocto 'z': 1e-21, # zepto 'a': 1e-18, # atto 'f': 1e-15, # femto 'p': 1e-12, # pico 'n': 1e-9, # nano 'u': 1e-6, # micro 'm': 1e-3, # mili '': 1e0, # None 'k': 1e3, # kilo 'M': 1e6, # mega 'G': 1e9, # giga 'T': 1e12, # tera 'P': 1e15, # peta 'E': 1e18, # exa 'Z': 1e21, # zetta 'Y': 1e24, # yotta } def si_format(x, precision=0, space=''): ''' Format a float according to the SI unit system, with the appropriate prefix letter. ''' if isinstance(x, float) or isinstance(x, int) or isinstance(x, np.float) or\ isinstance(x, np.int32) or isinstance(x, np.int64): if x == 0: factor = 1e0 prefix = '' else: sorted_si_prefixes = sorted(si_prefixes.items(), key=operator.itemgetter(1)) vals = [tmp[1] for tmp in sorted_si_prefixes] # vals = list(si_prefixes.values()) ix = np.searchsorted(vals, np.abs(x)) - 1 if np.abs(x) == vals[ix + 1]: ix += 1 factor = vals[ix] prefix = sorted_si_prefixes[ix][0] # prefix = list(si_prefixes.keys())[ix] return '{{:.{}f}}{}{}'.format(precision, space, prefix).format(x / factor) elif isinstance(x, list) or isinstance(x, tuple): return [si_format(item, precision, space) for item in x] elif isinstance(x, np.ndarray) and x.ndim == 1: return [si_format(float(item), precision, space) for item in x] else: print(type(x)) def getCycleAverage(t, y, T): ''' Compute the cycle-averaged profile of a signal given a specific periodicity. :param t: time vector (s) :param y: signal vector :param T: period (s) :return: cycle-averaged signal vector ''' nsamples = y.size ncycles = int((t[-1] - t[0]) // T) npercycle = int(nsamples // ncycles) return np.mean(np.reshape(y[:ncycles * npercycle], (ncycles, npercycle)), axis=1) def itrpLookupsFreq(lookups3D, freqs, Fdrive): """ Interpolate a dictionary of 3D lookups at a given frequency. :param lookups3D: dictionary of 3D lookups :param freqs: array of lookup frequencies (Hz) :param Fdrive: acoustic drive frequency (Hz) :return: a dictionary of 2D lookups interpolated a the given frequency """ # If Fdrive in lookup frequencies, simply take (A, Q) slice at Fdrive index if Fdrive in freqs: iFdrive = np.searchsorted(freqs, Fdrive) # logger.debug('Using lookups directly at %.2f kHz', freqs[iFdrive] * 1e-3) lookups2D = {key: np.squeeze(lookups3D[key][iFdrive, :, :]) for key in lookups3D.keys()} # Otherwise, interpolate linearly between 2 (A, Q) slices at Fdrive bounding values indexes else: ilb = np.searchsorted(freqs, Fdrive) - 1 iub = ilb + 1 # logger.debug('Interpolating lookups between %.2f kHz and %.2f kHz', # freqs[ilb] * 1e-3, freqs[iub] * 1e-3) lookups2D_lb = {key: np.squeeze(lookups3D[key][ilb, :, :]) for key in lookups3D.keys()} lookups2D_ub = {key: np.squeeze(lookups3D[key][iub, :, :]) for key in lookups3D.keys()} Fratio = (Fdrive - freqs[ilb]) / (freqs[iub] - freqs[ilb]) lookups2D = {key: lookups2D_lb[key] + (lookups2D_ub[key] - lookups2D_lb[key]) * Fratio for key in lookups3D.keys()} return lookups2D def getLookups2D(mechname, a, Fdrive): ''' Retrieve appropriate 2D lookup tables and reference vectors for a given membrane mechanism, sonophore diameter and US frequency. :param mechname: name of membrane density mechanism :param a: sonophore diameter (m) :param Fdrive: US frequency (Hz) :return: 3-tuple with 1D numpy arrays of reference acoustic amplitudes and charge densities, and a dictionary of 2D lookup numpy arrays ''' # Check lookup file existence lookup_file = '{}_lookups_a{:.1f}nm.pkl'.format(mechname, a * 1e9) lookup_path = '{}/{}'.format(getLookupDir(), lookup_file) if not os.path.isfile(lookup_path): raise InputError('Missing lookup file: "{}"'.format(lookup_file)) # Load lookups dictionary with open(lookup_path, 'rb') as fh: lookups3D = pickle.load(fh) # Retrieve 1D inputs from lookups dictionary Fref = lookups3D.pop('f') Aref = lookups3D.pop('A') Qref = lookups3D.pop('Q') # Check that US frequency is within lookup range margin = 1e-9 # adding margin to compensate for eventual round error Frange = (Fref.min() - margin, Fref.max() + margin) if Fdrive < Frange[0] or Fdrive > Frange[1]: raise InputError('Invalid frequency: {}Hz (must be within {}Hz - {}Hz lookup interval)' .format(*si_format([Fdrive, *Frange], precision=2, space=' '))) # Interpolate 3D lookups at US frequency lookups2D = itrpLookupsFreq(lookups3D, Fref, Fdrive) return Aref, Qref, lookups2D def nDindexes(dims, index): ''' Find index positions in a n-dimensional array. :param dims: dimensions of the n-dimensional array (tuple or list) :param index: index position in the flattened n-dimensional array :return: list of indexes along each array dimension ''' dims = list(dims) # Find size of each array dimension dims.reverse() dimsizes = [1] r = 1 for x in dims[:-1]: r *= x dimsizes.append(r) dims.reverse() dimsizes.reverse() # Find indexes indexes = [] remainder = index for dimsize in dimsizes[:-1]: idim, remainder = divmod(remainder, dimsize) indexes.append(idim) indexes.append(remainder) return indexes def pow10_format(number, precision=2): ''' Format a number in power of 10 notation. ''' ret_string = '{0:.{1:d}e}'.format(number, precision) a, b = ret_string.split("e") a = float(a) b = int(b) return '{}10^{{{}}}'.format('{} * '.format(a) if a != 1. else '', b) diff --git a/README.md b/README.md index 019a206..28aecaf 100644 --- a/README.md +++ b/README.md @@ -1,68 +1,70 @@ Description ============ -PointNICE is a Python implementation of the **Neuronal Intramembrane Cavitation Excitation** (NICE) model introduced by Plaksin et. al in 2014 and initially developed in MATLAB by its authors. It contains optimized methods to predict the electrical response of point-neuron models to both acoustic and electrical stimuli. +Python implementation of the **multi-Scale Optimized Neuronal Intramembrane Cavitation** (SONIC) model to compute individual neural responses to acoustic stimuli, as predicted by the *intramembrane cavitation* hypothesis. + +This is an optmimized variant of the original **Neuronal Intramembrane Cavitation Excitation** (NICE) model introduced by Plaksin et. al in 2014. This package contains several core modules: - **bls** defines the underlying biomechanical model of intramembrane cavitation (**BilayerSonophore** class), and provides an integration method to predict compute the mechanical oscillations of the plasma membrane subject to a periodic acoustic perturbation. - **solvers** contains a simple solver for electrical stimuli (**SolverElec** class) as well as a tailored solver for acoustic stimuli (**SolverUS** class). The latter directly inherits from the BilayerSonophore class upon instantiation, and is hooked to a specific "channel mechanism" in order to link the mechanical model to an electrical model of membrane dynamics. It also provides several integration methods (detailed below) to compute the behaviour of the full electro-mechanical model subject to a continuous or pulsed ultrasonic stimulus. - **neurons** contains the definitions of the different channels mechanisms inherent to specific neurons, including several types of **cortical** and **thalamic** neurons. - **plt** defines plotting utilities to load results of several simulations and display/compare temporal profiles of multiple variables of interest across simulations. - **utils** defines generic utilities used across the different modules The **SolverUS** class incorporates optimized numerical integration methods to perform dynamic simulations of the model subject to acoustic perturbation, and compute the evolution of its mechanical and electrical variables: - a **classic** method that solves all variables for the entire duration of the simulation. This method uses very small time steps and is computationally expensive (simulation time: several hours) - a **hybrid** method (initially developed by Plaskin et al.) in which integration is performed in consecutive “slices” of time, during which the full system is solved until mechanical stabilization, at which point the electrical system is solely solved with predicted mechanical variables until the end of the slice. This method is more efficient (simulation time: several minutes) and provides accurate results. - a newly developed **effective** method that neglects the high amplitude oscillations of mechanical and electrical variables during each acoustic cycle, to instead grasp the net effect of the acoustic stimulus on the electrical system. To do so, the sole electrical system is solved using pre-computed coefficients that depend on membrane charge and acoustic amplitude. This method allows to run simulations of the electrical system in only a few seconds, with very accurate results of the net membrane charge density evolution. This package is meant to be easy to use as a predictive and comparative tool for researchers investigating ultrasonic and/or electrical neuro-stimulation experimentally. Installation ================== Install Python 3 if not already done. Open a terminal. Activate a Python3 environment if needed, e.g. on the tnesrv5 machine: source /opt/apps/anaconda3/bin activate Check that the appropriate version of pip is activated: pip --version -Go to the PointNICE directory (where the setup.py file is located) and install it as a package: +Go to the package directory (where the setup.py file is located) and install it: cd pip install -e . -PointNICE and all its dependencies will be installed. +*PySONIC* and all its dependencies will be installed. Usage ======= Command line scripts --------------------- To run single simulations of a given point-neuron model under specific stimulation parameters, you can use the `ASTIM_run.py` and `ESTIM_run.py` command-line scripts provided by the package. For instance, to simulate a regular-spiking neuron under continuous wave ultrasonic stimulation at 500kHz and 100kPa, for 150 ms: python ASTIM_run.py -n=RS -f=500 -A=100 -t=150 Similarly, to simulate the electrical stimulation of a thalamo-cortical neuron at 10 mA/m2 for 150 ms: python ESTIM_run.py -n=TC -A=10 -t=150 The simulation results will be save in an output PKL file in the current working directory. To view these results, you can use the dedicated Batch scripts --------------- To run a batch of simulations on different neuron types and spanning ranges of several stimulation parameters, you can run the `ASTIM_batch.py` and `ESTIM_batch.py` scripts. To do so, simply modify the **stim_params** and **neurons** variables with your own neuron types and parameter sweeps, and then run the scripts (without command-line arguments). diff --git a/deprecated/GUI/test_gui.py b/deprecated/GUI/test_gui.py index 894bedb..d21fb6a 100644 --- a/deprecated/GUI/test_gui.py +++ b/deprecated/GUI/test_gui.py @@ -1,173 +1,173 @@ #!/usr/bin/env python # -*- coding: utf-8 -*- # @Author: Theo Lemaire # @Date: 2017-08-25 17:16:56 # @Last Modified by: Theo Lemaire -# @Last Modified time: 2017-09-05 14:44:29 +# @Last Modified time: 2018-08-21 15:28:46 ''' Simple GUI to run ASTIM and ESTIM simulations. ''' import logging import tkinter as tk -from PointNICE.solvers import SolverUS, checkBatchLog, runAStim -from PointNICE.utils import getNeuronsDict, load_BLS_params +from sonic.solvers import SolverUS, checkBatchLog, runAStim +from sonic.utils import getNeuronsDict, load_BLS_params class UI(tk.Tk): def __init__(self, parent): tk.Tk.__init__(self, parent) self.parent = parent self.initialize() def initialize(self): self.neurons = getNeuronsDict() self.bls_params = load_BLS_params() self.batch_dir = '' self.log_filepath = '' self.grid() # ---------------------- Neuron parameters ---------------------- frameNeuron = tk.LabelFrame(self, text="Cell parameters", padx=10, pady=10) frameNeuron.pack(padx=20, pady=5, fill=tk.X) neurons_names = list(self.neurons.keys()) labelNeuronType = tk.Label(frameNeuron, text='Neuron type', anchor="w") labelNeuronType.grid(column=0, row=0, sticky='E') self.neuron_name = tk.StringVar() self.neuron_name.set(neurons_names[0]) neurons_drop = tk.OptionMenu(frameNeuron, self.neuron_name, *neurons_names) neurons_drop.grid(column=1, row=0, sticky='E') label_diam = tk.Label(frameNeuron, text='BLS diameter (nm)', anchor="w") label_diam.grid(column=0, row=1, sticky='E') self.diam = tk.DoubleVar() self.entry_diam = tk.Entry(frameNeuron, textvariable=self.diam, width=8) self.entry_diam.grid(column=1, row=1, sticky='W') self.diam.set(32.0) # ---------------------- ASTIM parameters ---------------------- frameASTIM = tk.LabelFrame(self, text="Stimulation parameters", padx=10, pady=10) frameASTIM.pack(padx=20, pady=5, fill=tk.X) labelFdrive = tk.Label(frameASTIM, text='Frequency (kHz)', anchor="w") labelFdrive.grid(column=0, row=0, sticky='E') self.Fdrive = tk.DoubleVar() self.entryFdrive = tk.Entry(frameASTIM, textvariable=self.Fdrive, width=8) self.entryFdrive.grid(column=1, row=0, sticky='W') self.Fdrive.set(200.0) labelAdrive = tk.Label(frameASTIM, text='Amplitude (kPa)', anchor="w") labelAdrive.grid(column=0, row=1, sticky='E') self.Adrive = tk.DoubleVar() self.entryAdrive = tk.Entry(frameASTIM, textvariable=self.Adrive, width=8) self.entryAdrive.grid(column=1, row=1, sticky='W') self.Adrive.set(300.0) labeltstim = tk.Label(frameASTIM, text='Duration (ms)', anchor="w") labeltstim.grid(column=0, row=2, sticky='E') self.tstim = tk.DoubleVar() self.entrytstim = tk.Entry(frameASTIM, textvariable=self.tstim, width=8) self.entrytstim.grid(column=1, row=2, sticky='W') self.tstim.set(100.0) labeltoffset = tk.Label(frameASTIM, text='Offset (ms)', anchor="w") labeltoffset.grid(column=0, row=3, sticky='E') self.toffset = tk.DoubleVar() self.entrytoffset = tk.Entry(frameASTIM, textvariable=self.toffset, width=8) self.entrytoffset.grid(column=1, row=3, sticky='W') self.toffset.set(20.0) labelPRF = tk.Label(frameASTIM, text='PRF (Hz)', anchor="w") labelPRF.grid(column=0, row=4, sticky='E') self.PRF = tk.DoubleVar() self.entryPRF = tk.Entry(frameASTIM, textvariable=self.PRF, width=8) self.entryPRF.grid(column=1, row=4, sticky='W') self.PRF.set(100.0) labelDF = tk.Label(frameASTIM, text='Duty cycle (%)', anchor="w") labelDF.grid(column=0, row=5, sticky='E') self.DF = tk.DoubleVar() self.entryDF = tk.Entry(frameASTIM, textvariable=self.DF, width=8) self.entryDF.grid(column=1, row=5, sticky='W') self.DF.set(100.0) # ---------------------- Simulation settings ---------------------- frameRun = tk.LabelFrame(self, text="Simulation settings", padx=10, pady=10) frameRun.pack(padx=20, pady=5, fill=tk.X) # frameRun = tk.Frame(self, padx=10, pady=10) # frameRun.pack(padx=20, fill=tk.X) selectdir_button = tk.Button(frameRun, text='...', command=self.OnSelectDirClick) selectdir_button.grid(column=0, row=0, sticky='EW') self.simdir = tk.StringVar() label_simdir = tk.Label(frameRun, textvariable=self.simdir, bg="white", width=65) label_simdir.grid(column=1, row=0, sticky='E') self.simdir.set('Output directory') button = tk.Button(frameRun, text=u"Run", command=self.OnRunClick) button.grid(column=0, row=1, columnspan=2, sticky='EW') self.labelVariable = tk.StringVar() label = tk.Label(frameRun, textvariable=self.labelVariable, anchor="w", fg="white", bg="blue", width=70) label.grid(column=0, row=2, columnspan=2, sticky='EW') self.labelVariable.set('...') # ---------------------- Grid settings ---------------------- # self.grid_columnconfigure(1, weight=1) self.resizable(False, False) def OnSelectDirClick(self): try: self.batch_dir, self.log_filepath = checkBatchLog('A-STIM') self.simdir.set(self.batch_dir) except AssertionError: self.simdir.set('Output directory') def OnRunClick(self): neuron = self.neurons[self.neuron_name.get()]() a = float(self.entry_diam.get()) Fdrive = float(self.entryFdrive.get()) * 1e3 Adrive = float(self.entryAdrive.get()) * 1e3 tstim = float(self.entrytstim.get()) * 1e-3 toffset = float(self.entrytoffset.get()) * 1e-3 PRF = float(self.entryPRF.get()) DF = float(self.entryDF.get()) * 1e-2 if DF == 100.0: log_str = ('Running ASTIM on {} neuron @ {:.0f} kHz, {:.0f} kPa, {:.0f} ms' .format(neuron.name, Fdrive * 1e-3, Adrive * 1e-3, tstim * 1e3)) else: log_str = ('Running ASTIM on {} neuron @ {:.0f} kHz, {:.0f} kPa, {:.0f} ms, ' '{:.0f} Hz PRF, {:.1f} % DC'.format(neuron.name, Fdrive * 1e-3, Adrive * 1e-3, tstim * 1e3, PRF, DF * 1e2)) self.labelVariable.set(log_str + ' ...') solver = SolverUS({'a': a * 1e-9, 'd': 0.0}, self.bls_params, neuron, Fdrive) output_filepath = runAStim(self.batch_dir, self.log_filepath, solver, neuron, self.bls_params, Fdrive, Adrive, tstim, toffset, PRF, DF, 'effective') self.labelVariable.set('results available @ {}'.format(output_filepath)) if __name__ == "__main__": app = UI(None) - app.title('PointNICE UI') + app.title('SONIC UI') app.mainloop() diff --git a/deprecated/miscellaneous/interpolate_lookups.py b/deprecated/miscellaneous/interpolate_lookups.py index c658214..85a2f96 100644 --- a/deprecated/miscellaneous/interpolate_lookups.py +++ b/deprecated/miscellaneous/interpolate_lookups.py @@ -1,39 +1,39 @@ import os import time import pickle import logging import numpy as np from scipy.interpolate import interp2d -from PointNICE.utils import logger, getLookupDir, InputError, itrpLookupsFreq +from sonic.utils import logger, getLookupDir, InputError, itrpLookupsFreq # Set logging level logger.setLevel(logging.INFO) # Check lookup file existence neuron = 'RS' a = 32e-9 Fdrive = 600e3 Adrive = 100e3 lookup_file = '{}_lookups_a{:.1f}nm.pkl'.format(neuron, a * 1e9) lookup_path = '{}/{}'.format(getLookupDir(), lookup_file) if not os.path.isfile(lookup_path): raise InputError('Missing lookup file: "{}"'.format(lookup_file)) # Load lookups dictionary with open(lookup_path, 'rb') as fh: lookups3D = pickle.load(fh) # Retrieve 1D inputs from lookups dictionary freqs = lookups3D.pop('f') amps = lookups3D.pop('A') charges = lookups3D.pop('Q') t0 = time.time() lookups2D = itrpLookupsFreq(lookups3D, freqs, Fdrive) logger.info('3D -> 2D projection in %.3f ms', (time.time() - t0) * 1e3) t0 = time.time() lookups1D = {key: np.squeeze(interp2d(amps, charges, lookups2D[key].T)(Adrive, charges)) for key in lookups2D.keys()} lookups1D['ng0'] = np.squeeze(interp2d(amps, charges, lookups2D['ng'].T)(0.0, charges)) logger.info('2D -> 1D projection in %.3f ms', (time.time() - t0) * 1e3) diff --git a/deprecated/update_lookups.py b/deprecated/update_lookups.py index 03c82b7..4732a71 100644 --- a/deprecated/update_lookups.py +++ b/deprecated/update_lookups.py @@ -1,96 +1,96 @@ import os import glob import re import pickle import numpy as np -from PointNICE.utils import getNeuronsDict +from sonic.utils import getNeuronsDict # Get list of implemented neurons names neurons = list(getNeuronsDict().keys()) # Define root directory and filename regular expression -root = 'C:/Users/admin/Google Drive/PhD/NICE model/PointNICE/PointNICE/lookups' +root = 'C:/Users/admin/Google Drive/PhD/NICE model/sonic/sonic/lookups' rgxp = re.compile('([A-Za-z]*)_lookups_a([0-9.]*)nm_f([0-9.]*)kHz.pkl') # For each neuron for neuron in neurons: print('--------- Aggregating lookups for {} neuron -------'.format(neuron)) # Get filepaths from all lookups file in directory neuron_root = '{}/{}'.format(root, neuron) filepaths = glob.glob('{}/*.pkl'.format(neuron_root)) # Create empty frequencies list and empty directory to hold aggregated coefficients freqs = [] agg_coeffs = {} ifile = 0 # Loop through each lookup file for fp in filepaths: # Get information from filename (a, f) filedir, filename = os.path.split(fp) mo = rgxp.fullmatch(filename) if mo: name = mo.group(1) a = float(mo.group(2)) * 1e-9 # nm f = float(mo.group(3)) * 1e3 # Hz else: print('error: lookup file does not match regular expression pattern') quit() # Add lookup frequency to list freqs.append(f) print('f =', f * 1e-3, 'kHz') # Open file and get coefficients dictionary with open(fp, 'rb') as fh: coeffs = pickle.load(fh) # If first file: initialization steps if ifile == 0: # Get names of output coefficients coeffs_keys = [ck for ck in coeffs.keys() if ck not in ['A', 'Q']] # Save input coefficients vectors separately A = coeffs['A'] Q = coeffs['Q'] print('nQ = ', len(Q)) # Initialize aggregating dictionary of output coefficients with empty lists agg_coeffs = {ck: [] for ck in coeffs_keys} # Append current coefficients to corresponding list in aggregating dictionary for ck in coeffs_keys: agg_coeffs[ck].append(coeffs[ck]) # Increment file index ifile += 1 # Transform lists of 2D arrays into 3D arrays inside aggregating dictionary for ck in agg_coeffs.keys(): # shape = agg_coeffs[ck][0].shape # for tmp in agg_coeffs[ck]: # if tmp.shape != shape: # print('dimensions error:', shape, tmp.shape) # quit() print(ck, len(agg_coeffs[ck]), [tmp.shape for tmp in agg_coeffs[ck]]) agg_coeffs[ck] = np.array(agg_coeffs[ck]) # Add the 3 input vectors to the dictionary agg_coeffs['f'] = np.array(freqs) agg_coeffs['A'] = A agg_coeffs['Q'] = Q # Print out all coefficients names and dimensions for ck in agg_coeffs.keys(): print(ck, agg_coeffs[ck].shape) # Save aggregated lookups in file filepath_out = '{}/{}_lookups_a{:.1f}nm.pkl'.format(root, neuron, a * 1e9) print(filepath_out) # with open(filepath_out, 'wb') as fh: # pickle.dump(agg_coeffs, fh) diff --git a/docs/PointNICE.SolverElec.rst b/docs/PointNICE.SolverElec.rst index da07e6a..e4040c7 100644 --- a/docs/PointNICE.SolverElec.rst +++ b/docs/PointNICE.SolverElec.rst @@ -1,10 +1,10 @@ Electrical stimulation ------------------------- -.. automodule:: PointNICE.solvers.SolverElec +.. automodule:: sonic.solvers.SolverElec .. autoclass:: SolverElec :members: :undoc-members: :show-inheritance: diff --git a/docs/PointNICE.SolverUS.rst b/docs/PointNICE.SolverUS.rst index b5636e0..f06640e 100644 --- a/docs/PointNICE.SolverUS.rst +++ b/docs/PointNICE.SolverUS.rst @@ -1,10 +1,10 @@ US stimulation ------------------------- -.. automodule:: PointNICE.solvers.SolverUS +.. automodule:: sonic.solvers.SolverUS .. autoclass:: SolverUS :members: :undoc-members: :show-inheritance: diff --git a/docs/PointNICE.base.rst b/docs/PointNICE.base.rst index 4739653..ef82f16 100644 --- a/docs/PointNICE.base.rst +++ b/docs/PointNICE.base.rst @@ -1,9 +1,9 @@ Standard Mechanism API ------------------------- -.. automodule:: PointNICE.channels.base +.. automodule:: sonic.channels.base .. autoclass:: BaseMech :members: :undoc-members: :show-inheritance: \ No newline at end of file diff --git a/docs/PointNICE.bls.rst b/docs/PointNICE.bls.rst index 1208896..5b06ede 100644 --- a/docs/PointNICE.bls.rst +++ b/docs/PointNICE.bls.rst @@ -1,7 +1,7 @@ Bilayer Sonophore ---------------- -.. automodule:: PointNICE.bls +.. automodule:: sonic.bls .. autoclass:: BilayerSonophore :members: diff --git a/docs/PointNICE.channels.rst b/docs/PointNICE.channels.rst index 1b5ca5f..6a93669 100644 --- a/docs/PointNICE.channels.rst +++ b/docs/PointNICE.channels.rst @@ -1,10 +1,10 @@ Channels ------------------------- .. toctree:: :maxdepth: 2 - PointNICE.base - PointNICE.cortical - PointNICE.thalamic - PointNICE.leech + sonic.base + sonic.cortical + sonic.thalamic + sonic.leech diff --git a/docs/PointNICE.cortical.rst b/docs/PointNICE.cortical.rst index 3b20cff..e2ed66a 100644 --- a/docs/PointNICE.cortical.rst +++ b/docs/PointNICE.cortical.rst @@ -1,24 +1,24 @@ Cortical neurons ------------------------- -.. automodule:: PointNICE.channels.cortical +.. automodule:: sonic.channels.cortical .. autoclass:: Cortical :members: :undoc-members: :show-inheritance: .. autoclass:: CorticalRS :members: :undoc-members: :show-inheritance: .. autoclass:: CorticalFS :members: :undoc-members: :show-inheritance: .. autoclass:: CorticalLTS :members: :undoc-members: :show-inheritance: diff --git a/docs/PointNICE.leech.rst b/docs/PointNICE.leech.rst index 47e79f5..fb31817 100644 --- a/docs/PointNICE.leech.rst +++ b/docs/PointNICE.leech.rst @@ -1,9 +1,9 @@ Leech neurons ------------------------- -.. automodule:: PointNICE.channels.leech +.. automodule:: sonic.channels.leech .. autoclass:: LeechTouch :members: :undoc-members: :show-inheritance: diff --git a/docs/PointNICE.solvers.rst b/docs/PointNICE.solvers.rst index 9be4200..ba020f9 100644 --- a/docs/PointNICE.solvers.rst +++ b/docs/PointNICE.solvers.rst @@ -1,9 +1,9 @@ Solvers ------------------------- .. toctree:: :maxdepth: 2 - PointNICE.SolverElec - PointNICE.SolverUS + sonic.SolverElec + sonic.SolverUS diff --git a/docs/PointNICE.thalamic.rst b/docs/PointNICE.thalamic.rst index 7f789c9..5a09231 100644 --- a/docs/PointNICE.thalamic.rst +++ b/docs/PointNICE.thalamic.rst @@ -1,14 +1,14 @@ Thalamic neurons ------------------------- -.. automodule:: PointNICE.channels.thalamic +.. automodule:: sonic.channels.thalamic .. autoclass:: Thalamic :members: :undoc-members: :show-inheritance: .. autoclass:: ThalamicRE :members: :undoc-members: :show-inheritance: diff --git a/docs/PointNICE.utils.rst b/docs/PointNICE.utils.rst index 9a87e6f..ee8d1fd 100644 --- a/docs/PointNICE.utils.rst +++ b/docs/PointNICE.utils.rst @@ -1,5 +1,5 @@ Utils ---------------- -.. automodule:: PointNICE.utils +.. automodule:: sonic.utils :members: diff --git a/docs/index.rst b/docs/index.rst index dbe100d..1cc870f 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -1,26 +1,26 @@ ***************************** -PointNICE Project +SONIC model ***************************** .. include:: ../README.md Modules: ========== .. toctree:: :maxdepth: 2 - PointNICE.bls - PointNICE.solvers - PointNICE.channels - PointNICE.utils + sonic.bls + sonic.solvers + sonic.channels + sonic.utils Indices and tables ================== * :ref:`genindex` * :ref:`modindex` * :ref:`search` diff --git a/plot/plot_ESTIM_anim.py b/plot/plot_ESTIM_anim.py index 4c7883f..9ea2350 100644 --- a/plot/plot_ESTIM_anim.py +++ b/plot/plot_ESTIM_anim.py @@ -1,82 +1,82 @@ #!/usr/bin/env python # -*- coding: utf-8 -*- # @Author: Theo Lemaire # @Date: 2016-10-11 20:35:38 # @Email: theo.lemaire@epfl.ch # @Last Modified by: Theo Lemaire -# @Last Modified time: 2018-04-17 15:46:41 +# @Last Modified time: 2018-08-21 16:07:31 """ Run simulations of the HH system with injected electric current, and plot resulting dynamics. """ import numpy as np import matplotlib.pyplot as plt import matplotlib.cm as cm import matplotlib.patches as patches -from PointNICE.solvers import SolverElec -from PointNICE.neurons import * +from PySONIC.solvers import SolverElec +from PySONIC.neurons import * # -------------- SIMULATION ----------------- # Create channels mechanism neuron = CorticalIB() for i in range(len(neuron.states_names)): print('{}0 = {:.2f}'.format(neuron.states_names[i], neuron.states0[i])) # Set pulse parameters tstim = 500e-3 # s toffset = 300e-3 # s Amin = -20.0 Amax = 20.0 amps = np.arange(Amin, Amax + 0.5, 1.0) nA = len(amps) root = 'C:/Users/Theo/Documents/E-STIM output/IB/anim' mymap = cm.get_cmap('coolwarm') sm = plt.cm.ScalarMappable(cmap=mymap, norm=plt.Normalize(Amin, Amax)) sm._A = [] i = 0 for Astim in amps: i += 1 # Run simulation print('sim {}/{} ({:.2f} mA/m2, {:.0f} ms)'.format(i, nA, Astim, tstim * 1e3)) solver = SolverElec() t, y, _ = solver.run(neuron, Astim, tstim, toffset) Vm = y[0, :] # Plot membrane potential profile fs = 12 fig, ax = plt.subplots(figsize=(10, 3.5)) ax.set_xlabel('$time\ (ms)$', fontsize=fs) ax.set_ylabel('$V_m\ (mV)$', fontsize=fs) ax.set_ylim(-150.0, 60.0) ax.set_xticks([0.0, 500.0]) ax.set_yticks([-100, 50.0]) ax.locator_params(axis='y', nbins=2) for item in ax.get_yticklabels(): item.set_fontsize(fs) for item in ax.get_xticklabels(): item.set_fontsize(fs) (ybottom, ytop) = ax.get_ylim() ax.add_patch(patches.Rectangle((0.0, ybottom), tstim * 1e3, ytop - ybottom, facecolor='gold', alpha=0.2)) ax.plot(t * 1e3, Vm, linewidth=2) plt.tight_layout() fig.subplots_adjust(right=0.80) cbar_ax = fig.add_axes([0.82, 0.2, 0.02, 0.75]) fig.add_axes() fig.colorbar(sm, cax=cbar_ax, ticks=[Astim]) cbar_ax.set_yticklabels(['{:.2f} mA/m2'.format(Astim)], fontsize=fs) fig.savefig('{}/fig{:03d}.png'.format(root, i)) plt.close(fig) diff --git a/plot/plot_Ih_kinetics.py b/plot/plot_Ih_kinetics.py index 6e57777..93e3e93 100644 --- a/plot/plot_Ih_kinetics.py +++ b/plot/plot_Ih_kinetics.py @@ -1,68 +1,68 @@ #!/usr/bin/env python # -*- coding: utf-8 -*- # @Author: Theo Lemaire # @Date: 2016-10-11 20:35:38 # @Email: theo.lemaire@epfl.ch # @Last Modified by: Theo Lemaire -# @Last Modified time: 2018-03-12 19:45:05 +# @Last Modified time: 2018-08-21 16:07:32 """ Plot the voltage-dependent kinetics of the hyperpolarization-activated cationic current found in thalamo-cortical neurons. """ import numpy as np import matplotlib.pyplot as plt import matplotlib.cm as cm from matplotlib.colors import LogNorm -# from PointNICE.solvers import SolverElec -from PointNICE.neurons import ThalamoCortical -from PointNICE.utils import rescale +# from PySONIC.solvers import SolverElec +from PySONIC.neurons import ThalamoCortical +from PySONIC.utils import rescale # -------------- SIMULATION ----------------- # Create channels mechanism neuron = ThalamoCortical() # Input vectors nV = 100 nC = 10 CCa_min = 0.01 # uM CCa_max = 10 # um Vm = np.linspace(-100, 50, nV) # mV CCa = np.logspace(np.log10(CCa_min), np.log10(CCa_max), nC) # uM # Output matrix: relative activation (0-2) BA = neuron.betao(Vm) / neuron.alphao(Vm) P0 = neuron.k2 / (neuron.k2 + neuron.k1 * (CCa * 1e-6)**4) gH_rel = np.empty((nV, nC)) for i in range(nC): O_form = neuron.k4 / (neuron.k3 * (1 - P0[i]) + neuron.k4 * (1 + BA)) OL_form = (1 - O_form * (1 + BA)) gH_rel[:, i] = O_form + 2 * OL_form mymap = cm.get_cmap('viridis') sm = plt.cm.ScalarMappable(cmap=mymap, norm=LogNorm(CCa_min, CCa_max)) sm._A = [] fs = 18 fig, ax = plt.subplots(figsize=(8, 5)) ax.set_title('global activation', fontsize=fs) ax.set_xlabel('$V_m\ (mV)$', fontsize=fs) ax.set_ylabel('$(O + 2O_L)_{\infty}$', fontsize=fs) ax.set_yticks([0, 1, 2]) for i in range(nC): ax.plot(Vm, gH_rel[:, i], linewidth=2, c=mymap(rescale(np.log10(CCa[i]), np.log10(CCa_min), np.log10(CCa_max)))) fig.subplots_adjust(right=0.85) cbar_ax = fig.add_axes([0.87, 0.1, 0.02, 0.8]) fig.add_axes() fig.colorbar(sm, cax=cbar_ax) cbar_ax.set_ylabel('$[Ca^{2+}_i]\ (uM)$', fontsize=fs) plt.show() diff --git a/plot/plot_P_vs_I.py b/plot/plot_P_vs_I.py index 025ef83..be76caa 100644 --- a/plot/plot_P_vs_I.py +++ b/plot/plot_P_vs_I.py @@ -1,34 +1,34 @@ #!/usr/bin/env python # -*- coding: utf-8 -*- # @Author: Theo Lemaire # @Date: 2017-07-17 11:47:50 # @Email: theo.lemaire@epfl.ch # @Last Modified by: Theo Lemaire -# @Last Modified time: 2018-03-13 15:21:18 +# @Last Modified time: 2018-08-21 16:07:32 ''' plot profile of acoustic Intensity (in W/cm^2) vs Pressure (in kPa) ''' import numpy as np import matplotlib.pyplot as plt -from PointNICE.utils import Pressure2Intensity +from PySONIC.utils import Pressure2Intensity rho = 1075 # kg/m3 c = 1515 # m/s P = np.logspace(np.log10(1e1), np.log10(1e7), num=500) # Pa Int = Pressure2Intensity(P, rho, c) # W/m2 fig, ax = plt.subplots() ax.set_xlabel('$Pressure\ (kPa)$') ax.set_ylabel('$I_{SPPA}\ (W/cm^2)$') ax.set_xscale('log') ax.set_yscale('log') ax.plot(P * 1e-3, Int * 1e-4) Psnaps = np.logspace(1, 7, 7) # Pa for Psnap in Psnaps: Isnap = Pressure2Intensity(Psnap, rho, c) # W/m2 ax.plot(np.array([Psnap, Psnap]) * 1e-3, np.array([0.0, Isnap]) * 1e-4, '--', color='black') ax.plot(np.array([0, Psnap]) * 1e-3, np.array([Isnap, Isnap]) * 1e-4, '--', color='black') plt.show() diff --git a/plot/plot_batch.py b/plot/plot_batch.py index d8fa3ab..1f5ad6c 100644 --- a/plot/plot_batch.py +++ b/plot/plot_batch.py @@ -1,40 +1,40 @@ #!/usr/bin/env python # -*- coding: utf-8 -*- # @Author: Theo Lemaire # @Date: 2017-03-20 12:19:55 # @Email: theo.lemaire@epfl.ch # @Last Modified by: Theo Lemaire -# @Last Modified time: 2018-06-15 18:02:07 +# @Last Modified time: 2018-08-21 16:07:30 """ Batch plot profiles of several specific output variables of NICE simulations. """ import sys import logging -from PointNICE.utils import logger, OpenFilesDialog, InputError -from PointNICE.plt import plotBatch +from PySONIC.utils import logger, OpenFilesDialog, InputError +from PySONIC.plt import plotBatch # Set logging level logger.setLevel(logging.INFO) # Select data files pkl_filepaths, pkl_dir = OpenFilesDialog('pkl') if not pkl_filepaths: logger.error('No input file') sys.exit(1) yvars = { 'V_m': ['Vm'], 'i_{Na}\ kin.': ['m', 'h', 'm3h', 'n'], 'i_K\ kin.': ['n'], 'i_M\ kin.': ['p'] # 'i_{CaL}\ kin.': ['q', 'r', 'q2r'] } # Plot profiles try: plotBatch(pkl_dir, pkl_filepaths, title=True, vars_dict=yvars) except InputError as err: logger.error(err) sys.exit(1) diff --git a/plot/plot_comp.py b/plot/plot_comp.py index 39e0373..31c9e24 100644 --- a/plot/plot_comp.py +++ b/plot/plot_comp.py @@ -1,32 +1,32 @@ #!/usr/bin/env python # -*- coding: utf-8 -*- # @Author: Theo Lemaire # @Date: 2017-02-13 12:41:26 # @Email: theo.lemaire@epfl.ch # @Last Modified by: Theo Lemaire -# @Last Modified time: 2018-04-14 18:37:53 +# @Last Modified time: 2018-08-21 16:07:31 """ Compare profiles of several specific output variables of NICE simulations. """ import sys import logging -from PointNICE.utils import logger, OpenFilesDialog, InputError -from PointNICE.plt import plotComp +from PySONIC.utils import logger, OpenFilesDialog, InputError +from PySONIC.plt import plotComp # Set logging level logger.setLevel(logging.INFO) # Select data files pkl_filepaths, _ = OpenFilesDialog('pkl') if not pkl_filepaths: logger.error('No input file') sys.exit(1) nfiles = len(pkl_filepaths) # Comparative plot try: plotComp('Qm', pkl_filepaths) except InputError as err: logger.error(err) sys.exit(1) diff --git a/plot/plot_eff_coeffs.py b/plot/plot_eff_coeffs.py index 6ef2b00..f44615a 100644 --- a/plot/plot_eff_coeffs.py +++ b/plot/plot_eff_coeffs.py @@ -1,34 +1,34 @@ #!/usr/bin/env python # -*- coding: utf-8 -*- # @Author: Theo Lemaire # @Date: 2017-02-15 15:59:37 # @Email: theo.lemaire@epfl.ch # @Last Modified by: Theo Lemaire -# @Last Modified time: 2018-08-06 11:01:27 +# @Last Modified time: 2018-08-21 16:07:31 import numpy as np import matplotlib.pyplot as plt -from PointNICE.plt import plotEffVars -from PointNICE.neurons import * +from PySONIC.plt import plotEffVars +from PySONIC.neurons import * ''' Plot the profiles of effective variables as a function of charge density with amplitude color code. ''' # Set parameters neuron = CorticalRS() Fdrive = 500e3 amps = np.logspace(np.log10(1), np.log10(600), 10) * 1e3 charges = np.linspace(neuron.Vm0, 50, 100) * 1e-5 # Define variables to plot gates = ['m', 'h', 'n', 'p'] keys = ['V', 'ng'] for x in gates: keys += ['alpha{}'.format(x), 'beta{}'.format(x)] # Plot effective variables fig = plotEffVars(neuron, Fdrive, amps=amps, charges=charges, keys=keys, ncolmax=2, fs=8) plt.show() diff --git a/plot/plot_forces.py b/plot/plot_forces.py index 08b7097..31aecec 100644 --- a/plot/plot_forces.py +++ b/plot/plot_forces.py @@ -1,228 +1,228 @@ #!/usr/bin/env python # -*- coding: utf-8 -*- # @Author: Theo Lemaire # @Date: 2016-10-07 10:22:24 # @Email: theo.lemaire@epfl.ch # @Last Modified by: Theo Lemaire -# @Last Modified time: 2017-08-28 14:17:56 +# @Last Modified time: 2018-08-21 16:09:21 """ Analysis of the system geometric variables and interplaying forces at stake in a static quasi-steady NICE system. """ import time import numpy as np import matplotlib.pyplot as plt -import PointNICE -from PointNICE.utils import PmCompMethod +from PySONIC import BilayerSonophore +from PySONIC.utils import PmCompMethod plt_bool = 1 # Initialization: create a BLS instance a = 32e-9 # in-plane radius (m) Fdrive = 0.0 # dummy stimulation frequency Cm0 = 1e-2 # membrane resting capacitance (F/m2) Qm0 = -71.9e-5 # membrane resting charge density (C/m2) -bls = PointNICE.BilayerSonophore(a, Fdrive, Cm0, Qm0) +bls = BilayerSonophore(a, Fdrive, Cm0, Qm0) # Input 1: leaflet deflections ZMin = -0.45 * bls.Delta ZMax = 2 * bls.a nZ = 3000 Z = np.linspace(ZMin, ZMax, nZ) Zlb = -0.5 * bls.Delta Zub = bls.a # Input 2: acoustic perturbations PacMax = 9.5e4 nPac1 = 5 nPac2 = 100 Pac1 = np.linspace(-PacMax, PacMax, nPac1) Pac2 = np.linspace(-PacMax, PacMax, nPac2) # Input 3: membrane charge densities QmMin = bls.Qm0 QmMax = 50.0e-5 nQm = 7 Qm = np.linspace(QmMin, QmMax, nQm) # Outputs R = np.empty(nZ) Cm = np.empty(nZ) Pm_apex = np.empty(nZ) Pm_avg = np.empty(nZ) Pm_avg_predict = np.empty(nZ) Pg = np.empty(nZ) Pec = np.empty(nZ) Pel = np.empty(nZ) P0 = np.ones(nZ) * bls.P0 Pnet = np.empty(nZ) Pqs = np.empty((nZ, nPac1)) Pecdense = np.empty((nZ, nQm)) Pnetdense = np.empty((nZ, nQm)) Zeq = np.empty(nPac1) Zeq_dense = np.empty(nPac2) t0 = time.time() # Check net QS pressure at Z = 0 Peq0 = bls.PtotQS(0.0, bls.ng0, bls.Qm0, 0.0, PmCompMethod.direct) print('Net QS pressure at Z = 0.0 without perturbation: ' + '{:.2e}'.format(Peq0) + ' Pa') # Loop through the deflection vector for i in range(nZ): # 1-dimensional output vectors R[i] = bls.curvrad(Z[i]) Cm[i] = bls.Capct(Z[i]) Pm_apex[i] = bls.PMlocal(0.0, Z[i], R[i]) Pm_avg[i] = bls.PMavg(Z[i], R[i], bls.surface(Z[i])) Pm_avg_predict[i] = bls.PMavgpred(Z[i]) Pel[i] = bls.PEtot(Z[i], R[i]) Pg[i] = bls.gasmol2Pa(bls.ng0, bls.volume(Z[i])) Pec[i] = bls.Pelec(Z[i], bls.Qm0) Pnet[i] = bls.PtotQS(Z[i], bls.ng0, bls.Qm0, 0.0, PmCompMethod.direct) # loop through the acoustic perturbation vector an compute 2-dimensional # balance pressure output vector for j in range(nPac1): Pqs[i, j] = bls.PtotQS(Z[i], bls.ng0, bls.Qm0, Pac1[j], PmCompMethod.direct) for j in range(nQm): Pecdense[i, j] = bls.Pelec(Z[i], Qm[j]) Pnetdense[i, j] = bls.PtotQS(Z[i], bls.ng0, Qm[j], 0.0, PmCompMethod.direct) # Compute min local intermolecular pressure Pm_apex_min = np.amin(Pm_apex) iPm_apex_min = np.argmin(Pm_apex) print("min local intermolecular resultant pressure = %.2e Pa for z = %.2f nm" % (Pm_apex_min, Z[iPm_apex_min] * 1e9)) for j in range(nPac1): Zeq[j] = bls.balancedefQS(bls.ng0, bls.Qm0, Pac1[j], PmCompMethod.direct) for j in range(nPac2): Zeq_dense[j] = bls.balancedefQS(bls.ng0, bls.Qm0, Pac2[j], PmCompMethod.direct) t1 = time.time() print("computation completed in " + '{:.2f}'.format(t1 - t0) + " s") if plt_bool == 1: # 1: Intermolecular pressures fig1, ax = plt.subplots() fig1.canvas.set_window_title("1: integrated vs. predicted average intermolecular pressure") ax.set_xlabel('Z $(nm)$', fontsize=18) ax.set_ylabel('Pressures $(MPa)$', fontsize=18) ax.grid(True) ax.plot([Zlb * 1e9, Zlb * 1e9], [np.amin(Pm_avg) * 1e-6, np.amax(Pm_avg) * 1e-6], '--', color="blue", label="$-\Delta /2$") ax.plot([Zub * 1e9, Zub * 1e9], [np.amin(Pm_avg) * 1e-6, np.amax(Pm_avg) * 1e-6], '--', color="red", label="$a$") ax.plot(Z * 1e9, Pm_avg * 1e-6, '-', label="$P_{M, avg}$", color="green", linewidth=2.0) ax.plot(Z * 1e9, Pm_avg_predict * 1e-6, '-', label="$P_{M, avg-predict}$", color="red", linewidth=2.0) ax.set_xlim(ZMin * 1e9 - 5, ZMax * 1e9) ax.legend(fontsize=24) # 2: Capacitance and electric pressure fig2, ax = plt.subplots() fig2.canvas.set_window_title("2: Capacitance and electric equivalent pressure") ax.set_xlabel('Z $(nm)$', fontsize=18) ax.set_ylabel('$C_m \ (uF/cm^2)$', fontsize=18) ax.plot(Z * 1e9, Cm * 1e2, '-', label="$C_{m}$", color="black", linewidth=2.0) ax.set_xlim(ZMin * 1e9 - 5, ZMax * 1e9) ax2 = ax.twinx() ax2.set_ylabel('$P_{EC}\ (MPa)$', fontsize=18, color='magenta') ax2.plot(Z * 1e9, Pec * 1e-6, '-', label="$P_{EC}$", color="magenta", linewidth=2.0) # tmp: electric pressure for varying membrane charge densities figtmp, ax = plt.subplots() figtmp.canvas.set_window_title("electric pressure for varying membrane charges") ax.set_xlabel('Z $(nm)$', fontsize=18) ax.set_ylabel('$P_{EC} \ (MPa)$', fontsize=18) for j in range(nQm): lbl = "$Q_m$ = " + '{:.2f}'.format(Qm[j] * 1e5) + " nC/cm2" ax.plot(Z * 1e9, Pecdense[:, j] * 1e-6, '-', label=lbl, linewidth=2.0) ax.set_xlim(ZMin * 1e9 - 5, ZMax * 1e9) ax.legend() # tmp: net pressure for varying membrane potentials figtmp, ax = plt.subplots() figtmp.canvas.set_window_title("net pressure for varying membrane charges") ax.set_xlabel('Z $(nm)$', fontsize=18) ax.set_ylabel('$P_{net} \ (MPa)$', fontsize=18) for j in range(nQm): lbl = "$Q_m$ = " + '{:.2f}'.format(Qm[j] * 1e5) + " nC/cm2" ax.plot(Z * 1e9, Pnetdense[:, j] * 1e-6, '-', label=lbl, linewidth=2.0) ax.set_xlim(ZMin * 1e9 - 5, ZMax * 1e9) ax.legend() # 3: Net pressure without perturbation fig3, ax = plt.subplots() fig3.canvas.set_window_title("3: Net QS pressure without perturbation") ax.set_xlabel('Z $(nm)$', fontsize=18) ax.set_ylabel('Pressures $(kPa)$', fontsize=18) # ax.grid(True) # ax.plot([Zlb * 1e9, Zlb * 1e9], [np.amin(Pec) * 1e-3, np.amax(Pm_avg) * 1e-3], '--', # color="blue", label="$-\Delta / 2$") # ax.plot([Zub * 1e9, Zub * 1e9], [np.amin(Pec) * 1e-3, np.amax(Pm_avg) * 1e-3], '--', # color="red", label="$a$") ax.plot(Z * 1e9, Pg * 1e-3, '-', label="$P_{gas}$", linewidth=3.0, color='C0') ax.plot(Z * 1e9, -P0 * 1e-3, '-', label="$-P_{0}$", linewidth=3.0, color='C1') ax.plot(Z * 1e9, Pm_avg * 1e-3, '-', label="$P_{mol}$", linewidth=3.0, color='C2') ax.plot(Z * 1e9, Pec * 1e-3, '-', label="$P_{elec}$", linewidth=3.0, color='C3') ax.plot(Z * 1e9, Pel * 1e-3, '-', label="$P_{elastic}$", linewidth=3.0, color='C4') # ax.plot(Z * 1e9, (Pg - P0 + Pm_avg + Pec + Pel) * 1e-3, '--', label="$P_{net}$", linewidth=2.0, # color='black') # ax.plot(Z * 1e9, (Pg - P0 + Pm_avg + Pec - Pnet) * 1e-6, '--', label="$P_{net} diff$", # linewidth=2.0, color="blue") ax.set_xlim(ZMin * 1e9 - 5, 30) ax.set_ylim(-1500, 2000) ax.legend(fontsize=24) # ax.grid(True) # 4: QS pressure for different perturbations fig4, ax = plt.subplots() fig4.canvas.set_window_title("4: Net QS pressure for different acoustic perturbations") ax.set_xlabel('Z $(nm)$', fontsize=18) ax.set_ylabel('Pressures $(MPa)$', fontsize=18) ax.grid(True) ax.plot([Zlb * 1e9, Zlb * 1e9], [np.amin(Pqs[:, 0]) * 1e-6, np.amax(Pqs[:, nPac1 - 1]) * 1e-6], '--', color="blue", label="$-\Delta/2$") ax.plot([Zub * 1e9, Zub * 1e9], [np.amin(Pqs[:, 0]) * 1e-6, np.amax(Pqs[:, nPac1 - 1]) * 1e-6], '--', color="red", label="$a$") ax.set_xlim(ZMin * 1e9 - 5, ZMax * 1e9) for j in range(nPac1): lbl = "$P_{A}$ = %.2f MPa" % (Pac1[j] * 1e-6) ax.plot(Z * 1e9, Pqs[:, j] * 1e-6, '-', label=lbl, linewidth=2.0) ax.plot([Zeq[j] * 1e9, Zeq[j] * 1e9], [np.amin(Pqs[:, nPac1 - 1]) * 1e-6, np.amax(Pqs[:, 0]) * 1e-6], '--', color="black") ax.legend(fontsize=24) # 5: QS balance deflection for different acoustic perturbations fig5, ax = plt.subplots() fig5.canvas.set_window_title("5: QS balance deflection for different acoustic perturbations ") ax.set_xlabel('Perturbation $(MPa)$', fontsize=18) ax.set_ylabel('Z $(nm)$', fontsize=18) ax.plot([np.amin(Pac2) * 1e-6, np.amax(Pac2) * 1e-6], [Zlb * 1e9, Zlb * 1e9], '--', color="blue", label="$-\Delta / 2$") ax.plot([np.amin(Pac2) * 1e-6, np.amax(Pac2) * 1e-6], [Zub * 1e9, Zub * 1e9], '--', color="red", label="$a$") ax.plot([-bls.P0 * 1e-6, -bls.P0 * 1e-6], [np.amin(Zeq_dense) * 1e9, np.amax(Zeq_dense) * 1e9], '--', color="black", label="$-P_0$") ax.plot(Pac2 * 1e-6, Zeq_dense * 1e9, '-', label="$Z_{eq}$", linewidth=2.0) ax.set_xlim(-0.12, 0.12) ax.set_ylim(ZMin * 1e9 - 5, bls.a * 1e9 + 5) ax.legend(fontsize=24) plt.show() diff --git a/plot/plot_gating_kinetics.py b/plot/plot_gating_kinetics.py index b81eab4..13e70ea 100644 --- a/plot/plot_gating_kinetics.py +++ b/plot/plot_gating_kinetics.py @@ -1,20 +1,20 @@ #!/usr/bin/env python # -*- coding: utf-8 -*- # @Author: Theo Lemaire # @Date: 2016-10-11 20:35:38 # @Email: theo.lemaire@epfl.ch # @Last Modified by: Theo Lemaire -# @Last Modified time: 2018-05-03 16:15:47 +# @Last Modified time: 2018-08-21 16:07:32 """ Plot the voltage-dependent steady-states and time constants of activation and inactivation gates of the different ionic currents involved in the neuron's membrane. """ -from PointNICE.plt import plotGatingKinetics -from PointNICE.neurons import * +from PySONIC.plt import plotGatingKinetics +from PySONIC.neurons import * # Instantiate neuron(s) neurons = [CorticalLTS()] # Plot gating kinetics for each neuron(s) for neuron in neurons: plotGatingKinetics(neuron) diff --git a/plot/plot_rate_constants.py b/plot/plot_rate_constants.py index 02ebe39..87dda6b 100644 --- a/plot/plot_rate_constants.py +++ b/plot/plot_rate_constants.py @@ -1,12 +1,12 @@ # -*- coding: utf-8 -*- # @Author: Theo Lemaire # @Date: 2017-09-01 21:08:24 # @Last Modified by: Theo Lemaire -# @Last Modified time: 2018-05-25 15:57:01 +# @Last Modified time: 2018-08-21 16:07:32 -from PointNICE.plt import plotRateConstants -from PointNICE.neurons import * +from PySONIC.plt import plotRateConstants +from PySONIC.neurons import * neuron = CorticalRS() plotRateConstants(neuron) diff --git a/plot/plot_rheobase_amps.py b/plot/plot_rheobase_amps.py index b011f94..bff3dd3 100644 --- a/plot/plot_rheobase_amps.py +++ b/plot/plot_rheobase_amps.py @@ -1,56 +1,56 @@ # -*- coding: utf-8 -*- # @Author: Theo # @Date: 2018-04-30 21:06:10 # @Last Modified by: Theo Lemaire -# @Last Modified time: 2018-05-08 14:41:56 +# @Last Modified time: 2018-08-21 16:07:33 ''' Plot neuron-specific rheobase acoustic amplitudes for various duty cycles. ''' import sys import logging import numpy as np import matplotlib.pyplot as plt -from PointNICE.utils import logger, InputError, getNeuronsDict, si_format -from PointNICE.solvers import SolverUS +from PySONIC.utils import logger, InputError, getNeuronsDict, si_format +from PySONIC.solvers import SolverUS # Set logging level logger.setLevel(logging.INFO) # Set plot parameters fs = 15 # font size ps = 100 # scatter point size lw = 2 # linewidth # Define input parameters Fdrive = 500e3 # Hz a = 32e-9 # m DCs = np.arange(1, 101) / 1e2 neurons = ['RS', 'FS', 'LTS', 'RE', 'TC'] # Initialize figure fig, ax = plt.subplots() ax.set_xlabel('Duty cycle (%)', fontsize=fs) ax.set_ylabel('Rheobase amplitude (kPa)', fontsize=fs) for item in ax.get_xticklabels() + ax.get_yticklabels(): item.set_fontsize(fs) ax.set_yscale('log') ax.set_ylim([10, 600]) # Loop through neuron types for n in neurons: neuron = getNeuronsDict()[n]() try: # Find and plot rheobase amplitudes for duty cycles solver = SolverUS(a, neuron, Fdrive) logger.info('Computing %s neuron rheobase amplitudes at %sHz', neuron.name, si_format(Fdrive)) Athrs = solver.findRheobaseAmps(neuron, Fdrive, DCs, neuron.VT) ax.plot(DCs * 1e2, Athrs * 1e-3, label='{} neuron'.format(neuron.name)) except InputError as err: logger.error(err) sys.exit(1) ax.legend() fig.tight_layout() plt.show() diff --git a/plot/plot_spikes_batch.py b/plot/plot_spikes_batch.py index 20caa38..e377184 100644 --- a/plot/plot_spikes_batch.py +++ b/plot/plot_spikes_batch.py @@ -1,76 +1,76 @@ # -*- coding: utf-8 -*- # @Author: Theo # @Date: 2018-04-04 11:49:07 # @Last Modified by: Theo Lemaire -# @Last Modified time: 2018-04-04 12:06:32 +# @Last Modified time: 2018-08-21 16:07:33 ''' Plot detected spikes on charge profiles. ''' import sys import os import pickle import logging import numpy as np import matplotlib.pyplot as plt -from PointNICE.utils import logger, OpenFilesDialog, InputError -from PointNICE.solvers import findPeaks -from PointNICE.constants import * +from PySONIC.utils import logger, OpenFilesDialog, InputError +from PySONIC.solvers import findPeaks +from PySONIC.constants import * # Set logging level logger.setLevel(logging.INFO) # Set plot parameters fs = 15 # font size lw = 2 # linewidth # Select data files pkl_filepaths, pkl_dir = OpenFilesDialog('pkl') if not pkl_filepaths: logger.error('No input file') sys.exit(1) try: for fpath in pkl_filepaths: # Load charge profile from file fname = os.path.basename(fpath) logger.info('Loading data from "%s" file', fname) with open(fpath, 'rb') as fh: frame = pickle.load(fh) df = frame['data'] t = df['t'].values Qm = df['Qm'].values dt = t[1] - t[0] indexes = np.arange(t.size) mpd = int(np.ceil(SPIKE_MIN_DT / dt)) ipeaks, prominences, widths, ibounds = findPeaks(Qm, mph=SPIKE_MIN_QAMP, mpd=mpd, mpp=SPIKE_MIN_QPROM) if ipeaks is not None: widths *= dt tleftbounds = np.interp(ibounds[:, 0], indexes, t) trightbounds = np.interp(ibounds[:, 1], indexes, t) # Plot results fig, ax = plt.subplots(figsize=(8, 4)) ax.set_title(os.path.splitext(fname)[0], fontsize=fs) ax.set_xlabel('time (ms)', fontsize=fs) ax.set_ylabel('charge\ (nC/cm2)', fontsize=fs) ax.plot(t * 1e3, Qm * 1e5, color='C0', label='trace', linewidth=lw) if ipeaks is not None: ax.scatter(t[ipeaks] * 1e3, Qm[ipeaks] * 1e5 + 3, color='k', label='peaks', marker='v') for i in range(len(ipeaks)): ax.plot(np.array([t[ipeaks[i]]] * 2) * 1e3, np.array([Qm[ipeaks[i]], Qm[ipeaks[i]] - prominences[i]]) * 1e5, color='C1', label='prominences' if i == 0 else '') ax.plot(np.array([tleftbounds[i], trightbounds[i]]) * 1e3, np.array([Qm[ipeaks[i]] - 0.5 * prominences[i]] * 2) * 1e5, color='C2', label='widths' if i == 0 else '') ax.legend() plt.show() except InputError as err: logger.error(err) sys.exit(1) diff --git a/postpro/postpro_latency.py b/postpro/postpro_latency.py index f4fcb70..9221c82 100644 --- a/postpro/postpro_latency.py +++ b/postpro/postpro_latency.py @@ -1,61 +1,61 @@ #!/usr/bin/env python # -*- coding: utf-8 -*- # @Author: Theo Lemaire # @Date: 2016-10-31 10:10:41 # @Email: theo.lemaire@epfl.ch # @Last Modified by: Theo Lemaire -# @Last Modified time: 2017-08-28 14:18:34 +# @Last Modified time: 2018-08-21 16:07:33 """ Test relationship between stimulus intensity and response latency. """ import numpy as np import matplotlib.pyplot as plt -from PointNICE.utils import ImportExcelCol, Presssure2Intensity +from PySONIC.utils import ImportExcelCol, Presssure2Intensity # Define import settings xls_file = "C:/Users/admin/Desktop/Model output/NBLS spikes 0.35MHz/nbls_log_spikes_0.35MHz.xlsx" sheet = 'Data' # Import data f = ImportExcelCol(xls_file, sheet, 'E', 2) * 1e3 # Hz A = ImportExcelCol(xls_file, sheet, 'F', 2) * 1e3 # Pa T = ImportExcelCol(xls_file, sheet, 'G', 2) * 1e-3 # s N = ImportExcelCol(xls_file, sheet, 'Q', 2) L = ImportExcelCol(xls_file, sheet, 'R', 2) # ms # Retrieve unique values of latencies (for min. 2 spikes) and corresponding amplitudes iremove = np.where(N < 2)[0] A_true = np.delete(A, iremove) L_true = np.delete(L, iremove).astype(np.float) latencies, indices = np.unique(L_true, return_index=True) amplitudes = A_true[indices] # Convert amplitudes to intensities intensities = Pressure2Intensity(amplitudes) * 1e-4 # W/cm2 # Plot latency vs. amplitude fig1, ax = plt.subplots(figsize=(12, 9)) ax.set_xlabel("$Amplitude \ (kPa)$", fontsize=28) ax.set_ylabel("$Latency \ (ms)$", fontsize=28) ax.scatter(amplitudes * 1e-3, latencies, color='black', s=100) for item in ax.get_yticklabels(): item.set_fontsize(24) for item in ax.get_xticklabels(): item.set_fontsize(24) # Plot latency vs. intensity fig2, ax = plt.subplots(figsize=(12, 9)) ax.set_xlabel("$Intensity \ (W/cm^2)$", fontsize=28) ax.set_ylabel("$Latency \ (ms)$", fontsize=28) ax.scatter(intensities, latencies, color='black', s=100) ax.set_xticks([0, 0.2, 0.4, 0.6, 0.8]) ax.set_yticks([25, 35, 55, 65]) for item in ax.get_yticklabels(): item.set_fontsize(24) for item in ax.get_xticklabels(): item.set_fontsize(24) plt.show() diff --git a/postpro/postpro_rmse_charge.py b/postpro/postpro_rmse_charge.py index 0938889..8e420f8 100644 --- a/postpro/postpro_rmse_charge.py +++ b/postpro/postpro_rmse_charge.py @@ -1,77 +1,77 @@ #!/usr/bin/env python # -*- coding: utf-8 -*- # @Author: Theo Lemaire # @Date: 2016-11-01 16:35:43 # @Email: theo.lemaire@epfl.ch # @Last Modified by: Theo Lemaire -# @Last Modified time: 2018-03-13 12:35:30 +# @Last Modified time: 2018-08-21 16:07:33 """ Compute RMSE between charge profiles of NICE output. """ import sys import pickle import ntpath import numpy as np -from PointNICE.utils import OpenFilesDialog, rmse +from PySONIC.utils import OpenFilesDialog, rmse # Define options pkl_root = "../../Output/test Elec/" t_offset = 10e-3 # s # Select data files (PKL) pkl_filepaths, pkl_dir = OpenFilesDialog('pkl') # Quit if no file selected if not pkl_filepaths: print('error: no input file') sys.exit(1) # Quit if more than 2 files if len(pkl_filepaths) > 2: print('error: cannot compare more than 2 methods') sys.exit(1) # Load data from file 1 pkl_filename = ntpath.basename(pkl_filepaths[0]) print('Loading data from "' + pkl_filename + '"') with open(pkl_filepaths[0], 'rb') as fh: frame = pickle.load(fh) df = frame['data'] meta = frame['meta'] tstim1 = meta['tstim'] toffset1 = meta['toffset'] f1 = meta['Fdrive'] A1 = meta['Adrive'] t1 = df['t'].values Q1 = df['Qm'].values * 1e2 # nC/cm2 states1 = df['states'].values # Load data from file 2 pkl_filename = ntpath.basename(pkl_filepaths[1]) print('Loading data from "' + pkl_filename + '"') with open(pkl_filepaths[1], 'rb') as fh: frame = pickle.load(fh) df = frame['data'] meta = frame['meta'] tstim2 = meta['tstim'] toffset2 = meta['toffset'] f2 = meta['Fdrive'] A2 = meta['Adrive'] t2 = df['t'].values Q2 = df['Qm'].values * 1e2 # nC/cm2 states2 = df['states'].values if tstim1 != tstim2 or f1 != f2 or A1 != A2 or toffset1 != toffset2: print('error: different stimulation conditions') else: print('comparing charge profiles') tcomp = np.arange(0, tstim1 + toffset1, 1e-3) # every ms Qcomp1 = np.interp(tcomp, t1, Q1) Qcomp2 = np.interp(tcomp, t2, Q2) Q_rmse = rmse(Qcomp1, Qcomp2) print('rmse = {:.5f} nC/cm2'.format(Q_rmse * 1e5)) diff --git a/postpro/postpro_sensitivity_diameter.py b/postpro/postpro_sensitivity_diameter.py index 6d33b3a..0e20fcc 100644 --- a/postpro/postpro_sensitivity_diameter.py +++ b/postpro/postpro_sensitivity_diameter.py @@ -1,67 +1,67 @@ #!/usr/bin/env python # -*- coding: utf-8 -*- # @Author: Theo Lemaire # @Date: 2016-10-05 11:04:43 # @Email: theo.lemaire@epfl.ch # @Last Modified by: Theo Lemaire -# @Last Modified time: 2017-07-18 15:00:40 +# @Last Modified time: 2018-08-21 16:07:34 """ Test influence of structure diameter on BLS cavitation amplitude. """ import numpy as np from scipy.optimize import curve_fit import matplotlib.pyplot as plt -from PointNICE.utils import ImportExcelCol +from PySONIC.utils import ImportExcelCol def f(x, a_, b_): """ Fitting function """ return a_ * np.power(x, b_) # Import data xls_file = "C:/Users/admin/Desktop/Model output/BLS Z diameter/bls_logZ_diameter.xlsx" sheet = 'Data' rd = ImportExcelCol(xls_file, sheet, 'C', 2) * 1e-9 eAmax = ImportExcelCol(xls_file, sheet, 'M', 2) # Discard outliers rd = rd[0:-5] eAmax = eAmax[0:-5] # Compute best power fit for eAmax popt, pcov = curve_fit(f, rd, eAmax) (a, b) = popt if a < 1e-4: a_str = '{:.2e}'.format(a) else: a_str = '{:.4f}'.format(a) print("global least-square power fit: eAmax = " + a_str + " * a^" + '{:.2f}'.format(b)) # Compute predicted data and associated error eAmax_predicted = f(rd, a, b) residuals = eAmax - eAmax_predicted ss_res = np.sum(residuals**2) ss_tot = np.sum((eAmax - np.mean(eAmax))**2) r_squared_eAmax = 1 - (ss_res / ss_tot) print("R-squared = " + '{:.5f}'.format(r_squared_eAmax)) N = residuals.size std_err = np.sqrt(ss_res / N) print("standard error: sigma_err = " + str(std_err)) # Plot areal strain vs. in-plane radius (data and best fit) fig, ax = plt.subplots(figsize=(12, 9)) ax.set_xlabel("$a \ (nm)$", fontsize=28) ax.set_ylabel("$\epsilon_{A, max}$", fontsize=28) ax.scatter(rd * 1e9, eAmax, color='blue', linewidth=2, label="data") ax.plot(rd * 1e9, eAmax_predicted, '--', color='black', linewidth=2, label="model: $\epsilon_{A,max} \propto = a^{" + '{:.2f}'.format(b) + "}$") xlim = ax.get_xlim() ylim = ax.get_ylim() ax.text(xlim[0] + 0.1 * (xlim[1] - xlim[0]), ylim[0] + 0.6 * (ylim[1] - ylim[0]), "$R^2 = " + '{:.5f}'.format(r_squared_eAmax) + "$", fontsize=28, color="black") ax.legend(loc=4, fontsize=24) plt.show() diff --git a/postpro/postpro_sensitivity_embedding.py b/postpro/postpro_sensitivity_embedding.py index fd8551a..e4c48ca 100644 --- a/postpro/postpro_sensitivity_embedding.py +++ b/postpro/postpro_sensitivity_embedding.py @@ -1,73 +1,73 @@ #!/usr/bin/env python # -*- coding: utf-8 -*- # @Author: Theo Lemaire # @Date: 2016-10-05 11:04:43 # @Email: theo.lemaire@epfl.ch # @Last Modified by: Theo Lemaire -# @Last Modified time: 2017-07-18 15:51:33 +# @Last Modified time: 2018-08-21 16:07:34 """ Test influence of tissue embedding on BLS cavitation amplitude. """ import numpy as np from scipy.optimize import curve_fit import matplotlib.pyplot as plt -from PointNICE.utils import ImportExcelCol +from PySONIC.utils import ImportExcelCol def powerfit(x, a_, b_): """fitting function""" return a_ * np.power(x, b_) # Import data xls_file = "C:/Users/admin/Desktop/Model output/BLS Z tissue/bls_logZ_a32.0nm_embedding.xlsx" sheet = 'Data' rd = ImportExcelCol(xls_file, sheet, 'C', 2) * 1e-9 th = ImportExcelCol(xls_file, sheet, 'D', 2) * 1e-6 eAmax = ImportExcelCol(xls_file, sheet, 'M', 2) # Filter out rows that don't match a specific radius value a_ref = 32.0e-9 # (m) imatch = np.where(rd == a_ref) rd = rd[imatch] th = th[imatch] eAmax = eAmax[imatch] print(str(imatch[0].size) + " values matching required radius") # Compute best power fit for eAmax popt, pcov = curve_fit(powerfit, th, eAmax) (a, b) = popt if a < 1e-4: a_str = '{:.2e}'.format(a) else: a_str = '{:.4f}'.format(a) print("global least-square power fit: eAmax = " + a_str + " * d^" + '{:.2f}'.format(b)) # Compute predicted data and associated error eAmax_predicted = powerfit(th, a, b) residuals = eAmax - eAmax_predicted ss_res = np.sum(residuals**2) ss_tot = np.sum((eAmax - np.mean(eAmax))**2) r_squared_eAmax = 1 - (ss_res / ss_tot) print("R-squared = " + '{:.5f}'.format(r_squared_eAmax)) # Plot areal strain vs. thickness (data and best fit) fig, ax = plt.subplots(figsize=(12, 9)) ax.set_xlabel("$d \ (um)$", fontsize=28) ax.set_ylabel("$\epsilon_{A, max}$", fontsize=28) ax.scatter(th * 1e6, eAmax, color='blue', linewidth=2, label="data") ax.plot(th * 1e6, eAmax_predicted, '--', color='black', linewidth=2, label="model: $\epsilon_{A,max} \propto = d^{" + '{:.2f}'.format(b) + "}$") xlim = ax.get_xlim() ylim = ax.get_ylim() ax.text(xlim[0] + 0.4 * (xlim[1] - xlim[0]), ylim[0] + 0.5 * (ylim[1] - ylim[0]), "$R^2 = " + '{:.5f}'.format(r_squared_eAmax) + "$", fontsize=28, color="black") ax.legend(loc=1, fontsize=24) # Show plots plt.show() diff --git a/postpro/postpro_sensitivity_stim_embedded.py b/postpro/postpro_sensitivity_stim_embedded.py index 92fdb84..096ebe7 100644 --- a/postpro/postpro_sensitivity_stim_embedded.py +++ b/postpro/postpro_sensitivity_stim_embedded.py @@ -1,142 +1,142 @@ #!/usr/bin/env python # -*- coding: utf-8 -*- # @Author: Theo Lemaire # @Date: 2016-10-05 11:04:43 # @Email: theo.lemaire@epfl.ch # @Last Modified by: Theo Lemaire -# @Last Modified time: 2017-07-18 15:00:48 +# @Last Modified time: 2018-08-21 16:07:34 """ Test influence of acoustic amplitude and frequency on cavitation amplitude of embedded BLS. """ import numpy as np from scipy.optimize import curve_fit import matplotlib.pyplot as plt import matplotlib.cm as cm from mpl_toolkits.mplot3d import Axes3D -from PointNICE.utils import ImportExcelCol, ConstructMatrix +from PySONIC.utils import ImportExcelCol, ConstructMatrix def powerfit(X_, a_, b_, c_): """ Fitting function """ x, y = X_ return a_ * np.power(x, b_) * np.power(y, c_) # Import data xls_file = "C:/Users/admin/Desktop/Model output/BLS Z 32nm radius/10um embedding/bls_logZ_a32.0nm_d10.0um.xlsx" sheet = 'Data' f = ImportExcelCol(xls_file, sheet, 'E', 2) * 1e3 A = ImportExcelCol(xls_file, sheet, 'F', 2) * 1e3 eAmax = ImportExcelCol(xls_file, sheet, 'M', 2) # Compute best power fit p0 = 1e-3, 0.8, -0.5 popt, pcov = curve_fit(powerfit, (A, f), eAmax, p0) (a, b, c) = popt if a < 1e-4: a_str = '{:.2e}'.format(a) else: a_str = '{:.4f}'.format(a) print("global least-square power fit: eAmax = %s * A^%.2f * f^%.2f" % (a_str, b, c)) # Compute predicted data and associated error eAmax_predicted = powerfit((A, f), a, b, c) residuals = eAmax - eAmax_predicted ss_res = np.sum(residuals**2) ss_tot = np.sum((eAmax - np.mean(eAmax))**2) r_squared = 1 - (ss_res / ss_tot) print("R-squared = " + '{:.5f}'.format(r_squared)) # Reshape serialized data into 2 dimensions (freqs, amps, eAmax_2D, nholes) = ConstructMatrix(f, A, eAmax) nFreqs = freqs.size nAmps = amps.size fmax = np.amax(freqs) fmin = np.amin(freqs) Amax = np.amax(amps) Amin = np.amin(amps) print(str(nholes) + " hole(s) in reconstructed matrix") # Create colormap mymap = cm.get_cmap('jet') # Plot areal strain vs. amplitude (with frequency color code) fig, ax = plt.subplots(figsize=(12, 9)) ax.set_xlabel("$A \ (kPa)$", fontsize=28) ax.set_ylabel("$\epsilon_{A, max}$", fontsize=28) for i in range(nFreqs): ax.plot(amps * 1e-3, eAmax_2D[i, :], c=mymap((freqs[i] - fmin) / (fmax - fmin)), label='f = ' + str(freqs[i] * 1e-3) + ' kHz') for item in ax.get_yticklabels(): item.set_fontsize(24) for item in ax.get_xticklabels(): item.set_fontsize(24) sm_freq = plt.cm.ScalarMappable(cmap=mymap, norm=plt.Normalize(fmin * 1e-3, fmax * 1e-3)) sm_freq._A = [] cbar = plt.colorbar(sm_freq) cbar.ax.set_ylabel('$f \ (kHz)$', fontsize=28) for item in cbar.ax.get_yticklabels(): item.set_fontsize(24) # Plot areal strain vs. frequency (with amplitude color code) fig, ax = plt.subplots(figsize=(12, 9)) ax.set_xlabel("$f \ (kHz)$", fontsize=28) ax.set_ylabel("$\epsilon_{A, max}$", fontsize=28) for j in range(nAmps): ax.plot(freqs * 1e-3, eAmax_2D[:, j], c=mymap((amps[j] - Amin) / (Amax - Amin)), label='A = ' + str(amps[j] * 1e-3) + ' kPa') for item in ax.get_yticklabels(): item.set_fontsize(24) for item in ax.get_xticklabels(): item.set_fontsize(24) sm_amp = plt.cm.ScalarMappable(cmap=mymap, norm=plt.Normalize(Amin * 1e-3, Amax * 1e-3)) sm_amp._A = [] cbar = plt.colorbar(sm_amp) cbar.ax.set_ylabel("$A \ (kPa)$", fontsize=28) for item in cbar.ax.get_yticklabels(): item.set_fontsize(24) # 3D surface plot: eAmax = f(f,A) if nholes == 0: X, Y = np.meshgrid(freqs * 1e-6, amps * 1e-6) fig = plt.figure(figsize=(12, 9)) ax = fig.gca(projection=Axes3D.name) ax.plot_surface(X, Y, eAmax_2D, rstride=1, cstride=1, cmap=mymap, linewidth=0, antialiased=False) ax.set_xlabel("$A \ (MPa)$", fontsize=24, labelpad=20) ax.set_ylabel("$f \ (MHz)$", fontsize=24, labelpad=20) ax.set_zlabel("$\epsilon_{A, max}$", fontsize=24, labelpad=20) ax.view_init(30, 135) for item in ax.get_yticklabels(): item.set_fontsize(24) for item in ax.get_xticklabels(): item.set_fontsize(24) for item in ax.get_zticklabels(): item.set_fontsize(24) # Plot optimal power fit vs. areal strain (with frequency color code) fig, ax = plt.subplots(figsize=(12, 9)) ax.set_xlabel("$%s\ A^{%.2f}\ f^{%.2f}$" % (a_str, b, c), fontsize=28) ax.set_ylabel("$\epsilon_{A, max}$", fontsize=28) for i in range(nFreqs): ax.scatter(a * amps**b * freqs[i]**c, eAmax_2D[i, :], s=40, c=mymap((freqs[i] - fmin) / (fmax - fmin)), label='f = %f kHz' % (freqs[i] * 1e-3)) ax.set_xlim([0.0, 1.1 * (a * Amax**b * fmin**c)]) ax.set_ylim([0.0, 1.1 * eAmax_2D[0, -1]]) ax.text(0.4 * eAmax_2D[0, -1], 0.9 * eAmax_2D[0, -1], "$R^2 = " + '{:.5f}'.format(r_squared) + "$", fontsize=24, color="black") ax.set_xticks([0, np.round(np.amax(eAmax_2D) * 1e2) / 1e2]) ax.set_yticks([np.round(np.amax(eAmax_2D) * 1e2) / 1e2]) for item in ax.get_yticklabels(): item.set_fontsize(24) for item in ax.get_xticklabels(): item.set_fontsize(24) cbar = plt.colorbar(sm_freq) cbar.ax.set_ylabel('$f \ (kHz)$', fontsize=28) for item in cbar.ax.get_yticklabels(): item.set_fontsize(24) plt.show() diff --git a/postpro/postpro_sensitivity_stim_exposed.py b/postpro/postpro_sensitivity_stim_exposed.py index fa993a8..ab73c86 100644 --- a/postpro/postpro_sensitivity_stim_exposed.py +++ b/postpro/postpro_sensitivity_stim_exposed.py @@ -1,70 +1,70 @@ #!/usr/bin/env python # -*- coding: utf-8 -*- # @Author: Theo Lemaire # @Date: 2016-10-05 11:04:43 # @Email: theo.lemaire@epfl.ch # @Last Modified by: Theo Lemaire -# @Last Modified time: 2017-07-18 15:00:50 +# @Last Modified time: 2018-08-21 16:07:34 """ Test influence of acoustic pressure amplitude on cavitation amplitude of exposed BLS. """ import sys import numpy as np from scipy.optimize import curve_fit import matplotlib.pyplot as plt -sys.path.append('C:/Users/admin/Google Drive/PhD/NICE model/PointNICE') -from PointNICE.utils import ImportExcelCol +sys.path.append('C:/Users/admin/Google Drive/PhD/NICE model/sonic') +from PySONIC.utils import ImportExcelCol def powerfit(x, a_, b_): """ Fitting function. """ return a_ * np.power(x, b_) # Import data xls_file = "C:/Users/admin/Desktop/Model output/BLS Z 32nm radius/0um embedding/bls_logZ_a32.0nm_d0.0um.xlsx" sheet = 'Data' A = ImportExcelCol(xls_file, sheet, 'F', 2) * 1e3 eAmax = ImportExcelCol(xls_file, sheet, 'M', 2) # Sort data by increasing Pac amplitude Asort = A.argsort() A = A[Asort] eAmax = eAmax[Asort] # Compute best power fit for eAmax popt, pcov = curve_fit(powerfit, A, eAmax) (a, b) = popt if a < 1e-4: a_str = '{:.2e}'.format(a) else: a_str = '{:.4f}'.format(a) print("global least-square power fit: eAmax = %s * A^%.2f" % (a_str, b)) # Compute predicted data and associated error eAmax_predicted = powerfit(A, a, b) residuals = eAmax - eAmax_predicted ss_res = np.sum(residuals**2) ss_tot = np.sum((eAmax - np.mean(eAmax))**2) r_squared_eAmax = 1 - (ss_res / ss_tot) print("R-squared = " + '{:.5f}'.format(r_squared_eAmax)) # Plot areal strain vs. acoustic pressure amplitude (data and best fit) fig, ax = plt.subplots(figsize=(12, 9)) ax.set_xlabel("$A \ (kPa)$", fontsize=28) ax.set_ylabel("$\epsilon_{A, max}$", fontsize=28) ax.scatter(A * 1e-3, eAmax, color='blue', linewidth=2, label="data") ax.plot(A * 1e-3, eAmax_predicted, '--', color='black', linewidth=2, label="model: $\epsilon_{A,max} \propto = A^{" + '{:.2f}'.format(b) + "}$") xlim = ax.get_xlim() ylim = ax.get_ylim() ax.text(xlim[0] + 0.1 * (xlim[1] - xlim[0]), ylim[0] + 0.6 * (ylim[1] - ylim[0]), "$R^2 = " + '{:.5f}'.format(r_squared_eAmax) + "$", fontsize=28, color="black") ax.legend(loc=4, fontsize=24) for item in ax.get_yticklabels(): item.set_fontsize(24) for item in ax.get_xticklabels(): item.set_fontsize(24) plt.show() diff --git a/postpro/postpro_spikerate.py b/postpro/postpro_spikerate.py index ad39085..07cf642 100644 --- a/postpro/postpro_spikerate.py +++ b/postpro/postpro_spikerate.py @@ -1,78 +1,78 @@ #!/usr/bin/env python # -*- coding: utf-8 -*- # @Author: Theo Lemaire # @Date: 2016-10-31 11:27:34 # @Email: theo.lemaire@epfl.ch # @Last Modified by: Theo Lemaire -# @Last Modified time: 2017-08-28 14:18:32 +# @Last Modified time: 2018-08-21 16:07:34 """ Test relationship between stimulus intensity spike rate. """ import numpy as np from scipy.optimize import curve_fit import matplotlib.pyplot as plt -from PointNICE.utils import ImportExcelCol, Pressure2Intensity +from PySONIC.utils import ImportExcelCol, Pressure2Intensity def fitfunc(x, a, b): """ Fitting function """ return a * np.power(x, b) # Import data xls_file = "C:/Users/admin/Desktop/Model output/NBLS spikes 0.35MHz/nbls_log_spikes_0.35MHz.xlsx" sheet = 'Data' f = ImportExcelCol(xls_file, sheet, 'E', 2) * 1e3 # Hz A = ImportExcelCol(xls_file, sheet, 'F', 2) * 1e3 # Pa T = ImportExcelCol(xls_file, sheet, 'G', 2) * 1e-3 # s N = ImportExcelCol(xls_file, sheet, 'Q', 2) FR = ImportExcelCol(xls_file, sheet, 'S', 2) # ms # Retrieve available spike rates values (for min. 3 spikes) and corresponding amplitudes iremove = np.where(N < 15)[0] A_true = np.delete(A, iremove) spikerates = np.delete(FR, iremove).astype(np.float) amplitudes = np.delete(A, iremove) # Convert amplitudes to intensities intensities = Pressure2Intensity(amplitudes) * 1e-4 # W/cm2 # Power law least square fitting popt, pcov = curve_fit(fitfunc, intensities, spikerates) print('power product fit: FR = %.2f I^%.2f' % (popt[0], popt[1])) # Compute predicted data and associated error spikerates_predicted = fitfunc(intensities, popt[0], popt[1]) residuals = spikerates - spikerates_predicted ss_res = np.sum(residuals**2) ss_tot = np.sum((spikerates - np.mean(spikerates))**2) r_squared = 1 - (ss_res / ss_tot) print("R-squared = " + '{:.5f}'.format(r_squared)) # Plot latency vs. amplitude fig1, ax = plt.subplots(figsize=(12, 9)) ax.set_xlabel("$Amplitude \ (kPa)$", fontsize=28) ax.set_ylabel("$Spike\ Rate \ (spikes/ms)$", fontsize=28) ax.scatter(amplitudes * 1e-3, spikerates, color='black') ax.set_ylim(0, 1.1 * np.amax(spikerates)) for item in ax.get_yticklabels(): item.set_fontsize(24) for item in ax.get_xticklabels(): item.set_fontsize(24) # Plot latency vs. intensity fig2, ax = plt.subplots(figsize=(12, 9)) ax.set_xlabel("$Intensity \ (W/cm^2)$", fontsize=28) ax.set_ylabel("$Spike\ Rate \ (spikes/ms)$", fontsize=28) ax.scatter(intensities, spikerates, color='black', label='$data$') ax.plot(intensities, spikerates_predicted, color='blue', label='$%.2f\ I^{%.2f}$' % (popt[0], popt[1])) ax.set_ylim(0, 1.1 * np.amax(spikerates)) ax.legend(fontsize=28) for item in ax.get_yticklabels(): item.set_fontsize(24) for item in ax.get_xticklabels(): item.set_fontsize(24) plt.show() diff --git a/postpro/postpro_spikes.py b/postpro/postpro_spikes.py index 07508ca..76ff204 100644 --- a/postpro/postpro_spikes.py +++ b/postpro/postpro_spikes.py @@ -1,120 +1,120 @@ #!/usr/bin/env python # -*- coding: utf-8 -*- # @Author: Theo Lemaire # @Date: 2016-10-27 09:50:55 # @Email: theo.lemaire@epfl.ch # @Last Modified by: Theo Lemaire -# @Last Modified time: 2017-08-28 14:18:29 +# @Last Modified time: 2018-08-21 16:07:35 """ Test influence of acoustic intensity and duration on number of spikes. """ import numpy as np import matplotlib.pyplot as plt import matplotlib.cm as cm from mpl_toolkits.mplot3d import Axes3D -from PointNICE.utils import ImportExcelCol, ConstructMatrix, Pressure2Intensity +from PySONIC.utils import ImportExcelCol, ConstructMatrix, Pressure2Intensity # Define options plot2d_bool = 0 plot3d_show = 1 plot3d_save = 0 plt_root = "../Output/effective spikes 2D/" plt_save_ext = '.png' # Import data xls_file = "../../Output/effective spikes 2D/nbls_log_spikes.xlsx" sheet = 'Data' f_all = ImportExcelCol(xls_file, sheet, 'E', 2) * 1e3 # Hz A_all = ImportExcelCol(xls_file, sheet, 'F', 2) * 1e3 # Pa T_all = ImportExcelCol(xls_file, sheet, 'G', 2) * 1e-3 # s N_all = ImportExcelCol(xls_file, sheet, 'Q', 2) # number of spikes freqs = np.unique(f_all) for Fdrive in freqs: # Select data A = A_all[f_all == Fdrive] T = T_all[f_all == Fdrive] N = N_all[f_all == Fdrive] # Reshape serialized data into 2 dimensions (durations, amps, nspikes, nholes) = ConstructMatrix(T, A, N) nspikes2 = nspikes.conj().T # conjugate tranpose of nspikes matrix (for surface plot) # Convert to appropriate units intensities = Pressure2Intensity(amplitudes) * 1e-4 # W/cm2 durations = durations * 1e3 # ms nDurations = durations.size nIntensities = intensities.size Tmax = np.amax(durations) Tmin = np.amin(durations) Imax = np.amax(intensities) Imin = np.amin(intensities) print(str(nholes) + " hole(s) in reconstructed matrix") mymap = cm.get_cmap('jet') if plot2d_bool == 1: # Plot spikes vs. intensity (with duration color code) fig, ax = plt.subplots(figsize=(12, 9)) ax.set_xlabel("$I \ (W/cm^2)$", fontsize=28) ax.set_ylabel("$\#\ spikes$", fontsize=28) for i in range(nIntensities): ax.plot(intensities, nspikes[i, :], c=mymap((durations[i] - Tmin) / (Tmax - Tmin)), label='t = ' + str(durations[i]) + ' ms') sm_duration = plt.cm.ScalarMappable(cmap=mymap, norm=plt.Normalize(Tmin, Tmax)) sm_duration._A = [] cbar = plt.colorbar(sm_duration) cbar.ax.set_ylabel('$duration \ (ms)$', fontsize=28) # Plot spikes vs. duration (with intensity color code) fig, ax = plt.subplots(figsize=(12, 9)) ax.set_xlabel("$duration \ (ms)$", fontsize=28) ax.set_ylabel("$\#\ spikes$", fontsize=28) for j in range(nDurations): ax.plot(durations, nspikes[:, j], c=mymap((intensities[j] - Imin) / (Imax - Imin)), label='I = ' + str(intensities[j]) + ' W/cm2') sm_int = plt.cm.ScalarMappable(cmap=mymap, norm=plt.Normalize(Imin, Imax)) sm_int._A = [] cbar = plt.colorbar(sm_int) cbar.ax.set_ylabel("$I \ (W/cm^2)$", fontsize=28) if plot3d_show == 1 and nholes == 0: # 3D surface plot: nspikes = f(duration, intensity) X, Y = np.meshgrid(durations, intensities) fig = plt.figure(figsize=(12, 9)) ax = fig.gca(projection=Axes3D.name) ax.plot_surface(X, Y, nspikes2, rstride=1, cstride=1, cmap=mymap, linewidth=0, antialiased=False) ax.set_xlabel("$duration \ (ms)$", fontsize=24, labelpad=20) ax.set_ylabel("$intensity \ (W/cm^2)$", fontsize=24, labelpad=20) ax.set_zlabel("$\#\ spikes$", fontsize=24, labelpad=20) csetx = ax.contour(X, Y, nspikes2, zdir='x', offset=150, cmap=cm.coolwarm) csety = ax.contour(X, Y, nspikes2, zdir='y', offset=0.8, cmap=cm.coolwarm) ax.view_init(33, -126) ax.set_xticks([0, 50, 100, 150]) ax.set_yticks([0, 0.2, 0.4, 0.6, 0.8]) ax.set_zticks([0, 20, 40, 60, 80]) for item in ax.get_yticklabels(): item.set_fontsize(24) for item in ax.get_xticklabels(): item.set_fontsize(24) for item in ax.get_zticklabels(): item.set_fontsize(24) # Save figure if needed if plot3d_save == 1: plt_filename = '{}spikes_{:.0f}KHz{}'.format(plt_root, Fdrive * 1e-3, plt_save_ext) plt.savefig(plt_filename) print('Saving figure to "' + plt_root + '"') plt.close() plt.show() diff --git a/postpro/postpro_threshold_duration.py b/postpro/postpro_threshold_duration.py index d44ab94..94a88bc 100644 --- a/postpro/postpro_threshold_duration.py +++ b/postpro/postpro_threshold_duration.py @@ -1,67 +1,67 @@ #!/usr/bin/env python # -*- coding: utf-8 -*- # @Author: Theo Lemaire # @Date: 2016-10-30 21:48:45 # @Email: theo.lemaire@epfl.ch # @Last Modified by: Theo Lemaire -# @Last Modified time: 2017-08-28 14:18:27 +# @Last Modified time: 2018-08-21 16:07:35 """ Test relationship between stimulus duration and minimum acoustic amplitude / intensity / energy for AP generation. """ import numpy as np import matplotlib.pyplot as plt -from PointNICE.utils import ImportExcelCol, Pressure2Intensity +from PySONIC.utils import ImportExcelCol, Pressure2Intensity # Import data xls_file = "C:/Users/admin/Desktop/Model output/NBLS titration duration 0.35MHz/nbls_log_titration_duration_0.35MHz.xlsx" sheet = 'Data' f = ImportExcelCol(xls_file, sheet, 'E', 2) * 1e3 # Hz A = ImportExcelCol(xls_file, sheet, 'F', 2) * 1e3 # Pa T = ImportExcelCol(xls_file, sheet, 'G', 2) * 1e-3 # s N = ImportExcelCol(xls_file, sheet, 'Q', 2) # Convert to appropriate units durations = T * 1e3 # ms Trange = np.amax(durations) - np.amin(durations) amplitudes = A * 1e-3 # kPa intensities = Pressure2Intensity(A) * 1e-4 # W/cm2 energies = intensities * durations # mJ/cm2 # Plot threshold amplitude vs. duration fig1, ax = plt.subplots(figsize=(12, 9)) ax.set_xlabel("$duration \ (ms)$", fontsize=28) ax.set_ylabel("$Amplitude \ (kPa)$", fontsize=28) ax.scatter(durations, amplitudes, color='black', s=100) ax.set_xlim(np.amin(durations) - 0.1 * Trange, np.amax(durations) + 0.1 * Trange) for item in ax.get_yticklabels(): item.set_fontsize(24) for item in ax.get_xticklabels(): item.set_fontsize(24) # Plot threshold intensity vs. duration fig2, ax = plt.subplots(figsize=(12, 9)) ax.set_xlabel("$duration \ (ms)$", fontsize=28) ax.set_ylabel("$Intensity \ (W/cm^2)$", fontsize=28) ax.scatter(durations, intensities, color='black', s=100) ax.set_xlim(np.amin(durations) - 0.1 * Trange, np.amax(durations) + 0.1 * Trange) ax.set_yticks([np.floor(np.amin(intensities) * 1e2) / 1e2, np.ceil(np.amax(intensities) * 1e2) / 1e2]) for item in ax.get_yticklabels(): item.set_fontsize(24) for item in ax.get_xticklabels(): item.set_fontsize(24) # Plot threshold energy vs. duration fig3, ax = plt.subplots(figsize=(12, 9)) ax.set_xlabel("$duration \ (ms)$", fontsize=28) ax.set_ylabel("$Energy \ (mJ/cm^2)$", fontsize=28) ax.scatter(durations, energies, color='black', s=100) ax.set_xlim(np.amin(durations) - 0.1 * Trange, np.amax(durations) + 0.1 * Trange) for item in ax.get_yticklabels(): item.set_fontsize(24) for item in ax.get_xticklabels(): item.set_fontsize(24) plt.show() diff --git a/postpro/postpro_threshold_frequency.py b/postpro/postpro_threshold_frequency.py index e635449..78986ab 100644 --- a/postpro/postpro_threshold_frequency.py +++ b/postpro/postpro_threshold_frequency.py @@ -1,64 +1,64 @@ #!/usr/bin/env python # -*- coding: utf-8 -*- # @Author: Theo Lemaire # @Date: 2016-10-30 21:48:45 # @Email: theo.lemaire@epfl.ch # @Last Modified by: Theo Lemaire -# @Last Modified time: 2017-08-28 14:18:23 +# @Last Modified time: 2018-08-21 16:07:35 """ Test relationship between stimulus frequency and minimum acoustic intensity for AP generation. """ import numpy as np import matplotlib.pyplot as plt import matplotlib.ticker as ticker -from PointNICE.utils import ImportExcelCol, Pressure2Intensity +from PySONIC.utils import ImportExcelCol, Pressure2Intensity # Import data xls_file = "C:/Users/admin/Desktop/Model output/NBLS titration frequency 30ms/nbls_log_titration_frequency_30ms.xlsx" sheet = 'Data' f = ImportExcelCol(xls_file, sheet, 'E', 2) * 1e3 # Hz A = ImportExcelCol(xls_file, sheet, 'F', 2) * 1e3 # Pa T = ImportExcelCol(xls_file, sheet, 'G', 2) * 1e-3 # s N = ImportExcelCol(xls_file, sheet, 'Q', 2) # Convert to appropriate units frequencies = f * 1e-6 # MHz amplitudes = A * 1e-3 # kPa intensities = Pressure2Intensity(A) * 1e-4 # W/cm2 # Plot threshold amplitude vs. duration fig1, ax = plt.subplots(figsize=(12, 9)) ax.set_xscale('log') ax.set_xlabel("$Frequency \ (MHz)$", fontsize=28) ax.set_ylabel("$Amplitude \ (kPa)$", fontsize=28) ax.scatter(frequencies, amplitudes, color='black', s=100) ax.set_xlim(1.5e-1, 5e0) ax.set_xscale('log') ax.set_xticks([0.2, 1, 4]) ax.get_xaxis().set_major_formatter(ticker.ScalarFormatter()) ax.set_yticks([np.floor(np.amin(amplitudes)), np.ceil(np.amax(amplitudes))]) for item in ax.get_yticklabels(): item.set_fontsize(24) for item in ax.get_xticklabels(): item.set_fontsize(24) # Plot threshold intensity vs. duration fig2, ax = plt.subplots(figsize=(12, 9)) ax.set_xscale('log') ax.set_xlabel("$Frequency \ (MHz)$", fontsize=28) ax.set_ylabel("$Intensity \ (W/cm^2)$", fontsize=28) ax.scatter(frequencies, intensities, color='black', s=100) ax.set_xlim(1.5e-1, 5e0) ax.set_xscale('log') ax.set_xticks([0.2, 1, 4]) ax.get_xaxis().set_major_formatter(ticker.ScalarFormatter()) ax.set_yticks([np.floor(np.amin(intensities) * 1e2) / 1e2, np.ceil(np.amax(intensities) * 1e2) / 1e2]) for item in ax.get_yticklabels(): item.set_fontsize(24) for item in ax.get_xticklabels(): item.set_fontsize(24) plt.show() diff --git a/setup.py b/setup.py index 2140cb7..65a2823 100644 --- a/setup.py +++ b/setup.py @@ -1,50 +1,49 @@ #!/usr/bin/env python # -*- coding: utf-8 -*- # @Author: Theo Lemaire # @Date: 2017-06-13 09:40:02 # @Email: theo.lemaire@epfl.ch # @Last Modified by: Theo Lemaire -# @Last Modified time: 2018-04-17 18:06:04 +# @Last Modified time: 2018-08-21 16:05:54 from setuptools import setup def readme(): with open('README.md', encoding="utf8") as f: return f.read() -setup(name='PointNICE', +setup(name='PySONIC', version='1.0', - description='A Python framework to predict the electrical response of various neuron types\ - to ultrasonic stimulation, according to the Neuronal Intramembrane Cavitation\ - Excitation (NICE) model. The framework couples an optimized implementation of\ - the Bilayer Sonophore (BLS) model with Hodgkin-Huxley "point-neuron" models.', + description='Python implementation of the **multi-Scale Optimized Neuronal Intramembrane \ + Cavitation** (SONIC) model to compute individual neural responses to acoustic \ + stimuli, as predicted by the *intramembrane cavitation* hypothesis.', long_description=readme(), url='???', classifiers=[ 'Development Status :: 4 - Beta', 'Intended Audience :: Science/Research', 'Programming Language :: Python :: 3', 'Topic :: Scientific/Engineering :: Physics' ], - keywords=('ultrasound ultrasonic neuromodulation neurostimulation excitation\ - biophysical model intramembrane cavitation NICE'), + keywords=('SONIC NICE acoustic ultrasound ultrasonic neuromodulation neurostimulation excitation\ + computational model intramembrane cavitation'), author='Théo Lemaire', author_email='theo.lemaire@epfl.ch', license='MIT', - packages=['PointNICE'], + packages=['PySONIC'], scripts=['sim/run_ESTIM.py', 'sim/run_ASTIM.py'], install_requires=[ 'numpy>=1.10', 'scipy>=0.17', 'matplotlib>=2' 'pandas>=0.21', 'openpyxl>=2.4', 'pyyaml>=3.11', 'pycallgraph>=1.0.1', 'colorlog>=3.0.1', 'progressbar2>=3.18.1', 'lockfile>=0.1.2' ], zip_safe=False) diff --git a/sim/batch_ASTIM.py b/sim/batch_ASTIM.py index b7c0270..88209f6 100644 --- a/sim/batch_ASTIM.py +++ b/sim/batch_ASTIM.py @@ -1,73 +1,73 @@ #!/usr/bin/env python # -*- coding: utf-8 -*- # @Author: Theo Lemaire # @Date: 2017-02-13 18:16:09 # @Email: theo.lemaire@epfl.ch # @Last Modified by: Theo Lemaire -# @Last Modified time: 2018-07-23 11:24:09 +# @Last Modified time: 2018-08-21 16:07:35 """ Run batch acoustic simulations of specific "point-neuron" models. """ import sys import os import logging import numpy as np from argparse import ArgumentParser -from PointNICE.utils import logger, InputError -from PointNICE.solvers import setBatchDir, checkBatchLog, runAStimBatch -from PointNICE.plt import plotBatch +from PySONIC.utils import logger, InputError +from PySONIC.solvers import setBatchDir, checkBatchLog, runAStimBatch +from PySONIC.plt import plotBatch # Set logging level logger.setLevel(logging.INFO) # Neurons neurons = ['RS'] # Stimulation parameters stim_params = { 'freqs': [500e3], # Hz 'amps': [100e3], # Pa 'durations': [100e-3], # s 'PRFs': [100.0], # Hz 'DCs': [0.1, 0.5, 1.], 'offsets': [0] } if __name__ == '__main__': # Define argument parser ap = ArgumentParser() ap.add_argument('-m', '--multiprocessing', default=False, action='store_true', help='Use multiprocessing') ap.add_argument('-v', '--verbose', default=False, action='store_true', help='Increase verbosity') ap.add_argument('-p', '--plot', default=False, action='store_true', help='Plot results') # Parse arguments args = ap.parse_args() if args.verbose: logger.setLevel(logging.DEBUG) else: logger.setLevel(logging.INFO) try: # Select output directory batch_dir = setBatchDir() log_filepath, _ = checkBatchLog(batch_dir, 'A-STIM') # Run A-STIM batch pkl_filepaths = runAStimBatch(batch_dir, log_filepath, neurons, stim_params, int_method='effective', multiprocess=args.multiprocessing) pkl_dir, _ = os.path.split(pkl_filepaths[0]) # Plot resulting profiles if args.plot: yvars = {'Q_m': ['Qm']} plotBatch(pkl_dir, pkl_filepaths, yvars) except InputError as err: logger.error(err) sys.exit(1) diff --git a/sim/batch_ESTIM.py b/sim/batch_ESTIM.py index 9190344..9e23c90 100644 --- a/sim/batch_ESTIM.py +++ b/sim/batch_ESTIM.py @@ -1,68 +1,68 @@ # -*- coding: utf-8 -*- # @Author: Theo Lemaire # @Date: 2017-08-24 11:55:07 # @Last Modified by: Theo Lemaire -# @Last Modified time: 2018-07-23 17:30:52 +# @Last Modified time: 2018-08-21 16:07:36 """ Run batch electrical simulations of specific "point-neuron" models. """ import sys import os import logging import numpy as np from argparse import ArgumentParser -from PointNICE.utils import logger, InputError -from PointNICE.solvers import setBatchDir, checkBatchLog, runEStimBatch -from PointNICE.plt import plotBatch +from PySONIC.utils import logger, InputError +from PySONIC.solvers import setBatchDir, checkBatchLog, runEStimBatch +from PySONIC.plt import plotBatch # Neurons neurons = ['RS'] # Stimulation parameters stim_params = { 'amps': [10.0], # mA/m2 'durations': [300e-3], # np.array([20, 40, 60, 80, 100, 150, 200, 250, 300]) * 1e-3, # s 'PRFs': [1e2], # np.array([0.1, 0.2, 0.5, 1.0, 2.0, 5.0, 10.0]) * 1e3, # Hz 'DCs': [0.7, 0.9, 1.0], # np.array([1, 2, 5, 10, 25, 50, 75, 100]) * 1e-2 'offsets': [100e-3] } if __name__ == '__main__': # Define argument parser ap = ArgumentParser() ap.add_argument('-m', '--multiprocessing', default=False, action='store_true', help='Use multiprocessing') ap.add_argument('-v', '--verbose', default=False, action='store_true', help='Increase verbosity') ap.add_argument('-p', '--plot', default=False, action='store_true', help='Plot results') # Parse arguments args = ap.parse_args() if args.verbose: logger.setLevel(logging.DEBUG) else: logger.setLevel(logging.INFO) try: # Select output directory batch_dir = setBatchDir() log_filepath, _ = checkBatchLog(batch_dir, 'E-STIM') # Run E-STIM batch pkl_filepaths = runEStimBatch(batch_dir, log_filepath, neurons, stim_params, multiprocess=args.multiprocessing) pkl_dir, _ = os.path.split(pkl_filepaths[0]) # Plot resulting profiles if args.plot: yvars = {'V_m': ['Vm']} plotBatch(pkl_dir, pkl_filepaths, yvars) except InputError as err: logger.error(err) sys.exit(1) diff --git a/sim/batch_MECH.py b/sim/batch_MECH.py index 7825cb4..4586e85 100644 --- a/sim/batch_MECH.py +++ b/sim/batch_MECH.py @@ -1,71 +1,71 @@ #!/usr/bin/env python # -*- coding: utf-8 -*- # @Author: Theo Lemaire # @Date: 2016-11-21 10:46:56 # @Email: theo.lemaire@epfl.ch # @Last Modified by: Theo Lemaire -# @Last Modified time: 2018-07-23 14:39:27 +# @Last Modified time: 2018-08-21 16:07:36 """ Run batch simulations of the NICE mechanical model with imposed charge densities """ import sys import os import logging import numpy as np from argparse import ArgumentParser -from PointNICE.utils import logger, InputError -from PointNICE.solvers import setBatchDir, checkBatchLog, runMechBatch -from PointNICE.neurons import * -from PointNICE.plt import plotBatch +from PySONIC.utils import logger, InputError +from PySONIC.solvers import setBatchDir, checkBatchLog, runMechBatch +from PySONIC.neurons import * +from PySONIC.plt import plotBatch a = 32e-9 # in-plane diameter (m) # Electrical properties of the membrane neuron = CorticalRS() Cm0 = neuron.Cm0 Qm0 = neuron.Vm0 * 1e-5 # Stimulation parameters stim_params = { 'freqs': [500e3], # Hz 'amps': [50e3], # Pa 'charges': np.linspace(-72, 0, 3) * 1e-5 # C/m2 } if __name__ == '__main__': # Define argument parser ap = ArgumentParser() ap.add_argument('-m', '--multiprocessing', default=False, action='store_true', help='Use multiprocessing') ap.add_argument('-v', '--verbose', default=False, action='store_true', help='Increase verbosity') ap.add_argument('-p', '--plot', default=False, action='store_true', help='Plot results') # Parse arguments args = ap.parse_args() if args.verbose: logger.setLevel(logging.DEBUG) else: logger.setLevel(logging.INFO) try: # Select output directory batch_dir = setBatchDir() log_filepath, _ = checkBatchLog(batch_dir, 'MECH') # Run MECH batch pkl_filepaths = runMechBatch(batch_dir, log_filepath, Cm0, Qm0, stim_params, a, multiprocess=args.multiprocessing) pkl_dir, _ = os.path.split(pkl_filepaths[0]) # Plot resulting profiles if args.plot: plotBatch(pkl_dir, pkl_filepaths) except InputError as err: logger.error(err) sys.exit(1) diff --git a/sim/lookups_ASTIM.py b/sim/lookups_ASTIM.py index 2239d8d..110ee77 100644 --- a/sim/lookups_ASTIM.py +++ b/sim/lookups_ASTIM.py @@ -1,94 +1,94 @@ #!/usr/bin/env python # -*- coding: utf-8 -*- # @Author: Theo Lemaire # @Date: 2017-06-02 17:50:10 # @Email: theo.lemaire@epfl.ch # @Last Modified by: Theo Lemaire -# @Last Modified time: 2018-07-20 18:39:03 +# @Last Modified time: 2018-08-21 16:07:36 """ Create lookup table for specific neuron. """ import os import pickle import logging import numpy as np from argparse import ArgumentParser -from PointNICE.solvers import computeAStimLookups -from PointNICE.utils import logger, InputError, getNeuronsDict, getLookupDir -from PointNICE.neurons import * +from PySONIC.solvers import computeAStimLookups +from PySONIC.utils import logger, InputError, getNeuronsDict, getLookupDir +from PySONIC.neurons import * # Default parameters default = { 'neuron': 'RS', 'a': 32.0, # nm 'freqs': np.array([20., 100., 500., 1e3, 2e3, 3e3, 4e3]), # kHz 'amps': np.insert(np.logspace(np.log10(0.1), np.log10(600), num=50), 0, 0.0), # kPa } def main(): # Define argument parser ap = ArgumentParser() # Stimulation parameters ap.add_argument('-n', '--neuron', type=str, default=default['neuron'], help='Neuron name (string)') ap.add_argument('-a', '--diameter', type=float, default=default['a'], help='Sonophore diameter (nm)') ap.add_argument('-f', '--frequencies', nargs='+', type=float, default=None, help='Acoustic drive frequencies (kHz)') ap.add_argument('-A', '--amplitudes', nargs='+', type=float, default=None, help='Acoustic pressure amplitudes (kPa)') # Boolean parameters ap.add_argument('-m', '--multiprocessing', default=False, action='store_true', help='Use multiprocessing') ap.add_argument('-v', '--verbose', default=False, action='store_true', help='Increase verbosity') # Parse arguments args = ap.parse_args() neuron_str = args.neuron a = args.diameter * 1e-9 # m freqs = (default['freqs'] if args.frequencies is None else np.array(args.frequencies)) * 1e3 # Hz amps = (default['amps'] if args.amplitudes is None else np.array(args.amplitudes)) * 1e3 # Pa if args.verbose: logger.setLevel(logging.DEBUG) else: logger.setLevel(logging.INFO) # Check neuron name validity if neuron_str not in getNeuronsDict(): raise InputError('Unknown neuron type: "{}"'.format(neuron_str)) neuron = getNeuronsDict()[neuron_str]() # Check if lookup file already exists lookup_file = '{}_lookups_a{:.1f}nm.pkl'.format(neuron.name, a * 1e9) lookup_filepath = '{0}/{1}'.format(getLookupDir(), lookup_file) if os.path.isfile(lookup_filepath): logger.warning('"%s" file already exists and will be overwritten. ' + 'Continue? (y/n)', lookup_file) user_str = input() if user_str not in ['y', 'Y']: logger.info('%s Lookup creation canceled', neuron.name) sys.exit(0) try: # Compute lookups lookup_dict = computeAStimLookups(neuron, a, freqs, amps, multiprocess=args.multiprocessing) # Save dictionary in lookup file logger.info('Saving %s neuron lookup table in file: "%s"', neuron.name, lookup_file) with open(lookup_filepath, 'wb') as fh: pickle.dump(lookup_dict, fh) except InputError as err: logger.error(err) sys.exit(1) if __name__ == '__main__': main() diff --git a/sim/run_ASTIM.py b/sim/run_ASTIM.py index a0dfec1..ee501a0 100644 --- a/sim/run_ASTIM.py +++ b/sim/run_ASTIM.py @@ -1,105 +1,105 @@ #!/usr/bin/env python # -*- coding: utf-8 -*- # @Author: Theo Lemaire # @Date: 2017-02-13 18:16:09 # @Email: theo.lemaire@epfl.ch # @Last Modified by: Theo Lemaire -# @Last Modified time: 2018-07-23 13:27:52 +# @Last Modified time: 2018-08-21 16:07:36 """ Script to run ASTIM simulations from command line. """ import sys import os import logging from argparse import ArgumentParser -from PointNICE.utils import logger, getNeuronsDict, InputError -from PointNICE.solvers import checkBatchLog, SolverUS, AStimWorker -from PointNICE.plt import plotBatch +from PySONIC.utils import logger, getNeuronsDict, InputError +from PySONIC.solvers import checkBatchLog, SolverUS, AStimWorker +from PySONIC.plt import plotBatch # Default parameters default = { 'neuron': 'RS', 'a': 32.0, # nm 'f': 500.0, # kHz 'A': 100.0, # kPa 't': 150.0, # ms 'off': 100.0, # ms 'PRF': 100.0, # Hz 'DC': 100.0, # % 'int_method': 'effective' } def main(): # Define argument parser ap = ArgumentParser() # ASTIM parameters ap.add_argument('-n', '--neuron', type=str, default=default['neuron'], help='Neuron name (string)') ap.add_argument('-a', '--diameter', type=float, default=default['a'], help='Sonophore diameter (nm)') ap.add_argument('-f', '--frequency', type=float, default=default['f'], help='Acoustic drive frequency (kHz)') ap.add_argument('-A', '--amplitude', type=float, default=default['A'], help='Acoustic pressure amplitude (kPa)') ap.add_argument('-t', '--duration', type=float, default=default['t'], help='Stimulus duration (ms)') ap.add_argument('--offset', type=float, default=default['off'], help='Offset duration (ms)') ap.add_argument('--PRF', type=float, default=default['PRF'], help='PRF (Hz)') ap.add_argument('--DC', type=float, default=default['DC'], help='Duty cycle (%%)') ap.add_argument('-o', '--outputdir', type=str, default=os.getcwd(), help='Output directory') ap.add_argument('-m', '--method', type=str, default=default['int_method'], help='Numerical integration method ("classic", "hybrid" or "effective"') # Boolean parameters ap.add_argument('-v', '--verbose', default=False, action='store_true', help='Increase verbosity') ap.add_argument('-p', '--plot', default=False, action='store_true', help='Plot results') # Parse arguments args = ap.parse_args() neuron_str = args.neuron a = args.diameter * 1e-9 # m Fdrive = args.frequency * 1e3 # Hz Adrive = args.amplitude * 1e3 # Pa tstim = args.duration * 1e-3 # s toffset = args.offset * 1e-3 # s PRF = args.PRF # Hz DC = args.DC * 1e-2 output_dir = args.outputdir int_method = args.method if args.verbose: logger.setLevel(logging.DEBUG) else: logger.setLevel(logging.INFO) try: if neuron_str not in getNeuronsDict(): raise InputError('Unknown neuron type: "{}"'.format(neuron_str)) log_filepath, _ = checkBatchLog(output_dir, 'A-STIM') neuron = getNeuronsDict()[neuron_str]() worker = AStimWorker(1, output_dir, log_filepath, SolverUS(a, neuron, Fdrive), neuron, Fdrive, Adrive, tstim, toffset, PRF, DC, int_method, 1) logger.info('%s', worker) outfile = worker.__call__() logger.info('Finished') if args.plot: plotBatch(output_dir, [outfile]) except InputError as err: logger.error(err) sys.exit(1) if __name__ == '__main__': main() diff --git a/sim/run_ESTIM.py b/sim/run_ESTIM.py index fc92185..f3d7c06 100644 --- a/sim/run_ESTIM.py +++ b/sim/run_ESTIM.py @@ -1,93 +1,93 @@ #!/usr/bin/env python # -*- coding: utf-8 -*- # @Author: Theo Lemaire # @Date: 2017-02-13 18:16:09 # @Email: theo.lemaire@epfl.ch # @Last Modified by: Theo Lemaire -# @Last Modified time: 2018-07-23 13:56:40 +# @Last Modified time: 2018-08-21 16:07:37 """ Script to run ESTIM simulations from command line. """ import sys import os import logging from argparse import ArgumentParser -from PointNICE.utils import logger, getNeuronsDict, InputError -from PointNICE.solvers import checkBatchLog, SolverElec, EStimWorker -from PointNICE.plt import plotBatch +from PySONIC.utils import logger, getNeuronsDict, InputError +from PySONIC.solvers import checkBatchLog, SolverElec, EStimWorker +from PySONIC.plt import plotBatch # Default parameters default = { 'neuron': 'RS', 'A': 10.0, # mA/m2 't': 150.0, # ms 'off': 20.0, # ms 'PRF': 100.0, # Hz 'DC': 100.0 # % } def main(): # Define argument parser ap = ArgumentParser() # ASTIM parameters ap.add_argument('-n', '--neuron', type=str, default=default['neuron'], help='Neuron name (string)') ap.add_argument('-A', '--amplitude', type=float, default=default['A'], help='Stimulus amplitude (mA/m2)') ap.add_argument('-t', '--duration', type=float, default=default['t'], help='Stimulus duration (ms)') ap.add_argument('--offset', type=float, default=default['off'], help='Offset duration (ms)') ap.add_argument('--PRF', type=float, default=default['PRF'], help='PRF (Hz)') ap.add_argument('--DC', type=float, default=default['DC'], help='Duty cycle (%%)') ap.add_argument('-o', '--outputdir', type=str, default=os.getcwd(), help='Output directory') # Boolean arguments ap.add_argument('-v', '--verbose', default=False, action='store_true', help='Increase verbosity') ap.add_argument('-p', '--plot', default=False, action='store_true', help='Plot results') # Parse arguments args = ap.parse_args() neuron_str = args.neuron Astim = args.amplitude # mA/m2 tstim = args.duration * 1e-3 # s toffset = args.offset * 1e-3 # s PRF = args.PRF # Hz DC = args.DC * 1e-2 output_dir = args.outputdir if args.verbose: logger.setLevel(logging.DEBUG) else: logger.setLevel(logging.INFO) try: if neuron_str not in getNeuronsDict(): raise InputError('Unknown neuron type: "{}"'.format(neuron_str)) log_filepath, _ = checkBatchLog(output_dir, 'E-STIM') neuron = getNeuronsDict()[neuron_str]() worker = EStimWorker(1, output_dir, log_filepath, SolverElec(), neuron, Astim, tstim, toffset, PRF, DC, 1) logger.info('%s', worker) outfile = worker.__call__() logger.info('Finished') if args.plot: plotBatch(output_dir, [outfile]) except InputError as err: logger.error(err) sys.exit(1) if __name__ == '__main__': main() diff --git a/sim/run_MECH.py b/sim/run_MECH.py index 82a85c0..e39afa2 100644 --- a/sim/run_MECH.py +++ b/sim/run_MECH.py @@ -1,94 +1,94 @@ # -*- coding: utf-8 -*- # @Author: Theo Lemaire # @Date: 2018-03-15 18:33:59 # @Last Modified by: Theo Lemaire -# @Last Modified time: 2018-07-23 14:30:27 +# @Last Modified time: 2018-08-21 16:07:37 """ Script to run MECH simulations from command line. """ import sys import os import logging from argparse import ArgumentParser -from PointNICE.utils import logger, InputError -from PointNICE.bls import BilayerSonophore -from PointNICE.solvers import checkBatchLog, MechWorker -from PointNICE.plt import plotBatch +from PySONIC.utils import logger, InputError +from PySONIC.bls import BilayerSonophore +from PySONIC.solvers import checkBatchLog, MechWorker +from PySONIC.plt import plotBatch # Default parameters default = { 'a': 32.0, # nm 'd': 0.0, # um 'f': 500.0, # kHz 'A': 100.0, # kPa 'Cm0': 1.0, # uF/cm2 'Qm0': 0.0, # nC/cm2 'Qm': 0.0, # nC/cm2 } def main(): # Define argument parser ap = ArgumentParser() # ASTIM parameters ap.add_argument('-a', '--diameter', type=float, default=default['a'], help='Sonophore diameter (nm)') ap.add_argument('-d', '--embedding', type=float, default=default['d'], help='Embedding depth (um)') ap.add_argument('-f', '--frequency', type=float, default=default['f'], help='Acoustic drive frequency (kHz)') ap.add_argument('-A', '--amplitude', type=float, default=default['A'], help='Acoustic pressure amplitude (kPa)') ap.add_argument('-Cm0', '--restcapct', type=float, default=default['Cm0'], help='Membrane resting capacitance (uF/cm2)') ap.add_argument('-Qm0', '--restcharge', type=float, default=default['Qm0'], help='Membrane resting charge density (nC/cm2)') ap.add_argument('-Qm', '--charge', type=float, default=default['Qm'], help='Applied charge density (nC/cm2)') ap.add_argument('-o', '--outputdir', type=str, default=os.getcwd(), help='Output directory') # Boolean parameters ap.add_argument('-v', '--verbose', default=False, action='store_true', help='Increase verbosity') ap.add_argument('-p', '--plot', default=False, action='store_true', help='Plot results') # Parse arguments args = ap.parse_args() a = args.diameter * 1e-9 # m d = args.embedding * 1e-6 # m Fdrive = args.frequency * 1e3 # Hz Adrive = args.amplitude * 1e3 # Pa Cm0 = args.restcapct * 1e-2 # F/m2 Qm0 = args.restcharge * 1e-5 # C/m2 Qm = args.charge * 1e-5 # C/m2 output_dir = args.outputdir if args.verbose: logger.setLevel(logging.DEBUG) else: logger.setLevel(logging.INFO) try: log_filepath, _ = checkBatchLog(output_dir, 'MECH') worker = MechWorker(1, output_dir, log_filepath, BilayerSonophore(a, Fdrive, Cm0, Qm0, d), Fdrive, Adrive, Qm, 1) logger.info('%s', worker) outfile = worker.__call__() logger.info('Finished') if args.plot: plotBatch(output_dir, [outfile]) except InputError as err: logger.error(err) sys.exit(1) if __name__ == '__main__': main() diff --git a/sim/titration_batch_ASTIM.py b/sim/titration_batch_ASTIM.py index fbedde2..10fe9da 100644 --- a/sim/titration_batch_ASTIM.py +++ b/sim/titration_batch_ASTIM.py @@ -1,70 +1,70 @@ #!/usr/bin/env python # -*- coding: utf-8 -*- # @Author: Theo Lemaire # @Date: 2017-02-13 18:16:09 # @Email: theo.lemaire@epfl.ch # @Last Modified by: Theo Lemaire -# @Last Modified time: 2018-07-24 11:59:48 +# @Last Modified time: 2018-08-21 16:07:37 """ Run batch acoustic titrations of specific "point-neuron" models. """ import sys import os import logging import numpy as np from argparse import ArgumentParser -from PointNICE.utils import logger, InputError -from PointNICE.solvers import setBatchDir, checkBatchLog, titrateAStimBatch -from PointNICE.plt import plotBatch +from PySONIC.utils import logger, InputError +from PySONIC.solvers import setBatchDir, checkBatchLog, titrateAStimBatch +from PySONIC.plt import plotBatch # Neurons neurons = ['RS'] # Stimulation parameters a = 32e-9 stim_params = { 'freqs': [5e5], # Hz # 'amps': [100e3], # Pa 'durations': [100e-3], # s 'PRFs': [1e2], # Hz 'DCs': [0.5, 1.0] } if __name__ == '__main__': # Define argument parser ap = ArgumentParser() ap.add_argument('-m', '--multiprocessing', default=False, action='store_true', help='Use multiprocessing') ap.add_argument('-v', '--verbose', default=False, action='store_true', help='Increase verbosity') ap.add_argument('-p', '--plot', default=False, action='store_true', help='Plot results') # Parse arguments args = ap.parse_args() if args.verbose: logger.setLevel(logging.DEBUG) else: logger.setLevel(logging.INFO) try: # Select output directory batch_dir = setBatchDir() log_filepath, _ = checkBatchLog(batch_dir, 'A-STIM') # Run titration batch pkl_filepaths = titrateAStimBatch(batch_dir, log_filepath, neurons, stim_params, a, multiprocess=args.multiprocessing) pkl_dir, _ = os.path.split(pkl_filepaths[0]) # Plot resulting profiles if args.plot: plotBatch(pkl_dir, pkl_filepaths, {'Q_m': ['Qm']}) except InputError as err: logger.error(err) sys.exit(1) diff --git a/sim/titration_batch_ESTIM.py b/sim/titration_batch_ESTIM.py index 03fc723..a7200f8 100644 --- a/sim/titration_batch_ESTIM.py +++ b/sim/titration_batch_ESTIM.py @@ -1,66 +1,66 @@ # -*- coding: utf-8 -*- # @Author: Theo Lemaire # @Date: 2017-08-25 14:50:39 # @Last Modified by: Theo Lemaire -# @Last Modified time: 2018-07-24 11:54:34 +# @Last Modified time: 2018-08-21 16:07:37 """ Run batch electrical titrations of specific "point-neuron" models. """ import sys import os import logging import numpy as np from argparse import ArgumentParser -from PointNICE.utils import logger, InputError -from PointNICE.solvers import setBatchDir, checkBatchLog, titrateEStimBatch -from PointNICE.plt import plotBatch +from PySONIC.utils import logger, InputError +from PySONIC.solvers import setBatchDir, checkBatchLog, titrateEStimBatch +from PySONIC.plt import plotBatch # Neurons neurons = ['RS'] # Stimulation parameters stim_params = { # 'amps': [20.0], # mA/m2 'durations': [0.5], # s 'PRFs': [1e1, 1e2], # Hz 'DCs': [1.0] } if __name__ == '__main__': # Define argument parser ap = ArgumentParser() ap.add_argument('-m', '--multiprocessing', default=False, action='store_true', help='Use multiprocessing') ap.add_argument('-v', '--verbose', default=False, action='store_true', help='Increase verbosity') ap.add_argument('-p', '--plot', default=False, action='store_true', help='Plot results') # Parse arguments args = ap.parse_args() if args.verbose: logger.setLevel(logging.DEBUG) else: logger.setLevel(logging.INFO) try: # Select output directory batch_dir = setBatchDir() log_filepath, _ = checkBatchLog(batch_dir, 'E-STIM') # Run titration batch pkl_filepaths = titrateEStimBatch(batch_dir, log_filepath, neurons, stim_params, multiprocess=args.multiprocessing) pkl_dir, _ = os.path.split(pkl_filepaths[0]) # Plot resulting profiles if args.plot: yvars = {'V_m': ['Vm']} plotBatch(pkl_dir, pkl_filepaths, yvars) except InputError as err: logger.error(err) sys.exit(1) diff --git a/tests/graphs.py b/tests/graphs.py index a0e4f3d..2e2560f 100644 --- a/tests/graphs.py +++ b/tests/graphs.py @@ -1,107 +1,107 @@ #!/usr/bin/env python # -*- coding: utf-8 -*- # @Author: Theo Lemaire # @Date: 2017-06-14 18:37:45 # @Email: theo.lemaire@epfl.ch # @Last Modified by: Theo Lemaire -# @Last Modified time: 2018-03-16 12:01:22 +# @Last Modified time: 2018-08-21 16:08:13 ''' Test the basic functionalities of the package and output graphs of the call flows. ''' import logging from pycallgraph import PyCallGraph from pycallgraph.output import GraphvizOutput -from PointNICE.utils import logger -from PointNICE import BilayerSonophore, SolverUS, SolverElec -from PointNICE.neurons import CorticalRS +from PySONIC.utils import logger +from PySONIC import BilayerSonophore, SolverUS, SolverElec +from PySONIC.neurons import CorticalRS # Set logging level logger.setLevel(logging.DEBUG) # Create Graphviz output object graphviz = GraphvizOutput() def graph_BLS(): logger.info('Graph 1: BLS initialization') a = 32e-9 # nm Fdrive = 3.5e5 # Hz Cm0 = 1e-2 # membrane resting capacitance (F/m2) Qm0 = -80e-5 # membrane resting charge density (C/m2) graphviz.output_file = 'graphs/bls_init.png' with PyCallGraph(output=graphviz): bls = BilayerSonophore(a, Fdrive, Cm0, Qm0) logger.info('Graph 2: Mechanical simulation') Adrive = 1e5 # Pa graphviz.output_file = 'graphs/MECH_sim.png' with PyCallGraph(output=graphviz): bls.run(Fdrive, Adrive, Qm0) def graph_neuron_init(): logger.info('Graph 1: Channels mechanism initialization') graphviz.output_file = 'graphs/RS_neuron_init.png' with PyCallGraph(output=graphviz): CorticalRS() def graph_ESTIM(): rs_neuron = CorticalRS() logger.info('Graph 1: SolverElec initialization') graphviz.output_file = 'graphs/ESTIM_solver_init.png' with PyCallGraph(output=graphviz): solver = SolverElec() logger.info('Graph 2: E-STIM simulation') Astim = 1.0 # mA/m2 tstim = 1e-3 # s toffset = 1e-3 # s graphviz.output_file = 'graphs/ESTIM_sim.png' with PyCallGraph(output=graphviz): solver.run(rs_neuron, Astim, tstim, toffset) def graph_ASTIM(): rs_neuron = CorticalRS() a = 32e-9 # nm Fdrive = 3.5e5 # Hz Adrive = 1e5 # Pa logger.info('Graph 1: SolverUS initialization') graphviz.output_file = 'graphs/ASTIM_solver_init.png' with PyCallGraph(output=graphviz): solver = SolverUS(a, rs_neuron, Fdrive) logger.info('Graph 2: A-STIM classic simulation') tstim = 1e-6 # s toffset = 0.0 # s graphviz.output_file = 'graphs/ASTIM_sim_classic.png' with PyCallGraph(output=graphviz): solver.run(rs_neuron, Fdrive, Adrive, tstim, toffset, sim_type='classic') logger.info('Graph 3: A-STIM effective simulation') tstim = 1e-3 # s toffset = 0.0 # s graphviz.output_file = 'graphs/ASTIM_sim_effective.png' with PyCallGraph(output=graphviz): solver.run(rs_neuron, Fdrive, Adrive, tstim, toffset, sim_type='effective') logger.info('Graph 4: A-STIM hybrid simulation') tstim = 1e-3 # s toffset = 0.0 # s graphviz.output_file = 'graphs/ASTIM_sim_hybrid.png' with PyCallGraph(output=graphviz): solver.run(rs_neuron, Fdrive, Adrive, tstim, toffset, sim_type='hybrid') if __name__ == '__main__': logger.info('Starting graphs') graph_BLS() graph_neuron_init() graph_ESTIM() graph_ASTIM() logger.info('All graphs successfully created') diff --git a/tests/test_basic.py b/tests/test_basic.py index fcaa889..7aaa890 100644 --- a/tests/test_basic.py +++ b/tests/test_basic.py @@ -1,260 +1,260 @@ #!/usr/bin/env python # -*- coding: utf-8 -*- # @Author: Theo Lemaire # @Date: 2017-06-14 18:37:45 # @Email: theo.lemaire@epfl.ch # @Last Modified by: Theo Lemaire -# @Last Modified time: 2018-08-21 14:24:51 +# @Last Modified time: 2018-08-21 16:08:13 ''' Test the basic functionalities of the package. ''' import os import sys import logging import time import cProfile import pstats from argparse import ArgumentParser -from PointNICE.utils import logger, InputError -from PointNICE import BilayerSonophore, SolverElec, SolverUS -from PointNICE.neurons import * +from PySONIC.utils import logger, InputError +from PySONIC import BilayerSonophore, SolverElec, SolverUS +from PySONIC.neurons import * def test_MECH(is_profiled=False): ''' Mechanical simulation. ''' logger.info('Test: running MECH simulation') # Create BLS instance a = 32e-9 # m Fdrive = 350e3 # Hz Cm0 = 1e-2 # membrane resting capacitance (F/m2) Qm0 = -80e-5 # membrane resting charge density (C/m2) bls = BilayerSonophore(a, Fdrive, Cm0, Qm0) # Stimulation parameters Adrive = 100e3 # Pa Qm = 50e-5 # C/m2 # Run simulation if is_profiled: pfile = 'tmp.stats' cProfile.runctx('bls.run(Fdrive, Adrive, Qm)', globals(), locals(), pfile) stats = pstats.Stats(pfile) os.remove(pfile) stats.strip_dirs() stats.sort_stats('cumulative') stats.print_stats() else: bls.run(Fdrive, Adrive, Qm) def test_ESTIM(is_profiled=False): ''' Electrical simulation ''' logger.info('Test: running ESTIM simulation') # Initialize neuron neuron = CorticalRS() # Initialize solver solver = SolverElec() # Stimulation parameters Astim = 10.0 # mA/m2 tstim = 100e-3 # s toffset = 50e-3 # s # Run simulation if is_profiled: pfile = 'tmp.stats' cProfile.runctx('solver.run(neuron, Astim, tstim, toffset)', globals(), locals(), pfile) stats = pstats.Stats(pfile) os.remove(pfile) stats.strip_dirs() stats.sort_stats('cumulative') stats.print_stats() else: solver.run(neuron, Astim, tstim, toffset, PRF=None, DC=1.0) solver.run(neuron, Astim, tstim, toffset, PRF=100.0, DC=0.05) def test_ASTIM_effective(is_profiled=False): ''' Effective acoustic simulation ''' logger.info('Test: ASTIM effective simulation') # Default parameters neuron = CorticalRS() a = 32e-9 # m Fdrive = 500e3 # Hz Adrive = 100e3 # Pa tstim = 50e-3 # s toffset = 10e-3 # s # Run simulation if is_profiled: solver = SolverUS(a, neuron, Fdrive) pfile = 'tmp.stats' cProfile.runctx("solver.run(neuron, Fdrive, Adrive, tstim, toffset, sim_type='effective')", globals(), locals(), pfile) stats = pstats.Stats(pfile) os.remove(pfile) stats.strip_dirs() stats.sort_stats('cumulative') stats.print_stats() else: # test error 1: no lookups exist for sonophore diameter try: solver = SolverUS(50e-9, neuron, Fdrive) solver.run(neuron, Fdrive, Adrive, tstim, toffset, sim_type='effective') except InputError as err: logger.debug('No lookups for sonophore diameter: OK') # test error 2: frequency outside of lookups range Foutside = 10e3 # Hz try: solver = SolverUS(a, neuron, Foutside) solver.run(neuron, Foutside, Adrive, tstim, toffset, sim_type='effective') except InputError as err: logger.debug('Out of range frequency: OK') # test error 3: amplitude outside of lookups range Aoutside = 1e6 # Pa try: solver = SolverUS(a, neuron, Fdrive) solver.run(neuron, Fdrive, Aoutside, tstim, toffset, sim_type='effective') except InputError as err: logger.debug('Out of range amplitude: OK') # test: normal stimulation completion (CW and PW) solver = SolverUS(a, neuron, Fdrive) solver.run(neuron, Fdrive, Adrive, tstim, toffset, sim_type='effective') solver.run(neuron, Fdrive, Adrive, tstim, toffset, PRF=100.0, DC=0.05, sim_type='effective') def test_ASTIM_classic(is_profiled=False): ''' Classic acoustic simulation ''' logger.info('Test: running ASTIM classic simulation') # Initialize neuron neuron = CorticalRS() # Stimulation parameters Fdrive = 500e3 # Hz Adrive = 100e3 # Pa tstim = 1e-6 # s toffset = 1e-6 # s # Initialize solver a = 32e-9 # m solver = SolverUS(a, neuron, Fdrive) # Run simulation if is_profiled: pfile = 'tmp.stats' cProfile.runctx("solver.run(neuron, Fdrive, Adrive, tstim, toffset, sim_type='classic')", globals(), locals(), pfile) stats = pstats.Stats(pfile) os.remove(pfile) stats.strip_dirs() stats.sort_stats('cumulative') stats.print_stats() else: solver.run(neuron, Fdrive, Adrive, tstim, toffset, sim_type='classic') def test_ASTIM_hybrid(is_profiled=False): ''' Hybrid acoustic simulation ''' logger.info('Test: running ASTIM hybrid simulation') # Initialize neuron neuron = CorticalRS() # Stimulation parameters Fdrive = 350e3 # Hz Adrive = 100e3 # Pa tstim = 1e-3 # s toffset = 1e-3 # s # Initialize solver a = 32e-9 # m solver = SolverUS(a, neuron, Fdrive) # Run simulation if is_profiled: pfile = 'tmp.stats' cProfile.runctx("solver.run(neuron, Fdrive, Adrive, tstim, toffset, sim_type='hybrid')", globals(), locals(), pfile) stats = pstats.Stats(pfile) os.remove(pfile) stats.strip_dirs() stats.sort_stats('cumulative') stats.print_stats() else: solver.run(neuron, Fdrive, Adrive, tstim, toffset, sim_type='hybrid') def test_all(): t0 = time.time() test_MECH() test_ESTIM() test_ASTIM_effective() test_ASTIM_classic() test_ASTIM_hybrid() tcomp = time.time() - t0 logger.info('All tests completed in %.0f s', tcomp) def main(): # Define valid test sets valid_testsets = [ 'MECH', 'ESTIM', 'ASTIM_effective', 'ASTIM_classic', 'ASTIM_hybrid', 'all' ] # Define argument parser ap = ArgumentParser() ap.add_argument('-t', '--testset', type=str, default='all', choices=valid_testsets, help='Specific test set') ap.add_argument('-v', '--verbose', default=False, action='store_true', help='Increase verbosity') ap.add_argument('-p', '--profile', default=False, action='store_true', help='Profile test set') # Parse arguments args = ap.parse_args() if args.verbose: logger.setLevel(logging.DEBUG) else: logger.setLevel(logging.INFO) if args.profile and args.testset == 'all': logger.error('profiling can only be run on individual tests') sys.exit(2) # Run test if args.testset == 'all': test_all() else: possibles = globals().copy() possibles.update(locals()) method = possibles.get('test_{}'.format(args.testset)) method(args.profile) sys.exit(0) if __name__ == '__main__': main() diff --git a/tests/test_intermolecular_pressure.py b/tests/test_intermolecular_pressure.py index ff5f611..65fc724 100644 --- a/tests/test_intermolecular_pressure.py +++ b/tests/test_intermolecular_pressure.py @@ -1,76 +1,76 @@ import logging import numpy as np import matplotlib import matplotlib.pyplot as plt -from PointNICE.utils import logger, si_format, PmCompMethod, rmse, rsquared -from PointNICE.plt import cm2inch -from PointNICE.neurons import CorticalRS -from PointNICE.solvers import BilayerSonophore -from PointNICE.constants import * +from PySONIC.utils import logger, si_format, PmCompMethod, rmse, rsquared +from PySONIC.plt import cm2inch +from PySONIC.neurons import CorticalRS +from PySONIC.solvers import BilayerSonophore +from PySONIC.constants import * # Set logging level logger.setLevel(logging.INFO) # Plot parameters matplotlib.rcParams['pdf.fonttype'] = 42 matplotlib.rcParams['ps.fonttype'] = 42 matplotlib.rcParams['font.family'] = 'arial' fs = 8 # font size lw = 2 # linewidth ps = 15 # scatter point size # Create standard bls object neuron = CorticalRS() A = 100e3 f = 500e3 a = 32e-9 Cm0 = neuron.Cm0 Qm0 = neuron.Vm0 * Cm0 * 1e-3 Qm = Qm0 # Create sonophore object bls = BilayerSonophore(a, f, Cm0, Qm0) # Compare profiles of direct and approximated intermolecular pressures along Z Z = np.linspace(-0.4 * bls.Delta_, bls.a, 1000) Pm_direct = bls.v_PMavg(Z, bls.v_curvrad(Z), bls.surface(Z)) Pm_approx = bls.PMavgpred(Z) fig, ax = plt.subplots(figsize=cm2inch(6, 5)) for skey in ['right', 'top']: ax.spines[skey].set_visible(False) ax.set_xlabel('Z (nm)', fontsize=fs) ax.set_ylabel('Pressure (kPa)', fontsize=fs) ax.set_xticks([0, bls.a * 1e9]) ax.set_xticklabels(['0', 'a']) ax.set_yticks([-10, 0, 40]) ax.set_ylim([-10, 50]) for item in ax.get_xticklabels() + ax.get_yticklabels(): item.set_fontsize(fs) ax.plot(Z * 1e9, Pm_direct * 1e-3, label='$\mathregular{P_m}$') ax.plot(Z * 1e9, Pm_approx * 1e-3, label='$\mathregular{P_{m,approx}}$') ax.axhline(y=0, color='k') ax.legend(fontsize=fs, frameon=False) fig.tight_layout() # Run simulation with integrated intermolecular pressure _, y, _ = bls.run(f, A, Qm, Pm_comp_method=PmCompMethod.direct) Z1, _ = y[:, -NPC_FULL:] deltaZ1 = Z1.max() - Z1.min() logger.info('simulation with standard Pm: Zmin = %.2f nm, Zmax = %.2f nm, dZ = %.2f nm', Z1.min() * 1e9, Z1.max() * 1e9, deltaZ1 * 1e9) # Run simulation with predicted intermolecular pressure _, y, _ = bls.run(f, A, Qm, Pm_comp_method=PmCompMethod.predict) Z2, _ = y[:, -NPC_FULL:] deltaZ2 = Z2.max() - Z2.min() logger.info('simulation with predicted Pm: Zmin = %.2f nm, Zmax = %.2f nm, dZ = %.2f nm', Z2.min() * 1e9, Z2.max() * 1e9, deltaZ2 * 1e9) error_Z = rmse(Z1, Z2) r2_Z = rsquared(Z1, Z2) logger.info('Z-error: R2 = %.4f, RMSE = %.4f nm (%.4f%% dZ)', r2_Z, error_Z * 1e9, error_Z / deltaZ1 * 1e2) plt.show() diff --git a/tests/test_values.py b/tests/test_values.py index b9afaba..7ff57d5 100644 --- a/tests/test_values.py +++ b/tests/test_values.py @@ -1,254 +1,254 @@ #!/usr/bin/env python # -*- coding: utf-8 -*- # @Author: Theo Lemaire # @Date: 2017-06-14 18:37:45 # @Email: theo.lemaire@epfl.ch # @Last Modified by: Theo Lemaire -# @Last Modified time: 2018-07-24 11:46:07 +# @Last Modified time: 2018-08-21 16:08:13 ''' Run functionalities of the package and test validity of outputs. ''' import sys import logging from argparse import ArgumentParser import numpy as np -from PointNICE.utils import logger, getNeuronsDict -from PointNICE import BilayerSonophore, SolverElec, SolverUS -from PointNICE.solvers import findPeaks, EStimTitrator, AStimTitrator -from PointNICE.constants import * +from PySONIC.utils import logger, getNeuronsDict +from PySONIC import BilayerSonophore, SolverElec, SolverUS +from PySONIC.solvers import findPeaks, EStimTitrator, AStimTitrator +from PySONIC.constants import * # Set logging level logger.setLevel(logging.INFO) # List of implemented neurons neurons = getNeuronsDict() neurons = list(neurons.values()) def test_MECH(): ''' Maximal negative and positive deflections of the BLS structure for a specific sonophore size, resting membrane properties and stimulation parameters. ''' logger.info('Starting test: Mechanical simulation') # Create BLS instance a = 32e-9 # m Fdrive = 350e3 # Hz Cm0 = 1e-2 # membrane resting capacitance (F/m2) Qm0 = -80e-5 # membrane resting charge density (C/m2) bls = BilayerSonophore(a, Fdrive, Cm0, Qm0) # Run mechanical simulation Adrive = 100e3 # Pa Qm = 50e-5 # C/m2 _, y, _ = bls.run(Fdrive, Adrive, Qm) # Check validity of deflection extrema Z, _ = y Zlast = Z[-NPC_FULL:] Zmin, Zmax = (Zlast.min(), Zlast.max()) logger.info('Zmin = %.2f nm, Zmax = %.2f nm', Zmin * 1e9, Zmax * 1e9) Zmin_ref, Zmax_ref = (-0.116e-9, 5.741e-9) assert np.abs(Zmin - Zmin_ref) < 1e-12, 'Unexpected sonophore compression amplitude' assert np.abs(Zmax - Zmax_ref) < 1e-12, 'Unexpected sonophore expansion amplitude' logger.info('Passed test: Mechanical simulation') def test_resting_potential(): ''' Neurons membrane potential in free conditions should stabilize to their specified resting potential value. ''' conv_err_msg = ('{} neuron membrane potential in free conditions does not converge to ' 'stable value (gap after 20s: {:.2e} mV)') value_err_msg = ('{} neuron steady-state membrane potential in free conditions differs ' 'significantly from specified resting potential (gap = {:.2f} mV)') logger.info('Starting test: neurons resting potential') # Initialize solver solver = SolverElec() for neuron_class in neurons: # Simulate each neuron in free conditions neuron = neuron_class() logger.info('%s neuron simulation in free conditions', neuron.name) _, y, _ = solver.run(neuron, Astim=0.0, tstim=20.0, toffset=0.0) Vm_free, *_ = y # Check membrane potential convergence Vm_free_last, Vm_free_beforelast = (Vm_free[-1], Vm_free[-2]) Vm_free_conv = Vm_free_last - Vm_free_beforelast assert np.abs(Vm_free_conv) < 1e-5, conv_err_msg.format(neuron.name, Vm_free_conv) # Check membrane potential convergence to resting potential Vm_free_diff = Vm_free_last - neuron.Vm0 assert np.abs(Vm_free_diff) < 0.1, value_err_msg.format(neuron.name, Vm_free_diff) logger.info('Passed test: neurons resting potential') def test_ESTIM(): ''' Threshold E-STIM amplitude and needed to obtain an action potential and response latency should match reference values. ''' Athr_err_msg = ('{} neuron threshold amplitude for excitation does not match reference value' '(gap = {:.2f} mA/m2)') latency_err_msg = ('{} neuron latency for excitation at threshold amplitude does not match ' 'reference value (gap = {:.2f} ms)') logger.info('Starting test: E-STIM titration') # Initialize solver solver = SolverElec() # Stimulation parameters tstim = 100e-3 # s toffset = 50e-3 # s # Reference values Athr_refs = {'FS': 6.91, 'LTS': 1.54, 'RS': 5.03, 'RE': 3.61, 'TC': 4.05, 'LeechT': 4.66, 'LeechP': 13.72, 'IB': 3.08} latency_refs = {'FS': 101.00e-3, 'LTS': 128.56e-3, 'RS': 103.81e-3, 'RE': 148.50e-3, 'TC': 63.46e-3, 'LeechT': 21.32e-3, 'LeechP': 36.84e-3, 'IB': 121.04e-3} for neuron_class in neurons: # Perform titration for each neuron neuron = neuron_class() logger.info('%s neuron titration', neuron.name) Athr, t, y, _, latency, _ = EStimTitrator(1, solver, neuron, None, tstim, toffset, None, 1.0, 1).__call__() Vm = y[0, :] # Check that final number of spikes is 1 # n_spikes, _, _ = detectSpikes(t, Vm, SPIKE_MIN_VAMP, SPIKE_MIN_DT) dt = t[1] - t[0] ipeaks, *_ = findPeaks(Vm, SPIKE_MIN_VAMP, int(np.ceil(SPIKE_MIN_DT / dt)), SPIKE_MIN_VPROM) n_spikes = ipeaks.size assert n_spikes == 1, 'Number of spikes after titration should be exactly 1' # Check threshold amplitude Athr_diff = Athr - Athr_refs[neuron.name] assert np.abs(Athr_diff) < 0.1, Athr_err_msg.format(neuron.name, Athr_diff) # Check response latency lat_diff = (latency - latency_refs[neuron.name]) * 1e3 assert np.abs(lat_diff) < 1.0, latency_err_msg.format(neuron.name, lat_diff) logger.info('Passed test: E-STIM titration') def test_ASTIM(): ''' Threshold A-STIM amplitude and needed to obtain an action potential and response latency should match reference values. ''' Athr_err_msg = ('{} neuron threshold amplitude for excitation does not match reference value' '(gap = {:.2f} kPa)') latency_err_msg = ('{} neuron latency for excitation at threshold amplitude does not match ' 'reference value (gap = {:.2f} ms)') logger.info('Starting test: A-STIM titration') # Sonophore diameter a = 32e-9 # m # Stimulation parameters Fdrive = 350e3 # Hz tstim = 50e-3 # s toffset = 30e-3 # s # Reference values Athr_refs = {'FS': 38.96e3, 'LTS': 24.90e3, 'RS': 50.90e3, 'RE': 46.36e3, 'TC': 23.14e3, 'LeechT': 21.02e3, 'LeechP': 22.23e3, 'IB': 91.26e3} latency_refs = {'FS': 54.96e-3, 'LTS': 57.46e-3, 'RS': 75.09e-3, 'RE': 79.75e-3, 'TC': 70.73e-3, 'LeechT': 43.25e-3, 'LeechP': 58.01e-3, 'IB': 79.35e-3} # Titration for each neuron for neuron_class in neurons: # Initialize neuron neuron = neuron_class() logger.info('%s neuron titration', neuron.name) # Initialize solver solver = SolverUS(a, neuron, Fdrive) # Perform titration Athr, t, y, _, latency, _ = AStimTitrator(1, solver, neuron, Fdrive, None, tstim, toffset, None, 1.0, int_method='effective').__call__() Qm = y[2] # Check that final number of spikes is 1 dt = t[1] - t[0] ipeaks, *_ = findPeaks(Qm, SPIKE_MIN_QAMP, int(np.ceil(SPIKE_MIN_DT / dt)), SPIKE_MIN_QPROM) n_spikes = ipeaks.size assert n_spikes == 1, 'Number of spikes after titration should be exactly 1' # Check threshold amplitude Athr_diff = (Athr - Athr_refs[neuron.name]) * 1e-3 assert np.abs(Athr_diff) < 0.1, Athr_err_msg.format(neuron.name, Athr_diff) # Check response latency lat_diff = (latency - latency_refs[neuron.name]) * 1e3 assert np.abs(lat_diff) < 1.0, latency_err_msg.format(neuron.name, lat_diff) logger.info('Passed test: A-STIM titration') def test_all(): logger.info('Starting tests') test_MECH() test_resting_potential() test_ESTIM() test_ASTIM() logger.info('All tests successfully passed') def main(): # Define valid test sets valid_testsets = [ 'MECH', 'resting_potential', 'ESTIM', 'ASTIM', 'all' ] # Define argument parser ap = ArgumentParser() ap.add_argument('-t', '--testset', type=str, default='all', choices=valid_testsets, help='Specific test set') ap.add_argument('-v', '--verbose', default=False, action='store_true', help='Increase verbosity') # Parse arguments args = ap.parse_args() if args.verbose: logger.setLevel(logging.DEBUG) else: logger.setLevel(logging.INFO) # Run test try: if args.testset == 'all': test_all() else: possibles = globals().copy() possibles.update(locals()) method = possibles.get('test_{}'.format(args.testset)) method() sys.exit(0) except AssertionError as e: logger.error(e) sys.exit(1) if __name__ == '__main__': main()