diff --git a/PySONIC/core/__init__.py b/PySONIC/core/__init__.py index 5be4fae..321b96c 100644 --- a/PySONIC/core/__init__.py +++ b/PySONIC/core/__init__.py @@ -1,16 +1,16 @@ #!/usr/bin/env python # -*- coding: utf-8 -*- # @Author: Theo Lemaire # @Date: 2017-06-06 13:36:00 # @Email: theo.lemaire@epfl.ch # @Last Modified by: Theo Lemaire -# @Last Modified time: 2019-05-31 16:58:23 +# @Last Modified time: 2019-06-02 13:32:37 from .simulators import PWSimulator, PeriodicSimulator -from .batches import * +from .batches import Batch, createQueue from .model import Model from .pneuron import PointNeuron from .bls import BilayerSonophore, PmCompMethod, LennardJones from .nbls import NeuronalBilayerSonophore from .nmodl_generator import NmodlGenerator \ No newline at end of file diff --git a/PySONIC/core/batches.py b/PySONIC/core/batches.py index 0e378c0..0fb6f16 100644 --- a/PySONIC/core/batches.py +++ b/PySONIC/core/batches.py @@ -1,206 +1,186 @@ # -*- coding: utf-8 -*- # @Author: Theo Lemaire # @Date: 2017-08-22 14:33:04 # @Last Modified by: Theo Lemaire -# @Last Modified time: 2019-05-31 16:57:59 +# @Last Modified time: 2019-06-02 14:20:06 ''' Utility functions used in simulations ''' import os import lockfile import logging import multiprocessing as mp import numpy as np import pandas as pd from ..utils import logger class Consumer(mp.Process): ''' Generic consumer process, taking tasks from a queue and outputing results in another queue. ''' def __init__(self, queue_in, queue_out): mp.Process.__init__(self) self.queue_in = queue_in self.queue_out = queue_out logger.debug('Starting %s', self.name) def run(self): while True: nextTask = self.queue_in.get() if nextTask is None: logger.debug('Exiting %s', self.name) self.queue_in.task_done() break answer = nextTask() self.queue_in.task_done() self.queue_out.put(answer) return -class Worker(): +class Worker: ''' Generic worker class calling a specific function with a given set of parameters. ''' - def __init__(self, wid, simfunc, params, loglevel): + def __init__(self, wid, func, params, loglevel): ''' Worker constructor. :param wid: worker ID - :param simfunc: function object + :param func: function object :param params: list of method parameters :param loglevel: logging level ''' self.id = wid - self.simfunc = simfunc + self.func = func self.params = params self.loglevel = loglevel def __call__(self): - ''' Caller to the specific object method. ''' + ''' Caller to the function with specific parameters. ''' logger.setLevel(self.loglevel) - return self.id, self.simfunc(*self.params) + return self.id, self.func(*self.params) + + +class Batch: + ''' Generic interface to run batches of function calls. ''' + + def __init__(self, func, queue): + ''' Batch constructor. + + :param func: function object + :param queue: list of list of function parameters + ''' + self.func = func + self.queue = queue + + def __call__(self, *args, **kwargs): + ''' Call the internal run method. ''' + return self.run(*args, **kwargs) + + def getNConsumers(self): + ''' Determine number of consumers based on queue length and number of available CPUs. ''' + return min(mp.cpu_count(), len(self.queue)) + + def start(self): + ''' Create tasks and results queues, and start consumers. ''' + mp.freeze_support() + self.tasks = mp.JoinableQueue() + self.results = mp.Queue() + self.consumers = [Consumer(self.tasks, self.results) for i in range(self.getNConsumers())] + for c in self.consumers: + c.start() + + def assign(self, loglevel): + ''' Assign tasks to workers. ''' + for i, params in enumerate(self.queue): + worker = Worker(i, self.func, params, loglevel) + self.tasks.put(worker, block=False) + + def join(self): + ''' Put all tasks to None and join the queue. ''' + for i in range(len(self.consumers)): + self.tasks.put(None, block=False) + self.tasks.join() + + def get(self): + ''' Extract and re-order results. ''' + outputs, idxs = [], [] + for i in range(len(self.queue)): + wid, out = self.results.get() + outputs.append(out) + idxs.append(wid) + return [x for _, x in sorted(zip(idxs, outputs))] + + def stop(self): + ''' Close tasks and results queues. ''' + self.tasks.close() + self.results.close() + + def run(self, mpi=False, loglevel=logging.INFO): + ''' Run batch with or without multiprocessing. ''' + if mpi: + self.start() + self.assign(loglevel) + self.join() + outputs = self.get() + self.stop() + else: + outputs = [self.func(*params) for params in self.queue] + return outputs def createQueue(*dims): ''' Create a serialized 2D array of all parameter combinations for a series of individual parameter sweeps. :param dims: list of lists (or 1D arrays) of input parameters :return: list of parameters (list) for each simulation ''' ndims = len(dims) dims_in = [dims[1], dims[0]] inds_out = [1, 0] if ndims > 2: dims_in += dims[2:] inds_out += list(range(2, ndims)) queue = np.stack(np.meshgrid(*dims_in), -1).reshape(-1, ndims) queue = queue[:, inds_out] return queue.tolist() -def runBatch(simfunc, queue, mpi=False, loglevel=logging.INFO): - ''' Run batch of a simulation function with various combinations of parameters. - - :param queue: array of all stimulation parameters combinations - :param mpi: boolean stating whether or not to use multiprocessing - ''' - nsims = len(queue) - - if mpi: - mp.freeze_support() - tasks = mp.JoinableQueue() - results = mp.Queue() - nconsumers = min(mp.cpu_count(), nsims) - consumers = [Consumer(tasks, results) for i in range(nconsumers)] - for w in consumers: - w.start() - - # Run simulations - outputs = [] - for i, params in enumerate(queue): - if mpi: - worker = Worker(i, simfunc, params, loglevel) - tasks.put(worker, block=False) - else: - outputs.append(simfunc(*params)) - - if mpi: - for i in range(nconsumers): - tasks.put(None, block=False) - tasks.join() - idxs = [] - for i in range(nsims): - wid, out = results.get() - outputs.append(out) - idxs.append(wid) - outputs = [x for _, x in sorted(zip(idxs, outputs))] - - # Close tasks and results queues - tasks.close() - results.close() - - return outputs - - -def runBatch2(obj, method_str, queue, mpi=False, loglevel=logging.INFO): - ''' Run batch of simulations of a model object for various combinations of parameters. - - :param queue: array of all stimulation parameters combinations - :param mpi: boolean stating whether or not to use multiprocessing - ''' - nsims = len(queue) - - if mpi: - mp.freeze_support() - tasks = mp.JoinableQueue() - results = mp.Queue() - nconsumers = min(mp.cpu_count(), nsims) - consumers = [Consumer(tasks, results) for i in range(nconsumers)] - for w in consumers: - w.start() - - # Run simulations - outputs = [] - for i, params in enumerate(queue): - if mpi: - worker = Worker(i, obj, method_str, params, loglevel) - tasks.put(worker, block=False) - else: - outputs.append(getattr(obj, method_str)(*params)) - - if mpi: - for i in range(nconsumers): - tasks.put(None, block=False) - tasks.join() - idxs = [] - for i in range(nsims): - wid, out = results.get() - outputs.append(out) - idxs.append(wid) - outputs = [x for _, x in sorted(zip(idxs, outputs))] - - # Close tasks and results queues - tasks.close() - results.close() - - return outputs - - def xlslog(filepath, logentry, 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 filepath: absolute or relative path to the Excel workbook :param logentry: log entry (dictionary) to add to log file :param sheetname: name of the Excel spreadsheet to which data is appended :return: boolean indicating success (1) or failure (0) of operation ''' # Parse log dataframe from Excel file if it exists, otherwise create new one if not os.path.isfile(filepath): df = pd.DataFrame(columns=logentry.keys()) else: df = pd.read_excel(filepath, sheet_name=sheetname) # Add log entry to log dataframe df = df.append(pd.Series(logentry), ignore_index=True) # Write log dataframe to Excel file try: lock = lockfile.FileLock(filepath) lock.acquire() writer = pd.ExcelWriter(filepath) df.to_excel(writer, sheet_name=sheetname, index=False) writer.save() 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"', filepath) user_str = input() if user_str in ['y', 'Y']: return xlslog(filepath, logentry, sheetname) else: return 0 diff --git a/PySONIC/core/bls.py b/PySONIC/core/bls.py index 224c363..e344e17 100644 --- a/PySONIC/core/bls.py +++ b/PySONIC/core/bls.py @@ -1,785 +1,734 @@ #!/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: 2019-06-01 18:38:14 +# @Last Modified time: 2019-06-02 13:03:21 from enum import Enum import os import json -import inspect import numpy as np import pandas as pd import scipy.integrate as integrate from scipy.optimize import brentq, curve_fit -from .batches import createQueue from .model import Model from .simulators import PeriodicSimulator from ..utils import logger, si_format from ..constants import * class PmCompMethod(Enum): ''' Enum: types of computation method for the intermolecular pressure ''' direct = 1 predict = 2 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)) class BilayerSonophore(Model): ''' 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) 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) tscale = 'us' # relevant temporal scale of the model def __init__(self, a, Cm0, Qm0, Fdrive=None, embedding_depth=0.0): ''' Constructor of the class. :param a: in-plane radius of the sonophore structure within the membrane (m) :param Cm0: membrane resting capacitance (F/m2) :param Qm0: membrane resting charge density (C/m2) :param Fdrive: frequency of acoustic perturbation (Hz) :param embedding_depth: depth of the embedding tissue around the membrane (m) ''' - # Extract resting constants and geometry self.Cm0 = Cm0 self.Qm0 = Qm0 self.a = a self.d = embedding_depth self.S0 = np.pi * self.a**2 # Derive frequency-dependent tissue elastic modulus if Fdrive is not None: G_tissue = self.alpha * Fdrive # G'' (Pa) self.kA_tissue = 2 * G_tissue * self.d # kA of the tissue layer (N/m) else: self.kA_tissue = 0. # Check existence of lookups for derived parameters lookups = self.getLookups() akey = '{:.1f}'.format(a * 1e9) Qkey = '{:.2f}'.format(Qm0 * 1e5) # If no lookup, compute parameters and store them in lookup if akey not in lookups or Qkey not in lookups[akey]: # 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 if akey not in lookups: lookups[akey] = {Qkey: {'LJ_approx': LJ_approx, 'Delta_eq': D_eq}} else: lookups[akey][Qkey] = {'LJ_approx': LJ_approx, 'Delta_eq': D_eq} logger.debug('Saving BLS derived parameters to lookup file') self.saveLookups(lookups) # If lookup exists, load parameters from it else: logger.debug('Loading BLS derived parameters from lookup file') self.LJ_approx = lookups[akey][Qkey]['LJ_approx'] self.Delta = lookups[akey][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 __repr__(self): - return 'BilayerSonophore({}m, {}F/cm2, {}C/cm2, embedding_depth={}m'.format( - si_format([self.a, self.Cm0 * 1e-4, self.Qm0 * 1e-4, self.embedding_depth], - precision=1, space=' ')) - - def pprint(self): - return '{}m radius BilayerSonophore'.format( - si_format(self.a, precision=0, space=' ')) + s = '{}({:.1f} nm'.format(self.__class__.__name__, self.a * 1e9) + if self.d > 0.: + s += ', d={}m'.format(si_format(self.d, precision=1, space=' ')) + return s + ')' def filecode(self, Fdrive, Adrive, Qm): return 'MECH_{:.0f}nm_{:.0f}kHz_{:.1f}kPa_{:.1f}nCcm2'.format( self.a * 1e9, Fdrive * 1e-3, Adrive * 1e-3, Qm * 1e5) def getLookupsPath(self): return os.path.join(os.path.split(__file__)[0], 'bls_lookups.json') def getLookups(self): try: with open(self.getLookupsPath()) as fh: sample = json.load(fh) return sample except FileNotFoundError: return {} def saveLookups(self, lookups): with open(self.getLookupsPath(), 'w') as fh: json.dump(lookups, fh, indent=2) - def pparams(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 getPltScheme(self): return { 'P_{AC}': ['Pac'], 'Z': ['Z'], 'n_g': ['ng'] } def getPltVars(self, wrapleft='df["', wrapright='"]'): ''' Return a dictionary with information about all plot variables related to the model. ''' return { 'Pac': { 'desc': 'acoustic pressure', 'label': 'P_{AC}', 'unit': 'kPa', 'factor': 1e-3, 'func': 'Pacoustic({0}t{1}, meta["Adrive"] * {0}stimstate{1}, meta["Fdrive"])'.format( wrapleft, wrapright) }, 'Z': { 'desc': 'leaflets deflection', 'label': 'Z', 'unit': 'nm', 'factor': 1e9, 'bounds': (-1.0, 10.0) }, 'ng': { 'desc': 'gas content', 'label': 'n_g', 'unit': '10^{-22}\ mol', 'factor': 1e22, 'bounds': (1.0, 15.0) }, 'Pmavg': { 'desc': 'average intermolecular pressure', 'label': 'P_M', 'unit': 'kPa', 'factor': 1e-3, 'func': 'PMavgpred({0}Z{1})'.format(wrapleft, wrapright) }, 'Telastic': { 'desc': 'leaflet elastic tension', 'label': 'T_E', 'unit': 'mN/m', 'factor': 1e3, 'func': 'TEleaflet({0}Z{1})'.format(wrapleft, wrapright) }, 'Cm': { 'desc': 'membrane capacitance', 'label': 'C_m', 'unit': 'uF/cm^2', 'factor': 1e2, 'bounds': (0.0, 1.5), 'func': 'v_Capct({0}Z{1})'.format(wrapleft, wrapright) } } def curvrad(self, Z): ''' Leaflet curvature radius (signed variable) :param Z: leaflet apex deflection (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): ''' Surface area of the stretched leaflet (spherical cap formula) :param Z: leaflet apex deflection (m) :return: stretched leaflet surface (m^2) ''' return np.pi * (self.a**2 + Z**2) def volume(self, Z): ''' Volume of the inter-leaflet space (cylinder +/- 2 spherical caps) :param Z: leaflet apex deflection (m) :return: bilayer sonophore inner volume (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): ''' Areal strain of the stretched leaflet epsilon = (S - S0)/S0 = (Z/a)^2 :param Z: leaflet apex deflection (m) :return: areal strain (dimensionless) ''' return (Z / self.a)**2 def Capct(self, Z): ''' Membrane capacitance (parallel-plate capacitor evaluated at average inter-layer distance) :param Z: leaflet apex deflection (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): ''' Evolution of membrane capacitance :param Z: leaflet apex deflection (m) :param U: leaflet apex deflection velocity (m/s) :return: time 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): ''' Local leaflet deflection at specific radial distance (signed) :param r: in-plane distance from center of the sonophore (m) :param Z: leaflet apex deflection (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): ''' Time-varying acoustic pressure :param t: time (s) :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): ''' Local intermolecular pressure :param r: in-plane distance from center of the sonophore (m) :param Z: leaflet apex deflection (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): ''' Average intermolecular pressure across the leaflet (computed 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 (Pa) .. warning:: quadratic integration is computationally expensive. ''' # Integrate intermolecular force over an infinitely thin ring of radius r from 0 to a fTotal, _ = integrate.quad(lambda r, Z, R: 2 * np.pi * r * self.PMlocal(r, Z, R), 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 Zmin = brentq(lambda Z, Pmmax: self.PMavg(Z, self.curvrad(Z), self.surface(Z)) - PMmax, 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): ''' Approximated average intermolecular pressure (using nonlinearly fitted Lennard-Jones function) :param Z: leaflet apex deflection (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): ''' Electrical pressure term :param Z: leaflet apex deflection (m) :param Qm: membrane charge density (C/m2) :return: electrical 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)) + def dualPressure(Delta): + x = (self.Delta_ / Delta) + return (self.pDelta * (x**self.m - x**self.n) + self.Pelec(0.0, Qm)) Delta_eq = brentq(f, 0.1 * self.Delta_, 2.0 * self.Delta_, xtol=1e-16) logger.debug('∆eq = %.2f nm', Delta_eq * 1e9) - return (Delta_eq, f(Delta_eq)) + return (Delta_eq, dualPressure(Delta_eq)) def gasFlux(self, Z, P): ''' Gas molar flux through the sonophore boundary layers :param Z: leaflet apex deflection (m) :param P: internal gas pressure (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): ''' Internal gas pressure for a given molar content :param ng: internal molar content (mol) :param V: sonophore inner volume (m^3) :return: internal gas pressure (Pa) ''' return ng * Rg * self.T / V def gasPa2mol(self, P, V): ''' Internal gas molar content for a given pressure :param P: internal gas pressure (Pa) :param V: sonophore inner volume (m^3) :return: internal gas molar content (mol) ''' return P * V / (Rg * self.T) def PtotQS(self, Z, ng, Qm, Pac, Pm_comp_method): ''' Net quasi-steady pressure for a given acoustic pressure (Ptot = Pm + Pg + Pec - P0 - Pac) :param Z: leaflet apex deflection (m) :param ng: internal molar content (mol) :param Qm: membrane charge density (C/m2) :param Pac: acoustic pressure (Pa) :param Pm_comp_method: computation method for 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): ''' Quasi-steady equilibrium deflection for a given acoustic pressure (computed by approximating the root of quasi-steady pressure) :param ng: internal molar content (mol) :param Qm: membrane charge density (C/m2) :param Pac: external acoustic perturbation (Pa) :param Pm_comp_method: computation method for average intermolecular pressure :return: leaflet deflection canceling quasi-steady pressure (m) ''' 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): ''' Elastic tension in leaflet :param Z: leaflet apex deflection (m) :return: circumferential elastic tension (N/m) ''' return self.kA * self.arealstrain(Z) def TEtissue(self, Z): ''' Elastic tension in surrounding viscoelastic layer :param Z: leaflet apex deflection (m) :return: circumferential elastic tension (N/m) ''' return self.kA_tissue * self.arealstrain(Z) def TEtot(self, Z): ''' Total elastic tension (leaflet + surrounding viscoelastic layer) :param Z: leaflet apex deflection (m) :return: circumferential elastic tension (N/m) ''' return self.TEleaflet(Z) + self.TEtissue(Z) def PEtot(self, Z, R): ''' Total elastic tension pressure (leaflet + surrounding viscoelastic layer) :param Z: leaflet apex deflection (m) :param R: leaflet curvature radius (m) :return: elastic tension pressure (Pa) ''' return - self.TEtot(Z) / R def PVleaflet(self, U, R): ''' Viscous stress pressure in leaflet :param U: leaflet apex deflection velocity (m/s) :param R: leaflet curvature radius (m) :return: leaflet viscous stress pressure (Pa) ''' return - 12 * U * self.delta0 * self.muS / R**2 def PVfluid(self, U, R): ''' Viscous stress pressure in surrounding medium :param U: leaflet apex deflection velocity (m/s) :param R: leaflet curvature radius (m) :return: fluid viscous stress pressure (Pa) ''' return - 4 * U * self.muL / np.abs(R) def accP(self, Ptot, R): ''' Leaflet transverse acceleration resulting from pressure imbalance :param Ptot: net pressure (Pa) :param R: leaflet curvature radius (m) :return: pressure-driven acceleration (m/s^2) ''' return Ptot / (self.rhoL * np.abs(R)) def accNL(self, U, R): ''' Leaflet transverse nonlinear acceleration :param U: leaflet apex deflection velocity (m/s) :param R: leaflet curvature radius (m) :return: nonlinear acceleration term (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 derivatives(self, y, t, Adrive, Fdrive, Qm, phi, Pm_comp_method=PmCompMethod.predict): ''' Evolution of the mechanical system :param y: vector of HH system variables at time t :param t: time instant (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: computation method for 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 checkInputs(self, Fdrive, Adrive, Qm, phi): ''' Check validity of stimulation parameters :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) ''' if not all(isinstance(param, float) for param in [Fdrive, Adrive, Qm, phi]): raise TypeError('Invalid stimulation parameters (must be float typed)') if Fdrive <= 0: raise ValueError('Invalid US driving frequency: {} kHz (must be strictly positive)' .format(Fdrive * 1e-3)) if Adrive < 0: raise ValueError('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 ValueError('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 ValueError('Invalid US pressure phase: {:.2f} rad (must be within [0, 2 PI[ rad' .format(phi)) + def meta(self, Fdrive, Adrive, Qm): + ''' Return information about object and simulation parameters. + + :param Fdrive: US frequency (Hz) + :param Adrive: acoustic pressure amplitude (Pa) + :param Qm: applied membrane charge density (C/m2) + :return: meta-data dictionary + ''' + return { + 'a': self.a, + 'd': self.d, + 'Cm0': self.Cm0, + 'Qm0': self.Qm0, + 'Fdrive': Fdrive, + 'Adrive': Adrive, + 'Qm': Qm + } + def simulate(self, Fdrive, Adrive, Qm, phi=np.pi, Pm_comp_method=PmCompMethod.predict): ''' Simulate system until periodic stabilization for a specific set of ultrasound parameters, and return output data in a dataframe. :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: 2-tuple with the output dataframe and computation time. ''' - logger.info('%s: simulation @ f = %sHz, A = %sPa, Q = %sC/cm2', - self.pprint(), *si_format([Fdrive, Adrive, Qm * 1e-4], 2, space=' ')) + self, *si_format([Fdrive, Adrive, Qm * 1e-4], 2, space=' ')) # Check validity of stimulation parameters self.checkInputs(Fdrive, Adrive, Qm, phi) # Determine time step dt = 1 / (NPC_FULL * Fdrive) # Compute non-zero deflection value for a small perturbation (solving quasi-steady equation) Pac = self.Pacoustic(dt, Adrive, Fdrive, phi) Z0 = self.balancedefQS(self.ng0, Qm, Pac, Pm_comp_method) # Set initial conditions y0 = np.array([0., Z0, self.ng0]) # Initialize simulator and compute solution simulator = PeriodicSimulator( lambda y, t: self.derivatives(y, t, Adrive, Fdrive, Qm, phi, Pm_comp_method), ivars_to_check=[1, 2]) (t, y, stim), tcomp = simulator(y0, dt, 1. / Fdrive, monitor_time=True) logger.debug('completed in %ss', si_format(tcomp, 1)) # Set last stimulation state to zero stim[-1] = 0 # Store output in dataframe data = pd.DataFrame({ 't': t, 'stimstate': stim, 'Z': y[:, 1], 'ng': y[:, 2] }) # Return dataframe and computation time return data, tcomp - def meta(self, Fdrive, Adrive, Qm): - ''' Return information about object and simulation parameters. - - :param Fdrive: US frequency (Hz) - :param Adrive: acoustic pressure amplitude (Pa) - :param Qm: applied membrane charge density (C/m2) - :return: meta-data dictionary - ''' - return { - 'a': self.a, - 'd': self.d, - 'Cm0': self.Cm0, - 'Qm0': self.Qm0, - 'Fdrive': Fdrive, - 'Adrive': Adrive, - 'Qm': Qm - } - - def createQueue(self, freqs, amps, charges): - ''' Create a serialized 2D array of all parameter combinations for a series of individual - parameter sweeps. - - :param freqs: list (or 1D-array) of US frequencies - :param amps: list (or 1D-array) of acoustic amplitudes - :param charges: list (or 1D-array) of membrane charge densities - :return: list of parameters (list) for each simulation - ''' - return createQueue(freqs, amps, charges) - def getCycleProfiles(self, Fdrive, Adrive, Qm): ''' Simulate mechanical system and compute pressures over the last acoustic cycle :param Fdrive: acoustic drive frequency (Hz) :param Adrive: acoustic drive amplitude (Pa) :param Qm: imposed membrane charge density (C/m2) :return: dataframe with the time, kinematic and pressure profiles over the last cycle. ''' - # Run default simulation and compute relevant profiles logger.info('Running mechanical simulation (a = %sm, f = %sHz, A = %sPa)', si_format(self.a, 1), si_format(Fdrive, 1), si_format(Adrive, 1)) - data, tcomp = self.simulate(Fdrive, Adrive, Qm, Pm_comp_method=PmCompMethod.direct) + data, _ = self.simulate(Fdrive, Adrive, Qm, Pm_comp_method=PmCompMethod.direct) t, Z, ng = [data.loc[-NPC_FULL:, key].values for key in ['t', 'Z', 'ng']] dt = (t[-1] - t[0]) / (NPC_FULL - 1) t -= t[0] # Compute pressure cyclic profiles logger.info('Computing pressure cyclic profiles') R = self.v_curvrad(Z) U = np.diff(Z) / dt U = np.hstack((U, U[-1])) data = { 't': t, 'Z': Z, 'Cm': self.v_Capct(Z), 'P_M': self.v_PMavg(Z, R, self.surface(Z)), 'P_Q': self.Pelec(Z, Qm), 'P_{VE}': self.PEtot(Z, R) + self.PVleaflet(U, R), 'P_V': self.PVfluid(U, R), 'P_G': self.gasmol2Pa(ng, self.volume(Z)), 'P_0': - np.ones(Z.size) * self.P0 } return pd.DataFrame(data, columns=data.keys()) diff --git a/PySONIC/core/bls_lookups.json b/PySONIC/core/bls_lookups.json index 7842d9c..8c1b32d 100644 --- a/PySONIC/core/bls_lookups.json +++ b/PySONIC/core/bls_lookups.json @@ -1,627 +1,638 @@ { "32.0": { "-80.00": { "LJ_approx": { "x0": 1.7875580514692446e-09, "C": 14506.791031634148, "nrep": 3.911252335063797, "nattr": 0.9495868868453603 }, "Delta_eq": 1.2344323203763867e-09 }, "-71.40": { "LJ_approx": { "x0": 1.710159362626304e-09, "C": 16757.44053535206, "nrep": 3.9149844779001572, "nattr": 0.9876139143736086 }, "Delta_eq": 1.2566584760815426e-09 }, "-54.00": { "LJ_approx": { "x0": 1.588820457014353e-09, "C": 21124.28839722447, "nrep": 3.9219530533405726, "nattr": 1.0531179666960837 }, "Delta_eq": 1.302942739961778e-09 }, "-71.90": { "LJ_approx": { "x0": 1.7142977983903395e-09, "C": 16627.43538695451, "nrep": 3.9147721975981384, "nattr": 0.9855168537576823 }, "Delta_eq": 1.2553492695740507e-09 }, "-89.50": { "LJ_approx": { "x0": 1.8913883171160228e-09, "C": 12016.525797229067, "nrep": 3.9069373029335464, "nattr": 0.9021994595277029 }, "Delta_eq": 1.2107230911508513e-09 }, "-61.93": { "LJ_approx": { "x0": 1.6390264131559902e-09, "C": 19180.97634755811, "nrep": 3.9188840789597705, "nattr": 1.0250251620607604 }, "Delta_eq": 1.281743450351987e-09 }, "-53.00": { "LJ_approx": { "x0": 1.5830321174402216e-09, "C": 21361.655211483354, "nrep": 3.9223254792281588, "nattr": 1.0564645995714745 }, "Delta_eq": 1.305609024046854e-09 }, "-53.58": { "LJ_approx": { "x0": 1.5863754403940291e-09, "C": 21224.206759769622, "nrep": 3.922109852077156, "nattr": 1.0545313303221624 }, "Delta_eq": 1.3040630712174578e-09 }, "-48.87": { "LJ_approx": { "x0": 1.5603070731170595e-09, "C": 22321.280954434333, "nrep": 3.9238276518118833, "nattr": 1.0698008224359472 }, "Delta_eq": 1.3165739825437056e-09 }, "0.00": { "LJ_approx": { "x0": 1.429523524073023e-09, "C": 28748.036227122713, "nrep": 3.9338919786768276, "nattr": 1.1551044201542804 }, "Delta_eq": 1.4e-09 }, "-70.00": { "LJ_approx": { "x0": 1.698788510560293e-09, "C": 17120.631318195712, "nrep": 3.915575488491436, "nattr": 0.9934238714780391 }, "Delta_eq": 1.2603339470322538e-09 }, "-58.00": { "LJ_approx": { "x0": 1.6131662659976035e-09, "C": 20156.47605325608, "nrep": 3.9204295233162925, "nattr": 1.0393049952285334 }, "Delta_eq": 1.2922508866011204e-09 }, "-140.00": { "LJ_approx": { "x0": 3.5396589580589484e-09, "C": 1255.8321160636908, "nrep": 3.879907809497444, "nattr": 0.4190657482583384 }, "Delta_eq": 1.1019265101358682e-09 } }, "64.0": { "-80.00": { "LJ_approx": { "x0": 1.783357531675752e-09, "C": 14639.319598806138, "nrep": 3.9113027551187414, "nattr": 0.9404151935643594 }, "Delta_eq": 1.2344323203763867e-09 }, "-71.90": { "LJ_approx": { "x0": 1.7103254451796522e-09, "C": 16775.90747591089, "nrep": 3.9148582072320104, "nattr": 0.9747613204242506 }, "Delta_eq": 1.2553492695740507e-09 }, "-71.40": { "LJ_approx": { "x0": 1.7061996856525807e-09, "C": 16906.878806702443, "nrep": 3.9150725853841957, "nattr": 0.976778398349503 }, "Delta_eq": 1.2566584760815426e-09 }, "-54.00": { "LJ_approx": { "x0": 1.585264156646392e-09, "C": 21303.176047683613, "nrep": 3.92211004079812, "nattr": 1.0402641595550777 }, "Delta_eq": 1.302942739961778e-09 }, "-89.50": { "LJ_approx": { "x0": 1.886870747500225e-09, "C": 12129.260307725155, "nrep": 3.906945602966425, "nattr": 0.895598309376088 }, "Delta_eq": 1.2107230911508513e-09 }, "-61.93": { "LJ_approx": { "x0": 1.635297932833928e-09, "C": 19347.359224637305, "nrep": 3.9190113341505843, "nattr": 1.0129039638328117 }, "Delta_eq": 1.281743450351987e-09 }, "-58.00": { "LJ_approx": { "x0": 1.6095250707781465e-09, "C": 20329.27848623273, "nrep": 3.92057191136993, "nattr": 1.0267862446034561 }, "Delta_eq": 1.2922508866011204e-09 }, "-140.00": { "LJ_approx": { "x0": 3.5398097121754107e-09, "C": 1255.8690004347025, "nrep": 3.8798490490747404, "nattr": 0.4604604439108636 }, "Delta_eq": 1.1019265101358682e-09 } }, "50.0": { "-71.90": { "LJ_approx": { "x0": 1.7114794411958874e-09, "C": 16732.841575829825, "nrep": 3.914827883392826, "nattr": 0.9781401389550711 }, "Delta_eq": 1.2553492695740507e-09 } }, "10.0": { "0.00": { "LJ_approx": { "x0": 1.4403460578039628e-09, "C": 27932.27792195569, "nrep": 3.9334138654752686, "nattr": 1.19526523864855 }, "Delta_eq": 1.4e-09 }, "-71.90": { "LJ_approx": { "x0": 1.7286986021591825e-09, "C": 16087.514816365254, "nrep": 3.9147885683678543, "nattr": 1.012616990226475 }, "Delta_eq": 1.2553492695740507e-09 } }, "100.0": { "0.00": { "LJ_approx": { "x0": 1.4254455131143225e-09, "C": 29048.417918044444, "nrep": 3.9342659189249254, "nattr": 1.1351227816904121 }, "Delta_eq": 1.4e-09 }, "-71.90": { "LJ_approx": { "x0": 1.7087681652667724e-09, "C": 16833.83962398515, "nrep": 3.914908533680663, "nattr": 0.9697102045586926 }, "Delta_eq": 1.2553492695740507e-09 }, "-71.40": { "LJ_approx": { "x0": 1.7046480280451768e-09, "C": 16965.15489682674, "nrep": 3.9151238997284845, "nattr": 0.9716928395857687 }, "Delta_eq": 1.2566584760815426e-09 }, "-54.00": { "LJ_approx": { "x0": 1.5838767580748841e-09, "C": 21372.412565003375, "nrep": 3.922191259312523, "nattr": 1.0344167918733638 }, "Delta_eq": 1.302942739961778e-09 }, "-89.50": { "LJ_approx": { "x0": 1.8850831984948754e-09, "C": 12173.857567383837, "nrep": 3.906959887367545, "nattr": 0.8923154523792497 }, "Delta_eq": 1.2107230911508513e-09 }, "-61.93": { "LJ_approx": { "x0": 1.6338405482612552e-09, "C": 19411.96069791924, "nrep": 3.9190798895919405, "nattr": 1.0073171165886772 }, "Delta_eq": 1.281743450351987e-09 } }, "500.0": { "0.00": { "LJ_approx": { "x0": 1.4236928207491738e-09, "C": 29174.423851140436, "nrep": 3.934489087598531, "nattr": 1.1244706663694597 }, "Delta_eq": 1.4e-09 } }, "15.0": { "-71.90": { "LJ_approx": { "x0": 1.722207527516432e-09, "C": 16331.124295102756, "nrep": 3.914721476976037, "nattr": 1.0021304453280393 }, "Delta_eq": 1.2553492695740507e-09 }, "-71.40": { "LJ_approx": { "x0": 1.7180439454408102e-09, "C": 16459.15520614834, "nrep": 3.9149304238974905, "nattr": 1.0043742068193204 }, "Delta_eq": 1.2566584760815426e-09 }, "-54.00": { "LJ_approx": { "x0": 1.5959361190370466e-09, "C": 20763.823187183425, "nrep": 3.921785737782814, "nattr": 1.0737976563473925 }, "Delta_eq": 1.302942739961778e-09 }, "-89.50": { "LJ_approx": { "x0": 1.9002701433896942e-09, "C": 11795.576706463997, "nrep": 3.9070059150621814, "nattr": 0.9114123498082551 }, "Delta_eq": 1.2107230911508513e-09 }, "-61.93": { "LJ_approx": { "x0": 1.646473044478369e-09, "C": 18846.9803104403, "nrep": 3.918766624746869, "nattr": 1.044220870098494 }, "Delta_eq": 1.281743450351987e-09 } }, "70.0": { "-61.93": { "LJ_approx": { "x0": 1.6349587829329923e-09, "C": 19362.426097728276, "nrep": 3.9190260877993097, "nattr": 1.0116609492531905 }, "Delta_eq": 1.281743450351987e-09 }, "-71.90": { "LJ_approx": { "x0": 1.7099628637265945e-09, "C": 16789.420291577666, "nrep": 3.9148688185890483, "nattr": 0.9736446481917082 }, "Delta_eq": 1.2553492695740507e-09 }, "-71.40": { "LJ_approx": { "x0": 1.7058388214243274e-09, "C": 16920.455606556396, "nrep": 3.9150834388621805, "nattr": 0.9756530742858303 }, "Delta_eq": 1.2566584760815426e-09 }, "-54.00": { "LJ_approx": { "x0": 1.584940743805642e-09, "C": 21319.35643207058, "nrep": 3.9221277053335264, "nattr": 1.0389567310289958 }, "Delta_eq": 1.302942739961778e-09 }, "-89.50": { "LJ_approx": { "x0": 1.886457143504779e-09, "C": 12139.584658299475, "nrep": 3.9069481854501382, "nattr": 0.8948872453823127 }, "Delta_eq": 1.2107230911508513e-09 } }, "150.0": { "-71.90": { "LJ_approx": { "x0": 1.707781542586275e-09, "C": 16870.340248462282, "nrep": 3.914948339378328, "nattr": 0.9660711186661354 }, "Delta_eq": 1.2553492695740507e-09 }, "-71.40": { "LJ_approx": { "x0": 1.7036651779798684e-09, "C": 17001.86272239105, "nrep": 3.9151643485118117, "nattr": 0.9680342060844059 }, "Delta_eq": 1.2566584760815426e-09 }, "-54.00": { "LJ_approx": { "x0": 1.5830041112065094e-09, "C": 21415.64649760579, "nrep": 3.9222512220871013, "nattr": 1.0303387653647968 }, "Delta_eq": 1.302942739961778e-09 }, "-89.50": { "LJ_approx": { "x0": 1.883936689519767e-09, "C": 12202.390459945682, "nrep": 3.906974649816042, "nattr": 0.8898102498996743 }, "Delta_eq": 1.2107230911508513e-09 }, "-61.93": { "LJ_approx": { "x0": 1.6329212539031057e-09, "C": 19452.44141782297, "nrep": 3.9191317215599946, "nattr": 1.0033715654432673 }, "Delta_eq": 1.281743450351987e-09 } }, "16.0": { "-71.90": { "LJ_approx": { "x0": 1.721337196662225e-09, "C": 16363.738563915058, "nrep": 3.9147202446411637, "nattr": 1.0005179992350384 }, "Delta_eq": 1.2553492695740507e-09 }, "-71.40": { "LJ_approx": { "x0": 1.717176984027311e-09, "C": 16491.97282711717, "nrep": 3.9149293522242252, "nattr": 1.0027563589061772 }, "Delta_eq": 1.2566584760815426e-09 }, "-54.00": { "LJ_approx": { "x0": 1.5951507107307484e-09, "C": 20803.713978702526, "nrep": 3.921796023871708, "nattr": 1.0717457519944718 }, "Delta_eq": 1.302942739961778e-09 }, "-89.50": { "LJ_approx": { "x0": 1.899300769652515e-09, "C": 11819.650350288994, "nrep": 3.906993085458305, "nattr": 0.9105930969008011 }, "Delta_eq": 1.2107230911508513e-09 }, "-61.93": { "LJ_approx": { "x0": 1.6456524054221337e-09, "C": 18883.851022856303, "nrep": 3.918771854603072, "nattr": 1.042336408142498 }, "Delta_eq": 1.281743450351987e-09 }, "-58.00": { "LJ_approx": { "x0": 1.6196423302303851e-09, "C": 19847.346684716063, "nrep": 3.920294617611314, "nattr": 1.0573210567487794 }, "Delta_eq": 1.2922508866011204e-09 }, "-140.00": { "LJ_approx": { "x0": 3.536300349187651e-09, "C": 1259.968827699725, "nrep": 3.8800144975132485, "nattr": 0.35722933384459793 }, "Delta_eq": 1.1019265101358682e-09 } }, "40.0": { "-71.90": { "LJ_approx": { "x0": 1.7127556130633909e-09, "C": 16685.139617894773, "nrep": 3.9147997833590855, "nattr": 0.9816110605284186 }, "Delta_eq": 1.2553492695740507e-09 } }, "30.0": { "-71.90": { "LJ_approx": { "x0": 1.7147994934556977e-09, "C": 16608.65261608246, "nrep": 3.914764637335829, "nattr": 0.9867268079339511 }, "Delta_eq": 1.2553492695740507e-09 } }, "25.4": { "-71.90": { "LJ_approx": { "x0": 1.7162265501918752e-09, "C": 16555.217050637588, "nrep": 3.9147464050871856, "nattr": 0.9900398844251959 }, "Delta_eq": 1.2553492695740507e-09 }, "-61.93": { "LJ_approx": { "x0": 1.6408385667642563e-09, "C": 19099.831853142834, "nrep": 3.9188405082474103, "nattr": 1.0301853851264013 }, "Delta_eq": 1.281743450351987e-09 } }, "40.3": { "-71.90": { "LJ_approx": { "x0": 1.7127058661258177e-09, "C": 16686.9995037205, "nrep": 3.914800799246736, "nattr": 0.981479342025535 }, "Delta_eq": 1.2553492695740507e-09 }, "-61.93": { "LJ_approx": { "x0": 1.637531858311385e-09, "C": 19247.790954282773, "nrep": 3.918928365198691, "nattr": 1.020450016688262 }, "Delta_eq": 1.281743450351987e-09 } }, "45.0": { "-71.90": { "LJ_approx": { "x0": 1.712051746586677e-09, "C": 16711.456749369456, "nrep": 3.914814642254888, "nattr": 0.979726839958776 }, "Delta_eq": 1.2553492695740507e-09 } }, "20.0": { "-71.90": { "LJ_approx": { "x0": 1.7186388439609698e-09, "C": 16464.85454836168, "nrep": 3.9147266088229755, "nattr": 0.9952322612710219 }, "Delta_eq": 1.2553492695740507e-09 } }, "60.0": { "-71.90": { "LJ_approx": { "x0": 1.710603828012074e-09, "C": 16765.5278159216, "nrep": 3.9148503824940146, "nattr": 0.9756024890579372 }, "Delta_eq": 1.2553492695740507e-09 } }, "34.0": { "-71.90": { "LJ_approx": { "x0": 1.7138494069983225e-09, "C": 16644.21766668786, "nrep": 3.9147795488410644, "nattr": 0.9844103642751335 }, "Delta_eq": 1.2553492695740507e-09 } }, "22.6": { "-71.90": { "LJ_approx": { "x0": 1.717347083819261e-09, "C": 16513.244775760173, "nrep": 3.914735582752492, "nattr": 0.9925083846044295 }, "Delta_eq": 1.2553492695740507e-09 } }, "45.3": { "-71.90": { "LJ_approx": { "x0": 1.7120143529753677e-09, "C": 16712.8535932463, "nrep": 3.9148154954695285, "nattr": 0.9796234512533043 }, "Delta_eq": 1.2553492695740507e-09 } + }, + "31.0": { + "0.00": { + "LJ_approx": { + "x0": 1.429704934686545e-09, + "C": 28734.55271989346, + "nrep": 3.9338783401809607, + "nattr": 1.155904475115303 + }, + "Delta_eq": 1.4e-09 + } } } \ No newline at end of file diff --git a/PySONIC/core/model.py b/PySONIC/core/model.py index 9db39db..c3c0bb0 100644 --- a/PySONIC/core/model.py +++ b/PySONIC/core/model.py @@ -1,96 +1,102 @@ #!/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: 2019-06-01 16:46:31 +# @Last Modified time: 2019-06-02 13:23:59 import pickle import abc import inspect import numpy as np +from .batches import createQueue from ..utils import logger class Model(metaclass=abc.ABCMeta): ''' Generic model interface. ''' @property @abc.abstractmethod def tscale(self): ''' Relevant temporal scale of the model. ''' raise NotImplementedError @property @abc.abstractmethod def __repr__(self): raise NotImplementedError - @property - @abc.abstractmethod - def pprint(self): - raise NotImplementedError + def params(self): + ''' Gather all model parameters in a dictionary ''' + def toAvoid(p): + return (p.startswith('__') and p.endswith('__')) or p.startswith('_abc_') + class_attrs = inspect.getmembers(self.__class__, lambda a: not(inspect.isroutine(a))) + inst_attrs = inspect.getmembers(self, lambda a: not(inspect.isroutine(a))) + class_attrs = [a for a in class_attrs if not toAvoid(a[0])] + inst_attrs = [a for a in inst_attrs if not toAvoid(a[0]) and a not in class_attrs] + params_dict = {a[0]: a[1] for a in class_attrs + inst_attrs} + return params_dict @property @abc.abstractmethod def filecode(self, *args): raise NotImplementedError def getDesc(self): return inspect.getdoc(self).splitlines()[0] @property @abc.abstractmethod def getPltScheme(self): raise NotImplementedError @property @abc.abstractmethod def getPltVars(self, *args, **kwargs): raise NotImplementedError @property @abc.abstractmethod def checkInputs(self, *args): raise NotImplementedError - @property - @abc.abstractmethod - def simulate(self, *args, **kwargs): - raise NotImplementedError - @property @abc.abstractmethod def meta(self, *args): raise NotImplementedError @property @abc.abstractmethod - def createQueue(self, *args): + def simulate(self, *args, **kwargs): raise NotImplementedError + def simQueue(self, *args): + ''' Create a simulation queue from a combination of simulation parameters. ''' + return createQueue(*args) + def runAndSave(self, outdir, *args): ''' Simulate system and save results in a PKL file. ''' # If no amplitude provided, perform titration to find it if None in args: iA = args.index(None) new_args = [x for x in args if x is not None] Athr = self.titrate(*new_args) if np.isnan(Athr): logger.error('Could not find threshold excitation amplitude') return None new_args.insert(iA, Athr) args = new_args # Simulate model, save inf file and return file path data, tcomp = self.simulate(*args) meta = self.meta(*args) meta['tcomp'] = tcomp outpath = '{}/{}.pkl'.format(outdir, self.filecode(*args)) with open(outpath, 'wb') as fh: pickle.dump({'meta': meta, 'data': data}, fh) logger.debug('simulation data exported to "%s"', outpath) return outpath diff --git a/PySONIC/core/nbls.py b/PySONIC/core/nbls.py index b33cbbc..40bf83e 100644 --- a/PySONIC/core/nbls.py +++ b/PySONIC/core/nbls.py @@ -1,663 +1,646 @@ #!/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: 2019-06-01 19:51:26 +# @Last Modified time: 2019-06-02 13:22:11 from copy import deepcopy import logging import numpy as np import pandas as pd -from scipy.integrate import solve_ivp from scipy.interpolate import interp1d -from .simulators import PWSimulator, HybridSimulator +from .simulators import PWSimulator, HybridSimulator, PeriodicSimulator from .bls import BilayerSonophore from .pneuron import PointNeuron from .batches import createQueue from ..utils import * from ..constants import * from ..postpro import getFixedPoints class NeuronalBilayerSonophore(BilayerSonophore): ''' This class inherits from the BilayerSonophore class and receives an PointNeuron instance at initialization, to define the electro-mechanical NICE model and its SONIC variant. ''' tscale = 'ms' # relevant temporal scale of the model - defvar = 'Q' # default plot variable def __init__(self, a, neuron, Fdrive=None, embedding_depth=0.0): ''' Constructor of the class. :param a: in-plane radius 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, PointNeuron): raise ValueError('Invalid neuron type: "{}" (must inherit from PointNeuron class)' .format(neuron.name)) self.neuron = neuron # Initialize BilayerSonophore parent object BilayerSonophore.__init__(self, a, neuron.Cm0, neuron.Cm0 * neuron.Vm0 * 1e-3, embedding_depth) def __repr__(self): - return 'NeuronalBilayerSonophore({}m, {})'.format( - si_format(self.a, precision=1, space=' '), - self.neuron) + s = '{}({:.1f} nm, {}'.format(self.__class__.__name__, self.a * 1e9, self.neuron) + if self.d > 0.: + s += ', d={}m'.format(si_format(self.d, precision=1, space=' ')) + return s + ')' - def pprint(self): - return '{}m radius NBLS - {} neuron'.format( - si_format(self.a, precision=0, space=' '), - self.neuron.name) + def params(self): + params = super().params() + params.update(self.neuron.params()) + return params def getPltVars(self, wrapleft='df["', wrapright='"]'): pltvars = super().getPltVars(wrapleft, wrapright) pltvars.update(self.neuron.getPltVars(wrapleft, wrapright)) return pltvars def getPltScheme(self): return self.neuron.getPltScheme() def filecode(self, Fdrive, Adrive, tstim, toffset, PRF, DC, method): return 'ASTIM_{}_{}_{:.0f}nm_{:.0f}kHz_{:.2f}kPa_{:.0f}ms_{}{}'.format( self.neuron.name, 'CW' if DC == 1 else 'PW', self.a * 1e9, Fdrive * 1e-3, Adrive * 1e-3, tstim * 1e3, 'PRF{:.2f}Hz_DC{:.2f}%_'.format(PRF, DC * 1e2) if DC < 1. else '', method) def fullDerivatives(self, y, t, 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 Adrive: acoustic drive amplitude (Pa) :param Fdrive: acoustic drive frequency (Hz) :param phi: acoustic drive phase (rad) :return: vector of derivatives ''' dydt_mech = BilayerSonophore.derivatives(self, y[:3], t, Adrive, Fdrive, y[3], phi) dydt_elec = self.neuron.Qderivatives(y[3:], t, self.Capct(y[1])) return dydt_mech + dydt_elec def effDerivatives(self, y, t, lkp): ''' 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 y: vector of HH system variables at time t :param t: specific instant in time (s) :param lkp: 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 Vmeff = self.neuron.interpVmeff(Qm, lkp) dQmdt = - self.neuron.iNet(Vmeff, states) * 1e-3 dstates = self.neuron.derEffStates(Qm, states, lkp) # Return derivatives vector return [dQmdt, *[dstates[k] for k in self.neuron.states]] - def interpEffVariable(self, key, Qm, stim, lkp_on, lkp_off): + def interpEffVariable(self, key, Qm, stim, lkps1D): ''' Interpolate Q-dependent effective variable along solution. :param key: lookup variable key :param Qm: charge density solution vector :param stim: stimulation state solution vector - :param lkp_on: lookups for ON states - :param lkp_off: lookups for OFF states + :param lkps1D: dictionary of lookups for ON and OFF states :return: interpolated effective variable vector ''' x = np.zeros(stim.size) x[stim == 0] = np.interp( - Qm[stim == 0], lkp_on['Q'], lkp_on[key], left=np.nan, right=np.nan) + Qm[stim == 0], lkps1D['ON']['Q'], lkps1D['ON'][key], left=np.nan, right=np.nan) x[stim == 1] = np.interp( - Qm[stim == 1], lkp_off['Q'], lkp_off[key], left=np.nan, right=np.nan) + Qm[stim == 1], lkps1D['ON']['Q'], lkps1D['OFF'][key], left=np.nan, right=np.nan) return x def runFull(self, Fdrive, Adrive, tstim, toffset, PRF, DC, phi=np.pi): ''' Compute solutions of the full electro-mechanical 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 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: 2-tuple with the output dataframe and computation time. ''' - # Determine time step dt = 1 / (NPC_FULL * Fdrive) # Compute non-zero deflection value for a small perturbation (solving quasi-steady equation) Pac = self.Pacoustic(dt, Adrive, Fdrive, phi) Z0 = self.balancedefQS(self.ng0, self.Qm0, Pac) # Set initial conditions steady_states = self.neuron.steadyStates(self.neuron.Vm0) y0 = np.concatenate(( [0., Z0, self.ng0, self.Qm0], [steady_states[k] for k in self.neuron.states])) # Initialize simulator and compute solution logger.debug('Computing detailed solution') simulator = PWSimulator( lambda y, t: self.fullDerivatives(y, t, Adrive, Fdrive, phi), lambda y, t: self.fullDerivatives(y, t, 0., 0., 0.)) (t, y, stim), tcomp = simulator( y0, dt, tstim, toffset, PRF, DC, print_progress=logger.getEffectiveLevel() <= logging.INFO, target_dt=CLASSIC_TARGET_DT, monitor_time=True) logger.debug('completed in %ss', si_format(tcomp, 1)) # Store output in dataframe data = pd.DataFrame({ 't': t, 'stimstate': stim, 'Z': y[:, 1], 'ng': y[:, 2], 'Qm': y[:, 3] }) data['Vm'] = data['Qm'].values / self.v_Capct(data['Z'].values) * 1e3 # mV for i in range(len(self.neuron.states)): data[self.neuron.states[i]] = y[:, i + 4] # Return dataframe and computation time return data, tcomp + def runHybrid(self, Fdrive, Adrive, tstim, toffset, PRF, DC, phi=np.pi): + ''' Compute solutions of the system for a specific set of + US stimulation parameters, using a hybrid integration scheme. + + :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 + + ''' + # Determine time steps + dt_dense, dt_sparse = [1. / (n * Fdrive) for n in [NPC_FULL, NPC_HH]] + + # Compute non-zero deflection value for a small perturbation (solving quasi-steady equation) + Pac = self.Pacoustic(dt_dense, Adrive, Fdrive, phi) + Z0 = self.balancedefQS(self.ng0, self.Qm0, Pac) + + # Set initial conditions + steady_states = self.neuron.steadyStates(self.neuron.Vm0) + y0 = np.concatenate(( + [0., Z0, self.ng0, self.Qm0], + [steady_states[k] for k in self.neuron.states], + )) + is_dense_var = np.array([True] * 3 + [False] * (len(self.neuron.states) + 1)) + + # Initialize simulator and compute solution + logger.debug('Computing hybrid solution') + simulator = HybridSimulator( + lambda y, t: self.fullDerivatives(y, t, Adrive, Fdrive, phi), + lambda y, t: self.fullDerivatives(y, t, 0., 0., 0.), + lambda t, y, Cm: self.neuron.Qderivatives(y, t, Cm), + lambda yref: self.Capct(yref[1]), + is_dense_var, + ivars_to_check=[1, 2]) + (t, y, stim), tcomp = simulator( + y0, dt_dense, dt_sparse, 1. / Fdrive, tstim, toffset, PRF, DC, + monitor_time=True) + logger.debug('completed in %ss', si_format(tcomp, 1)) + + # Store output in dataframe + data = pd.DataFrame({ + 't': t, + 'stimstate': stim, + 'Z': y[:, 1], + 'ng': y[:, 2], + 'Qm': y[:, 3] + }) + data['Vm'] = data['Qm'].values / self.v_Capct(data['Z'].values) * 1e3 # mV + for i in range(len(self.neuron.states)): + data[self.neuron.states[i]] = y[:, i + 4] + + # Return dataframe and computation time + return data, tcomp + def computeEffVars(self, Fdrive, Adrive, Qm, fs): ''' Compute "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. :param Fdrive: acoustic drive frequency (Hz) :param Adrive: acoustic drive amplitude (Pa) :param Qm: imposed charge density (C/m2) :param fs: list of sonophore membrane coverage fractions :return: list with computation time and a list of dictionaries of effective variables ''' - # Run simulation and retrieve deflection and gas content vectors from last cycle data, tcomp = BilayerSonophore.simulate(self, Fdrive, Adrive, Qm) Z_last = data.loc[-NPC_FULL:, 'Z'].values # m Cm_last = self.v_Capct(Z_last) # F/m2 # For each coverage fraction effvars = [] for x in fs: # Compute membrane capacitance and membrane potential vectors Cm = x * Cm_last + (1 - x) * self.Cm0 # F/m2 Vm = Qm / Cm * 1e3 # mV # Compute average cycle value for membrane potential and rate constants effvars.append({'V': np.mean(Vm)}) effvars[-1].update(self.neuron.computeEffRates(Vm)) # Log process log = '{}: lookups @ {}Hz, {}Pa, {:.2f} nC/cm2'.format( self, *si_format([Fdrive, Adrive], precision=1, space=' '), Qm * 1e5) if len(fs) > 1: log += ', fs = {:.0f} - {:.0f}%'.format(fs.min() * 1e2, fs.max() * 1e2) log += ', tcomp = {:.3f} s'.format(tcomp) logger.info(log) # Return effective coefficients return [tcomp, effvars] def runSONIC(self, Fdrive, Adrive, tstim, toffset, PRF, DC): ''' 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 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 (-) :return: 3-tuple with the time profile, the effective solution matrix and a state vector ''' - # Load appropriate 2D lookups - Aref, Qref, lookups2D, _ = getLookups2D(self.neuron.name, a=self.a, Fdrive=Fdrive) + Aref, Qref, lkps2D, _ = getLookups2D(self.neuron.name, a=self.a, Fdrive=Fdrive) # Check that acoustic amplitude is within lookup range Adrive = isWithin('amplitude', Adrive, (Aref.min(), Aref.max())) # Interpolate 2D lookups at zero and US amplitude logger.debug('Interpolating lookups at A = %.2f kPa and A = 0', Adrive * 1e-3) - lookups_on = {key: interp1d(Aref, y2D, axis=0)(Adrive) for key, y2D in lookups2D.items()} - lookups_off = {key: interp1d(Aref, y2D, axis=0)(0.0) for key, y2D in lookups2D.items()} + lkps1D = {state: {key: interp1d(Aref, y2D, axis=0)(val) for key, y2D in lkps2D.items()} + for state, val in {'ON': Adrive, 'OFF': 0.}.items()} # Add reference charge vector to 1D lookup dictionaries - lookups_on['Q'] = Qref - lookups_off['Q'] = Qref + for state in lkps1D.keys(): + lkps1D[state]['Q'] = Qref # Set initial conditions steady_states = self.neuron.steadyStates(self.neuron.Vm0) - y0 = np.insert( - np.array([steady_states[k] for k in self.neuron.states]), - 0, self.Qm0) + y0 = np.insert(np.array([steady_states[k] for k in self.neuron.states]), 0, self.Qm0) # Initialize simulator and compute solution logger.debug('Computing effective solution') simulator = PWSimulator( - lambda y, t: self.effDerivatives(y, t, lookups_on), - lambda y, t: self.effDerivatives(y, t, lookups_off)) + lambda y, t: self.effDerivatives(y, t, lkps1D['ON']), + lambda y, t: self.effDerivatives(y, t, lkps1D['OFF'])) (t, y, stim), tcomp = simulator(y0, DT_EFF, tstim, toffset, PRF, DC, monitor_time=True) logger.debug('completed in %ss', si_format(tcomp, 1)) # Store output in dataframe data = pd.DataFrame({ 't': t, 'stimstate': stim, 'Qm': y[:, 0] }) for key in ['ng', 'V']: - data[key] = self.interpEffVariable( - key, data['Qm'].values, stim, lookups_on, lookups_off) + data[key] = self.interpEffVariable(key, data['Qm'].values, stim, lkps1D) data['Z'] = np.array([self.balancedefQS(ng, Qm) for ng, Qm in zip( data['ng'].values, data['Qm'].values)]) # m data['Vm'] = data['Qm'].values / self.v_Capct(data['Z'].values) * 1e3 # mV for i in range(len(self.neuron.states)): data[self.neuron.states[i]] = y[:, i + 1] # Return dataframe and computation time return data, tcomp - def runHybrid(self, Fdrive, Adrive, tstim, toffset, PRF, DC, phi=np.pi): - ''' Compute solutions of the system for a specific set of - US stimulation parameters, using a hybrid integration scheme. + def meta(self, Fdrive, Adrive, tstim, toffset, PRF, DC, method): + ''' Return information about object and simulation parameters. - :param Fdrive: acoustic drive frequency (Hz) + :param Fdrive: US 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 - + :param tstim: stimulus duration (s) + :param toffset: stimulus offset (s) + :param PRF: pulse repetition frequency (Hz) + :param DC: stimulus duty cycle (-) + :param method: integration method + :return: meta-data dictionary ''' - - # Determine time step - dt_dense = 1 / (NPC_FULL * Fdrive) - dt_sparse = 1 / (NPC_HH * Fdrive) - - # Compute non-zero deflection value for a small perturbation (solving quasi-steady equation) - Pac = self.Pacoustic(dt_dense, Adrive, Fdrive, phi) - Z0 = self.balancedefQS(self.ng0, self.Qm0, Pac) - - # Set initial conditions - steady_states = self.neuron.steadyStates(self.neuron.Vm0) - y0 = np.concatenate(( - [0., Z0, self.ng0, self.Qm0], - [steady_states[k] for k in self.neuron.states], - )) - is_dense_var = np.array([True] * 3 + [False] * (len(self.neuron.states) + 1)) - - # Initialize simulator and compute solution - logger.debug('Computing hybrid solution') - simulator = HybridSimulator( - lambda y, t: self.fullDerivatives(y, t, Adrive, Fdrive, phi), - lambda y, t: self.fullDerivatives(y, t, 0., 0., 0.), - lambda t, y, Cm: self.neuron.Qderivatives(y, t, Cm), - lambda yref: self.Capct(yref[1]), - is_dense_var, - ivars_to_check=[1, 2]) - (t, y, stim), tcomp = simulator( - y0, dt_dense, dt_sparse, 1. / Fdrive, tstim, toffset, PRF, DC, - monitor_time=True) - logger.debug('completed in %ss', si_format(tcomp, 1)) - - # Store output in dataframe - data = pd.DataFrame({ - 't': t, - 'stimstate': stim, - 'Z': y[:, 1], - 'ng': y[:, 2], - 'Qm': y[:, 3] - }) - data['Vm'] = data['Qm'].values / self.v_Capct(data['Z'].values) * 1e3 # mV - for i in range(len(self.neuron.states)): - data[self.neuron.states[i]] = y[:, i + 4] - - # Return dataframe and computation time - return data, tcomp + return { + 'neuron': self.neuron.name, + 'a': self.a, + 'd': self.d, + 'Fdrive': Fdrive, + 'Adrive': Adrive, + 'tstim': tstim, + 'toffset': toffset, + 'PRF': PRF, + 'DC': DC, + 'method': method + } def simulate(self, Fdrive, Adrive, tstim, toffset, PRF=100., DC=1.0, method='sonic'): ''' Simulate the electro-mechanical model for a specific set of US stimulation parameters, and return output data in a dataframe. :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 method: selected integration method :return: 2-tuple with the output dataframe and computation time. ''' - logger.info( - '%s: %s @ f = %sHz, %st = %ss (%ss offset)%s', - self, - 'titration' if Adrive is None else 'simulation', - si_format(Fdrive, 0, space=' '), - 'A = {}Pa, '.format(si_format(Adrive, 2, space=' ')) if Adrive is not None else '', + '%s: simulation @ f = %sHz, A = %sPa, t = %ss (%ss offset)%s', + self, si_format(Fdrive, 0, space=' '), si_format(Adrive, 2, space=' '), *si_format([tstim, toffset], 1, space=' '), (', PRF = {}Hz, DC = {:.2f}%'.format( si_format(PRF, 2, space=' '), DC * 1e2) if DC < 1.0 else '')) # Check validity of stimulation parameters BilayerSonophore.checkInputs(self, Fdrive, Adrive, 0.0, 0.0) self.neuron.checkInputs(Adrive, tstim, toffset, PRF, DC) # Call appropriate simulation function try: simfunc = { 'full': self.runFull, 'hybrid': self.runHybrid, 'sonic': self.runSONIC }[method] except KeyError: raise ValueError('Invalid integration method: "{}"'.format(method)) data, tcomp = simfunc(Fdrive, Adrive, tstim, toffset, PRF, DC) # Log number of detected spikes nspikes = self.neuron.getNSpikes(data) logger.debug('{} spike{} detected'.format(nspikes, plural(nspikes))) # Return dataframe and computation time return data, tcomp - def meta(self, Fdrive, Adrive, tstim, toffset, PRF, DC, method): - ''' Return information about object and simulation parameters. - - :param Fdrive: US frequency (Hz) - :param Adrive: acoustic drive amplitude (Pa) - :param tstim: stimulus duration (s) - :param toffset: stimulus offset (s) - :param PRF: pulse repetition frequency (Hz) - :param DC: stimulus duty cycle (-) - :param method: integration method - :return: meta-data dictionary - ''' - return { - 'neuron': self.neuron.name, - 'a': self.a, - 'd': self.d, - 'Fdrive': Fdrive, - 'Adrive': Adrive, - 'tstim': tstim, - 'toffset': toffset, - 'PRF': PRF, - 'DC': DC, - 'method': method - } - @cache(os.path.join(os.path.split(__file__)[0], 'astim_titrations.log')) def titrate(self, Fdrive, tstim, toffset, PRF=100., DC=1., method='sonic', xfunc=None, Arange=None): ''' Use a binary search to determine the threshold amplitude needed to obtain neural excitation for a given frequency, duration, PRF and duty cycle. :param Fdrive: US frequency (Hz) :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 method: integration method :param xfunc: function determining whether condition is reached from simulation output :param Arange: search interval for Adrive, iteratively refined :return: determined threshold amplitude (Pa) ''' # Default output function if xfunc is None: xfunc = self.neuron.titrationFunc # Default amplitude interval if Arange is None: Arange = (0, getLookups2D(self.neuron.name, a=self.a, Fdrive=Fdrive)[0].max()) return binarySearch( lambda x: xfunc(self.simulate(*x)[0]), [Fdrive, tstim, toffset, PRF, DC, method], 1, Arange, TITRATION_ASTIM_DA_MAX ) - def createQueue(self, freqs, amps, durations, offsets, PRFs, DCs, method): + def simQueue(self, freqs, amps, durations, offsets, PRFs, DCs, method): ''' 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 freqs: list (or 1D-array) of US frequencies :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 :params method: integration method :return: list of parameters (list) for each simulation ''' if amps is None: amps = [np.nan] DCs = np.array(DCs) queue = [] if 1.0 in DCs: queue += createQueue(freqs, amps, durations, offsets, min(PRFs), 1.0) if np.any(DCs != 1.0): queue += createQueue(freqs, amps, durations, offsets, PRFs, DCs[DCs != 1.0]) for item in queue: if np.isnan(item[1]): item[1] = None item.append(method) return queue def quasiSteadyStates(self, Fdrive, amps=None, charges=None, DCs=1.0, squeeze_output=False): ''' Compute the quasi-steady state values of the neuron's gating variables for a combination of US amplitudes, charge densities and duty cycles, at a specific US frequency. :param Fdrive: US frequency (Hz) :param amps: US amplitudes (Pa) :param charges: membrane charge densities (C/m2) :param DCs: duty cycle value(s) :return: 4-tuple with reference values of US amplitude and charge density, as well as interpolated Vmeff and QSS gating variables ''' # Get DC-averaged lookups interpolated at the appropriate amplitudes and charges amps, charges, lookups = getLookupsDCavg( self.neuron.name, self.a, Fdrive, amps, charges, DCs) # Compute QSS states using these lookups nA, nQ, nDC = lookups['V'].shape QSS = {k: np.empty((nA, nQ, nDC)) for k in self.neuron.states} for iA in range(nA): for iDC in range(nDC): QSS_1D = self.neuron.quasiSteadyStates( {k: v[iA, :, iDC] for k, v in lookups.items()}) for k in QSS.keys(): QSS[k][iA, :, iDC] = QSS_1D[k] # Compress outputs if needed if squeeze_output: QSS = {k: v.squeeze() for k, v in QSS.items()} lookups = {k: v.squeeze() for k, v in lookups.items()} # Return reference inputs and outputs return amps, charges, lookups, QSS def iNetQSS(self, Qm, Fdrive, Adrive, DC): ''' Compute quasi-steady state net membrane current for a given combination of US parameters and a given membrane charge density. :param Qm: membrane charge density (C/m2) :param Fdrive: US frequency (Hz) :param Adrive: US amplitude (Pa) :param DC: duty cycle (-) :return: net membrane current (mA/m2) ''' _, _, lookups, QSS = self.quasiSteadyStates( Fdrive, amps=Adrive, charges=Qm, DCs=DC, squeeze_output=True) return self.neuron.iNet(lookups['V'], np.array(list(QSS.values()))) # mA/m2 - def evaluateStability(self, Qm0, states0, lkp): ''' Integrate the effective differential system from a given starting point, until clear convergence or clear divergence is found. :param Qm0: initial membrane charge density (C/m2) :param states0: dictionary of initial states values :param lkp: dictionary of 1D data points of "effective" coefficients over the charge domain, for specific frequency and amplitude values. :return: boolean indicating convergence state ''' # Initialize y0 vector # t0 = 0. y0 = np.array([Qm0] + list(states0.values())) # Initialize simulator and compute solution simulator = PeriodicSimulator( lambda y, t: self.effDerivatives(y, t, lkp), ivars_to_check=[0]) simulator.stopfunc = simulator.isAsymptoticallyStable nmax = int(QSS_HISTORY_INTERVAL // QSS_INTEGRATION_INTERVAL) t, y, stim = simulator.compute(y0, DT_EFF, QSS_INTEGRATION_INTERVAL, nmax=nmax) logger.debug('completed in %ss', si_format(tcomp, 1)) conv = t[-1] < QSS_HISTORY_INTERVAL # # Initializing empty list to record evolution of charge deviation # n = int(QSS_HISTORY_INTERVAL // QSS_INTEGRATION_INTERVAL) # size of history # dQ = [] # # As long as there is no clear charge convergence or divergence # conv, div = False, False # tf, yf = t0, y0 # while not conv and not div: # # Integrate system for small interval and retrieve final charge deviation # t0, y0 = tf, yf # sol = solve_ivp( # lambda t, y: self.effDerivatives(y, t, lkp), # [t0, t0 + QSS_INTEGRATION_INTERVAL], y0, # method='LSODA' # ) # tf, yf = sol.t[-1], sol.y[:, -1] # dQ.append(yf[0] - Qm0) # # logger.debug('{:.0f} ms: dQ = {:.5f} nC/cm2, avg dQ = {:.5f} nC/cm2'.format( # # tf * 1e3, dQ[-1] * 1e5, np.mean(dQ[-n:]) * 1e5)) # # If last charge deviation is too large -> divergence # if np.abs(dQ[-1]) > QSS_Q_DIV_THR: # div = True # # If last charge deviation or average deviation in recent history # # is small enough -> convergence # for x in [dQ[-1], np.mean(dQ[-n:])]: # if np.abs(x) < QSS_Q_CONV_THR: # conv = True # # If max integration duration is been reached -> error # if tf > QSS_MAX_INTEGRATION_DURATION: # raise ValueError('too many iterations') # logger.debug('{}vergence after {:.0f} ms: dQ = {:.5f} nC/cm2'.format( # {True: 'con', False: 'di'}[conv], tf * 1e3, dQ[-1] * 1e5)) return conv - def quasiSteadyStateFixedPoints(self, Fdrive, Adrive, DC, lkp, dQdt): + def fixedPointsQSS(self, Fdrive, Adrive, DC, lkp, dQdt): ''' Compute QSS fixed points along the charge dimension for a given combination of US parameters, and determine their stability. :param Fdrive: US frequency (Hz) :param Adrive: US amplitude (Pa) :param DC: duty cycle (-) :param lkp: lookup dictionary for effective variables along charge dimension :param dQdt: charge derivative profile along charge dimension :return: 2-tuple with values of stable and unstable fixed points ''' logger.debug('A = {:.2f} kPa, DC = {:.0f}%'.format(Adrive * 1e-3, DC * 1e2)) # Extract stable and unstable fixed points from QSS charge variation profile dfunc = lambda Qm: - self.iNetQSS(Qm, Fdrive, Adrive, DC) SFP_candidates = getFixedPoints(lkp['Q'], dQdt, filter='stable', der_func=dfunc).tolist() UFPs = getFixedPoints(lkp['Q'], dQdt, filter='unstable', der_func=dfunc).tolist() SFPs = [] pltvars = self.getPltVars() # For each candidate SFP for i, Qm in enumerate(SFP_candidates): logger.debug('Q-SFP = {:.2f} nC/cm2'.format(Qm * 1e5)) # Re-compute QSS *_, QSS_FP = self.quasiSteadyStates(Fdrive, amps=Adrive, charges=Qm, DCs=DC, squeeze_output=True) # Simulate from unperturbed QSS and evaluate stability if not self.evaluateStability(Qm, QSS_FP, lkp): logger.warning('diverging system at ({:.2f} kPa, {:.2f} nC/cm2)'.format( Adrive * 1e-3, Qm * 1e5)) UFPs.append(Qm) else: # For each state unstable_states = [] for x in self.neuron.states: pltvar = pltvars[x] unit_str = pltvar.get('unit', '') factor = pltvar.get('factor', 1) is_stable_direction = [] for sign in [-1, +1]: # Perturb state with small offset QSS_perturbed = deepcopy(QSS_FP) QSS_perturbed[x] *= (1 + sign * QSS_REL_OFFSET) # If gating state, bound within [0., 1.] if self.neuron.isVoltageGated(x): QSS_perturbed[x] = np.clip(QSS_perturbed[x], 0., 1.) logger.debug('{}: {:.5f} -> {:.5f} {}'.format( x, QSS_FP[x] * factor, QSS_perturbed[x] * factor, unit_str)) # Simulate from perturbed QSS and evaluate stability is_stable_direction.append( self.evaluateStability(Qm, QSS_perturbed, lkp)) # Check if system shows stability upon x-state perturbation # in both directions if not np.all(is_stable_direction): unstable_states.append(x) # Classify fixed point as stable only if all states show stability is_stable_FP = len(unstable_states) == 0 {True: SFPs, False: UFPs}[is_stable_FP].append(Qm) logger.info('{}stable fixed-point at ({:.2f} kPa, {:.2f} nC/cm2){}'.format( '' if is_stable_FP else 'un', Adrive * 1e-3, Qm * 1e5, '' if is_stable_FP else ', caused by {} states'.format(unstable_states))) return SFPs, UFPs diff --git a/PySONIC/core/pneuron.py b/PySONIC/core/pneuron.py index d4385b6..80b64ca 100644 --- a/PySONIC/core/pneuron.py +++ b/PySONIC/core/pneuron.py @@ -1,603 +1,588 @@ #!/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: 2019-06-01 16:46:01 +# @Last Modified time: 2019-06-02 13:21:40 import abc import inspect import re import numpy as np import pandas as pd from .batches import createQueue from .model import Model from .simulators import PWSimulator from ..postpro import findPeaks from ..constants import * from ..utils import si_format, logger, plural, binarySearch class PointNeuron(Model): ''' Generic point-neuron model interface. ''' tscale = 'ms' # relevant temporal scale of the model - defvar = 'V' # default plot variable + + def __init__(self): + self.Qm0 = self.Cm0 * self.Vm0 * 1e-3 # C/cm2 def __repr__(self): return self.__class__.__name__ - def pprint(self): - return '{} neuron'.format(self.__class__.__name__) - def filecode(self, Astim, tstim, toffset, PRF, DC): ''' File naming convention. ''' return 'ESTIM_{}_{}_{:.1f}mA_per_m2_{:.0f}ms{}'.format( self.name, 'CW' if DC == 1 else 'PW', Astim, tstim * 1e3, '_PRF{:.2f}Hz_DC{:.2f}%'.format(PRF, DC * 1e2) if DC < 1. else '') @property @abc.abstractmethod def name(self): raise NotImplementedError @property @abc.abstractmethod def Cm0(self): raise NotImplementedError @property @abc.abstractmethod def Vm0(self): raise NotImplementedError @abc.abstractmethod def currents(self, Vm, states): ''' Compute all ionic currents per unit area. :param Vm: membrane potential (mV) :states: state probabilities of the ion channels :return: dictionary of ionic currents per unit area (mA/m2) ''' def iNet(self, Vm, states): ''' net membrane current :param Vm: membrane potential (mV) :states: states of ion channels gating and related variables :return: current per unit area (mA/m2) ''' return sum(self.currents(Vm, states).values()) def dQdt(self, Vm, states): ''' membrane charge density variation rate :param Vm: membrane potential (mV) :states: states of ion channels gating and related variables :return: variation rate (mA/m2) ''' return -self.iNet(Vm, states) def titrationFunc(self, *args, **kwargs): ''' Default titration function. ''' return self.isExcited(*args, **kwargs) def currentToConcentrationRate(self, z_ion, depth): ''' Compute the conversion factor from a specific ionic current (in mA/m2) into a variation rate of submembrane ion concentration (in M/s). :param: z_ion: ion valence :param depth: submembrane depth (m) :return: conversion factor (Mmol.m-1.C-1) ''' return 1e-6 / (z_ion * depth * FARADAY) def nernst(self, z_ion, Cion_in, Cion_out, T): ''' Nernst potential of a specific ion given its intra and extracellular concentrations. :param z_ion: ion valence :param Cion_in: intracellular ion concentration :param Cion_out: extracellular ion concentration :param T: temperature (K) :return: ion Nernst potential (mV) ''' return (Rg * T) / (z_ion * FARADAY) * np.log(Cion_out / Cion_in) * 1e3 def vtrap(self, x, y): ''' Generic function used to compute rate constants. ''' return x / (np.exp(x / y) - 1) def efun(self, x): ''' Generic function used to compute rate constants. ''' return x / (np.exp(x) - 1) def ghkDrive(self, Vm, Z_ion, Cion_in, Cion_out, T): ''' Use the Goldman-Hodgkin-Katz equation to compute the electrochemical driving force of a specific ion species for a given membrane potential. :param Vm: membrane potential (mV) :param Cin: intracellular ion concentration (M) :param Cout: extracellular ion concentration (M) :param T: temperature (K) :return: electrochemical driving force of a single ion particle (mC.m-3) ''' x = Z_ion * FARADAY * Vm / (Rg * T) * 1e-3 # [-] eCin = Cion_in * self.efun(-x) # M eCout = Cion_out * self.efun(x) # M return FARADAY * (eCin - eCout) * 1e6 # mC/m3 - def getDesc(self): - return inspect.getdoc(self).splitlines()[0] - def getCurrentsNames(self): return list(self.currents(np.nan, [np.nan] * len(self.states)).keys()) def getPltScheme(self): pltscheme = { 'Q_m': ['Qm'], 'V_m': ['Vm'] } pltscheme['I'] = self.getCurrentsNames() + ['iNet'] for cname in self.getCurrentsNames(): if 'Leak' not in cname: key = 'i_{{{}}}\ kin.'.format(cname[1:]) cargs = inspect.getargspec(getattr(self, cname))[0][1:] pltscheme[key] = [var for var in cargs if var not in ['Vm', 'Cai']] return pltscheme def getPltVars(self, wrapleft='df["', wrapright='"]'): ''' Return a dictionary with information about all plot variables related to the neuron. ''' - pltvars = { 'Qm': { 'desc': 'membrane charge density', 'label': 'Q_m', 'unit': 'nC/cm^2', 'factor': 1e5, 'bounds': (-100, 50) }, 'Vm': { 'desc': 'membrane potential', 'label': 'V_m', 'unit': 'mV', 'y0': self.Vm0, 'bounds': (-150, 70) }, 'ELeak': { 'constant': 'obj.ELeak', 'desc': 'non-specific leakage current resting potential', 'label': 'V_{leak}', 'unit': 'mV', 'ls': '--', 'color': 'k' } } for cname in self.getCurrentsNames(): cfunc = getattr(self, cname) cargs = inspect.getargspec(cfunc)[0][1:] pltvars[cname] = { 'desc': inspect.getdoc(cfunc).splitlines()[0], 'label': 'I_{{{}}}'.format(cname[1:]), 'unit': 'A/m^2', 'factor': 1e-3, 'func': '{}({})'.format(cname, ', '.join(['{}{}{}'.format(wrapleft, a, wrapright) for a in cargs])) } for var in cargs: if var not in ['Vm', 'Cai']: vfunc = getattr(self, 'der{}{}'.format(var[0].upper(), var[1:])) desc = cname + re.sub( '^Evolution of', '', inspect.getdoc(vfunc).splitlines()[0]) pltvars[var] = { 'desc': desc, 'label': var, 'bounds': (-0.1, 1.1) } pltvars['iNet'] = { 'desc': inspect.getdoc(getattr(self, 'iNet')).splitlines()[0], 'label': 'I_{net}', 'unit': 'A/m^2', 'factor': 1e-3, 'func': 'iNet({0}Vm{1}, {2}{3}{4}.values.T)'.format( wrapleft, wrapright, wrapleft[:-1], self.states, wrapright[1:]), 'ls': '--', 'color': 'black' } pltvars['dQdt'] = { 'desc': inspect.getdoc(getattr(self, 'dQdt')).splitlines()[0], 'label': 'dQ_m/dt', 'unit': 'A/m^2', 'factor': 1e-3, 'func': 'dQdt({0}Vm{1}, {2}{3}{4}.values.T)'.format( wrapleft, wrapright, wrapleft[:-1], self.states, wrapright[1:]), 'ls': '--', 'color': 'black' } for x in self.getGates(): for rate in ['alpha', 'beta']: pltvars['{}{}'.format(rate, x)] = { 'label': '\\{}_{{{}}}'.format(rate, x), 'unit': 'ms^{-1}', 'factor': 1e-3 } return pltvars def getRatesNames(self, states): ''' Return a list of names of the alpha and beta rates of the neuron. ''' return list(sum( - [['alpha{}'.format(x.lower()), 'beta{}'.format(x.lower())] for x in states], - [] - )) - - def Qm0(self): - ''' Return the resting charge density (in C/m2). ''' - return self.Cm0 * self.Vm0 * 1e-3 # C/cm2 + [['alpha{}'.format(x.lower()), 'beta{}'.format(x.lower())] for x in states], [])) @abc.abstractmethod def steadyStates(self, Vm): ''' Compute the steady-state values for a specific membrane potential value. :param Vm: membrane potential (mV) :return: dictionary 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 computeEffRates(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: a dictionary of rate average constants (s-1) ''' def interpEffRates(self, Qm, lkp, keys=None): ''' Interpolate effective rate constants for a given charge density using reference lookup vectors. :param Qm: membrane charge density (C/m2) :states: state probabilities of the ion channels :param lkp: dictionary of 1D vectors of "effective" coefficients over the charge domain, for specific frequency and amplitude values. :return: dictionary of interpolated rate constants ''' if keys is None: keys = self.rates return {k: np.interp(Qm, lkp['Q'], lkp[k], left=np.nan, right=np.nan) for k in keys} def interpVmeff(self, Qm, lkp): ''' Interpolate the effective membrane potential for a given charge density using reference lookup vectors. :param Qm: membrane charge density (C/m2) :param lkp: dictionary of 1D vectors of "effective" coefficients over the charge domain, for specific frequency and amplitude values. :return: dictionary of interpolated rate constants ''' return np.interp(Qm, lkp['Q'], lkp['V'], left=np.nan, right=np.nan) @abc.abstractmethod def derEffStates(self, Qm, states, lkp): ''' 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 lkp: dictionary of 1D vectors of "effective" coefficients over the charge domain, for specific frequency and amplitude values. ''' def Qbounds(self): ''' Determine bounds of membrane charge physiological range for a given neuron. ''' return np.array([np.round(self.Vm0 - 25.0), 50.0]) * self.Cm0 * 1e-3 # C/m2 def isVoltageGated(self, state): ''' Determine whether a given state is purely voltage-gated or not.''' return 'alpha{}'.format(state.lower()) in self.rates def getGates(self): ''' Retrieve the names of the neuron's states that match an ion channel gating. ''' gates = [] for x in self.states: if self.isVoltageGated(x): gates.append(x) return gates def qsStates(self, lkp, states): ''' Compute a collection of quasi steady states using the standard xinf = ax / (ax + Bx) equation. :param lkp: dictionary of 1D vectors of "effective" coefficients over the charge domain, for specific frequency and amplitude values. :return: dictionary of quasi-steady states ''' return { x: lkp['alpha{}'.format(x)] / (lkp['alpha{}'.format(x)] + lkp['beta{}'.format(x)]) for x in states } @abc.abstractmethod def quasiSteadyStates(self, lkp): ''' Compute the quasi-steady states of a neuron for a range of membrane charge densities, based on 1-dimensional lookups interpolated at a given sonophore diameter, US frequency, US amplitude and duty cycle. :param lkp: dictionary of 1D vectors of "effective" coefficients over the charge domain, for specific frequency and amplitude values. :return: dictionary of quasi-steady states ''' 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 def Vderivatives(self, y, t, Iinj): ''' Compute the derivatives of a V-cast HH system for a specific value of injected current. :param y: vector of HH system variables at time t :param t: time value (s, unused) :param Iinj: injected current (mA/m2) :return: vector of HH system derivatives at time t ''' Vm, *states = y Iionic = self.iNet(Vm, states) # mA/m2 dVmdt = (- Iionic + Iinj) / self.Cm0 # mV/s dstates = self.derStates(Vm, states) return [dVmdt, *[dstates[k] for k in self.states]] def Qderivatives(self, y, t, Cm=None): ''' 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 Cm: membrane capacitance (F/m2) :return: vector of HH system derivatives at time t ''' if Cm is None: Cm = self.Cm0 Qm, *states = y Vm = Qm / Cm * 1e3 # mV dQmdt = - self.iNet(Vm, states) * 1e-3 # A/m2 dstates = self.derStates(Vm, states) return [dQmdt, *[dstates[k] for k in self.states]] def checkInputs(self, Astim, tstim, toffset, PRF, DC): ''' Check validity of electrical stimulation parameters. :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 (-) ''' # Check validity of stimulation parameters if not all(isinstance(param, float) for param in [Astim, tstim, toffset, DC]): raise TypeError('Invalid stimulation parameters (must be float typed)') if tstim <= 0: raise ValueError('Invalid stimulus duration: {} ms (must be strictly positive)' .format(tstim * 1e3)) if toffset < 0: raise ValueError('Invalid stimulus offset: {} ms (must be positive or null)' .format(toffset * 1e3)) if DC <= 0.0 or DC > 1.0: raise ValueError('Invalid duty cycle: {} (must be within ]0; 1])'.format(DC)) if DC < 1.0: if not isinstance(PRF, float): raise TypeError('Invalid PRF value (must be float typed)') if PRF is None: raise AttributeError('Missing PRF value (must be provided when DC < 1)') if PRF < 1 / tstim: raise ValueError('Invalid PRF: {} Hz (PR interval exceeds stimulus duration)' .format(PRF)) + def meta(self, Astim, tstim, toffset, PRF, DC): + ''' Return information about object and simulation parameters. + + :param Astim: stimulus amplitude (mA/m2) + :param tstim: stimulus duration (s) + :param toffset: stimulus offset (s) + :param PRF: pulse repetition frequency (Hz) + :param DC: stimulus duty cycle (-) + :return: meta-data dictionary + ''' + return { + 'neuron': self.name, + 'Astim': Astim, + 'tstim': tstim, + 'toffset': toffset, + 'PRF': PRF, + 'DC': DC + } + def simulate(self, Astim, tstim, toffset, PRF=100., DC=1.0): ''' Simulate a specific neuron model for a specific set of electrical parameters, and return output data in a dataframe. :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: 2-tuple with the output dataframe and computation time. ''' - logger.info( - '%s: %s @ %st = %ss (%ss offset)%s', - self, - 'titration' if Astim is None else 'simulation', - 'A = {}A/m2, '.format(si_format(Astim, 2, space=' ')) if Astim is not None else '', - *si_format([tstim, toffset], 1, space=' '), + '%s: simulation @ A = %sA/m2, t = %ss (%ss offset)%s', + self, si_format(Astim, 2, space=' '), *si_format([tstim, toffset], 1, space=' '), (', PRF = {}Hz, DC = {:.2f}%'.format( si_format(PRF, 2, space=' '), DC * 1e2) if DC < 1.0 else '')) # Check validity of stimulation parameters self.checkInputs(Astim, tstim, toffset, PRF, DC) # Set initial conditions steady_states = self.steadyStates(self.Vm0) y0 = np.array([self.Vm0, *[steady_states[k] for k in self.states]]) # Initialize simulator and compute solution logger.debug('Computing solution') simulator = PWSimulator( lambda y, t: self.Vderivatives(y, t, Astim), lambda y, t: self.Vderivatives(y, t, 0.)) (t, y, stim), tcomp = simulator(y0, DT_ESTIM, tstim, toffset, PRF, DC, monitor_time=True) logger.debug('completed in %ss', si_format(tcomp, 1)) # Store output in dataframe data = pd.DataFrame({ 't': t, 'stimstate': stim, 'Vm': y[:, 0], 'Qm': y[:, 0] * self.Cm0 * 1e-3 }) data['Qm'] = data['Vm'].values * self.Cm0 * 1e-3 for i in range(len(self.states)): data[self.states[i]] = y[:, i + 1] # Log number of detected spikes nspikes = self.getNSpikes(data) logger.debug('{} spike{} detected'.format(nspikes, plural(nspikes))) # Return dataframe and computation time return data, tcomp - def meta(self, Astim, tstim, toffset, PRF, DC): - ''' Return information about object and simulation parameters. - - :param Astim: stimulus amplitude (mA/m2) - :param tstim: stimulus duration (s) - :param toffset: stimulus offset (s) - :param PRF: pulse repetition frequency (Hz) - :param DC: stimulus duty cycle (-) - :return: meta-data dictionary - ''' - return { - 'neuron': self.name, - 'Astim': Astim, - 'tstim': tstim, - 'toffset': toffset, - 'PRF': PRF, - 'DC': DC - } - - def createQueue(self, amps, durations, offsets, PRFs, DCs): + def simQueue(self, 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: list of parameters (list) for each simulation ''' if amps is None: amps = [np.nan] DCs = np.array(DCs) queue = [] if 1.0 in DCs: queue += createQueue(amps, durations, offsets, min(PRFs), 1.0) if np.any(DCs != 1.0): queue += createQueue(amps, durations, offsets, PRFs, DCs[DCs != 1.0]) for item in queue: if np.isnan(item[0]): item[0] = None return queue def getNSpikes(self, data): ''' Compute number of spikes in charge profile of simulation output. :param data: dataframe containing output time series :return: number of detected spikes ''' dt = np.diff(data.ix[:1, 't'].values)[0] ipeaks, *_ = findPeaks( data['Qm'].values, SPIKE_MIN_QAMP, int(np.ceil(SPIKE_MIN_DT / dt)), SPIKE_MIN_QPROM ) return ipeaks.size def getStabilizationValue(self, data): ''' Determine stabilization value from the charge profile of a simulation output. :param data: dataframe containing output time series :return: charge stabilization value (or np.nan if no stabilization detected) ''' # Extract charge signal posterior to observation window t, Qm = [data[key].values for key in ['t', 'Qm']] Qm = y[2, t > TMIN_STABILIZATION] # Compute variation range Qm_range = np.ptp(Qm) logger.debug('%.2f nC/cm2 variation range over the last %.0f ms', Qm_range * 1e5, TMIN_STABILIZATION * 1e3) # Return final value only if stabilization is detected if np.ptp(Qm) < QSS_Q_DIV_THR: return Qm[-1] else: return np.nan def isExcited(self, data): ''' Determine if neuron is excited from simulation output. :param data: dataframe containing output time series :return: boolean stating whether neuron is excited or not ''' return self.getNSpikes(data) > 0 def isSilenced(self, data): ''' Determine if neuron is silenced from simulation output. :param data: dataframe containing output time series :return: boolean stating whether neuron is silenced or not ''' return not np.isinan(self.getStabilizationValue(data)) def titrate(self, tstim, toffset, PRF, DC, xfunc=None, Arange=(0., 2 * TITRATION_ESTIM_A_MAX)): ''' Use a binary search to determine the threshold amplitude needed to obtain neural excitation for a given duration, PRF and duty cycle. :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 xfunc: function determining whether condition is reached from simulation output :param Arange: search interval for Astim, iteratively refined :return: excitation threshold amplitude (mA/m2) ''' # Default output function if xfunc is None: xfunc = self.titrationFunc return binarySearch( lambda x: xfunc(self.simulate(*x)[0]), [tstim, toffset, PRF, DC], 0, Arange, TITRATION_ESTIM_DA_MAX ) diff --git a/PySONIC/neurons/cortical.py b/PySONIC/neurons/cortical.py index 4b2cb24..6b1b714 100644 --- a/PySONIC/neurons/cortical.py +++ b/PySONIC/neurons/cortical.py @@ -1,670 +1,671 @@ #!/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: 2019-05-16 15:24:03 +# @Last Modified time: 2019-06-02 12:32:10 import numpy as np from ..core import PointNeuron class Cortical(PointNeuron): ''' Generic cortical 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.* ''' # Generic biophysical parameters of cortical cells Cm0 = 1e-2 # Cell membrane resting capacitance (F/m2) Vm0 = 0.0 # Dummy value for membrane potential (mV) ENa = 50.0 # Sodium Nernst potential (mV) EK = -90.0 # Potassium Nernst potential (mV) ECa = 120.0 # # Calcium Nernst potential (mV) def __init__(self): + super().__init__() self.states = ['m', 'h', 'n', 'p'] self.rates = self.getRatesNames(self.states) def alpham(self, Vm): ''' Voltage-dependent activation rate of m-gate :param Vm: membrane potential (mV) :return: rate (s-1) ''' Vdiff = Vm - self.VT alpha = 0.32 * self.vtrap(13 - Vdiff, 4) # ms-1 return alpha * 1e3 # s-1 def betam(self, Vm): ''' Voltage-dependent inactivation rate of m-gate :param Vm: membrane potential (mV) :return: rate (s-1) ''' Vdiff = Vm - self.VT beta = 0.28 * self.vtrap(Vdiff - 40, 5) # ms-1 return beta * 1e3 # s-1 def alphah(self, Vm): ''' Voltage-dependent activation rate of h-gate :param Vm: membrane potential (mV) :return: rate (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): ''' Voltage-dependent inactivation rate of h-gate :param Vm: membrane potential (mV) :return: rate (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): ''' Voltage-dependent activation rate of n-gate :param Vm: membrane potential (mV) :return: rate (s-1) ''' Vdiff = Vm - self.VT alpha = 0.032 * self.vtrap(15 - Vdiff, 5) # ms-1 return alpha * 1e3 # s-1 def betan(self, Vm): ''' Voltage-dependent inactivation rate of n-gate :param Vm: membrane potential (mV) :return: rate (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): ''' Voltage-dependent steady-state opening of p-gate :param Vm: membrane potential (mV) :return: steady-state opening (-) ''' return 1.0 / (1 + np.exp(-(Vm + 35) / 10)) def taup(self, Vm): ''' Voltage-dependent adaptation time for adaptation of p-gate :param Vm: membrane potential (mV) :return: adaptation time (s) ''' return self.TauMax / (3.3 * np.exp((Vm + 35) / 20) + np.exp(-(Vm + 35) / 20)) # s def derM(self, Vm, m): ''' Evolution of m-gate open-probability :param Vm: membrane potential (mV) :param m: open-probability of m-gate (-) :return: time derivative of m-gate open-probability (s-1) ''' return self.alpham(Vm) * (1 - m) - self.betam(Vm) * m def derH(self, Vm, h): ''' Evolution of h-gate open-probability :param Vm: membrane potential (mV) :param h: open-probability of h-gate (-) :return: time derivative of h-gate open-probability (s-1) ''' return self.alphah(Vm) * (1 - h) - self.betah(Vm) * h def derN(self, Vm, n): ''' Evolution of n-gate open-probability :param Vm: membrane potential (mV) :param n: open-probability of n-gate (-) :return: time derivative of n-gate open-probability (s-1) ''' return self.alphan(Vm) * (1 - n) - self.betan(Vm) * n def derP(self, Vm, p): ''' Evolution of p-gate open-probability :param Vm: membrane potential (mV) :param p: open-probability of p-gate (-) :return: time derivative of p-gate open-probability (s-1) ''' return (self.pinf(Vm) - p) / self.taup(Vm) def iNa(self, m, h, Vm): ''' Sodium current :param m: open-probability of m-gate (-) :param h: open-probability of h-gate (-) :param Vm: membrane potential (mV) :return: current per unit area (mA/m2) ''' return self.gNabar * m**3 * h * (Vm - self.ENa) def iKd(self, n, Vm): ''' delayed-rectifier Potassium current :param n: open-probability of n-gate (-) :param Vm: membrane potential (mV) :return: current per unit area (mA/m2) ''' return self.gKdbar * n**4 * (Vm - self.EK) def iM(self, p, Vm): ''' slow non-inactivating Potassium current :param p: open-probability of p-gate (-) :param Vm: membrane potential (mV) :return: current per unit area (mA/m2) ''' return self.gMbar * p * (Vm - self.EK) def iLeak(self, Vm): ''' non-specific leakage current :param Vm: membrane potential (mV) :return: current per unit area (mA/m2) ''' return self.gLeak * (Vm - self.ELeak) def currents(self, Vm, states): ''' Overriding of abstract parent method. ''' m, h, n, p = states return { 'iNa': self.iNa(m, h, Vm), 'iKd': self.iKd(n, Vm), 'iM': self.iM(p, Vm), 'iLeak': self.iLeak(Vm) } # mA/m2 def steadyStates(self, Vm): ''' Overriding of abstract parent method. ''' return { 'm': self.alpham(Vm) / (self.alpham(Vm) + self.betam(Vm)), 'h': self.alphah(Vm) / (self.alphah(Vm) + self.betah(Vm)), 'n': self.alphan(Vm) / (self.alphan(Vm) + self.betan(Vm)), 'p': self.pinf(Vm) } def derStates(self, Vm, states): ''' Overriding of abstract parent method. ''' m, h, n, p = states return { 'm': self.derM(Vm, m), 'h': self.derH(Vm, h), 'n': self.derN(Vm, n), 'p': self.derP(Vm, p) } def computeEffRates(self, Vm): ''' Overriding of abstract parent method. ''' return { 'alpham': np.mean(self.alpham(Vm)), 'betam': np.mean(self.betam(Vm)), 'alphah': np.mean(self.alphah(Vm)), 'betah': np.mean(self.betah(Vm)), 'alphan': np.mean(self.alphan(Vm)), 'betan': np.mean(self.betan(Vm)), 'alphap': np.mean(self.pinf(Vm) / self.taup(Vm)), 'betap': np.mean((1 - self.pinf(Vm)) / self.taup(Vm)) } def derEffStates(self, Qm, states, lkp): ''' Overriding of abstract parent method. ''' rates = self.interpEffRates(Qm, lkp) m, h, n, p = states return { 'm': rates['alpham'] * (1 - m) - rates['betam'] * m, 'h': rates['alphah'] * (1 - h) - rates['betah'] * h, 'n': rates['alphan'] * (1 - n) - rates['betan'] * n, 'p': rates['alphap'] * (1 - p) - rates['betap'] * p } def quasiSteadyStates(self, lkp): ''' Overriding of abstract parent method. ''' return self.qsStates(lkp, ['m', 'h', 'n', 'p']) class CorticalRS(Cortical): ''' Cortical regular spiking 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) gNabar = 560.0 # Max. conductance of Sodium current (S/m^2) gKdbar = 60.0 # Max. conductance of delayed Potassium current (S/m^2) gMbar = 0.75 # Max. conductance of slow non-inactivating Potassium current (S/m^2) gLeak = 0.205 # Conductance of non-specific leakage current (S/m^2) ELeak = -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) def __init__(self): super().__init__() class CorticalFS(Cortical): ''' Cortical fast-spiking 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) gNabar = 580.0 # Max. conductance of Sodium current (S/m^2) gKdbar = 39.0 # Max. conductance of delayed Potassium current (S/m^2) gMbar = 0.787 # Max. conductance of slow non-inactivating Potassium current (S/m^2) gLeak = 0.38 # Conductance of non-specific leakage current (S/m^2) ELeak = -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) def __init__(self): super().__init__() class CorticalLTS(Cortical): ''' Cortical low-threshold spiking neuron 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) gNabar = 500.0 # Max. conductance of Sodium current (S/m^2) gKdbar = 40.0 # Max. conductance of delayed Potassium current (S/m^2) gMbar = 0.28 # Max. conductance of slow non-inactivating Potassium current (S/m^2) gCaTbar = 4.0 # Max. conductance of low-threshold Calcium current (S/m^2) gLeak = 0.19 # Conductance of non-specific leakage current (S/m^2) ELeak = -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) def __init__(self): super().__init__() self.states += ['s', 'u'] self.rates = self.getRatesNames(self.states) def sinf(self, Vm): ''' Voltage-dependent steady-state opening of s-gate :param Vm: membrane potential (mV) :return: steady-state opening (-) ''' return 1.0 / (1.0 + np.exp(-(Vm + self.Vx + 57.0) / 6.2)) def taus(self, Vm): ''' Voltage-dependent adaptation time for adaptation of s-gate :param Vm: membrane potential (mV) :return: adaptation time (s) ''' x = 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 / x) * 1e-3 # s def uinf(self, Vm): ''' Voltage-dependent steady-state opening of u-gate :param Vm: membrane potential (mV) :return: steady-state opening (-) ''' return 1.0 / (1.0 + np.exp((Vm + self.Vx + 81.0) / 4.0)) # prob def tauu(self, Vm): ''' Voltage-dependent adaptation time for adaptation of u-gate :param Vm: membrane potential (mV) :return: adaptation time (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): ''' Evolution of s-gate open-probability :param Vm: membrane potential (mV) :param s: open-probability of s-gate (-) :return: time derivative of s-gate open-probability (s-1) ''' return (self.sinf(Vm) - s) / self.taus(Vm) def derU(self, Vm, u): ''' Evolution of u-gate open-probability :param Vm: membrane potential (mV) :param u: open-probability of u-gate (-) :return: time derivative of u-gate open-probability (s-1) ''' return (self.uinf(Vm) - u) / self.tauu(Vm) def iCaT(self, s, u, Vm): ''' low-threshold (T-type) Calcium current :param s: open-probability of s-gate (-) :param u: open-probability of u-gate (-) :param Vm: membrane potential (mV) :return: current per unit area (mA/m2) ''' return self.gCaTbar * s**2 * u * (Vm - self.ECa) def currents(self, Vm, states): ''' Overriding of abstract parent method. ''' m, h, n, p, s, u = states currents = super().currents(Vm, [m, h, n, p]) currents['iCaT'] = self.iCaT(s, u, Vm) # mA/m2 return currents def steadyStates(self, Vm): ''' Overriding of abstract parent method. ''' # Voltage-gated steady-states sstates = super().steadyStates(Vm) sstates['s'] = self.sinf(Vm) sstates['u'] = self.uinf(Vm) return sstates def derStates(self, Vm, states): ''' Overriding of abstract parent method. ''' # Unpack input states *NaK_states, s, u = states # Call parent method to compute Sodium and Potassium channels states derivatives dstates = super().derStates(Vm, NaK_states) # Compute Calcium channels states derivatives dstates['s'] = self.derS(Vm, s) dstates['u'] = self.derU(Vm, u) return dstates def computeEffRates(self, Vm): ''' Overriding of abstract parent method. ''' # Call parent method to compute Sodium and Potassium effective rate constants effrates = super().computeEffRates(Vm) # Compute Calcium effective rate constants effrates['alphas'] = np.mean(self.sinf(Vm) / self.taus(Vm)) effrates['betas'] = np.mean((1 - self.sinf(Vm)) / self.taus(Vm)) effrates['alphau'] = np.mean(self.uinf(Vm) / self.tauu(Vm)) effrates['betau'] = np.mean((1 - self.uinf(Vm)) / self.tauu(Vm)) return effrates def derEffStates(self, Qm, states, lkp): ''' Overriding of abstract parent method. ''' # Unpack input states *NaK_states, s, u = states # Call parent method to compute Sodium and Potassium channels states derivatives dstates = super().derEffStates(Qm, NaK_states, lkp) # Compute Calcium channels states derivatives Ca_rates = self.interpEffRates(Qm, lkp, keys=self.getRatesNames(['s', 'u'])) dstates['s'] = Ca_rates['alphas'] * (1 - s) - Ca_rates['betas'] * s dstates['u'] = Ca_rates['alphau'] * (1 - u) - Ca_rates['betau'] * u # Merge all states derivatives and return return dstates def quasiSteadyStates(self, lkp): ''' Overriding of abstract parent method. ''' qsstates = super().quasiSteadyStates(lkp) qsstates.update(self.qsStates(lkp, ['s', 'u'])) return qsstates class CorticalIB(Cortical): ''' Cortical intrinsically bursting neuron 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) gNabar = 500 # Max. conductance of Sodium current (S/m^2) gKdbar = 50 # Max. conductance of delayed Potassium current (S/m^2) gMbar = 0.3 # Max. conductance of slow non-inactivating Potassium current (S/m^2) gCaLbar = 1.0 # Max. conductance of L-type Calcium current (S/m^2) gLeak = 0.1 # Conductance of non-specific leakage current (S/m^2) ELeak = -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) def __init__(self): super().__init__() self.states += ['q', 'r'] self.rates = self.getRatesNames(self.states) def alphaq(self, Vm): ''' Voltage-dependent activation rate of q-gate :param Vm: membrane potential (mV) :return: rate (s-1) ''' alpha = 0.055 * self.vtrap(-(Vm + 27), 3.8) # ms-1 return alpha * 1e3 # s-1 def betaq(self, Vm): ''' Voltage-dependent inactivation rate of q-gate :param Vm: membrane potential (mV) :return: rate (s-1) ''' beta = 0.94 * np.exp(-(Vm + 75) / 17) # ms-1 return beta * 1e3 # s-1 def alphar(self, Vm): ''' Voltage-dependent activation rate of r-gate :param Vm: membrane potential (mV) :return: rate (s-1) ''' alpha = 0.000457 * np.exp(-(Vm + 13) / 50) # ms-1 return alpha * 1e3 # s-1 def betar(self, Vm): ''' Voltage-dependent inactivation rate of r-gate :param Vm: membrane potential (mV) :return: rate (s-1) ''' beta = 0.0065 / (np.exp(-(Vm + 15) / 28) + 1) # ms-1 return beta * 1e3 # s-1 def derQ(self, Vm, q): ''' Evolution of q-gate open-probability :param Vm: membrane potential (mV) :param q: open-probability of q-gate (-) :return: time derivative of q-gate open-probability (s-1) ''' return self.alphaq(Vm) * (1 - q) - self.betaq(Vm) * q def derR(self, Vm, r): ''' Evolution of r-gate open-probability :param Vm: membrane potential (mV) :param r: open-probability of r-gate (-) :return: time derivative of r-gate open-probability (s-1) ''' return self.alphar(Vm) * (1 - r) - self.betar(Vm) * r def iCaL(self, q, r, Vm): ''' high-threshold (L-type) Calcium current :param q: open-probability of q-gate (-) :param r: open-probability of r-gate (-) :param Vm: membrane potential (mV) :return: current per unit area (mA/m2) ''' return self.gCaLbar * q**2 * r * (Vm - self.ECa) def currents(self, Vm, states): ''' Overriding of abstract parent method. ''' m, h, n, p, q, r = states return { 'iNa': self.iNa(m, h, Vm), 'iKd': self.iKd(n, Vm), 'iM': self.iM(p, Vm), 'iCaL': self.iCaL(q, r, Vm), 'iLeak': self.iLeak(Vm) } # mA/m2 def steadyStates(self, Vm): ''' Overriding of abstract parent method. ''' # Voltage-gated steady-states sstates = super().steadyStates(Vm) sstates['q'] = self.alphaq(Vm) / (self.alphaq(Vm) + self.betaq(Vm)) sstates['r'] = self.alphar(Vm) / (self.alphar(Vm) + self.betar(Vm)) return sstates def derStates(self, Vm, states): ''' Overriding of abstract parent method. ''' # Unpack input states *NaK_states, q, r = states # Call parent method to compute Sodium and Potassium channels states derivatives dstates = super().derStates(Vm, NaK_states) # Compute L-type Calcium channels states derivatives dstates['q'] = self.derQ(Vm, q) dstates['r'] = self.derR(Vm, r) # Merge all states derivatives and return return dstates def computeEffRates(self, Vm): ''' Overriding of abstract parent method. ''' # Call parent method to compute Sodium and Potassium effective rate constants effrates = super().computeEffRates(Vm) # Compute Calcium effective rate constants effrates['alphaq'] = np.mean(self.alphaq(Vm)) effrates['betaq'] = np.mean(self.betaq(Vm)) effrates['alphar'] = np.mean(self.alphar(Vm)) effrates['betar'] = np.mean(self.betar(Vm)) return effrates def derEffStates(self, Qm, states, lkp): ''' Overriding of abstract parent method. ''' # Unpack input states *NaK_states, q, r = states # Call parent method to compute Sodium and Potassium channels states derivatives dstates = super().derEffStates(Qm, NaK_states, lkp) # Compute Calcium channels states derivatives Ca_rates = self.interpEffRates(Qm, lkp, keys=self.getRatesNames(['q', 'r'])) dstates['q'] = Ca_rates['alphaq'] * (1 - q) - Ca_rates['betaq'] * q dstates['r'] = Ca_rates['alphar'] * (1 - r) - Ca_rates['betar'] * r # Merge all states derivatives and return return dstates def quasiSteadyStates(self, lkp): ''' Overriding of abstract parent method. ''' qsstates = super().quasiSteadyStates(lkp) qsstates.update(self.qsStates(lkp, ['q', 'r'])) return qsstates diff --git a/PySONIC/neurons/fh.py b/PySONIC/neurons/fh.py index 967896e..d84d168 100644 --- a/PySONIC/neurons/fh.py +++ b/PySONIC/neurons/fh.py @@ -1,287 +1,288 @@ # -*- coding: utf-8 -*- # @Author: Theo Lemaire # @Date: 2019-01-07 18:41:06 # @Last Modified by: Theo Lemaire -# @Last Modified time: 2019-05-16 15:24:02 +# @Last Modified time: 2019-06-02 12:32:49 import numpy as np from ..core import PointNeuron from ..constants import CELSIUS_2_KELVIN, Z_Na, Z_K class FrankenhaeuserHuxley(PointNeuron): ''' Xenopus myelinated fiber node Reference: *Frankenhaeuser, B., and Huxley, A.F. (1964). The action potential in the myelinated nerve fibre of Xenopus laevis as computed on the basis of voltage clamp data. J Physiol 171, 302–315.* ''' # Name of channel mechanism name = 'FH' # Cell biophysical parameters Cm0 = 2e-2 # Cell membrane resting capacitance (F/m2) Vm0 = -70. # Membrane resting potential (mV) celsius = 20.0 # Temperature (Celsius) gLeak = 300.3 # Leakage conductance (S/m2) ELeak = -69.974 # Leakage resting potential (mV) pNabar = 8e-5 # Sodium permeability constant (m/s) pPbar = .54e-5 # Non-specific permeability constant (m/s) pKbar = 1.2e-5 # Potassium permeability constant (m/s) Nai = 13.74e-3 # Sodium intracellular concentration (M) Nao = 114.5e-3 # Sodium extracellular concentration (M) Ki = 120e-3 # Potassium intracellular concentration (M) Ko = 2.5e-3 # Potassium extracellular concentration (M) def __init__(self): + super().__init__() self.states = ['m', 'h', 'n', 'p'] self.rates = self.getRatesNames(self.states) self.q10 = 3**((self.celsius - 20) / 10) self.T = self.celsius + CELSIUS_2_KELVIN def getPltVars(self, wrapleft='df["', wrapright='"]'): pltvars = super().getPltVars(wrapleft, wrapright) pltvars['Qm']['bounds'] = (-150, 50) return pltvars def alpham(self, Vm): ''' Voltage-dependent activation rate of m-gate :param Vm: membrane potential (mV) :return: rate (s-1) ''' Vdiff = Vm - self.Vm0 alpha = 0.36 * self.vtrap(22. - Vdiff, 3.) # ms-1 return self.q10 * alpha * 1e3 # s-1 def betam(self, Vm): ''' Voltage-dependent inactivation rate of m-gate :param Vm: membrane potential (mV) :return: rate (s-1) ''' Vdiff = Vm - self.Vm0 beta = 0.4 * self.vtrap(Vdiff - 13., 20.) # ms-1 return self.q10 * beta * 1e3 # s-1 def alphah(self, Vm): ''' Voltage-dependent activation rate of h-gate :param Vm: membrane potential (mV) :return: rate (s-1) ''' Vdiff = Vm - self.Vm0 alpha = 0.1 * self.vtrap(Vdiff + 10.0, 6.) # ms-1 return self.q10 * alpha * 1e3 # s-1 def betah(self, Vm): ''' Voltage-dependent inactivation rate of h-gate :param Vm: membrane potential (mV) :return: rate (s-1) ''' Vdiff = Vm - self.Vm0 beta = 4.5 / (np.exp((45. - Vdiff) / 10.) + 1) # ms-1 return self.q10 * beta * 1e3 # s-1 def alphan(self, Vm): ''' Voltage-dependent activation rate of n-gate :param Vm: membrane potential (mV) :return: rate (s-1) ''' Vdiff = Vm - self.Vm0 alpha = 0.02 * self.vtrap(35. - Vdiff, 10.0) # ms-1 return self.q10 * alpha * 1e3 # s-1 def betan(self, Vm): ''' Voltage-dependent inactivation rate of n-gate :param Vm: membrane potential (mV) :return: rate (s-1) ''' Vdiff = Vm - self.Vm0 beta = 0.05 * self.vtrap(Vdiff - 10., 10.) # ms-1 return self.q10 * beta * 1e3 # s-1 def alphap(self, Vm): ''' Voltage-dependent activation rate of p-gate :param Vm: membrane potential (mV) :return: rate (s-1) ''' Vdiff = Vm - self.Vm0 alpha = 0.006 * self.vtrap(40. - Vdiff, 10.0) # ms-1 return self.q10 * alpha * 1e3 # s-1 def betap(self, Vm): ''' Voltage-dependent inactivation rate of p-gate :param Vm: membrane potential (mV) :return: rate (s-1) ''' Vdiff = Vm - self.Vm0 beta = 0.09 * self.vtrap(Vdiff + 25., 20.) # ms-1 return self.q10 * beta * 1e3 # s-1 def derM(self, Vm, m): ''' Evolution of m-gate open-probability :param Vm: membrane potential (mV) :param m: open-probability of m-gate (-) :return: time derivative of m-gate open-probability (s-1) ''' return self.alpham(Vm) * (1 - m) - self.betam(Vm) * m def derH(self, Vm, h): ''' Evolution of h-gate open-probability :param Vm: membrane potential (mV) :param h: open-probability of h-gate (-) :return: time derivative of h-gate open-probability (s-1) ''' return self.alphah(Vm) * (1 - h) - self.betah(Vm) * h def derN(self, Vm, n): ''' Evolution of n-gate open-probability :param Vm: membrane potential (mV) :param n: open-probability of n-gate (-) :return: time derivative of n-gate open-probability (s-1) ''' return self.alphan(Vm) * (1 - n) - self.betan(Vm) * n def derP(self, Vm, p): ''' Evolution of p-gate open-probability :param Vm: membrane potential (mV) :param p: open-probability of p-gate (-) :return: time derivative of p-gate open-probability (s-1) ''' return self.alphap(Vm) * (1 - p) - self.betap(Vm) * p def iNa(self, m, h, Vm): ''' Sodium current :param m: open-probability of m-gate :param h: open-probability of h-gate :param Vm: membrane potential (mV) :return: current per unit area (mA/m2) ''' iNa_drive = self.ghkDrive(Vm, Z_Na, self.Nai, self.Nao, self.T) # mC/m3 return self.pNabar * m**2 * h * iNa_drive def iKd(self, n, Vm): ''' delayed-rectifier Potassium current :param n: open-probability of n-gate :param Vm: membrane potential (mV) :return: current per unit area (mA/m2) ''' iKd_drive = self.ghkDrive(Vm, Z_K, self.Ki, self.Ko, self.T) # mC/m3 return self.pKbar * n**2 * iKd_drive def iP(self, p, Vm): ''' non-specific delayed current :param p: open-probability of p-gate :param Vm: membrane potential (mV) :return: current per unit area (mA/m2) ''' iP_drive = self.ghkDrive(Vm, Z_Na, self.Nai, self.Nao, self.T) # mC/m3 return self.pPbar * p**2 * iP_drive def iLeak(self, Vm): ''' non-specific leakage current :param Vm: membrane potential (mV) :return: current per unit area (mA/m2) ''' return self.gLeak * (Vm - self.ELeak) def currents(self, Vm, states): ''' Overriding of abstract parent method. ''' m, h, n, p = states return { 'iNa': self.iNa(m, h, Vm), 'iKd': self.iKd(n, Vm), 'iP': self.iP(p, Vm), 'iLeak': self.iLeak(Vm) } # mA/m2 def steadyStates(self, Vm): ''' Overriding of abstract parent method. ''' # Solve the equation dx/dt = 0 at Vm for each x-state return { 'm': self.alpham(Vm) / (self.alpham(Vm) + self.betam(Vm)), 'h': self.alphah(Vm) / (self.alphah(Vm) + self.betah(Vm)), 'n': self.alphan(Vm) / (self.alphan(Vm) + self.betan(Vm)), 'p': self.alphap(Vm) / (self.alphap(Vm) + self.betap(Vm)) } def derStates(self, Vm, states): ''' Overriding of abstract parent method. ''' m, h, n, p = states return { 'm': self.derM(Vm, m), 'h': self.derH(Vm, h), 'n': self.derN(Vm, n), 'p': self.derP(Vm, p) } def computeEffRates(self, Vm): ''' Overriding of abstract parent method. ''' # Compute average cycle value for rate constants return { 'alpham': np.mean(self.alpham(Vm)), 'betam': np.mean(self.betam(Vm)), 'alphah': np.mean(self.alphah(Vm)), 'betah': np.mean(self.betah(Vm)), 'alphan': np.mean(self.alphan(Vm)), 'betan': np.mean(self.betan(Vm)), 'alphap': np.mean(self.alphap(Vm)), 'betap': np.mean(self.betap(Vm)) } def derEffStates(self, Qm, states, lkp): ''' Overriding of abstract parent method. ''' rates = self.interpEffRates(Qm, lkp) m, h, n, p = states return { 'm': rates['alpham'] * (1 - m) - rates['betam'] * m, 'h': rates['alphah'] * (1 - h) - rates['betah'] * h, 'n': rates['alphan'] * (1 - n) - rates['betan'] * n, 'p': rates['alphap'] * (1 - p) - rates['betap'] * p } def quasiSteadyStates(self, lkp): ''' Overriding of abstract parent method. ''' return self.qsStates(lkp, ['m', 'h', 'n', 'p']) diff --git a/PySONIC/neurons/leech.py b/PySONIC/neurons/leech.py index ac23002..4cce787 100644 --- a/PySONIC/neurons/leech.py +++ b/PySONIC/neurons/leech.py @@ -1,962 +1,965 @@ #!/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: 2019-05-16 15:25:06 +# @Last Modified time: 2019-06-02 12:33:41 from functools import partialmethod import numpy as np from ..core import PointNeuron from ..constants import FARADAY, Rg, Z_Na, Z_Ca class LeechTouch(PointNeuron): ''' Leech touch sensory neuron 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) ENa = 45.0 # Sodium Nernst potential (mV) EK = -62.0 # Potassium Nernst potential (mV) ECa = 60.0 # Calcium Nernst potential (mV) ELeak = -48.0 # Non-specific leakage Nernst potential (mV) EPumpNa = -300.0 # Sodium pump current reversal potential (mV) gNabar = 3500.0 # Max. conductance of Sodium current (S/m^2) gKbar = 900.0 # Max. conductance of Potassium current (S/m^2) gCabar = 20.0 # Max. conductance of Calcium current (S/m^2) gKCabar = 236.0 # Max. conductance of Calcium-dependent Potassium current (S/m^2) gLeak = 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) taur_Na = 16.0 # Na+ removal taur_Ca = 1.25 # Ca2+ removal # Time constants for the iPumpNa and iKCa currents activation # from specific intracellular ions (s) taua_PumpNa = 0.1 # iPumpNa activation from intracellular Na+ taua_KCa = 0.01 # iKCa activation from intracellular Ca2+ def __init__(self): + super().__init__() self.states = ['m', 'h', 'n', 's', 'Nai', 'ANai', 'Cai', 'ACai'] self.rates = self.getRatesNames(['m', 'h', 'n', 's']) self.K_Na = self.K_Na_original * self.surface * self.curr_factor self.K_Ca = self.K_Ca_original * self.surface * self.curr_factor # ----------------- 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 _derCion(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 _derAion(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 derNai(self, Nai, m, h, Vm): ''' Derivative of Sodium concentration in intracellular pool. ''' return self._derCion(Nai, self.iNa(m, h, Vm), self.K_Na, self.taur_Na) def derANa(self, ANa, Nai): ''' Derivative of Sodium pool-dependent activation function for iPumpNa. ''' return self._derAion(ANa, Nai, self.taua_PumpNa) def derCai(self, Cai, s, Vm): ''' Derivative of Calcium concentration in intracellular pool. ''' return self._derCion(Cai, self.iCa(s, Vm), self.K_Ca, self.taur_Ca) def derACa(self, ACa, Cai): ''' Derivative of Calcium pool-dependent activation function for iKCa. ''' return self._derAion(ACa, Cai, self.taua_KCa) # ------------------ Currents ------------------- def iNa(self, m, h, Vm): ''' Sodium current :param m: open-probability of m-gate :param h: open-probability of h-gate :param Vm: membrane potential (mV) :return: current per unit area (mA/m2) ''' return self.gNabar * m**3 * h * (Vm - self.ENa) def iK(self, n, Vm): ''' Delayed-rectifier Potassium current :param n: open-probability of n-gate :param Vm: membrane potential (mV) :return: current per unit area (mA/m2) ''' return self.gKbar * n**2 * (Vm - self.EK) def iCa(self, s, Vm): ''' Calcium current :param s: open-probability of s-gate :param u: open-probability of u-gate :param Vm: membrane potential (mV) :return: current per unit area (mA/m2) ''' ''' Calcium inward current. ''' return self.gCabar * s * (Vm - self.ECa) def iKCa(self, ACa, Vm): ''' Calcium-activated Potassium current :param ACa: Calcium pool-dependent gate opening :param Vm: membrane potential (mV) :return: current per unit area (mA/m2) ''' return self.gKCabar * ACa * (Vm - self.EK) def iPumpNa(self, ANa, Vm): ''' NaK-ATPase pump current :param ANa: Sodium pool-dependent gate opening. :param Vm: membrane potential (mV) :return: current per unit area (mA/m2) ''' return self.gPumpNa * ANa * (Vm - self.EPumpNa) def iLeak(self, Vm): ''' Non-specific leakage current :param Vm: membrane potential (mV) :return: current per unit area (mA/m2) ''' return self.gLeak * (Vm - self.ELeak) def currents(self, Vm, states): ''' Overriding of abstract parent method. ''' m, h, n, s, _, ANa, _, ACa = states return { 'iNa': self.iNa(m, h, Vm), 'iK': self.iK(n, Vm), 'iCa': self.iCa(s, Vm), 'iLeak': self.iLeak(Vm), 'iPumpNa': self.iPumpNa(ANa, Vm), 'iKCa': self.iKCa(ACa, Vm) } # mA/m2 def steadyStates(self, Vm): ''' Overriding of abstract parent method. ''' # Standard gating dynamics: Solve the equation dx/dt = 0 at Vm for each x-state sstates = { 'm': self.minf(Vm), 'h': self.hinf(Vm), 'n': self.ninf(Vm), 's': self.sinf(Vm) } # PumpNa pool concentration and activation steady-state sstates['CNa'] = - self.K_Na * self.iNa(sstates['m'], sstates['h'], Vm) sstates['ANa'] = sstates['CNa'] # KCa current pool concentration and activation steady-state sstates['CCa'] = - self.K_Ca * self.iCa(sstates['s'], Vm) sstates['ACa'] = sstates['CCa'] return sstates def derStates(self, Vm, states): ''' Overriding of abstract parent method. ''' m, h, n, s, Nai, ANa, Cai, ACa = states # Standard gating states derivatives dstates = { 'm': self.derM(Vm, m), 'h': self.derH(Vm, h), 'n': self.derN(Vm, n), 's': self.derS(Vm, s) } # PumpNa current pool concentration and activation state dstates['CNa'] = self.derNai(Nai, m, h, Vm) dstates['ANa'] = self.derANa(ANa, Nai) # KCa current pool concentration and activation state dstates['CCa'] = self.derCai(Cai, s, Vm) dstates['ACa'] = self.derACa(ACa, Cai) # Pack derivatives and return return dstates def computeEffRates(self, Vm): ''' Overriding of abstract parent method. ''' return { 'alpham': np.mean(self.minf(Vm) / self.taum(Vm)), 'betam': np.mean((1 - self.minf(Vm)) / self.taum(Vm)), 'alphah': np.mean(self.hinf(Vm) / self.tauh(Vm)), 'betah': np.mean((1 - self.hinf(Vm)) / self.tauh(Vm)), 'alphan': np.mean(self.ninf(Vm) / self.taun(Vm)), 'betan': np.mean((1 - self.ninf(Vm)) / self.taun(Vm)), 'alphas': np.mean(self.sinf(Vm) / self.taus(Vm)), 'betas': np.mean((1 - self.sinf(Vm)) / self.taus(Vm)) } def derEffStates(self, Qm, states, lkp): ''' Overriding of abstract parent method. ''' rates = self.interpEffRates(Qm, lkp) Vmeff = self.interpVmeff(Qm, lkp) m, h, n, s, Nai, ANa, Cai, ACa = states # Standard gating states derivatives dstates = { 'm': rates['alpham'] * (1 - m) - rates['betam'] * m, 'h': rates['alphah'] * (1 - h) - rates['betah'] * h, 'n': rates['alphan'] * (1 - n) - rates['betan'] * n, 's': rates['alphas'] * (1 - s) - rates['betas'] * s } # PumpNa current pool concentration and activation state dstates['CNa'] = self.derNai(Nai, m, h, Vmeff) dstates['ANa'] = self.derANa(ANa, Nai) # KCa current pool concentration and activation state dstates['CCa'] = self.derCai(Cai, s, Vmeff) dstates['ACa'] = self.derACa(ACa, Cai) # Pack derivatives and return return dstates def quasiSteadyStates(self, lkp): ''' Overriding of abstract parent method. ''' qsstates = self.qsStates(lkp, ['m', 'h', 'n', 's']) # PumpNa pool concentration and activation steady-state qsstates['CNa'] = - self.K_Na * self.iNa(qsstates['m'], qsstates['h'], lkp['V']) qsstates['ANa'] = qsstates['CNa'] # KCa current pool concentration and activation steady-state qsstates['CCa'] = - self.K_Ca * self.iCa(qsstates['s'], lkp['V']) qsstates['ACa'] = qsstates['CCa'] return qsstates class LeechMech(PointNeuron): ''' Generic leech neuron 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) + def __init__(self): + super().__init__() 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, Cai): ''' Compute the alpha rate for the open-probability of Calcium-dependent Potassium channels. :param Cai: intracellular Calcium concentration (M) :return: rate constant (s-1) ''' alpha = 0.1 * Cai / 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, Cai): ''' Compute the evolution of the open-probability of Calcium-dependent Potassium channels. :param c: open-probability of Calcium-dependent Potassium channels (prob) :param Cai: intracellular Calcium concentration (M) :return: derivative of open-probability w.r.t. time (prob/s) ''' return self.alphaC(Cai) * (1 - c) - self.betaC * c def iNa(self, m, h, Vm, Nai): ''' Sodium current :param m: open-probability of m-gate :param h: open-probability of Sodium channels :param Vm: membrane potential (mV) :param Nai: intracellular Sodium concentration (M) :return: current per unit area (mA/m2) ''' GNa = self.gNabar * m**4 * h ENa = self.nernst(Z_Na, Nai, self.C_Na_out, self.T) # mV return GNa * (Vm - ENa) def iK(self, n, Vm): ''' Delayed-rectifier Potassium current :param n: open-probability of n-gate :param Vm: membrane potential (mV) :return: current per unit area (mA/m2) ''' GK = self.gKbar * n**2 return GK * (Vm - self.EK) def iCa(self, s, Vm, Cai): ''' Calcium current :param s: open-probability of s-gate :param Vm: membrane potential (mV) :param Cai: intracellular Calcium concentration (M) :return: current per unit area (mA/m2) ''' GCa = self.gCabar * s ECa = self.nernst(Z_Ca, Cai, self.C_Ca_out, self.T) # mV return GCa * (Vm - ECa) def iKCa(self, c, Vm): ''' Calcium-activated Potassium current :param c: open-probability of c-gate :param Vm: membrane potential (mV) :return: current per unit area (mA/m2) ''' GKCa = self.gKCabar * c return GKCa * (Vm - self.EK) def iLeak(self, Vm): ''' Non-specific leakage current :param Vm: membrane potential (mV) :return: current per unit area (mA/m2) ''' return self.gLeak * (Vm - self.ELeak) class LeechPressure(LeechMech): ''' Leech pressure sensory neuron 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) Nai0 = 0.01 # Initial Sodium intracellular concentration (M) Cai0 = 1e-7 # Initial Calcium intracellular concentration (M) # ENa = 60 # Sodium Nernst potential, from MOD file on ModelDB (mV) # ECa = 125 # Calcium Nernst potential, from MOD file on ModelDB (mV) EK = -68.0 # Potassium Nernst potential (mV) ELeak = -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) gNabar = 3500.0 # Max. conductance of Sodium current (S/m^2) gKbar = 60.0 # Max. conductance of Potassium current (S/m^2) gCabar = 0.02 # Max. conductance of Calcium current (S/m^2) gKCabar = 8.0 # Max. conductance of Calcium-dependent Potassium current (S/m^2) gLeak = 5.0 # Conductance of non-specific leakage current (S/m^2) diam = 50e-6 # Cell soma diameter (m) 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 / (Z_Na * FARADAY) * 1e-6 # Sodium (M/s) self.K_Ca = SV_ratio / (Z_Ca * FARADAY) * 1e-6 # Calcium (M/s) # Names and initial states of the channels state probabilities self.states = ['m', 'h', 'n', 's', 'c', 'Nai', 'Cai'] # Names of the channels effective coefficients self.rates = self.getRatesNames(['m', 'h', 'n', 's']) def iPumpNa(self, Nai): ''' NaK-ATPase pump current :param Nai: intracellular Sodium concentration (M) :return: current per unit area (mA/m2) ''' return self.INaPmax / (1 + np.exp((self.khalf_Na - Nai) / self.ksteep_Na)) def iPumpCa(self, Cai): ''' Calcium pump current :param Cai: intracellular Calcium concentration (M) :return: current per unit area (mA/m2) ''' return self.iCaS * (Cai - self.Cai0) / 1.5 def currents(self, Vm, states): ''' Overriding of abstract parent method. ''' m, h, n, s, c, Nai, Cai = states return { 'iNa': self.iNa(m, h, Vm, Nai), 'iK': self.iK(n, Vm), 'iCa': self.iCa(s, Vm, Cai), 'iKCa': self.iKCa(c, Vm), 'iLeak': self.iLeak(Vm), 'iPumpNa': self.iPumpNa(Nai) / 3., 'iPumpCa': self.iPumpCa(Cai) } # mA/m2 def steadyStates(self, Vm): ''' Overriding of abstract parent method. ''' sstates = { 'm': self.alpham(Vm) / (self.alpham(Vm) + self.betam(Vm)), 'h': self.alphah(Vm) / (self.alphah(Vm) + self.betah(Vm)), 'n': self.alphan(Vm) / (self.alphan(Vm) + self.betan(Vm)), 's': self.alphas(Vm) / (self.alphas(Vm) + self.betas(Vm)), 'Nai': self.Nai0, 'Cai': self.Cai0 } sstates['c'] = self.alphaC(sstates['Cai']) / (self.alphaC(sstates['Cai']) + self.betaC) return sstates def derStates(self, Vm, states): ''' Overriding of abstract parent method. ''' m, h, n, s, c, Nai, Cai = states return { 'm': self.derM(Vm, m), 'h': self.derH(Vm, h), 'n': self.derN(Vm, n), 's': self.derS(Vm, s), 'c': self.derC(c, Cai), 'Nai': -(self.iNa(m, h, Vm, Nai) + self.iPumpNa(Nai)) * self.K_Na, # M/s 'Cai': -(self.iCa(s, Vm, Cai) + self.iPumpCa(Cai)) * self.K_Ca # M/s' } def computeEffRates(self, Vm): ''' Overriding of abstract parent method. ''' # Compute average cycle value for rate constants return { 'alpham': np.mean(self.alpham(Vm)), 'betam': np.mean(self.betam(Vm)), 'alphah': np.mean(self.alphah(Vm)), 'betah': np.mean(self.betah(Vm)), 'alphan': np.mean(self.alphan(Vm)), 'betan': np.mean(self.betan(Vm)), 'alphas': np.mean(self.alphas(Vm)), 'betas': np.mean(self.betas(Vm)) } def derEffStates(self, Qm, states, lkp): ''' Overriding of abstract parent method. ''' rates = self.interpEffRates(Qm, lkp) Vmeff = self.interpVmeff(Qm, lkp) m, h, n, s, c, Nai, Cai = states return { 'm': rates['alpham'] * (1 - m) - rates['betam'] * m, 'h': rates['alphah'] * (1 - h) - rates['betah'] * h, 'n': rates['alphan'] * (1 - n) - rates['betan'] * n, 's': rates['alphas'] * (1 - s) - rates['betas'] * s, 'c': self.derC(c, Cai), 'Nai': -(self.iNa(m, h, Vmeff, Nai) + self.iPumpNa(Nai)) * self.K_Na, # M/s 'Cai': -(self.iCa(s, Vmeff, Cai) + self.iPumpCa(Cai)) * self.K_Ca # M/s } def quasiSteadyStates(self, lkp): ''' Overriding of abstract parent method. ''' qsstates = self.qsStates(lkp, ['m', 'h', 'n', 's']) qsstates.update({'Nai': self.Nai0, 'Cai': self.Cai0}) qsstates['c'] = self.alphaC(qsstates['Cai']) / (self.alphaC(qsstates['Cai']) + self.betaC) return qsstates class LeechRetzius(LeechMech): ''' Leech Retzius neuron 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) ENa = 50.0 # Sodium Nernst potential, from retztemp.ses file on ModelDB (mV) ECa = 125.0 # Calcium Nernst potential, from cachdend.mod file on ModelDB (mV) EK = -79.0 # Potassium Nernst potential, from retztemp.ses file on ModelDB (mV) ELeak = -30.0 # Non-specific leakage Nernst potential, from leakdend.mod file on ModelDB (mV) gNabar = 1250.0 # Max. conductance of Sodium current (S/m^2) gKbar = 10.0 # Max. conductance of Potassium current (S/m^2) GAMax = 100.0 # Max. conductance of transient Potassium current (S/m^2) gCabar = 4.0 # Max. conductance of Calcium current (S/m^2) gKCabar = 130.0 # Max. conductance of Calcium-dependent Potassium current (S/m^2) gLeak = 1.25 # Conductance of non-specific leakage current (S/m^2) Vhalf = -73.1 # mV Cai = 5e-8 # Calcium intracellular concentration, from retztemp.ses file (M) def __init__(self): ''' Constructor of the class. ''' self.states = ['m', 'h', 'n', 's', 'c', 'a', 'b'] self.rates = self.getRatesNames([self.states]) 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 * FARADAY / (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 * FARADAY / (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 iA(self, a, b, Vm): ''' Transient Potassium current :param a: open-probability of a-gate :param b: open-probability of b-gate :param Vm: membrane potential (mV) :return: current per unit area (mA/m2) ''' GK = self.GAMax * a * b return GK * (Vm - self.EK) def currents(self, Vm, states): ''' Overriding of abstract parent method. ''' m, h, n, s, c, a, b = states return { 'iNa': self.iNa(m, h, Vm), 'iK': self.iK(n, Vm), 'iCa': self.iCa(s, Vm), 'iLeak': self.iLeak(Vm), 'iKCa': self.iKCa(c, Vm), 'iA': self.iA(a, b, Vm) } # mA/m2 def steadyStates(self, Vm): ''' Overriding of abstract parent method. ''' return { 'm': self.alpham(Vm) / (self.alpham(Vm) + self.betam(Vm)), 'h': self.alphah(Vm) / (self.alphah(Vm) + self.betah(Vm)), 'n': self.alphan(Vm) / (self.alphan(Vm) + self.betan(Vm)), 's': self.alphas(Vm) / (self.alphas(Vm) + self.betas(Vm)), 'c': self.alphaC(self.Cai) / (self.alphaC(self.Cai) + self.betaC), 'a': self.ainf(Vm), 'b': self.binf(Vm) } def derStates(self, Vm, states): ''' Overriding of abstract parent method. ''' m, h, n, s, c, a, b = states return { 'm': self.derM(Vm, m), 'h': self.derH(Vm, h), 'n': self.derN(Vm, n), 's': self.derS(Vm, s), 'c': self.derC(c, self.Cai), 'a': self.derA(Vm, a), 'b': self.derB(Vm, b) } def computeEffRates(self, Vm): ''' Overriding of abstract parent method. ''' # Compute average cycle value for rate constants return { 'alpham': np.mean(self.alpham(Vm)), 'betam': np.mean(self.betam(Vm)), 'alphah': np.mean(self.alphah(Vm)), 'betah': np.mean(self.betah(Vm)), 'alphan': np.mean(self.alphan(Vm)), 'betan': np.mean(self.betan(Vm)), 'alphas': np.mean(self.alphas(Vm)), 'betas': np.mean(self.betas(Vm)), 'alphaa': np.mean(self.ainf(Vm) / self.taua(Vm)), 'betaa': np.mean((1 - self.ainf(Vm)) / self.taua(Vm)), 'alphab': np.mean(self.binf(Vm) / self.taub(Vm)), 'betab': np.mean((1 - self.binf(Vm)) / self.taub(Vm)) } def derEffStates(self, Qm, states, lkp): ''' Overriding of abstract parent method. ''' rates = self.interpEffRates(Qm, lkp) m, h, n, s, c, a, b = states # Standard gating states derivatives return { 'm': rates['alpham'] * (1 - m) - rates['betam'] * m, 'h': rates['alphah'] * (1 - h) - rates['betah'] * h, 'n': rates['alpham'] * (1 - n) - rates['betam'] * n, 's': rates['alphas'] * (1 - s) - rates['betas'] * s, 'a': rates['alphaa'] * (1 - a) - rates['betaa'] * a, 'b': rates['alphab'] * (1 - b) - rates['betab'] * b, 'c': self.derC(c, self.Cai) } def quasiSteadyStates(self, lkp): ''' Overriding of abstract parent method. ''' qsstates = self.qsStates(lkp, ['m', 'h', 'n', 's', 'a', 'b']) qsstates['c'] = self.alphaC(self.Cai) / (self.alphaC(self.Cai) + self.betaC), return qsstates diff --git a/PySONIC/neurons/stn.py b/PySONIC/neurons/stn.py index 1575e2e..9189700 100644 --- a/PySONIC/neurons/stn.py +++ b/PySONIC/neurons/stn.py @@ -1,618 +1,619 @@ # -*- coding: utf-8 -*- # @Author: Theo Lemaire # @Date: 2018-11-29 16:56:45 # @Last Modified by: Theo Lemaire -# @Last Modified time: 2019-06-01 16:37:32 +# @Last Modified time: 2019-06-02 12:33:58 import numpy as np from scipy.optimize import brentq from ..core import PointNeuron from ..constants import FARADAY, Z_Ca class OtsukaSTN(PointNeuron): ''' Sub-thalamic nucleus neuron References: *Otsuka, T., Abe, T., Tsukagawa, T., and Song, W.-J. (2004). Conductance-Based Model of the Voltage-Dependent Generation of a Plateau Potential in Subthalamic Neurons. Journal of Neurophysiology 92, 255–264.* *Tarnaud, T., Joseph, W., Martens, L., and Tanghe, E. (2018). Computational Modeling of Ultrasonic Subthalamic Nucleus Stimulation. IEEE Trans Biomed Eng.* ''' name = 'STN' # Resting parameters Cm0 = 1e-2 # Cell membrane resting capacitance (F/m2) Vm0 = -58.0 # Resting membrane potential (mV) Cai0 = 5e-9 # M (5 nM) # Reversal potentials ENa = 60.0 # Sodium Nernst potential (mV) EK = -90.0 # Potassium Nernst potential (mV) # Physical constants T = 306.15 # K (33°C) # Calcium dynamics Cao = 2e-3 # M (2 mM) taur_Cai = 0.5e-3 # decay time constant for intracellular Ca2+ dissolution (s) # Leakage current gLeak = 3.5 # Conductance of non-specific leakage current (S/m^2) ELeak = -60.0 # Leakage reversal potential (mV) # Fast Na current gNabar = 490.0 # Max. conductance of Sodium current (S/m^2) thetax_m = -40 # mV thetax_h = -45.5 # mV kx_m = -8 # mV kx_h = 6.4 # mV tau0_m = 0.2 * 1e-3 # s tau1_m = 3 * 1e-3 # s tau0_h = 0 * 1e-3 # s tau1_h = 24.5 * 1e-3 # s thetaT_m = -53 # mV thetaT1_h = -50 # mV thetaT2_h = -50 # mV sigmaT_m = -0.7 # mV sigmaT1_h = -15 # mV sigmaT2_h = 16 # mV # Delayed rectifier K+ current gKdbar = 570.0 # Max. conductance of delayed-rectifier Potassium current (S/m^2) thetax_n = -41 # mV kx_n = -14 # mV tau0_n = 0 * 1e-3 # s tau1_n = 11 * 1e-3 # s thetaT1_n = -40 # mV thetaT2_n = -40 # mV sigmaT1_n = -40 # mV sigmaT2_n = 50 # mV # T-type Ca2+ current gCaTbar = 50.0 # Max. conductance of low-threshold Calcium current (S/m^2) thetax_p = -56 # mV thetax_q = -85 # mV kx_p = -6.7 # mV kx_q = 5.8 # mV tau0_p = 5 * 1e-3 # s tau1_p = 0.33 * 1e-3 # s tau0_q = 0 * 1e-3 # s tau1_q = 400 * 1e-3 # s thetaT1_p = -27 # mV thetaT2_p = -102 # mV thetaT1_q = -50 # mV thetaT2_q = -50 # mV sigmaT1_p = -10 # mV sigmaT2_p = 15 # mV sigmaT1_q = -15 # mV sigmaT2_q = 16 # mV # L-type Ca2+ current gCaLbar = 150.0 # Max. conductance of high-threshold Calcium current (S/m^2) thetax_c = -30.6 # mV thetax_d1 = -60 # mV thetax_d2 = 0.1 * 1e-6 # M kx_c = -5 # mV kx_d1 = 7.5 # mV kx_d2 = 0.02 * 1e-6 # M tau0_c = 45 * 1e-3 # s tau1_c = 10 * 1e-3 # s tau0_d1 = 400 * 1e-3 # s tau1_d1 = 500 * 1e-3 # s tau_d2 = 130 * 1e-3 # s thetaT1_c = -27 # mV thetaT2_c = -50 # mV thetaT1_d1 = -40 # mV thetaT2_d1 = -20 # mV sigmaT1_c = -20 # mV sigmaT2_c = 15 # mV sigmaT1_d1 = -15 # mV sigmaT2_d1 = 20 # mV # A-type K+ current gAbar = 50.0 # Max. conductance of A-type Potassium current (S/m^2) thetax_a = -45 # mV thetax_b = -90 # mV kx_a = -14.7 # mV kx_b = 7.5 # mV tau0_a = 1 * 1e-3 # s tau1_a = 1 * 1e-3 # s tau0_b = 0 * 1e-3 # s tau1_b = 200 * 1e-3 # s thetaT_a = -40 # mV thetaT1_b = -60 # mV thetaT2_b = -40 # mV sigmaT_a = -0.5 # mV sigmaT1_b = -30 # mV sigmaT2_b = 10 # mV # Ca2+-activated K+ current gKCabar = 10.0 # Max. conductance of Calcium-dependent Potassium current (S/m^2) thetax_r = 0.17 * 1e-6 # M kx_r = -0.08 * 1e-6 # M tau_r = 2 * 1e-3 # s def __init__(self): + super().__init__() self.states = ['a', 'b', 'c', 'd1', 'd2', 'm', 'h', 'n', 'p', 'q', 'r', 'Cai'] self.rates = self.getRatesNames(['a', 'b', 'c', 'd1', 'm', 'h', 'n', 'p', 'q']) self.deff = self.getEffectiveDepth(self.Cai0, self.Vm0) # m self.iCa_to_Cai_rate = self.currentToConcentrationRate(Z_Ca, self.deff) # Mmol.m-1.C-1 def getPltScheme(self): pltscheme = super().getPltScheme() pltscheme['[Ca^{2+}]_i'] = ['Cai'] return pltscheme def getPltVars(self, wrapleft='df["', wrapright='"]'): pltvars = super().getPltVars(wrapleft, wrapright) pltvars['Cai'] = { 'desc': 'submembrane Ca2+ concentration', 'label': '[Ca^{2+}]_i', 'unit': 'uM', 'factor': 1e6 } return pltvars def titrationFunc(self, *args, **kwargs): ''' Overriding default titration function. ''' return self.isSilenced(*args, **kwargs) def getEffectiveDepth(self, Cai, Vm): ''' Compute effective depth that matches a given membrane potential and intracellular Calcium concentration. :return: effective depth (m) ''' iCaT = self.iCaT(self.pinf(Vm), self.qinf(Vm), Vm, Cai) # mA/m2 iCaL = self.iCaL(self.cinf(Vm), self.d1inf(Vm), self.d2inf(Cai), Vm, Cai) # mA/m2 return -(iCaT + iCaL) / (Z_Ca * FARADAY * Cai / self.taur_Cai) * 1e-6 # m def _xinf(self, var, theta, k): ''' Generic function computing the steady-state opening of a particular channel gate at a given voltage or ion concentration. :param var: membrane potential (mV) or ion concentration (mM) :param theta: half-(in)activation voltage or concentration (mV or mM) :param k: slope parameter of (in)activation function (mV or mM) :return: steady-state opening (-) ''' return 1 / (1 + np.exp((var - theta) / k)) def ainf(self, Vm): return self._xinf(Vm, self.thetax_a, self.kx_a) def binf(self, Vm): return self._xinf(Vm, self.thetax_b, self.kx_b) def cinf(self, Vm): return self._xinf(Vm, self.thetax_c, self.kx_c) def d1inf(self, Vm): return self._xinf(Vm, self.thetax_d1, self.kx_d1) def d2inf(self, Cai): return self._xinf(Cai, self.thetax_d2, self.kx_d2) def minf(self, Vm): return self._xinf(Vm, self.thetax_m, self.kx_m) def hinf(self, Vm): return self._xinf(Vm, self.thetax_h, self.kx_h) def ninf(self, Vm): return self._xinf(Vm, self.thetax_n, self.kx_n) def pinf(self, Vm): return self._xinf(Vm, self.thetax_p, self.kx_p) def qinf(self, Vm): return self._xinf(Vm, self.thetax_q, self.kx_q) def rinf(self, Cai): return self._xinf(Cai, self.thetax_r, self.kx_r) def _taux1(self, Vm, theta, sigma, tau0, tau1): ''' Generic function computing the voltage-dependent, activation/inactivation time constant of a particular ion channel at a given voltage (first variant). :param Vm: membrane potential (mV) :param theta: voltage at which (in)activation time constant is half-maximal (mV) :param sigma: slope parameter of (in)activation time constant function (mV) :param tau0: minimal time constant (s) :param tau1: modulated time constant (s) :return: (in)activation time constant (s) ''' return tau0 + tau1 / (1 + np.exp(-(Vm - theta) / sigma)) def taua(self, Vm): return self._taux1(Vm, self.thetaT_a, self.sigmaT_a, self.tau0_a, self.tau1_a) def taum(self, Vm): return self._taux1(Vm, self.thetaT_m, self.sigmaT_m, self.tau0_m, self.tau1_m) def _taux2(self, Vm, theta1, theta2, sigma1, sigma2, tau0, tau1): ''' Generic function computing the voltage-dependent, activation/inactivation time constant of a particular ion channel at a given voltage (second variant). :param Vm: membrane potential (mV) :param theta: voltage at which (in)activation time constant is half-maximal (mV) :param sigma: slope parameter of (in)activation time constant function (mV) :param tau0: minimal time constant (s) :param tau1: modulated time constant (s) :return: (in)activation time constant (s) ''' return tau0 + tau1 / (np.exp(-(Vm - theta1) / sigma1) + np.exp(-(Vm - theta2) / sigma2)) def taub(self, Vm): return self._taux2(Vm, self.thetaT1_b, self.thetaT2_b, self.sigmaT1_b, self.sigmaT2_b, self.tau0_b, self.tau1_b) def tauc(self, Vm): return self._taux2(Vm, self.thetaT1_c, self.thetaT2_c, self.sigmaT1_c, self.sigmaT2_c, self.tau0_c, self.tau1_c) def taud1(self, Vm): return self._taux2(Vm, self.thetaT1_d1, self.thetaT2_d1, self.sigmaT1_d1, self.sigmaT2_d1, self.tau0_d1, self.tau1_d1) def tauh(self, Vm): return self._taux2(Vm, self.thetaT1_h, self.thetaT2_h, self.sigmaT1_h, self.sigmaT2_h, self.tau0_h, self.tau1_h) def taun(self, Vm): return self._taux2(Vm, self.thetaT1_n, self.thetaT2_n, self.sigmaT1_n, self.sigmaT2_n, self.tau0_n, self.tau1_n) def taup(self, Vm): return self._taux2(Vm, self.thetaT1_p, self.thetaT2_p, self.sigmaT1_p, self.sigmaT2_p, self.tau0_p, self.tau1_p) def tauq(self, Vm): return self._taux2(Vm, self.thetaT1_q, self.thetaT2_q, self.sigmaT1_q, self.sigmaT2_q, self.tau0_q, self.tau1_q) def derA(self, Vm, a): ''' Evolution of a-gate open-probability :param Vm: membrane potential (mV) :param a: open-probability of a-gate (-) :return: time derivative of a-gate open-probability (s-1) ''' return (self.ainf(Vm) - a) / self.taua(Vm) def derB(self, Vm, b): ''' Evolution of b-gate open-probability :param Vm: membrane potential (mV) :param b: open-probability of b-gate (-) :return: time derivative of b-gate open-probability (s-1) ''' return (self.binf(Vm) - b) / self.taub(Vm) def derC(self, Vm, c): ''' Evolution of c-gate open-probability :param Vm: membrane potential (mV) :param c: open-probability of c-gate (-) :return: time derivative of c-gate open-probability (s-1) ''' return (self.cinf(Vm) - c) / self.tauc(Vm) def derD1(self, Vm, d1): ''' Evolution of d1-gate open-probability :param Vm: membrane potential (mV) :param d1: open-probability of d1-gate (-) :return: time derivative of d1-gate open-probability (s-1) ''' return (self.d1inf(Vm) - d1) / self.taud1(Vm) def derD2(self, Cai, d2): ''' Evolution of Calcium-dependent d2-gate open-probability :param Vm: membrane potential (mV) :param d2: open-probability of d2-gate (-) :return: time derivative of d2-gate open-probability (s-1) ''' return (self.d2inf(Cai) - d2) / self.tau_d2 def derM(self, Vm, m): ''' Evolution of m-gate open-probability :param Vm: membrane potential (mV) :param m: open-probability of m-gate (-) :return: time derivative of m-gate open-probability (s-1) ''' return (self.minf(Vm) - m) / self.taum(Vm) def derH(self, Vm, h): ''' Evolution of h-gate open-probability :param Vm: membrane potential (mV) :param h: open-probability of h-gate (-) :return: time derivative of h-gate open-probability (s-1) ''' return (self.hinf(Vm) - h) / self.tauh(Vm) def derN(self, Vm, n): ''' Evolution of n-gate open-probability :param Vm: membrane potential (mV) :param n: open-probability of n-gate (-) :return: time derivative of n-gate open-probability (s-1) ''' return (self.ninf(Vm) - n) / self.taun(Vm) def derP(self, Vm, p): ''' Evolution of p-gate open-probability :param Vm: membrane potential (mV) :param p: open-probability of p-gate (-) :return: time derivative of p-gate open-probability (s-1) ''' return (self.pinf(Vm) - p) / self.taup(Vm) def derQ(self, Vm, q): ''' Evolution of q-gate open-probability :param Vm: membrane potential (mV) :param q: open-probability of q-gate (-) :return: time derivative of q-gate open-probability (s-1) ''' return (self.qinf(Vm) - q) / self.tauq(Vm) def derR(self, Cai, r): ''' Evolution of Calcium-dependent r-gate open-probability :param Vm: membrane potential (mV) :param s: open-probability of r-gate (-) :return: time derivative of r-gate open-probability (s-1) ''' return (self.rinf(Cai) - r) / self.tau_r def derCai(self, p, q, c, d1, d2, Cai, Vm): ''' Evolution of Calcium concentration in submembrane space. :param p: open-probability of p-gate :param q: open-probability of q-gate :param c: open-probability of c-gate :param d1: open-probability of d1-gate :param d2: open-probability of d2-gate :param Cai: Calcium concentration in submembranal space (M) :param Vm: membrane potential (mV) :return: time derivative of Calcium concentration in submembrane space (M/s) ''' iCaT = self.iCaT(p, q, Vm, Cai) iCaL = self.iCaL(c, d1, d2, Vm, Cai) return - self.iCa_to_Cai_rate * (iCaT + iCaL) - Cai / self.taur_Cai def Caiinf(self, Vm, p, q, c, d1): ''' Find the steady-state intracellular Calcium concentration for a specific membrane potential and voltage-gated channel states. :param Vm: membrane potential (mV) :param p: open-probability of p-gate :param q: open-probability of q-gate :param c: open-probability of c-gate :param d1: open-probability of d1-gate :return: steady-state Calcium concentration in submembrane space (M) ''' if isinstance(Vm, np.ndarray): return np.array([self.Caiinf(Vm[i], p[i], q[i], c[i], d1[i]) for i in range(Vm.size)]) else: return brentq( lambda x: self.derCai(p, q, c, d1, self.d2inf(x), x, Vm), self.Cai0 * 1e-4, self.Cai0 * 1e3, xtol=1e-16 ) def iNa(self, m, h, Vm): ''' Sodium current :param m: open-probability of m-gate (-) :param h: open-probability of h-gate (-) :param Vm: membrane potential (mV) :return: current per unit area (mA/m2) ''' return self.gNabar * m**3 * h * (Vm - self.ENa) def iKd(self, n, Vm): ''' delayed-rectifier Potassium current :param n: open-probability of n-gate (-) :param Vm: membrane potential (mV) :return: current per unit area (mA/m2) ''' return self.gKdbar * n**4 * (Vm - self.EK) def iA(self, a, b, Vm): ''' A-type Potassium current :param a: open-probability of a-gate (-) :param b: open-probability of b-gate (-) :param Vm: membrane potential (mV) :return: current per unit area (mA/m2) ''' return self.gAbar * a**2 * b * (Vm - self.EK) def iCaT(self, p, q, Vm, Cai): ''' low-threshold (T-type) Calcium current :param p: open-probability of p-gate (-) :param q: open-probability of q-gate (-) :param Vm: membrane potential (mV) :param Cai: submembrane Calcium concentration (M) :return: current per unit area (mA/m2) ''' return self.gCaTbar * p**2 * q * (Vm - self.nernst(Z_Ca, Cai, self.Cao, self.T)) def iCaL(self, c, d1, d2, Vm, Cai): ''' high-threshold (L-type) Calcium current :param c: open-probability of c-gate (-) :param d1: open-probability of d1-gate (-) :param d2: open-probability of d2-gate (-) :param Vm: membrane potential (mV) :param Cai: submembrane Calcium concentration (M) :return: current per unit area (mA/m2) ''' return self.gCaLbar * c**2 * d1 * d2 * (Vm - self.nernst(Z_Ca, Cai, self.Cao, self.T)) def iKCa(self, r, Vm): ''' Calcium-activated Potassium current :param r: open-probability of r-gate (-) :param Vm: membrane potential (mV) :return: current per unit area (mA/m2) ''' return self.gKCabar * r**2 * (Vm - self.EK) def iLeak(self, Vm): ''' non-specific leakage current :param Vm: membrane potential (mV) :return: current per unit area (mA/m2) ''' return self.gLeak * (Vm - self.ELeak) def currents(self, Vm, states): ''' Overriding of abstract parent method. ''' a, b, c, d1, d2, m, h, n, p, q, r, Cai = states return { 'iNa': self.iNa(m, h, Vm), 'iKd': self.iKd(n, Vm), 'iA': self.iA(a, b, Vm), 'iCaT': self.iCaT(p, q, Vm, Cai), 'iCaL': self.iCaL(c, d1, d2, Vm, Cai), 'iKCa': self.iKCa(r, Vm), 'iLeak': self.iLeak(Vm) } # mA/m2 def steadyStates(self, Vm): ''' Overriding of abstract parent method. ''' # voltage-gated steady states sstates = { 'a': self.ainf(Vm), 'b': self.binf(Vm), 'c': self.cinf(Vm), 'd1': self.d1inf(Vm), 'm': self.minf(Vm), 'h': self.hinf(Vm), 'n': self.ninf(Vm), 'p': self.pinf(Vm), 'q': self.qinf(Vm) } sstates['Cai'] = self.Caiinf(Vm, sstates['p'], sstates['q'], sstates['c'], sstates['d1']) sstates['d2'] = self.d2inf(sstates['Cai']) sstates['r'] = self.rinf(sstates['Cai']) return sstates def derStates(self, Vm, states): ''' Overriding of abstract parent method. ''' a, b, c, d1, d2, m, h, n, p, q, r, Cai = states return { 'a': self.derA(Vm, a), 'b': self.derB(Vm, b), 'c': self.derC(Vm, c), 'd1': self.derD1(Vm, d1), 'd2': self.derD2(Cai, d2), 'm': self.derM(Vm, m), 'h': self.derH(Vm, h), 'n': self.derN(Vm, n), 'p': self.derP(Vm, p), 'q': self.derQ(Vm, q), 'r': self.derR(Cai, r), 'Cai': self.derCai(p, q, c, d1, d2, Cai, Vm), } def computeEffRates(self, Vm): ''' Overriding of abstract parent method. ''' # Compute average cycle value for rate constants return { 'alphaa': np.mean(self.ainf(Vm) / self.taua(Vm)), 'betaa': np.mean((1 - self.ainf(Vm)) / self.taua(Vm)), 'alphab': np.mean(self.binf(Vm) / self.taub(Vm)), 'betab': np.mean((1 - self.binf(Vm)) / self.taub(Vm)), 'alphac': np.mean(self.cinf(Vm) / self.tauc(Vm)), 'betac': np.mean((1 - self.cinf(Vm)) / self.tauc(Vm)), 'alphad1': np.mean(self.d1inf(Vm) / self.taud1(Vm)), 'betad1': np.mean((1 - self.d1inf(Vm)) / self.taud1(Vm)), 'alpham': np.mean(self.minf(Vm) / self.taum(Vm)), 'betam': np.mean((1 - self.minf(Vm)) / self.taum(Vm)), 'alphah': np.mean(self.hinf(Vm) / self.tauh(Vm)), 'betah': np.mean((1 - self.hinf(Vm)) / self.tauh(Vm)), 'alphan': np.mean(self.ninf(Vm) / self.taun(Vm)), 'betan': np.mean((1 - self.ninf(Vm)) / self.taun(Vm)), 'alphap': np.mean(self.pinf(Vm) / self.taup(Vm)), 'betap': np.mean((1 - self.pinf(Vm)) / self.taup(Vm)), 'alphaq': np.mean(self.qinf(Vm) / self.tauq(Vm)), 'betaq': np.mean((1 - self.qinf(Vm)) / self.tauq(Vm)) } def derEffStates(self, Qm, states, lkp): ''' Overriding of abstract parent method. ''' rates = self.interpEffRates(Qm, lkp) Vmeff = self.interpVmeff(Qm, lkp) a, b, c, d1, d2, m, h, n, p, q, r, Cai = states return { 'a': rates['alphaa'] * (1 - a) - rates['betaa'] * a, 'b': rates['alphab'] * (1 - b) - rates['betab'] * b, 'c': rates['alphac'] * (1 - c) - rates['betac'] * c, 'd1': rates['alphad1'] * (1 - d1) - rates['betad1'] * d1, 'd2': self.derD2(Cai, d2), 'm': rates['alpham'] * (1 - m) - rates['betam'] * m, 'h': rates['alphah'] * (1 - h) - rates['betah'] * h, 'n': rates['alphan'] * (1 - n) - rates['betan'] * n, 'p': rates['alphap'] * (1 - p) - rates['betap'] * p, 'q': rates['alphaq'] * (1 - q) - rates['betaq'] * q, 'r': self.derR(Cai, r), 'Cai': self.derCai(p, q, c, d1, d2, Cai, Vmeff) } def quasiSteadyStates(self, lkp): ''' Overriding of abstract parent method. ''' qsstates = self.qsStates(lkp, ['a', 'b', 'c', 'd1', 'm', 'h', 'n', 'p', 'q']) qsstates['Cai'] = self.Caiinf(lkp['V'], qsstates['p'], qsstates['q'], qsstates['c'], qsstates['d1']) qsstates['d2'] = self.d2inf(qsstates['Cai']) qsstates['r'] = self.rinf(qsstates['Cai']) return qsstates diff --git a/PySONIC/neurons/thalamic.py b/PySONIC/neurons/thalamic.py index c8fe68c..41d0399 100644 --- a/PySONIC/neurons/thalamic.py +++ b/PySONIC/neurons/thalamic.py @@ -1,667 +1,668 @@ #!/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: 2019-05-16 15:25:21 +# @Last Modified time: 2019-06-02 12:32:41 import numpy as np from ..core import PointNeuron from ..constants import Z_Ca class Thalamic(PointNeuron): ''' Generic thalamic neuron 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) ENa = 50.0 # Sodium Nernst potential (mV) EK = -90.0 # Potassium Nernst potential (mV) ECa = 120.0 # Calcium Nernst potential (mV) def __init__(self): + super().__init__() self.states = ['m', 'h', 'n', 's', 'u'] self.rates = self.getRatesNames(self.states) def alpham(self, Vm): ''' Voltage-dependent activation rate of m-gate :param Vm: membrane potential (mV) :return: rate (s-1) ''' Vdiff = Vm - self.VT alpha = 0.32 * self.vtrap(13 - Vdiff, 4) # ms-1 return alpha * 1e3 # s-1 def betam(self, Vm): ''' Voltage-dependent inactivation rate of m-gate :param Vm: membrane potential (mV) :return: rate (s-1) ''' Vdiff = Vm - self.VT beta = 0.28 * self.vtrap(Vdiff - 40, 5) # ms-1 return beta * 1e3 # s-1 def alphah(self, Vm): ''' Voltage-dependent activation rate of h-gate :param Vm: membrane potential (mV) :return: rate (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): ''' Voltage-dependent inactivation rate of h-gate :param Vm: membrane potential (mV) :return: rate (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): ''' Voltage-dependent activation rate of n-gate :param Vm: membrane potential (mV) :return: rate (s-1) ''' Vdiff = Vm - self.VT alpha = 0.032 * self.vtrap(15 - Vdiff, 5) # ms-1 return alpha * 1e3 # s-1 def betan(self, Vm): ''' Voltage-dependent inactivation rate of n-gate :param Vm: membrane potential (mV) :return: rate (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): ''' Evolution of m-gate open-probability :param Vm: membrane potential (mV) :param m: open-probability of m-gate (-) :return: time derivative of m-gate open-probability (s-1) ''' return self.alpham(Vm) * (1 - m) - self.betam(Vm) * m def derH(self, Vm, h): ''' Evolution of h-gate open-probability :param Vm: membrane potential (mV) :param h: open-probability of h-gate (-) :return: time derivative of h-gate open-probability (s-1) ''' return self.alphah(Vm) * (1 - h) - self.betah(Vm) * h def derN(self, Vm, n): ''' Evolution of n-gate open-probability :param Vm: membrane potential (mV) :param n: open-probability of n-gate (-) :return: time derivative of n-gate open-probability (s-1) ''' return self.alphan(Vm) * (1 - n) - self.betan(Vm) * n def derS(self, Vm, s): ''' Evolution of s-gate open-probability :param Vm: membrane potential (mV) :param s: open-probability of s-gate (-) :return: time derivative of s-gate open-probability (s-1) ''' return (self.sinf(Vm) - s) / self.taus(Vm) def derU(self, Vm, u): ''' Evolution of u-gate open-probability :param Vm: membrane potential (mV) :param u: open-probability of u-gate (-) :return: time derivative of u-gate open-probability (s-1) ''' return (self.uinf(Vm) - u) / self.tauu(Vm) def iNa(self, m, h, Vm): ''' Sodium current :param m: open-probability of m-gate (-) :param h: open-probability of h-gate (-) :param Vm: membrane potential (mV) :return: current per unit area (mA/m2) ''' return self.gNabar * m**3 * h * (Vm - self.ENa) def iKd(self, n, Vm): ''' delayed-rectifier Potassium current :param n: open-probability of n-gate (-) :param Vm: membrane potential (mV) :return: current per unit area (mA/m2) ''' return self.gKdbar * n**4 * (Vm - self.EK) def iCaT(self, s, u, Vm): ''' low-threshold (Ts-type) Calcium current :param s: open-probability of s-gate (-) :param u: open-probability of u-gate (-) :param Vm: membrane potential (mV) :return: current per unit area (mA/m2) ''' return self.gCaTbar * s**2 * u * (Vm - self.ECa) def iLeak(self, Vm): ''' non-specific leakage current :param Vm: membrane potential (mV) :return: current per unit area (mA/m2) ''' return self.gLeak * (Vm - self.ELeak) def currents(self, Vm, states): ''' Overriding of abstract parent method. ''' m, h, n, s, u = states return { 'iNa': self.iNa(m, h, Vm), 'iKd': self.iKd(n, Vm), 'iCaT': self.iCaT(s, u, Vm), 'iLeak': self.iLeak(Vm) } # mA/m2 def steadyStates(self, Vm): ''' Overriding of abstract parent method. ''' # Voltage-gated steady-states return { 'm': self.alpham(Vm) / (self.alpham(Vm) + self.betam(Vm)), 'h': self.alphah(Vm) / (self.alphah(Vm) + self.betah(Vm)), 'n': self.alphan(Vm) / (self.alphan(Vm) + self.betan(Vm)), 's': self.sinf(Vm), 'u': self.uinf(Vm) } def derStates(self, Vm, states): ''' Overriding of abstract parent method. ''' m, h, n, s, u = states return { 'm': self.derM(Vm, m), 'h': self.derH(Vm, h), 'n': self.derN(Vm, n), 's': self.derS(Vm, s), 'u': self.derU(Vm, u) } def computeEffRates(self, Vm): ''' Overriding of abstract parent method. ''' # Compute average cycle value for rate constants return { 'alpham': np.mean(self.alpham(Vm)), 'betam': np.mean(self.betam(Vm)), 'alphah': np.mean(self.alphah(Vm)), 'betah': np.mean(self.betah(Vm)), 'alphan': np.mean(self.alphan(Vm)), 'betan': np.mean(self.betan(Vm)), 'alphas': np.mean(self.sinf(Vm) / self.taus(Vm)), 'betas': np.mean((1 - self.sinf(Vm)) / self.taus(Vm)), 'alphau': np.mean(self.uinf(Vm) / self.tauu(Vm)), 'betau': np.mean((1 - self.uinf(Vm)) / self.tauu(Vm)) } def derEffStates(self, Qm, states, lkp): ''' Overriding of abstract parent method. ''' rates = self.interpEffRates(Qm, lkp) m, h, n, s, u = states return { 'm': rates['alpham'] * (1 - m) - rates['betam'] * m, 'h': rates['alphah'] * (1 - h) - rates['betah'] * h, 'n': rates['alphan'] * (1 - n) - rates['betan'] * n, 's': rates['alphas'] * (1 - s) - rates['betas'] * s, 'u': rates['alphau'] * (1 - u) - rates['betau'] * u } def quasiSteadyStates(self, lkp): ''' Overriding of abstract parent method. ''' return self.qsStates(lkp, ['m', 'h', 'n', 's', 'u']) class ThalamicRE(Thalamic): ''' 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) gNabar = 2000.0 # Max. conductance of Sodium current (S/m^2) gKdbar = 200.0 # Max. conductance of Potassium current (S/m^2) gCaTbar = 30.0 # Max. conductance of low-threshold Calcium current (S/m^2) gLeak = 0.5 # Conductance of non-specific leakage current (S/m^2) ELeak = -90.0 # Non-specific leakage Nernst potential (mV) VT = -67.0 # Spike threshold adjustment parameter (mV) def __init__(self): super().__init__() def sinf(self, Vm): ''' Voltage-dependent steady-state opening of s-gate :param Vm: membrane potential (mV) :return: steady-state opening (-) ''' return 1.0 / (1.0 + np.exp(-(Vm + 52.0) / 7.4)) # prob def taus(self, Vm): ''' Voltage-dependent adaptation time for adaptation of s-gate :param Vm: membrane potential (mV) :return: adaptation time (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): ''' Voltage-dependent steady-state opening of u-gate :param Vm: membrane potential (mV) :return: steady-state opening (-) ''' return 1.0 / (1.0 + np.exp((Vm + 80.0) / 5.0)) # prob def tauu(self, Vm): ''' Voltage-dependent adaptation time for adaptation of u-gate :param Vm: membrane potential (mV) :return: adaptation time (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): ''' Thalamo-cortical neuron 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) gNabar = 900.0 # bar. conductance of Sodium current (S/m^2) gKdbar = 100.0 # bar. conductance of Potassium current (S/m^2) gCaTbar = 20.0 # Max. conductance of low-threshold Calcium current (S/m^2) gKLeak = 0.138 # Conductance of leakage Potassium current (S/m^2) gHbar = 0.175 # Max. conductance of mixed cationic current (S/m^2) gLeak = 0.1 # Conductance of non-specific leakage current (S/m^2) EH = -40.0 # Mixed cationic current reversal potential (mV) ELeak = -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) taur_Cai = 5e-3 # decay time constant for intracellular Ca2+ dissolution (s) Cai_min = 50e-9 # minimal intracellular Calcium concentration (M) deff = 100e-9 # effective depth beneath membrane for intracellular [Ca2+] calculation 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) def __init__(self): super().__init__() self.iCa_to_Cai_rate = self.currentToConcentrationRate(Z_Ca, self.deff) self.states += ['O', 'C', 'P0', 'Cai'] self.rates += self.getRatesNames(['O']) def getPltScheme(self): pltscheme = super().getPltScheme() pltscheme['i_{H}\\ kin.'] = ['O', 'OL', 'P0'] pltscheme['[Ca^{2+}]_i'] = ['Cai'] return pltscheme def getPltVars(self, wrapleft='df["', wrapright='"]'): pltvars = super().getPltVars(wrapleft, wrapright) pltvars.update({ 'Cai': { 'desc': 'sumbmembrane Ca2+ concentration', 'label': '[Ca^{2+}]_i', 'unit': 'uM', 'factor': 1e6 }, 'OL': { 'desc': 'iH O-gate locked-opening', 'label': 'O_L', 'bounds': (-0.1, 1.1), 'func': 'OL({0}O{1}, {0}C{1})'.format(wrapleft, wrapright) }, 'P0': { 'desc': 'iH regulating factor activation', 'label': 'P_0', 'bounds': (-0.1, 1.1) } }) return pltvars def sinf(self, Vm): ''' Voltage-dependent steady-state opening of s-gate :param Vm: membrane potential (mV) :return: steady-state opening (-) ''' return 1.0 / (1.0 + np.exp(-(Vm + self.Vx + 57.0) / 6.2)) # prob def taus(self, Vm): ''' Voltage-dependent adaptation time for adaptation of s-gate :param Vm: membrane potential (mV) :return: adaptation time (s) ''' x = 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 / x) * 1e-3 # s def uinf(self, Vm): ''' Voltage-dependent steady-state opening of u-gate :param Vm: membrane potential (mV) :return: steady-state opening (-) ''' return 1.0 / (1.0 + np.exp((Vm + self.Vx + 81.0) / 4.0)) # prob def tauu(self, Vm): ''' Voltage-dependent adaptation time for adaptation of u-gate :param Vm: membrane potential (mV) :return: adaptation time (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): ''' Evolution of s-gate open-probability :param Vm: membrane potential (mV) :param s: open-probability of s-gate (-) :return: time derivative of s-gate open-probability (s-1) ''' return (self.sinf(Vm) - s) / self.taus(Vm) def derU(self, Vm, u): ''' Evolution of u-gate open-probability :param Vm: membrane potential (mV) :param u: open-probability of u-gate (-) :return: time derivative of u-gate open-probability (s-1) ''' return (self.uinf(Vm) - u) / self.tauu(Vm) def oinf(self, Vm): ''' Voltage-dependent steady-state opening of O-gate :param Vm: membrane potential (mV) :return: steady-state opening (-) ''' return 1.0 / (1.0 + np.exp((Vm + 75.0) / 5.5)) def tauo(self, Vm): ''' Voltage-dependent adaptation time for adaptation of O-gate :param Vm: membrane potential (mV) :return: adaptation time (s) ''' return 1 / (np.exp(-14.59 - 0.086 * Vm) + np.exp(-1.87 + 0.0701 * Vm)) * 1e-3 def alphao(self, Vm): ''' Voltage-dependent transition rate between closed and open forms of O-gate :param Vm: membrane potential (mV) :return: rate (s-1) ''' return self.oinf(Vm) / self.tauo(Vm) def betao(self, Vm): ''' Voltage-dependent transition rate between open and closed forms of O-gate :param Vm: membrane potential (mV) :return: rate (s-1) ''' return (1 - self.oinf(Vm)) / self.tauo(Vm) def derC(self, C, O, Vm): ''' Evolution of O-gate closed-probability :param C: closed-probability of O-gate (-) :param O: open-probability of O-gate (-) :param Vm: membrane potential (mV) :return: time derivative of O-gate closed-probability (s-1) ''' return self.betao(Vm) * O - self.alphao(Vm) * C def derO(self, C, O, P0, Vm): ''' Evolution of O-gate open-probability :param C: closed-probability of O-gate (-) :param O: open-probability of O-gate (-) :param P0: proportion of Ih channels regulating factor in unbound state (-) :param Vm: membrane potential (mV) :return: time derivative of O-gate open-probability (s-1) ''' return - self.derC(C, O, Vm) - self.k3 * O * (1 - P0) + self.k4 * (1 - O - C) def OL(self, O, C): ''' O-gate locked-open probability. :param O: open-probability of O-gate (-) :param C: closed-probability of O-gate (-) :return: loked-open-probability of O-gate (-) ''' return 1 - O - C def derP0(self, P0, Cai): ''' Evolution of unbound probability of Ih regulating factor. :param P0: unbound probability of Ih regulating factor (-) :param Cai: submembrane Calcium concentration (M) :return: time derivative of ubnound probability (s-1) ''' return self.k2 * (1 - P0) - self.k1 * P0 * Cai**self.nCa def derCai(self, Cai, s, u, Vm): ''' Evolution of submembrane Calcium concentration. Model of Ca2+ buffering and contribution from iCaT 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 Cai: submembrane Calcium concentration (M) :param s: open-probability of s-gate (-) :param u: open-probability of u-gate (-) :param Vm: membrane potential (mV) :return: time derivative of submembrane Calcium concentration (M/s) ''' return (self.Cai_min - Cai) / self.taur_Cai - self.iCa_to_Cai_rate * self.iCaT(s, u, Vm) def iKLeak(self, Vm): ''' Potassium leakage current :param Vm: membrane potential (mV) :return: current per unit area (mA/m2) ''' return self.gKLeak * (Vm - self.EK) def iH(self, O, C, Vm): ''' outward mixed cationic current :param C: closed-probability of O-gate (-) :param O: open-probability of O-gate (-) :param Vm: membrane potential (mV) :return: current per unit area (mA/m2) ''' return self.gHbar * (O + 2 * self.OL(O, C)) * (Vm - self.EH) def currents(self, Vm, states): ''' Overriding of abstract parent method. ''' m, h, n, s, u, O, C, _, _ = states currents = super().currents(Vm, [m, h, n, s, u]) currents['iKLeak'] = self.iKLeak(Vm) # mA/m2 currents['iH'] = self.iH(O, C, Vm) # mA/m2 return currents def Caiinf(self, Vm, s, u): ''' Find the steady-state intracellular Calcium concentration for a specific membrane potential and voltage-gated channel states. :param Vm: membrane potential (mV) :param s: open-probability of s-gate :param u: open-probability of u-gate :return: steady-state Calcium concentration in submembrane space (M) ''' return self.Cai_min - self.taur_Cai * self.iCa_to_Cai_rate * self.iCaT(s, u, Vm) def P0inf(self, Cai): ''' Find the steady-state unbound probability of Ih regulating factor for a specific intracellular Calcium concentration. :param Cai : Calcium concentration in submembrane space (M) :return: steady-state unbound probability of Ih regulating factor ''' return self.k2 / (self.k2 + self.k1 * Cai**self.nCa) def Oinf(self, Cai, Vm): ''' Find the steady-state O-gate open-probability for specific membrane potential and intracellular Calcium concentration. :param Cai : Calcium concentration in submembrane space (M) :param Vm: membrane potential (mV) :return: steady-state O-gate open-probability ''' BA = self.betao(Vm) / self.alphao(Vm) return self.k4 / (self.k3 * (1 - self.P0inf(Cai)) + self.k4 * (1 + BA)) def Cinf(self, Cai, Vm): ''' Find the steady-state O-gate closed-probability for specific membrane potential and intracellular Calcium concentration. :param Cai : Calcium concentration in submembrane space (M) :param Vm: membrane potential (mV) :return: steady-state O-gate closed-probability ''' BA = self.betao(Vm) / self.alphao(Vm) return BA * self.Oinf(Cai, Vm) def steadyStates(self, Vm): ''' Overriding of abstract parent method. ''' # Voltage-gated steady-states sstates = super().steadyStates(Vm) # Other steady-states sstates['Cai'] = self.Caiinf(Vm, sstates['s'], sstates['u']) sstates['P0'] = self.P0inf(sstates['Cai']) sstates['O'] = self.Oinf(sstates['Cai'], Vm) sstates['C'] = self.Cinf(sstates['Cai'], Vm) return sstates def derStates(self, Vm, states): ''' Overriding of abstract parent method. ''' m, h, n, s, u, O, C, P0, Cai = states NaKCa_states = [m, h, n, s, u] dstates = super().derStates(Vm, NaKCa_states) dstates['O'] = self.derO(C, O, P0, Vm) dstates['C'] = self.derC(C, O, Vm) dstates['P0'] = self.derP0(P0, Cai) dstates['Cai'] = self.derCai(Cai, s, u, Vm) return dstates def computeEffRates(self, Vm): ''' Overriding of abstract parent method. ''' # Compute effective coefficients for Sodium, Potassium and Calcium conductances effrates = super().computeEffRates(Vm) # Compute effective coefficients for Ih conductance effrates['alphao'] = np.mean(self.alphao(Vm)) effrates['betao'] = np.mean(self.betao(Vm)) return effrates def derEffStates(self, Qm, states, lkp): ''' Overriding of abstract parent method. ''' # Unpack states m, h, n, s, u, O, C, P0, Cai = states # Call parent method to compute channels states derivatives dstates = super().derEffStates(Qm, [m, h, n, s, u], lkp) iHrates = self.interpEffRates(Qm, lkp, keys=self.getRatesNames(['o'])) Vmeff = self.interpVmeff(Qm, lkp) # Ih effective states derivatives dstates['C'] = iHrates['betao'] * O - iHrates['alphao'] * C dstates['O'] = - dstates['C'] - self.k3 * O * (1 - P0) + self.k4 * (1 - O - C) dstates['P0'] = self.derP0(P0, Cai) dstates['Cai'] = self.derCai(Cai, s, u, Vmeff) # Merge derivatives and return return dstates def quasiSteadyStates(self, lkp): ''' Overriding of abstract parent method. ''' qsstates = super().quasiSteadyStates(lkp) qsstates['Cai'] = self.Caiinf(lkp['V'], qsstates['s'], qsstates['u']) qsstates['P0'] = self.P0inf(qsstates['Cai']) qsstates['O'] = self.Oinf(qsstates['Cai'], lkp['V']) qsstates['C'] = self.Cinf(qsstates['Cai'], lkp['V']) return qsstates diff --git a/PySONIC/plt/QSS.py b/PySONIC/plt/QSS.py index a7c958c..d6951e3 100644 --- a/PySONIC/plt/QSS.py +++ b/PySONIC/plt/QSS.py @@ -1,460 +1,464 @@ import inspect import pandas as pd import numpy as np import matplotlib.pyplot as plt from matplotlib import cm, colors from ..postpro import getFixedPoints -from ..core import NeuronalBilayerSonophore, runBatch +from ..core import NeuronalBilayerSonophore, Batch from .pltutils import * from ..utils import logger def plotVarQSSDynamics(neuron, a, Fdrive, Adrive, charges, varname, varrange, fs=12): ''' Plot the QSS-approximated derivative of a specific variable as function of the variable itself, as well as equilibrium values, for various membrane charge densities at a given acoustic amplitude. :param neuron: neuron object :param a: sonophore radius (m) :param Fdrive: US frequency (Hz) :param Adrive: US amplitude (Pa) :param charges: charge density vector (C/m2) :param varname: name of variable to plot :param varrange: range over which to compute the derivative :return: figure handle ''' # Extract information about variable to plot pltvar = neuron.getPltVars()[varname] # Get methods to compute derivative and steady-state of variable of interest derX_func = getattr(neuron, 'der{}{}'.format(varname[0].upper(), varname[1:])) Xinf_func = getattr(neuron, '{}inf'.format(varname)) derX_args = inspect.getargspec(derX_func)[0][1:] Xinf_args = inspect.getargspec(Xinf_func)[0][1:] # Get dictionary of charge and amplitude dependent QSS variables nbls = NeuronalBilayerSonophore(a, neuron, Fdrive) _, Qref, lookups, QSS = nbls.quasiSteadyStates( Fdrive, amps=Adrive, charges=charges, squeeze_output=True) df = QSS df['Vm'] = lookups['V'] # Create figure fig, ax = plt.subplots(figsize=(6, 4)) ax.set_title('{} neuron - QSS {} dynamics @ {:.2f} kPa'.format( neuron.name, pltvar['desc'], Adrive * 1e-3), fontsize=fs) ax.set_xscale('log') for key in ['top', 'right']: ax.spines[key].set_visible(False) ax.set_xlabel('$\\rm {}\ ({})$'.format(pltvar['label'], pltvar.get('unit', '')), fontsize=fs) ax.set_ylabel('$\\rm QSS\ d{}/dt\ ({}/s)$'.format(pltvar['label'], pltvar.get('unit', '1')), fontsize=fs) ax.set_ylim(-40, 40) ax.axhline(0, c='k', linewidth=0.5) y0_str = '{}0'.format(varname) if hasattr(neuron, y0_str): ax.axvline(getattr(neuron, y0_str) * pltvar.get('factor', 1), label=y0_str, c='k', linewidth=0.5) # For each charge value icolor = 0 for j, Qm in enumerate(charges): lbl = 'Q = {:.0f} nC/cm2'.format(Qm * 1e5) # Compute variable derivative as a function of its value, as well as equilibrium value, # keeping other variables at quasi steady-state derX_inputs = [varrange if arg == varname else df[arg][j] for arg in derX_args] Xinf_inputs = [df[arg][j] for arg in Xinf_args] dX_QSS = neuron.derCai(*derX_inputs) Xeq_QSS = neuron.Caiinf(*Xinf_inputs) # Plot variable derivative and its root as a function of the variable itself c = 'C{}'.format(icolor) ax.plot(varrange * pltvar.get('factor', 1), dX_QSS * pltvar.get('factor', 1), c=c, label=lbl) ax.axvline(Xeq_QSS * pltvar.get('factor', 1), linestyle='--', c=c) icolor += 1 ax.legend(frameon=False, fontsize=fs - 3) for item in ax.get_xticklabels() + ax.get_yticklabels(): item.set_fontsize(fs) fig.tight_layout() fig.canvas.set_window_title('{}_QSS_{}_dynamics_{:.2f}kPa'.format( neuron.name, varname, Adrive * 1e-3)) return fig def plotQSSvars(neuron, a, Fdrive, Adrive, fs=12): ''' Plot effective membrane potential, quasi-steady states and resulting membrane currents as a function of membrane charge density, for a given acoustic amplitude. :param neuron: neuron object :param a: sonophore radius (m) :param Fdrive: US frequency (Hz) :param Adrive: US amplitude (Pa) :return: figure handle ''' # Get neuron-specific pltvars pltvars = neuron.getPltVars() # Compute neuron-specific charge and amplitude dependent QS states at this amplitude nbls = NeuronalBilayerSonophore(a, neuron, Fdrive) _, Qref, lookups, QSS = nbls.quasiSteadyStates(Fdrive, amps=Adrive, squeeze_output=True) Vmeff = lookups['V'] # Compute QSS currents currents = neuron.currents(Vmeff, np.array([QSS[k] for k in neuron.states])) iNet = sum(currents.values()) # Compute fixed points in dQdt profile dQdt = -iNet Q_SFPs = getFixedPoints(Qref, dQdt, filter='stable') Q_UFPs = getFixedPoints(Qref, dQdt, filter='unstable') # Extract dimensionless states norm_QSS = {} for x in neuron.states: if 'unit' not in pltvars[x]: norm_QSS[x] = QSS[x] # Create figure fig, axes = plt.subplots(3, 1, figsize=(7, 9)) axes[-1].set_xlabel('$\\rm Q_m\ (nC/cm^2)$', fontsize=fs) for ax in axes: for skey in ['top', 'right']: ax.spines[skey].set_visible(False) for item in ax.get_xticklabels() + ax.get_yticklabels(): item.set_fontsize(fs) for item in ax.get_xticklabels(minor=True): item.set_visible(False) figname = '{} neuron - QSS dynamics @ {:.2f} kPa'.format(neuron.name, Adrive * 1e-3) fig.suptitle(figname, fontsize=fs) # Subplot: Vmeff ax = axes[0] ax.set_ylabel('$V_m^*$ (mV)', fontsize=fs) ax.plot(Qref * 1e5, Vmeff, color='k') ax.axhline(neuron.Vm0, linewidth=0.5, color='k') # Subplot: dimensionless quasi-steady states cset = plt.get_cmap('Dark2').colors + plt.get_cmap('tab10').colors ax = axes[1] ax.set_ylabel('QSS gating variables (-)', fontsize=fs) ax.set_yticks([0, 0.5, 1]) ax.set_ylim([-0.05, 1.05]) for i, (label, QS_state) in enumerate(norm_QSS.items()): ax.plot(Qref * 1e5, QS_state, label=label, c=cset[i]) # Subplot: currents ax = axes[2] cset = plt.get_cmap('tab10').colors ax.set_ylabel('QSS currents ($\\rm A/m^2$)', fontsize=fs) for i, (k, I) in enumerate(currents.items()): ax.plot(Qref * 1e5, -I * 1e-3, '--', c=cset[i], label='$\\rm -{}$'.format(neuron.getPltVars()[k]['label'])) ax.plot(Qref * 1e5, -iNet * 1e-3, color='k', label='$\\rm -I_{Net}$') ax.axhline(0, color='k', linewidth=0.5) if Q_SFPs.size > 0: ax.plot(Q_SFPs * 1e5, np.zeros(Q_SFPs.size), 'o', c='k', markersize=5, zorder=2) if Q_SFPs.size > 0: ax.plot(Q_UFPs * 1e5, np.zeros(Q_UFPs.size), 'o', c='k', markersize=5, mfc='none', zorder=2) fig.tight_layout() fig.subplots_adjust(right=0.8) for ax in axes[1:]: ax.legend(loc='center right', fontsize=fs, frameon=False, bbox_to_anchor=(1.3, 0.5)) for ax in axes[:-1]: ax.set_xticklabels([]) fig.canvas.set_window_title( '{}_QSS_states_vs_Qm_{:.2f}kPa'.format(neuron.name, Adrive * 1e-3)) return fig def plotQSSVarVsAmp(neuron, a, Fdrive, varname, amps=None, DC=1., fs=12, cmap='viridis', yscale='lin', zscale='lin'): ''' Plot a specific QSS variable (state or current) as a function of membrane charge density, for various acoustic amplitudes. :param neuron: neuron object :param a: sonophore radius (m) :param Fdrive: US frequency (Hz) :param amps: US amplitudes (Pa) :param DC: duty cycle (-) :param varname: extraction key for variable to plot :return: figure handle ''' # Determine stimulation modality if a is None and Fdrive is None: stim_type = 'elec' a = 32e-9 Fdrive = 500e3 else: stim_type = 'US' # Extract information about variable to plot pltvar = neuron.getPltVars()[varname] Qvar = neuron.getPltVars()['Qm'] Afactor = {'US': 1e-3, 'elec': 1.}[stim_type] # Q_SFPs = [] # Q_UFPs = [] log = 'plotting {} neuron QSS {} vs. amp for {} stimulation @ {:.0f}% DC'.format( neuron.name, varname, stim_type, DC * 1e2) logger.info(log) nbls = NeuronalBilayerSonophore(a, neuron, Fdrive) # Get reference dictionaries for zero amplitude _, Qref, lookups0, QSS0 = nbls.quasiSteadyStates(Fdrive, amps=0., squeeze_output=True) Vmeff0 = lookups0['V'] if stim_type == 'elec': # if E-STIM case, compute steady states with constant capacitance Vmeff0 = Qref / neuron.Cm0 * 1e3 QSS0 = neuron.steadyStates(Vmeff0) df0 = QSS0 df0['Vm'] = Vmeff0 # Create figure fig, ax = plt.subplots(figsize=(6, 4)) title = '{} neuron - {}steady-state {}'.format( neuron.name, 'quasi-' if amps is not None else '', pltvar['desc']) if amps is not None: title += '\nvs. {} amplitude @ {:.0f}% DC'.format(stim_type, DC * 1e2) ax.set_title(title, fontsize=fs) ax.set_xlabel('$\\rm {}\ ({})$'.format(Qvar['label'], Qvar['unit']), fontsize=fs) ax.set_ylabel('$\\rm QSS\ {}\ ({})$'.format(pltvar['label'], pltvar.get('unit', '')), fontsize=fs) if yscale == 'log': ax.set_yscale('log') for key in ['top', 'right']: ax.spines[key].set_visible(False) # Plot y-variable reference line, if any y0 = None y0_str = '{}0'.format(varname) if hasattr(neuron, y0_str): y0 = getattr(neuron, y0_str) * pltvar.get('factor', 1) elif varname in neuron.getCurrentsNames() + ['iNet', 'dQdt']: y0 = 0. y0_str = '' if y0 is not None: ax.axhline(y0, label=y0_str, c='k', linewidth=0.5) # Plot reference QSS profile of variable as a function of charge density var0 = extractPltVar( neuron, pltvar, pd.DataFrame({k: df0[k] for k in df0.keys()}), name=varname) ax.plot(Qref * Qvar['factor'], var0, '--', c='k', zorder=1, label='$\\rm A_{{{}}}=0$'.format(stim_type)) # if varname == 'dQdt': # Q_SFPs += getFixedPoints(Qref, var0, filter='stable').tolist() # Q_UFPs += getFixedPoints(Qref, var0, filter='unstable').tolist() # Define color code mymap = plt.get_cmap(cmap) zref = amps * Afactor if zscale == 'lin': norm = colors.Normalize(zref.min(), zref.max()) elif zscale == 'log': norm = colors.LogNorm(zref.min(), zref.max()) sm = cm.ScalarMappable(norm=norm, cmap=mymap) sm._A = [] # Get amplitude-dependent QSS dictionary if stim_type == 'US': # Get dictionary of charge and amplitude dependent QSS variables _, Qref, lookups, QSS = nbls.quasiSteadyStates( Fdrive, amps=amps, DCs=DC, squeeze_output=True) df = QSS df['Vm'] = lookups['V'] else: # Repeat zero-amplitude QSS dictionary for all amplitudes df = {k: np.tile(df0[k], (amps.size, 1)) for k in df0} # Plot QSS profiles for various amplitudes for i, A in enumerate(amps): var = extractPltVar( neuron, pltvar, pd.DataFrame({k: df[k][i] for k in df.keys()}), name=varname) if varname == 'dQdt' and stim_type == 'elec': var += A * DC * pltvar['factor'] ax.plot(Qref * Qvar['factor'], var, c=sm.to_rgba(A * Afactor), zorder=0) # if varname == 'dQdt': # # mark eq. point if starting point provided, otherwise mark all FPs # Q_SFPs += getFixedPoints(Qref, var, filter='stable').tolist() # Q_UFPs += getFixedPoints(Qref, var, filter='unstable').tolist() # # Plot fixed-points, if any # if len(Q_SFPs) > 0: # ax.plot(np.array(Q_SFPs) * Qvar['factor'], np.zeros(len(Q_SFPs)), 'o', c='k', # markersize=5, zorder=2) # if len(Q_UFPs) > 0: # ax.plot(np.array(Q_UFPs) * Qvar['factor'], np.zeros(len(Q_UFPs)), 'x', c='k', # markersize=5, zorder=2) # Add legend and adjust layout ax.legend(frameon=False, fontsize=fs) for item in ax.get_xticklabels() + ax.get_yticklabels(): item.set_fontsize(fs) fig.tight_layout() fig.subplots_adjust(bottom=0.15, top=0.9, right=0.80, hspace=0.5) # Plot amplitude colorbar if amps is not None: cbarax = fig.add_axes([0.85, 0.15, 0.03, 0.75]) fig.colorbar(sm, cax=cbarax) cbarax.set_ylabel( 'Amplitude ({})'.format({'US': 'kPa', 'elec': 'mA/m2'}[stim_type]), fontsize=fs) for item in cbarax.get_yticklabels(): item.set_fontsize(fs) title = '{}_{}SS_{}'.format(neuron.name, 'Q' if amps is not None else '', varname) if amps is not None: title += '_vs_{}A_{}_{:.0f}%DC'.format(zscale, stim_type, DC * 1e2) fig.canvas.set_window_title(title) return fig def plotEqChargeVsAmp(neurons, a, Fdrive, amps=None, tstim=250e-3, toffset=50e-3, PRF=100.0, DCs=[1.], fs=12, xscale='lin', titrate=False, mpi=False): ''' Plot the equilibrium membrane charge density as a function of acoustic amplitude, given an initial value of membrane charge density. :param neurons: neuron objects :param a: sonophore radius (m) :param Fdrive: US frequency (Hz) :param amps: US amplitudes (Pa) :return: figure handle ''' # Determine stimulation modality if a is None and Fdrive is None: stim_type = 'elec' a = 32e-9 Fdrive = 500e3 else: stim_type = 'US' logger.info('plotting equilibrium charges for %s stimulation', stim_type) # Create figure fig, ax = plt.subplots(figsize=(6, 4)) figname = 'charge stability vs. amplitude' ax.set_title(figname) ax.set_xlabel('Amplitude ({})'.format({'US': 'kPa', 'elec': 'mA/m2'}[stim_type]), fontsize=fs) ax.set_ylabel('$\\rm Q_m\ (nC/cm^2)$', fontsize=fs) if xscale == 'log': ax.set_xscale('log') for skey in ['top', 'right']: ax.spines[skey].set_visible(False) for item in ax.get_xticklabels() + ax.get_yticklabels(): item.set_fontsize(fs) Qrange = (np.inf, -np.inf) icolor = 0 for i, neuron in enumerate(neurons): nbls = NeuronalBilayerSonophore(a, neuron, Fdrive) # Compute reference charge variation array for zero amplitude _, Qref, lookups0, QSS0 = nbls.quasiSteadyStates(Fdrive, amps=0., squeeze_output=True) Qrange = (min(Qrange[0], Qref.min()), max(Qrange[1], Qref.max())) Vmeff0 = lookups0['V'] if stim_type == 'elec': # if E-STIM case, compute steady states with constant capacitance Vmeff0 = Qref / neuron.Cm0 * 1e3 QSS0 = neuron.steadyStates(Vmeff0) dQdt0 = -neuron.iNet(Vmeff0, np.array([QSS0[k] for k in neuron.states])) # mA/m2 # Compute 3D QSS charge variation array if stim_type == 'US': _, _, lookups, QSS = nbls.quasiSteadyStates(Fdrive, amps=amps, DCs=DCs) dQdt = -neuron.iNet(lookups['V'], np.array([QSS[k] for k in neuron.states])) # mA/m2 Afactor = 1e-3 else: Afactor = 1. dQdt = np.empty((amps.size, Qref.size, DCs.size)) for iA, A in enumerate(amps): for iDC, DC in enumerate(DCs): dQdt[iA, :, iDC] = dQdt0 + A * DC # For each duty cycle for iDC, DC in enumerate(DCs): color = 'k' if len(neurons) * len(DCs) == 1 else 'C{}'.format(icolor) # Initialize containers for stable and unstable fixed points SFPs = [] UFPs = [] stab_points = [] # Generate QSS batch queue QSS_queue = [] for iA, Adrive in enumerate(amps): lookups1D = {k: v[iA, :, iDC] for k, v in lookups.items()} lookups1D['Q'] = Qref QSS_queue.append([Fdrive, Adrive, DC, lookups1D, dQdt[iA, :, iDC]]) # Run batch to find stable and unstable fixed points at each amplitude - QSS_output = runBatch(nbls, 'quasiSteadyStateFixedPoints', QSS_queue, mpi=mpi) + QSS_batch = Batch(nbls.fixedPointsQSS, QSS_queue) + QSS_output = QSS_batch(mpi=mpi) # Generate simulations batch queue - sim_queue = nbls.createQueue([Fdrive], amps, [tstim], [toffset], [PRF], [DC], method) + sim_queue = nbls.simQueue([Fdrive], amps, [tstim], [toffset], [PRF], [DC], method) + for item in sim_queue: + item.insert(0, outdir) # Run batch to find stabilization points at each amplitude - sim_output = runBatch(nbls, 'runIfNone', sim_queue, extra_params=[outdir], mpi=mpi) + sim_batch = Batch(nbls.runIfNone, sim_queue) + sim_output = sim_batch(mpi=mpi) # Retrieve batch output for i, Adrive in enumerate(amps): SFPs += [(Adrive, Qm) for Qm in QSS_output[i][0]] UFPs += [(Adrive, Qm) for Qm in QSS_output[i][1]] # TODO: get stabilization point from simulation, if any # Plot charge SFPs and UFPs for each acoustic amplitude lbl = '{} neuron - {{}}stable fixed points @ {:.0f} % DC'.format( neuron.name, DC * 1e2) if len(SFPs) > 0: A_SFPs, Q_SFPs = np.array(SFPs).T ax.plot(np.array(A_SFPs) * Afactor, np.array(Q_SFPs) * 1e5, 'o', c=color, markersize=3, label=lbl.format('')) if len(UFPs) > 0: A_UFPs, Q_UFPs = np.array(UFPs).T ax.plot(np.array(A_UFPs) * Afactor, np.array(Q_UFPs) * 1e5, 'x', c=color, markersize=3, label=lbl.format('un')) # If specified, compute and plot the threshold excitation amplitude if titrate: if stim_type == 'US': Athr = nbls.titrate(Fdrive, tstim, toffset, PRF=PRF, DC=DC) ax.axvline(Athr * Afactor, c=color, linestyle='--') else: for Arange, ls in zip([(0., amps.max(amps.min(), 0.)), ()], ['--', '-.']): Athr = neuron.titrate(tstim, toffset, PRF=PRF, DC=DC, Arange=Arange) ax.axvline(Athr * Afactor, c=color, linestyle=ls) icolor += 1 # Post-process figure ax.set_ylim(np.array([Qrange[0], 0]) * 1e5) ax.legend(frameon=False, fontsize=fs) fig.tight_layout() fig.canvas.set_window_title('QSS_Qstab_vs_{}A_{}_{}_{}%DC{}'.format( xscale, '_'.join([n.name for n in neurons]), stim_type, '_'.join(['{:.0f}'.format(DC * 1e2) for DC in DCs]), '_with_thresholds' if titrate else '' )) return fig diff --git a/paper figures/fig5.py b/paper figures/fig5.py index 353a537..b9c6680 100644 --- a/paper figures/fig5.py +++ b/paper figures/fig5.py @@ -1,358 +1,358 @@ # -*- coding: utf-8 -*- # @Author: Theo # @Date: 2018-06-06 18:38:04 # @Last Modified by: Theo Lemaire -# @Last Modified time: 2019-05-31 15:14:48 +# @Last Modified time: 2019-06-02 11:53:17 ''' Sub-panels of the NICE and SONIC accuracies comparative figure. ''' import os import logging import numpy as np import matplotlib import matplotlib.pyplot as plt from argparse import ArgumentParser from PySONIC.utils import * from PySONIC.neurons import * from PySONIC.plt import plotComp, plotSpikingMetrics, cm2inch from utils import * # Plot parameters matplotlib.rcParams['pdf.fonttype'] = 42 matplotlib.rcParams['ps.fonttype'] = 42 matplotlib.rcParams['font.family'] = 'arial' # Figure basename figbase = os.path.splitext(__file__)[0] def Qprofiles_vs_amp(neuron, a, Fdrive, CW_Athrs, tstim, toffset, inputdir): ''' Comparison of resulting charge profiles for CW stimuli at sub-threshold, threshold and supra-threshold amplitudes. ''' Athr = CW_Athrs[neuron].loc[Fdrive * 1e-3] # kPa amps = np.array([Athr - 5., Athr, Athr + 20.]) * 1e3 # Pa subdir = os.path.join(inputdir, neuron) - sonic_fpaths = getSims(subdir, neuron, a, nbls.createQueue( + sonic_fpaths = getSims(subdir, neuron, a, nbls.simQueue( [Fdrive], amps, [tstim], [toffset], [None], [1.], 'sonic')) - full_fpaths = getSims(subdir, neuron, a, nbls.createQueue( + full_fpaths = getSims(subdir, neuron, a, nbls.simQueue( [Fdrive], amps, [tstim], [toffset], [None], [1.], 'full')) regimes = ['AT - 5 kPa', 'AT', 'AT + 20 kPa'] fig = plotComp( sum([[x, y] for x, y in zip(full_fpaths, sonic_fpaths)], []), 'Qm', labels=sum([['', x] for x in regimes], []), lines=['-', '--'] * len(regimes), colors=plt.get_cmap('Paired').colors[:2 * len(regimes)], fs=8, patches='one', xticks=[0, 250], yticks=[getNeuronsDict()[neuron].Vm0, 25], straightlegend=True, figsize=cm2inch(12.5, 5.8) ) fig.axes[0].get_xaxis().set_label_coords(0.5, -0.05) fig.subplots_adjust(bottom=0.2, right=0.95, top=0.95) fig.canvas.set_window_title(figbase + 'a Qprofiles') return fig def spikemetrics_vs_amp(neuron, a, Fdrive, amps, tstim, toffset, inputdir): ''' Comparison of spiking metrics for CW stimuli at various supra-threshold amplitudes. ''' subdir = os.path.join(inputdir, neuron) - sonic_fpaths = getSims(subdir, neuron, a, nbls.createQueue( + sonic_fpaths = getSims(subdir, neuron, a, nbls.simQueue( [Fdrive], amps, [tstim], [toffset], [None], [1.], 'sonic')) - full_fpaths = getSims(subdir, neuron, a, nbls.createQueue( + full_fpaths = getSims(subdir, neuron, a, nbls.simQueue( [Fdrive], amps, [tstim], [toffset], [None], [1.], 'full')) data_fpaths = {'full': full_fpaths, 'sonic': sonic_fpaths} metrics_files = {x: '{}_spikemetrics_vs_amplitude_{}.csv'.format(neuron, x) for x in ['full', 'sonic']} metrics_fpaths = {key: os.path.join(inputdir, value) for key, value in metrics_files.items()} xlabel = 'Amplitude (kPa)' metrics = getSpikingMetrics( subdir, neuron, amps * 1e-3, xlabel, data_fpaths, metrics_fpaths) fig = plotSpikingMetrics(amps * 1e-3, xlabel, {neuron: metrics}, logscale=True) fig.canvas.set_window_title(figbase + 'a spikemetrics') return fig def Qprofiles_vs_freq(neuron, a, freqs, CW_Athrs, tstim, toffset, inputdir): ''' Comparison of resulting charge profiles for supra-threshold CW stimuli at low and high US frequencies. ''' subdir = os.path.join(inputdir, neuron) sonic_fpaths, full_fpaths = [], [] for Fdrive in freqs: Athr = CW_Athrs[neuron].loc[Fdrive * 1e-3] # kPa Adrive = (Athr + 20.) * 1e3 # Pa - sonic_fpaths += getSims(subdir, neuron, a, nbls.createQueue( + sonic_fpaths += getSims(subdir, neuron, a, nbls.simQueue( [Fdrive], [Adrive], [tstim], [toffset], [None], [1.], 'sonic')) - full_fpaths += getSims(subdir, neuron, a, nbls.createQueue( + full_fpaths += getSims(subdir, neuron, a, nbls.simQueue( [Fdrive], [Adrive], [tstim], [toffset], [None], [1.], 'full')) fig = plotComp( sum([[x, y] for x, y in zip(full_fpaths, sonic_fpaths)], []), 'Qm', labels=sum([['', '{}Hz'.format(si_format(f))] for f in freqs], []), lines=['-', '--'] * len(freqs), colors=plt.get_cmap('Paired').colors[6:10], fs=8, patches='one', xticks=[0, 250], yticks=[getNeuronsDict()[neuron].Vm0, 25], straightlegend=True, figsize=cm2inch(12.5, 5.8), inset={'xcoords': [5, 40], 'ycoords': [-35, 45], 'xlims': [57.5, 58.5], 'ylims': [10, 35]} ) fig.axes[0].get_xaxis().set_label_coords(0.5, -0.05) fig.subplots_adjust(bottom=0.2, right=0.95, top=0.95) fig.canvas.set_window_title(figbase + 'b Qprofiles') return fig def spikemetrics_vs_freq(neuron, a, freqs, CW_Athrs, tstim, toffset, inputdir): ''' Comparison of spiking metrics for supra-threshold CW stimuli at various US frequencies. ''' subdir = os.path.join(inputdir, neuron) sonic_fpaths, full_fpaths = [], [] for Fdrive in freqs: Athr = CW_Athrs[neuron].loc[Fdrive * 1e-3] # kPa Adrive = (Athr + 20.) * 1e3 # Pa - sonic_fpaths += getSims(subdir, neuron, a, nbls.createQueue( + sonic_fpaths += getSims(subdir, neuron, a, nbls.simQueue( [Fdrive], [Adrive], [tstim], [toffset], [None], [1.], 'sonic')) - full_fpaths += getSims(subdir, neuron, a, nbls.createQueue( + full_fpaths += getSims(subdir, neuron, a, nbls.simQueue( [Fdrive], [Adrive], [tstim], [toffset], [None], [1.], 'full')) data_fpaths = {'full': full_fpaths, 'sonic': sonic_fpaths} metrics_files = {x: '{}_spikemetrics_vs_frequency_{}.csv'.format(neuron, x) for x in ['full', 'sonic']} metrics_fpaths = {key: os.path.join(inputdir, value) for key, value in metrics_files.items()} xlabel = 'Frequency (kHz)' metrics = getSpikingMetrics( subdir, neuron, freqs * 1e-3, xlabel, data_fpaths, metrics_fpaths) fig = plotSpikingMetrics(freqs * 1e-3, xlabel, {neuron: metrics}, logscale=True) fig.canvas.set_window_title(figbase + 'b spikemetrics') return fig def Qprofiles_vs_radius(neuron, radii, Fdrive, CW_Athrs, tstim, toffset, inputdir): ''' Comparison of resulting charge profiles for supra-threshold CW stimuli for small and large sonophore radii. ''' subdir = os.path.join(inputdir, neuron) sonic_fpaths, full_fpaths = [], [] for a in radii: Athr = CW_Athrs[neuron].loc[a * 1e9] # kPa Adrive = (Athr + 20.) * 1e3 # Pa - sonic_fpaths += getSims(subdir, neuron, a, nbls.createQueue( + sonic_fpaths += getSims(subdir, neuron, a, nbls.simQueue( [Fdrive], [Adrive], [tstim], [toffset], [None], [1.], 'sonic')) - full_fpaths += getSims(subdir, neuron, a, nbls.createQueue( + full_fpaths += getSims(subdir, neuron, a, nbls.simQueue( [Fdrive], [Adrive], [tstim], [toffset], [None], [1.], 'full')) tmp = plt.get_cmap('Paired').colors colors = tmp[2:4] + tmp[10:12] fig = plotComp( sum([[x, y] for x, y in zip(full_fpaths, sonic_fpaths)], []), 'Qm', labels=sum([['', '{:.0f} nm'.format(a * 1e9)] for a in radii], []), lines=['-', '--'] * len(radii), colors=colors, fs=8, patches='one', xticks=[0, 250], yticks=[getNeuronsDict()[neuron].Vm0, 25], straightlegend=True, figsize=cm2inch(12.5, 5.8) ) fig.axes[0].get_xaxis().set_label_coords(0.5, -0.05) fig.subplots_adjust(bottom=0.2, right=0.95, top=0.95) fig.canvas.set_window_title(figbase + 'c Qprofiles') return fig def spikemetrics_vs_radius(neuron, radii, Fdrive, CW_Athrs, tstim, toffset, inputdir): ''' Comparison of spiking metrics for supra-threshold CW stimuli with various sonophore diameters. ''' subdir = os.path.join(inputdir, neuron) sonic_fpaths, full_fpaths = [], [] for a in radii: Athr = CW_Athrs[neuron].loc[np.round(a * 1e9, 1)] # kPa Adrive = (Athr + 20.) * 1e3 # Pa - sonic_fpaths += getSims(subdir, neuron, a, nbls.createQueue( + sonic_fpaths += getSims(subdir, neuron, a, nbls.simQueue( [Fdrive], [Adrive], [tstim], [toffset], [None], [1.], 'sonic')) - full_fpaths += getSims(subdir, neuron, a, nbls.createQueue( + full_fpaths += getSims(subdir, neuron, a, nbls.simQueue( [Fdrive], [Adrive], [tstim], [toffset], [None], [1.], 'full')) data_fpaths = {'full': full_fpaths, 'sonic': sonic_fpaths} metrics_files = {x: '{}_spikemetrics_vs_radius_{}.csv'.format(neuron, x) for x in ['full', 'sonic']} metrics_fpaths = {key: os.path.join(inputdir, value) for key, value in metrics_files.items()} xlabel = 'Sonophore radius (nm)' metrics = getSpikingMetrics( subdir, neuron, radii * 1e9, xlabel, data_fpaths, metrics_fpaths) fig = plotSpikingMetrics(radii * 1e9, xlabel, {neuron: metrics}, logscale=True) fig.canvas.set_window_title(figbase + 'c spikemetrics') return fig def Qprofiles_vs_DC(neurons, a, Fdrive, Adrive, tstim, toffset, PRF, DC, inputdir): ''' Comparison of resulting charge profiles for PW stimuli at 5% duty cycle for different neuron types. ''' sonic_fpaths, full_fpaths = [], [] for neuron in neurons: subdir = os.path.join(inputdir, neuron) - sonic_fpaths += getSims(subdir, neuron, a, nbls.createQueue( + sonic_fpaths += getSims(subdir, neuron, a, nbls.simQueue( [Fdrive], [Adrive], [tstim], [toffset], [PRF], [DC], 'sonic')) - full_fpaths += getSims(subdir, neuron, a, nbls.createQueue( + full_fpaths += getSims(subdir, neuron, a, nbls.simQueue( [Fdrive], [Adrive], [tstim], [toffset], [PRF], [DC], 'full')) colors = list(plt.get_cmap('Paired').colors[:6]) del colors[2:4] fig = plotComp( sum([[x, y] for x, y in zip(full_fpaths, sonic_fpaths)], []), 'Qm', labels=sum([['', '{}, {:.0f}% DC'.format(x, DC * 1e2)] for x in neurons], []), lines=['-', '--'] * len(neurons), colors=colors, fs=8, patches='one', xticks=[0, 250], yticks=[min(getNeuronsDict()[n].Vm0 for n in neurons), 50], straightlegend=True, figsize=cm2inch(12.5, 5.8) ) fig.axes[0].get_xaxis().set_label_coords(0.5, -0.05) fig.subplots_adjust(bottom=0.2, right=0.95, top=0.95) fig.canvas.set_window_title(figbase + 'd Qprofiles') return fig def spikemetrics_vs_DC(neurons, a, Fdrive, Adrive, tstim, toffset, PRF, DCs, inputdir): ''' Comparison of spiking metrics for PW stimuli at various duty cycle for different neuron types. ''' metrics_dict = {} xlabel = 'Duty cycle (%)' colors = list(plt.get_cmap('Paired').colors[:6]) del colors[2:4] colors_dict = {} for i, neuron in enumerate(neurons): subdir = os.path.join(inputdir, neuron) - sonic_fpaths = getSims(subdir, neuron, a, nbls.createQueue( + sonic_fpaths = getSims(subdir, neuron, a, nbls.simQueue( [Fdrive], [Adrive], [tstim], [toffset], [PRF], DCs, 'sonic')) - full_fpaths = getSims(subdir, neuron, a, nbls.createQueue( + full_fpaths = getSims(subdir, neuron, a, nbls.simQueue( [Fdrive], [Adrive], [tstim], [toffset], [PRF], DCs, 'full')) metrics_files = {x: '{}_spikemetrics_vs_DC_{}.csv'.format(neuron, x) for x in ['full', 'sonic']} metrics_fpaths = {key: os.path.join(inputdir, value) for key, value in metrics_files.items()} sonic_fpaths = sonic_fpaths[1:] + [sonic_fpaths[0]] full_fpaths = full_fpaths[1:] + [full_fpaths[0]] data_fpaths = {'full': full_fpaths, 'sonic': sonic_fpaths} metrics_dict[neuron] = getSpikingMetrics( subdir, neuron, DCs * 1e2, xlabel, data_fpaths, metrics_fpaths) colors_dict[neuron] = {'full': colors[2 * i], 'sonic': colors[2 * i + 1]} fig = plotSpikingMetrics(DCs * 1e2, xlabel, metrics_dict, spikeamp=False, colors=colors_dict) fig.canvas.set_window_title(figbase + 'd spikemetrics') return fig def Qprofiles_vs_PRF(neuron, a, Fdrive, Adrive, tstim, toffset, PRFs, DC, inputdir): ''' Comparison of resulting charge profiles for PW stimuli at 5% duty cycle with different pulse repetition frequencies. ''' subdir = os.path.join(inputdir, neuron) - sonic_fpaths = getSims(subdir, neuron, a, nbls.createQueue( + sonic_fpaths = getSims(subdir, neuron, a, nbls.simQueue( [Fdrive], [Adrive], [tstim], [toffset], PRFs, [DC], 'sonic')) - full_fpaths = getSims(subdir, neuron, a, nbls.createQueue( + full_fpaths = getSims(subdir, neuron, a, nbls.simQueue( [Fdrive], [Adrive], [tstim], [toffset], PRFs, [DC], 'full')) patches = [False, True] * len(PRFs) patches[-1] = False fig = plotComp( sum([[x, y] for x, y in zip(full_fpaths, sonic_fpaths)], []), 'Qm', labels=sum([['', '{}Hz PRF'.format(si_format(PRF, space=' '))] for PRF in PRFs], []), lines=['-', '--'] * len(PRFs), colors=plt.get_cmap('Paired').colors[4:12], fs=8, patches=patches, xticks=[0, 250], yticks=[getNeuronsDict()[neuron].Vm0, 50], straightlegend=True, figsize=cm2inch(12.5, 5.8) ) fig.axes[0].get_xaxis().set_label_coords(0.5, -0.05) fig.subplots_adjust(bottom=0.2, right=0.95, top=0.95) fig.canvas.set_window_title(figbase + 'e Qprofiles') return fig def spikemetrics_vs_PRF(neuron, a, Fdrive, Adrive, tstim, toffset, PRFs, DC, inputdir): ''' Comparison of spiking metrics for PW stimuli at 5% duty cycle with different pulse repetition frequencies. ''' xlabel = 'PRF (Hz)' subdir = os.path.join(inputdir, neuron) - sonic_fpaths = getSims(subdir, neuron, a, nbls.createQueue( + sonic_fpaths = getSims(subdir, neuron, a, nbls.simQueue( [Fdrive], [Adrive], [tstim], [toffset], PRFs, [DC], 'sonic')) - full_fpaths = getSims(subdir, neuron, a, nbls.createQueue( + full_fpaths = getSims(subdir, neuron, a, nbls.simQueue( [Fdrive], [Adrive], [tstim], [toffset], PRFs, [DC], 'full')) data_fpaths = {'full': full_fpaths, 'sonic': sonic_fpaths} metrics_files = {x: '{}_spikemetrics_vs_PRF_{}.csv'.format(neuron, x) for x in ['full', 'sonic']} metrics_fpaths = {key: os.path.join(inputdir, value) for key, value in metrics_files.items()} metrics = getSpikingMetrics( subdir, neuron, PRFs, xlabel, data_fpaths, metrics_fpaths) fig = plotSpikingMetrics(PRFs, xlabel, {neuron: metrics}, spikeamp=False, logscale=True) fig.canvas.set_window_title(figbase + 'e spikemetrics') return fig def main(): ap = ArgumentParser() # Runtime options ap.add_argument('-v', '--verbose', default=False, action='store_true', help='Increase verbosity') ap.add_argument('-i', '--inputdir', type=str, help='Input directory') ap.add_argument('-f', '--figset', type=str, help='Figure set', default='a') ap.add_argument('-s', '--save', default=False, action='store_true', help='Save output figures as pdf') args = ap.parse_args() loglevel = logging.DEBUG if args.verbose is True else logging.INFO logger.setLevel(loglevel) inputdir = selectDirDialog() if args.inputdir is None else args.inputdir if inputdir == '': logger.error('No input directory chosen') return figset = args.figset logger.info('Generating panel {} of {}'.format(figset, figbase)) # Parameters radii = np.array([16, 22.6, 32, 45.3, 64]) * 1e-9 # m a = 32e-9 # m tstim = 150e-3 # s toffset = 100e-3 # s freqs = np.array([20e3, 100e3, 500e3, 1e6, 2e6, 3e6, 4e6]) # Hz Fdrive = 500e3 # Hz amps = np.array([50, 100, 300, 600]) * 1e3 # Pa Adrive = 100e3 # Pa PRFs_sparse = np.array([1e1, 1e2, 1e3, 1e4]) # Hz PRFs_dense = sum([[x, 2 * x, 5 * x] for x in PRFs_sparse[:-1]], []) + [PRFs_sparse[-1]] # Hz PRF = 100 # Hz DCs = np.array([5, 10, 25, 50, 75, 100]) * 1e-2 DC = 0.05 # Get threshold amplitudes if needed if 'a' in figset or 'b' in figset: CW_Athr_vs_Fdrive = getCWtitrations_vs_Fdrive( ['RS'], a, freqs, tstim, toffset, os.path.join(inputdir, 'CW_Athrs_vs_freqs.csv')) if 'c' in figset: CW_Athr_vs_radius = getCWtitrations_vs_radius( ['RS'], radii, Fdrive, tstim, toffset, os.path.join(inputdir, 'CW_Athrs_vs_radius.csv')) # Generate figures figs = [] if figset == 'a': figs.append(Qprofiles_vs_amp('RS', a, Fdrive, CW_Athr_vs_Fdrive, tstim, toffset, inputdir)) figs.append(spikemetrics_vs_amp('RS', a, Fdrive, amps, tstim, toffset, inputdir)) if figset == 'b': figs.append(Qprofiles_vs_freq( 'RS', a, [freqs.min(), freqs.max()], CW_Athr_vs_Fdrive, tstim, toffset, inputdir)) figs.append(spikemetrics_vs_freq('RS', a, freqs, CW_Athr_vs_Fdrive, tstim, toffset, inputdir)) if figset == 'c': figs.append(Qprofiles_vs_radius( 'RS', [radii.min(), radii.max()], Fdrive, CW_Athr_vs_radius, tstim, toffset, inputdir)) figs.append(spikemetrics_vs_radius( 'RS', radii, Fdrive, CW_Athr_vs_radius, tstim, toffset, inputdir)) if figset == 'd': figs.append(Qprofiles_vs_DC( ['RS', 'LTS'], a, Fdrive, Adrive, tstim, toffset, PRF, DC, inputdir)) figs.append(spikemetrics_vs_DC( ['RS', 'LTS'], a, Fdrive, Adrive, tstim, toffset, PRF, DCs, inputdir)) if figset == 'e': figs.append(Qprofiles_vs_PRF( 'LTS', a, Fdrive, Adrive, tstim, toffset, PRFs_sparse, DC, inputdir)) figs.append(spikemetrics_vs_PRF( 'LTS', a, Fdrive, Adrive, tstim, toffset, PRFs_dense, DC, inputdir)) if args.save: for fig in figs: figname = '{}.pdf'.format(fig.canvas.get_window_title()) fig.savefig(os.path.join(inputdir, figname), transparent=True) else: plt.show() if __name__ == '__main__': main() diff --git a/paper figures/fig6.py b/paper figures/fig6.py index 5db8d31..1b72abd 100644 --- a/paper figures/fig6.py +++ b/paper figures/fig6.py @@ -1,302 +1,302 @@ # -*- coding: utf-8 -*- # @Author: Theo # @Date: 2018-06-06 18:38:04 # @Last Modified by: Theo Lemaire -# @Last Modified time: 2019-05-31 15:15:06 +# @Last Modified time: 2019-06-02 11:53:17 ''' Sub-panels of the NICE and SONIC computation times comparative figure. ''' import os import logging import numpy as np import matplotlib import matplotlib.pyplot as plt from argparse import ArgumentParser from PySONIC.utils import * from PySONIC.neurons import * from PySONIC.plt import cm2inch from utils import * # Plot parameters matplotlib.rcParams['pdf.fonttype'] = 42 matplotlib.rcParams['ps.fonttype'] = 42 matplotlib.rcParams['font.family'] = 'arial' # Figure basename figbase = os.path.splitext(__file__)[0] time_indicators = [1, 60, 60**2, 60**2 * 24, 60**2 * 24 * 7] time_indicators_labels = ['1 s', '1 min', '1 hour', '1 day', '1 week'] def comptime_vs_amp(neuron, a, Fdrive, amps, tstim, toffset, inputdir, fs=8, lw=2, ps=4): ''' Comparative plot of computation times for different acoustic amplitudes. ''' # Get filepaths xlabel = 'Amplitude (kPa)' subdir = os.path.join(inputdir, neuron) - sonic_fpaths = getSims(subdir, neuron, a, nbls.createQueue( + sonic_fpaths = getSims(subdir, neuron, a, nbls.simQueue( [Fdrive], amps, [tstim], [toffset], [None], [1.], 'sonic')) - full_fpaths = getSims(subdir, neuron, a, nbls.createQueue( + full_fpaths = getSims(subdir, neuron, a, nbls.simQueue( [Fdrive], amps, [tstim], [toffset], [None], [1.], 'full')) data_fpaths = {'full': full_fpaths, 'sonic': sonic_fpaths} # Extract computation times (s) comptimes_fpath = os.path.join(inputdir, '{}_comptimes_vs_amps.csv'.format(neuron)) comptimes = getCompTimesQuant( inputdir, neuron, amps * 1e-3, xlabel, data_fpaths, comptimes_fpath) tcomp_lookup = getLookupsCompTime(neuron) # Extract threshold excitation amplitude CW_Athr_vs_Fdrive = getCWtitrations_vs_Fdrive( ['RS'], a, [Fdrive], tstim, toffset, os.path.join(inputdir, 'CW_Athrs_vs_freqs.csv')) Athr = CW_Athr_vs_Fdrive.loc[Fdrive * 1e-3, 'RS'] # Plot comparative profiles of computation times vs. amplitude fig, ax = plt.subplots(figsize=cm2inch(5.5, 5.8)) plt.subplots_adjust(bottom=0.2, left=0.25, right=0.95, top=0.95) ax.spines['top'].set_visible(False) ax.spines['right'].set_visible(False) ax.set_xlabel(xlabel, fontsize=fs, labelpad=1) ax.set_ylabel('Computation time (s)', fontsize=fs) ax.set_xscale('log') ax.set_yscale('log') ax.set_ylim((1e-1, 1e6)) for y, lbl in zip(time_indicators, time_indicators_labels): ax.axhline(y, linewidth=0.5, linestyle='--', c='k') ax.text(amps.max() * 1e-3, 1.2 * y, lbl, horizontalalignment='right', fontsize=fs) ax.get_yaxis().set_tick_params(which='minor', size=0) ax.get_yaxis().set_tick_params(which='minor', width=0) ax.axhline(tcomp_lookup, color='k', linewidth=lw) ax.axvline(Athr, linestyle='--', color='k', linewidth=1) colors = ['silver', 'dimgrey'] for i, key in enumerate(comptimes): ax.plot(amps * 1e-3, comptimes[key], 'o--', color=colors[i], linewidth=lw, label=key, markersize=ps) for item in ax.get_yticklabels(): item.set_fontsize(fs) for item in ax.get_xticklabels(): item.set_fontsize(fs) fig.canvas.set_window_title(figbase + 'a') return fig def comptime_vs_freq(neuron, a, freqs, CW_Athrs, tstim, toffset, inputdir, fs=8, lw=2, ps=4): ''' Comparative plot of computation times for different US frequencies. ''' # Get filepaths xlabel = 'Frequency (kHz)' subdir = os.path.join(inputdir, neuron) sonic_fpaths, full_fpaths = [], [] for Fdrive in freqs: Athr = CW_Athrs[neuron].loc[Fdrive * 1e-3] # kPa Adrive = (Athr + 20.) * 1e3 # Pa - sonic_fpaths += getSims(subdir, neuron, a, nbls.createQueue( + sonic_fpaths += getSims(subdir, neuron, a, nbls.simQueue( [Fdrive], [Adrive], [tstim], [toffset], [None], [1.], 'sonic')) - full_fpaths += getSims(subdir, neuron, a, nbls.createQueue( + full_fpaths += getSims(subdir, neuron, a, nbls.simQueue( [Fdrive], [Adrive], [tstim], [toffset], [None], [1.], 'full')) data_fpaths = {'full': full_fpaths, 'sonic': sonic_fpaths} # Extract computation times (s) comptimes_fpath = os.path.join(inputdir, '{}_comptimes_vs_freqs.csv'.format(neuron)) comptimes = getCompTimesQuant( inputdir, neuron, freqs * 1e-3, xlabel, data_fpaths, comptimes_fpath) tcomp_lookup = getLookupsCompTime(neuron) # Plot comparative profiles of computation time vs. frequency fig, ax = plt.subplots(figsize=cm2inch(5.5, 5.8)) plt.subplots_adjust(bottom=0.2, left=0.25, right=0.95, top=0.95) ax.spines['top'].set_visible(False) ax.spines['right'].set_visible(False) ax.set_xlabel(xlabel, fontsize=fs, labelpad=1) ax.set_ylabel('Computation time (s)', fontsize=fs) ax.set_xscale('log') ax.set_yscale('log') ax.set_ylim((1e-1, 1e6)) for y, lbl in zip(time_indicators, time_indicators_labels): ax.axhline(y, linewidth=0.5, linestyle='--', c='k') ax.text(freqs.max() * 1e-3, 1.2 * y, lbl, horizontalalignment='right', fontsize=fs) ax.get_yaxis().set_tick_params(which='minor', size=0) ax.get_yaxis().set_tick_params(which='minor', width=0) ax.axhline(tcomp_lookup, color='k', linewidth=lw) colors = ['silver', 'dimgrey'] for i, key in enumerate(comptimes): ax.plot(freqs * 1e-3, comptimes[key], 'o--', color=colors[i], linewidth=lw, label=key, markersize=ps) for item in ax.get_yticklabels(): item.set_fontsize(fs) for item in ax.get_xticklabels(): item.set_fontsize(fs) fig.canvas.set_window_title(figbase + 'b') return fig def comptime_vs_radius(neuron, radii, Fdrive, CW_Athrs, tstim, toffset, inputdir, fs=8, lw=2, ps=4): ''' Comparative plot of computation times for different sonophore radii. ''' # Get filepaths xlabel = 'Sonophore radius (nm)' subdir = os.path.join(inputdir, neuron) sonic_fpaths, full_fpaths = [], [] for a in radii: Athr = CW_Athrs[neuron].loc[np.round(a * 1e9, 1)] # kPa Adrive = (Athr + 20.) * 1e3 # Pa - sonic_fpaths += getSims(subdir, neuron, a, nbls.createQueue( + sonic_fpaths += getSims(subdir, neuron, a, nbls.simQueue( [Fdrive], [Adrive], [tstim], [toffset], [None], [1.], 'sonic')) - full_fpaths += getSims(subdir, neuron, a, nbls.createQueue( + full_fpaths += getSims(subdir, neuron, a, nbls.simQueue( [Fdrive], [Adrive], [tstim], [toffset], [None], [1.], 'full')) data_fpaths = {'full': full_fpaths, 'sonic': sonic_fpaths} # Extract computation times (s) comptimes_fpath = os.path.join(inputdir, '{}_comptimes_vs_radius.csv'.format(neuron)) comptimes = getCompTimesQuant( inputdir, neuron, radii * 1e9, xlabel, data_fpaths, comptimes_fpath) tcomp_lookup = getLookupsCompTime(neuron) # Plot comparative profiles of computation time vs. frequency fig, ax = plt.subplots(figsize=cm2inch(5.5, 5.8)) plt.subplots_adjust(bottom=0.2, left=0.25, right=0.95, top=0.95) ax.spines['top'].set_visible(False) ax.spines['right'].set_visible(False) ax.set_xlabel(xlabel, fontsize=fs, labelpad=1) ax.set_ylabel('Computation time (s)', fontsize=fs) ax.set_xscale('log') ax.set_yscale('log') ax.set_ylim((1e-1, 1e6)) for y, lbl in zip(time_indicators, time_indicators_labels): ax.axhline(y, linewidth=0.5, linestyle='--', c='k') ax.text(radii.max() * 1e9, 1.2 * y, lbl, horizontalalignment='right', fontsize=fs) ax.get_yaxis().set_tick_params(which='minor', size=0) ax.get_yaxis().set_tick_params(which='minor', width=0) ax.axhline(tcomp_lookup, color='k', linewidth=lw) colors = ['silver', 'dimgrey'] for i, key in enumerate(comptimes): ax.plot(radii * 1e9, comptimes[key], 'o--', color=colors[i], linewidth=lw, label=key, markersize=ps) for item in ax.get_yticklabels(): item.set_fontsize(fs) for item in ax.get_xticklabels(): item.set_fontsize(fs) fig.canvas.set_window_title(figbase + 'c') return fig def comptime_vs_DC(neurons, a, Fdrive, Adrive, tstim, toffset, PRF, DCs, inputdir, fs=8, lw=2, ps=4): ''' Comparative plot of computation times for different dity cycles and neuron types. ''' xlabel = 'Duty cycle (%)' colors = list(plt.get_cmap('Paired').colors[:6]) del colors[2:4] # Create figure fig, ax = plt.subplots(figsize=cm2inch(5.5, 5.8)) plt.subplots_adjust(bottom=0.2, left=0.25, right=0.95, top=0.95) ax.spines['top'].set_visible(False) ax.spines['right'].set_visible(False) ax.set_xlabel(xlabel, fontsize=fs, labelpad=-7) ax.set_ylabel('Computation time (s)', fontsize=fs) ax.set_xticks([DCs.min() * 1e2, DCs.max() * 1e2]) ax.set_yscale('log') ax.set_ylim((1e-1, 1e6)) for y, lbl in zip(time_indicators, time_indicators_labels): ax.axhline(y, linewidth=0.5, linestyle='--', c='k') ax.text(DCs.max() * 1e2, 1.2 * y, lbl, horizontalalignment='right', fontsize=fs) ax.get_yaxis().set_tick_params(which='minor', size=0) ax.get_yaxis().set_tick_params(which='minor', width=0) # Loop through neurons for i, neuron in enumerate(neurons): # Get filepaths subdir = os.path.join(inputdir, neuron) - sonic_fpaths = getSims(subdir, neuron, a, nbls.createQueue( + sonic_fpaths = getSims(subdir, neuron, a, nbls.simQueue( [Fdrive], [Adrive], [tstim], [toffset], [PRF], DCs, 'sonic')) - full_fpaths = getSims(subdir, neuron, a, nbls.createQueue( + full_fpaths = getSims(subdir, neuron, a, nbls.simQueue( [Fdrive], [Adrive], [tstim], [toffset], [PRF], DCs, 'full')) sonic_fpaths = sonic_fpaths[1:] + [sonic_fpaths[0]] full_fpaths = full_fpaths[1:] + [full_fpaths[0]] data_fpaths = {'full': full_fpaths, 'sonic': sonic_fpaths} # Extract computation times (s) comptimes_fpath = os.path.join(inputdir, '{}_comptimes_vs_DC.csv'.format(neuron)) comptimes = getCompTimesQuant( inputdir, neuron, DCs * 1e2, xlabel, data_fpaths, comptimes_fpath) # Plot ax.plot(DCs * 1e2, comptimes['full'], 'o--', color=colors[2 * i], linewidth=lw, markersize=ps) ax.plot(DCs * 1e2, comptimes['sonic'], 'o--', color=colors[2 * i + 1], linewidth=lw, markersize=ps, label=neuron) fig.canvas.set_window_title(figbase + 'd') return fig def main(): ap = ArgumentParser() # Runtime options ap.add_argument('-v', '--verbose', default=False, action='store_true', help='Increase verbosity') ap.add_argument('-i', '--inputdir', type=str, help='Input directory') ap.add_argument('-f', '--figset', type=str, nargs='+', help='Figure set', default='all') ap.add_argument('-s', '--save', default=False, action='store_true', help='Save output figures as pdf') args = ap.parse_args() loglevel = logging.DEBUG if args.verbose is True else logging.INFO logger.setLevel(loglevel) inputdir = selectDirDialog() if args.inputdir is None else args.inputdir if inputdir == '': logger.error('No input directory chosen') return figset = args.figset if figset == 'all': figset = ['a', 'b', 'c', 'd'] logger.info('Generating panels {} of {}'.format(figset, figbase)) # Parameters a = 32e-9 # m radii = np.array([16, 22.6, 32, 45.3, 64]) * 1e-9 # nm tstim = 150e-3 # s toffset = 100e-3 # s freqs = np.array([20e3, 100e3, 500e3, 1e6, 2e6, 3e6, 4e6]) # Hz Fdrive = 500e3 # Hz CW_Athr_vs_Fdrive = getCWtitrations_vs_Fdrive( ['RS'], a, freqs, tstim, toffset, os.path.join(inputdir, 'CW_Athrs_vs_freqs.csv')) Athr = CW_Athr_vs_Fdrive['RS'].loc[Fdrive * 1e-3] amps1 = np.array([Athr - 5, Athr, Athr + 20]) * 1e3 amps2 = np.array([50, 100, 300, 600]) * 1e3 # Pa amps = np.sort(np.hstack([amps1, amps2])) CW_Athr_vs_radius = getCWtitrations_vs_radius( ['RS'], radii, Fdrive, tstim, toffset, os.path.join(inputdir, 'CW_Athrs_vs_radius.csv')) Adrive = 100e3 # Pa PRF = 100 # Hz DCs = np.array([5, 10, 25, 50, 75, 100]) * 1e-2 # Generate figures figs = [] if 'a' in figset: figs.append(comptime_vs_amp('RS', a, Fdrive, amps, tstim, toffset, inputdir)) if 'b' in figset: figs.append(comptime_vs_freq('RS', a, freqs, CW_Athr_vs_Fdrive, tstim, toffset, inputdir)) if 'c' in figset: figs.append(comptime_vs_radius( 'RS', radii, Fdrive, CW_Athr_vs_radius, tstim, toffset, inputdir)) if 'd' in figset: figs.append(comptime_vs_DC( ['RS', 'LTS'], a, Fdrive, Adrive, tstim, toffset, PRF, DCs, inputdir)) if args.save: for fig in figs: figname = '{}.pdf'.format(fig.canvas.get_window_title()) fig.savefig(os.path.join(inputdir, figname), transparent=True) else: plt.show() if __name__ == '__main__': main() diff --git a/paper figures/utils.py b/paper figures/utils.py index b166b07..6bd62db 100644 --- a/paper figures/utils.py +++ b/paper figures/utils.py @@ -1,119 +1,120 @@ # -*- coding: utf-8 -*- # @Author: Theo Lemaire # @Date: 2018-10-01 20:45:29 # @Last Modified by: Theo Lemaire -# @Last Modified time: 2019-06-01 16:36:29 +# @Last Modified time: 2019-06-02 13:42:06 import os import numpy as np import pandas as pd from PySONIC.utils import * from PySONIC.core import NeuronalBilayerSonophore from PySONIC.neurons import * from PySONIC.postpro import computeSpikingMetrics def getCWtitrations_vs_Fdrive(neurons, a, freqs, tstim, toffset, fpath): fkey = 'Fdrive (kHz)' freqs = np.array(freqs) if os.path.isfile(fpath): df = pd.read_csv(fpath, sep=',', index_col=fkey) else: df = pd.DataFrame(index=freqs * 1e-3) for neuron in neurons: if neuron not in df: neuronobj = getNeuronsDict()[neuron]() nbls = NeuronalBilayerSonophore(a, neuronobj) for i, Fdrive in enumerate(freqs): logger.info('Running CW titration for %s neuron @ %sHz', neuron, si_format(Fdrive)) Athr = nbls.titrate(Fdrive, tstim, toffset) # Pa df.loc[Fdrive * 1e-3, neuron] = np.ceil(Athr * 1e-2) / 10 df.sort_index(inplace=True) df.to_csv(fpath, sep=',', index_label=fkey) return df def getCWtitrations_vs_radius(neurons, radii, Fdrive, tstim, toffset, fpath): akey = 'radius (nm)' radii = np.array(radii) if os.path.isfile(fpath): df = pd.read_csv(fpath, sep=',', index_col=akey) else: df = pd.DataFrame(index=radii * 1e9) for neuron in neurons: if neuron not in df: neuronobj = getNeuronsDict()[neuron]() for a in radii: nbls = NeuronalBilayerSonophore(a, neuronobj) logger.info( 'Running CW titration for %s neuron @ %sHz (%.2f nm sonophore radius)', neuron, si_format(Fdrive), a * 1e9) Athr = nbls.titrate(Fdrive, tstim, toffset) # Pa df.loc[a * 1e9, neuron] = np.ceil(Athr * 1e-2) / 10 df.sort_index(inplace=True) df.to_csv(fpath, sep=',', index_label=akey) return df def getSims(outdir, neuron, a, queue): fpaths = [] updated_queue = [] neuronobj = getNeuronsDict()[neuron]() nbls = NeuronalBilayerSonophore(a, neuronobj) for i, item in enumerate(queue): Fdrive, tstim, toffset, PRF, DC, Adrive, method = item fcode = nbls.filecode(Fdrive, Adrive, tstim, toffset, PRF, DC, method) fpath = os.path.join(outdir, '{}.pkl'.format(fcode)) if not os.path.isfile(fpath): print(fpath, 'does not exist') item.insert(0, outdir) updated_queue.append(item) fpaths.append(fpath) if len(updated_queue) > 0: print(updated_queue) # neuron = getNeuronsDict()[neuron]() # nbls = NeuronalBilayerSonophore(a, neuron) - # runBatch(nbls.runAndSave, updated_queue, extra_params=[outdir], mpi=True) + # batch = Batch(nbls.runAndSave, updated_queue) + # batch.run(mpi=True) return fpaths def getSpikingMetrics(outdir, neuron, xvar, xkey, data_fpaths, metrics_fpaths): metrics = {} for stype in data_fpaths.keys(): if os.path.isfile(metrics_fpaths[stype]): logger.info('loading spiking metrics from file: "%s"', metrics_fpaths[stype]) metrics[stype] = pd.read_csv(metrics_fpaths[stype], sep=',') else: logger.warning('computing %s spiking metrics vs. %s for %s neuron', stype, xkey, neuron) metrics[stype] = computeSpikingMetrics(data_fpaths[stype]) metrics[stype][xkey] = pd.Series(xvar, index=metrics[stype].index) metrics[stype].to_csv(metrics_fpaths[stype], sep=',', index=False) return metrics 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 getCompTimesQuant(outdir, neuron, xvars, xkey, data_fpaths, comptimes_fpath): if os.path.isfile(comptimes_fpath): logger.info('reading computation times from file: "%s"', comptimes_fpath) comptimes = pd.read_csv(comptimes_fpath, sep=',', index_col=xkey) else: logger.warning('extracting computation times for %s neuron', neuron) comptimes = pd.DataFrame(index=xvars) for stype in data_fpaths.keys(): for i, xvar in enumerate(xvars): comptimes.loc[xvar, stype] = extractCompTimes([data_fpaths[stype][i]]) comptimes.to_csv(comptimes_fpath, sep=',', index_label=xkey) return comptimes diff --git a/scripts/run_astim.py b/scripts/run_astim.py index 81a2352..34de513 100644 --- a/scripts/run_astim.py +++ b/scripts/run_astim.py @@ -1,158 +1,161 @@ #!/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: 2019-05-31 17:01:35 +# @Last Modified time: 2019-06-02 13:49:52 ''' Run A-STIM simulations of a specific point-neuron. ''' import os import logging import numpy as np import matplotlib.pyplot as plt from argparse import ArgumentParser -from PySONIC.core import NeuronalBilayerSonophore, runBatch +from PySONIC.core import NeuronalBilayerSonophore, Batch from PySONIC.utils import logger, selectDirDialog, parseUSAmps from PySONIC.neurons import getNeuronsDict from PySONIC.plt import plotBatch # Default parameters defaults = dict( neuron='RS', radius=[32.0], # nm freq=[500.0], # kHz amp=[100.0], # kPa duration=[100.0], # ms PRF=[100.0], # Hz DC=[100.0], # % offset=[50.], # ms method='sonic' ) -def runAStimBatch(outdir, nbls, stim_params, method, mpi=False): +def runAStimBatch(outdir, nbls, stim_params, method, mpi=False, loglevel=logging.INFO): ''' Run batch A-STIM simulations of the system for various neuron types and stimulation parameters. :param outdir: full path to output directory :param stim_params: dictionary containing sweeps for all stimulation parameters :param method: numerical integration method ("classic", "hybrid" or "sonic") :param mpi: boolean statting wether or not to use multiprocessing + :param loglevel: logging level :return: list of full paths to the output files ''' mandatory_params = ['freqs', 'durations', 'offsets', 'PRFs', 'DCs'] for mparam in mandatory_params: if mparam not in stim_params: raise ValueError('Missing stimulation parameter field: "{}"'.format(mparam)) logger.info("Starting A-STIM simulation batch") # Generate queue - queue = nbls.createQueue( + queue = nbls.simQueue( stim_params['freqs'], stim_params.get('amps', None), stim_params['durations'], stim_params['offsets'], stim_params['PRFs'], stim_params['DCs'], method ) for item in queue: item.insert(0, outdir) # Run batch - return runBatch(nbls.runAndSave, queue, mpi=mpi) + batch = Batch(nbls.runAndSave, queue) + return batch(mpi=mpi, loglevel=loglevel) def main(): ap = ArgumentParser() # Runtime options ap.add_argument('--mpi', 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', type=str, nargs='+', help='Variables to plot') ap.add_argument('-o', '--outputdir', type=str, default=None, help='Output directory') ap.add_argument('-t', '--titrate', default=False, action='store_true', help='Perform titration') ap.add_argument('-m', '--method', type=str, default=defaults['method'], help='Numerical integration method ("classic", "hybrid" or "sonic")') # Stimulation parameters ap.add_argument('-n', '--neuron', type=str, default=defaults['neuron'], help='Neuron name (string)') ap.add_argument('-a', '--radius', nargs='+', type=float, help='Sonophore radius (nm)') ap.add_argument('-f', '--freq', nargs='+', type=float, help='US frequency (kHz)') ap.add_argument('-A', '--amp', nargs='+', type=float, help='Acoustic pressure amplitude (kPa)') ap.add_argument('--Arange', type=str, nargs='+', help='Amplitude range [scale min max n] (kPa)') ap.add_argument('-I', '--intensity', nargs='+', type=float, help='Acoustic intensity (W/cm2)') ap.add_argument('--Irange', type=str, nargs='+', help='Intensity range [scale min max n] (W/cm2)') ap.add_argument('-d', '--duration', nargs='+', type=float, help='Stimulus duration (ms)') ap.add_argument('--offset', nargs='+', type=float, help='Offset duration (ms)') ap.add_argument('--PRF', nargs='+', type=float, help='PRF (Hz)') ap.add_argument('--DC', nargs='+', type=float, help='Duty cycle (%%)') ap.add_argument('--spanDC', default=False, action='store_true', help='Span DC from 1 to 100%') # Parse arguments args = {key: value for key, value in vars(ap.parse_args()).items() if value is not None} loglevel = logging.DEBUG if args['verbose'] is True else logging.INFO logger.setLevel(loglevel) outdir = args['outputdir'] if 'outputdir' in args else selectDirDialog() if outdir == '': logger.error('No output directory selected') quit() mpi = args['mpi'] titrate = args['titrate'] method = args['method'] neuron_str = args['neuron'] radii = np.array(args.get('radius', defaults['radius'])) * 1e-9 # m try: amps = parseUSAmps(args, defaults) except ValueError as err: logger.error(err) quit() if args['spanDC']: DCs = np.arange(1, 101) # % else: DCs = np.array(args.get('DC', defaults['DC'])) # % stim_params = dict( freqs=np.array(args.get('freq', defaults['freq'])) * 1e3, # Hz amps=amps, # Pa durations=np.array(args.get('duration', defaults['duration'])) * 1e-3, # s PRFs=np.array(args.get('PRF', defaults['PRF'])), # Hz DCs=DCs * 1e-2, # (-) offsets=np.array(args.get('offset', defaults['offset'])) * 1e-3 # s ) if titrate: stim_params['amps'] = None # Run A-STIM batch if neuron_str not in getNeuronsDict(): logger.error('Unknown neuron type: "%s"', neuron_str) return neuron = getNeuronsDict()[neuron_str]() pkl_filepaths = [] for a in radii: nbls = NeuronalBilayerSonophore(a, neuron) - pkl_filepaths += runAStimBatch(outdir, nbls, stim_params, method, mpi=mpi) + pkl_filepaths += runAStimBatch(outdir, nbls, stim_params, method, + mpi=mpi, loglevel=loglevel) pkl_dir, _ = os.path.split(pkl_filepaths[0]) # Plot resulting profiles if 'plot' in args: if args['plot'] == ['all']: pltscheme = None else: pltscheme = {x: [x] for x in args['plot']} plotBatch(pkl_filepaths, pltscheme=pltscheme) plt.show() if __name__ == '__main__': main() diff --git a/scripts/run_estim.py b/scripts/run_estim.py index c9bb4de..c42c283 100644 --- a/scripts/run_estim.py +++ b/scripts/run_estim.py @@ -1,129 +1,131 @@ # -*- coding: utf-8 -*- # @Author: Theo Lemaire # @Date: 2017-08-24 11:55:07 # @Last Modified by: Theo Lemaire -# @Last Modified time: 2019-05-31 17:04:40 +# @Last Modified time: 2019-06-02 13:48:33 ''' Run E-STIM simulations of a specific point-neuron. ''' import os import logging import numpy as np import matplotlib.pyplot as plt from argparse import ArgumentParser -from PySONIC.core import runBatch +from PySONIC.core import Batch from PySONIC.utils import logger, selectDirDialog, parseElecAmps from PySONIC.neurons import * from PySONIC.plt import plotBatch # Default parameters defaults = dict( neuron='RS', amp=[10.0], # mA/m2 duration=[100.0], # ms PRF=[100.0], # Hz DC=[100.0], # % offset=[50.], # ms method='sonic' ) -def runEStimBatch(outdir, neuron, stim_params, mpi=False): +def runEStimBatch(outdir, neuron, stim_params, mpi=False, loglevel=logging.INFO): ''' Run batch E-STIM simulations of the system for various neuron types and stimulation parameters. :param outdir: full path to output directory :param stim_params: dictionary containing sweeps for all stimulation parameters :param mpi: boolean statting wether or not to use multiprocessing + :param loglevel: logging level :return: list of full paths to the output files ''' mandatory_params = ['durations', 'offsets', 'PRFs', 'DCs'] for mparam in mandatory_params: if mparam not in stim_params: raise ValueError('Missing stimulation parameter field: "{}"'.format(mparam)) logger.info("Starting E-STIM simulation batch") # Generate simulations queue - queue = neuron.createQueue( + queue = neuron.simQueue( stim_params.get('amps', None), stim_params['durations'], stim_params['offsets'], stim_params['PRFs'], stim_params['DCs'] ) for item in queue: item.insert(0, outdir) # Run batch - return runBatch(neuron.runAndSave, queue, mpi=mpi) + batch = Batch(neuron.runAndSave, queue) + return batch(mpi=mpi, loglevel=loglevel) def main(): ap = ArgumentParser() # Runtime options ap.add_argument('--mpi', 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', type=str, nargs='+', help='Variables to plot') ap.add_argument('-o', '--outputdir', type=str, default=None, help='Output directory') ap.add_argument('-t', '--titrate', default=False, action='store_true', help='Perform titration') # Stimulation parameters ap.add_argument('-n', '--neuron', type=str, default=defaults['neuron'], help='Neuron name (string)') ap.add_argument('-A', '--amp', nargs='+', type=float, help='Injected current density (mA/m2)') ap.add_argument('--Arange', type=str, nargs='+', help='Amplitude range [scale min max n] (mA/m2)') ap.add_argument('-d', '--duration', nargs='+', type=float, help='Stimulus duration (ms)') ap.add_argument('--offset', nargs='+', type=float, help='Offset duration (ms)') ap.add_argument('--PRF', nargs='+', type=float, help='PRF (Hz)') ap.add_argument('--DC', nargs='+', type=float, help='Duty cycle (%%)') # Parse arguments args = {key: value for key, value in vars(ap.parse_args()).items() if value is not None} loglevel = logging.DEBUG if args['verbose'] is True else logging.INFO logger.setLevel(loglevel) outdir = args['outputdir'] if 'outputdir' in args else selectDirDialog() if outdir == '': logger.error('No output directory selected') quit() titrate = args['titrate'] neuron_str = args['neuron'] try: amps = parseElecAmps(args, defaults) except ValueError as err: logger.error(err) quit() stim_params = dict( amps=amps, durations=np.array(args.get('duration', defaults['duration'])) * 1e-3, # s PRFs=np.array(args.get('PRF', defaults['PRF'])), # Hz DCs=np.array(args.get('DC', defaults['DC'])) * 1e-2, # (-) offsets=np.array(args.get('offset', defaults['offset'])) * 1e-3 # s ) if titrate: stim_params['amps'] = None # Run E-STIM batch if neuron_str not in getNeuronsDict(): logger.error('Unknown neuron type: "%s"', neuron_str) return neuron = getNeuronsDict()[neuron_str]() - pkl_filepaths = runEStimBatch(outdir, neuron, stim_params, mpi=args['mpi']) + pkl_filepaths = runEStimBatch(outdir, neuron, stim_params, mpi=args['mpi'], loglevel=loglevel) pkl_dir, _ = os.path.split(pkl_filepaths[0]) # Plot resulting profiles if 'plot' in args: if args['plot'] == ['all']: pltscheme = None else: pltscheme = {x: [x] for x in args['plot']} plotBatch(pkl_filepaths, pltscheme=pltscheme) plt.show() if __name__ == '__main__': main() diff --git a/scripts/run_lookups.py b/scripts/run_lookups.py index ae17233..b888630 100644 --- a/scripts/run_lookups.py +++ b/scripts/run_lookups.py @@ -1,217 +1,217 @@ #!/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: 2019-05-31 17:07:01 +# @Last Modified time: 2019-06-02 13:45:43 ''' Create lookup table for specific neuron. ''' import os import itertools import pickle import logging import numpy as np from argparse import ArgumentParser from PySONIC.utils import logger, getNeuronLookupsFile from PySONIC.neurons import getNeuronsDict -from PySONIC.core import NeuronalBilayerSonophore, createQueue, runBatch +from PySONIC.core import NeuronalBilayerSonophore, createQueue, Batch # Default parameters defaults = dict( neuron='RS', radius=np.array([16.0, 32.0, 64.0]), # nm freq=np.array([20., 100., 500., 1e3, 2e3, 3e3, 4e3]), # kHz amp=np.insert(np.logspace(np.log10(0.1), np.log10(600), num=50), 0, 0.0), # kPa ) def computeAStimLookups(neuron, aref, fref, Aref, Qref, fsref=None, mpi=False, loglevel=logging.INFO): ''' Run simulations of the mechanical system for a multiple combinations of imposed sonophore radius, US frequencies, acoustic amplitudes charge densities and (spatially-averaged) sonophore membrane coverage fractions, compute effective coefficients and store them in a dictionary of n-dimensional arrays. :param neuron: neuron object :param aref: array of sonophore radii (m) :param fref: array of acoustic drive frequencies (Hz) :param Aref: array of acoustic drive amplitudes (Pa) :param Qref: array of membrane charge densities (C/m2) :param fsref: acoustic drive phase (rad) :param mpi: boolean statting wether or not to use multiprocessing :param loglevel: logging level :return: lookups dictionary ''' descs = { 'a': 'sonophore radii', 'f': 'US frequencies', 'A': 'US amplitudes', 'fs': 'sonophore membrane coverage fractions' } # Populate inputs dictionary inputs = { 'a': aref, # nm 'f': fref, # Hz 'A': Aref, # Pa 'Q': Qref # C/m2 } # Add fs to inputs if provided, otherwise add default value (1) err_fs = 'cannot span {} for more than 1 {}' if fsref is not None: for x in ['a', 'f']: assert inputs[x].size == 1, err_fs.format(descs['fs'], descs[x]) inputs['fs'] = fsref else: inputs['fs'] = np.array([1.]) # Check validity of input parameters for key, values in inputs.items(): if not (isinstance(values, list) or isinstance(values, np.ndarray)): raise TypeError( 'Invalid {} (must be provided as list or numpy array)'.format(descs[key])) if not all(isinstance(x, float) for x in values): raise TypeError('Invalid {} (must all be float typed)'.format(descs[key])) if len(values) == 0: raise ValueError('Empty {} array'.format(key)) if key in ('a', 'f') and min(values) <= 0: raise ValueError('Invalid {} (must all be strictly positive)'.format(descs[key])) if key in ('A', 'fs') and min(values) < 0: raise ValueError('Invalid {} (must all be positive or null)'.format(descs[key])) # Get dimensions of inputs that have more than one value dims = np.array([x.size for x in inputs.values()]) dims = dims[dims > 1] ncombs = dims.prod() - print(dims, ncombs) # Create simulation queue per radius queue = createQueue(fref, Aref, Qref) for i in range(len(queue)): queue[i].append(inputs['fs']) # Run simulations and populate outputs (list of lists) logger.info('Starting simulation batch for %s neuron', neuron.name) outputs = [] for a in aref: nbls = NeuronalBilayerSonophore(a, neuron) - outputs += runBatch(nbls.computeEffVars, queue, mpi=mpi, loglevel=loglevel) + batch = Batch(nbls.computeEffVars, queue) + outputs += batch(mpi=mpi, loglevel=loglevel) # Split comp times and effvars from outputs tcomps, effvars = [list(x) for x in zip(*outputs)] effvars = list(itertools.chain.from_iterable(effvars)) # Reshape effvars into nD arrays and add them to lookups dictionary logger.info('Reshaping output into lookup tables') varkeys = list(effvars[0].keys()) nout = len(effvars) assert nout == ncombs, 'number of outputs does not match number of combinations' lookups = {} for key in varkeys: effvar = [effvars[i][key] for i in range(nout)] lookups[key] = np.array(effvar).reshape(dims) # Reshape comp times into nD array (minus fs dimension) if fsref is not None: dims = dims[:-1] tcomps = np.array(tcomps).reshape(dims) # Store inputs, lookup data and comp times in dictionary df = { 'input': inputs, 'lookup': lookups, 'tcomp': tcomps } return df def main(): ap = ArgumentParser() # Runtime options ap.add_argument('--mpi', default=False, action='store_true', help='Use multiprocessing') ap.add_argument('-v', '--verbose', default=False, action='store_true', help='Increase verbosity') ap.add_argument('-t', '--test', default=False, action='store_true', help='Test configuration') # Stimulation parameters ap.add_argument('-n', '--neuron', type=str, default=defaults['neuron'], help='Neuron name (string)') ap.add_argument('-a', '--radius', nargs='+', type=float, help='Sonophore radius (nm)') ap.add_argument('-f', '--freq', nargs='+', type=float, help='US frequency (kHz)') ap.add_argument('-A', '--amp', nargs='+', type=float, help='Acoustic pressure amplitude (kPa)') ap.add_argument('-Q', '--charge', nargs='+', type=float, help='Membrane charge density (nC/cm2)') ap.add_argument('--spanFs', default=False, action='store_true', help='Span sonophore coverage fraction') # Parse arguments args = {key: value for key, value in vars(ap.parse_args()).items() if value is not None} loglevel = logging.DEBUG if args['verbose'] is True else logging.INFO logger.setLevel(loglevel) mpi = args['mpi'] neuron_str = args['neuron'] radii = np.array(args.get('radius', defaults['radius'])) * 1e-9 # m freqs = np.array(args.get('freq', defaults['freq'])) * 1e3 # Hz amps = np.array(args.get('amp', defaults['amp'])) * 1e3 # Pa # Check neuron name validity if neuron_str not in getNeuronsDict(): logger.error('Unknown neuron type: "%s"', neuron_str) return neuron = getNeuronsDict()[neuron_str]() # Determine charge vector if 'charge' in args: charges = np.array(args['charge']) * 1e-5 # C/m2 else: charges = np.arange(neuron.Qbounds()[0], neuron.Qbounds()[1] + 1e-5, 1e-5) # C/m2 # Determine fs vector fs = None if args['spanFs']: fs = np.linspace(0, 100, 101) * 1e-2 # (-) # Determine output filename lookup_path = { True: getNeuronLookupsFile(neuron.name), False: getNeuronLookupsFile(neuron.name, a=radii[0], Fdrive=freqs[0], fs=True) }[fs is None] # Combine inputs into single list inputs = [radii, freqs, amps, charges, fs] # Adapt inputs and output filename if test case if args['test']: for i, x in enumerate(inputs): if x is not None and x.size > 1: inputs[i] = np.array([x.min(), x.max()]) lookup_path = '{}_test{}'.format(*os.path.splitext(lookup_path)) # Check if lookup file already exists if os.path.isfile(lookup_path): logger.warning('"%s" file already exists and will be overwritten. ' + 'Continue? (y/n)', lookup_path) user_str = input() if user_str not in ['y', 'Y']: logger.error('%s Lookup creation canceled', neuron.name) return # Compute lookups df = computeAStimLookups(neuron, *inputs, mpi=mpi, loglevel=loglevel) # Save dictionary in lookup file logger.info('Saving %s neuron lookup table in file: "%s"', neuron.name, lookup_path) with open(lookup_path, 'wb') as fh: pickle.dump(df, fh) if __name__ == '__main__': main() diff --git a/scripts/run_mech.py b/scripts/run_mech.py index faf78fd..5ae1028 100644 --- a/scripts/run_mech.py +++ b/scripts/run_mech.py @@ -1,134 +1,138 @@ #!/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: 2019-05-31 17:05:15 +# @Last Modified time: 2019-06-02 13:46:53 ''' Run simulations of the NICE mechanical model. ''' import os import logging import numpy as np import matplotlib.pyplot as plt from argparse import ArgumentParser -from PySONIC.core import BilayerSonophore, runBatch +from PySONIC.core import BilayerSonophore, Batch from PySONIC.utils import logger, selectDirDialog, parseUSAmps from PySONIC.neurons import CorticalRS from PySONIC.plt import plotBatch # Default parameters defaults = dict( Cm0=CorticalRS().Cm0 * 1e2, # uF/m2 Qm0=CorticalRS().Vm0, # nC/m2 radius=[32.0], # nm embedding=[0.], # um freq=[500.0], # kHz amp=[100.0], # kPa charge=[0.] # nC/cm2 ) -def runMechBatch(outdir, bls, stim_params, mpi=False): +def runMechBatch(outdir, bls, stim_params, mpi=False, loglevel=logging.INFO): ''' Run batch simulations of the mechanical system with imposed values of charge density. :param outdir: full path to output directory :param bls: BilayerSonophore object :param stim_params: dictionary containing sweeps for all stimulation parameters :param mpi: boolean stating whether or not to use multiprocessing + :param loglevel: logging level :return: list of full paths to the output files ''' # Checking validity of stimulation parameters mandatory_params = ['freqs', 'amps', 'charges'] for mparam in mandatory_params: if mparam not in stim_params: raise ValueError('Missing stimulation parameter field: "{}"'.format(mparam)) logger.info("Starting mechanical simulation batch") # Unpack stimulation parameters freqs = np.array(stim_params['freqs']) amps = np.array(stim_params['amps']) charges = np.array(stim_params['charges']) - # Generate simulations queue and run batch - queue = bls.createQueue(freqs, amps, charges) + # Generate simulations queue + queue = bls.simQueue(freqs, amps, charges) for item in queue: item.insert(0, outdir) - return runBatch(bls.runAndSave, queue, mpi=mpi) + + # Run simulation batch + batch = Batch(bls.runAndSave, queue) + return batch(mpi=mpi, loglevel=loglevel) def main(): ap = ArgumentParser() # Runtime options ap.add_argument('--mpi', 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', type=str, nargs='+', help='Variables to plot') ap.add_argument('-o', '--outputdir', type=str, default=None, help='Output directory') # Stimulation parameters ap.add_argument('-a', '--radius', nargs='+', type=float, help='Sonophore radius (nm)') ap.add_argument('--Cm0', type=float, default=defaults['Cm0'], help='Resting membrane capacitance (uF/cm2)') ap.add_argument('--Qm0', type=float, default=defaults['Qm0'], help='Resting membrane charge density (nC/cm2)') ap.add_argument('-d', '--embedding', nargs='+', type=float, help='Embedding depth (um)') ap.add_argument('-f', '--freq', nargs='+', type=float, help='US frequency (kHz)') ap.add_argument('-A', '--amp', nargs='+', type=float, help='Acoustic pressure amplitude (kPa)') ap.add_argument('--Arange', type=str, nargs='+', help='Amplitude range [scale min max n] (kPa)') ap.add_argument('-I', '--intensity', nargs='+', type=float, help='Acoustic intensity (W/cm2)') ap.add_argument('--Irange', type=str, nargs='+', help='Intensity range [scale min max n] (W/cm2)') ap.add_argument('-Q', '--charge', nargs='+', type=float, help='Membrane charge density (nC/cm2)') # Parse arguments args = {key: value for key, value in vars(ap.parse_args()).items() if value is not None} loglevel = logging.DEBUG if args['verbose'] is True else logging.INFO logger.setLevel(loglevel) outdir = args['outputdir'] if 'outputdir' in args else selectDirDialog() if outdir == '': logger.error('No output directory selected') quit() mpi = args['mpi'] Cm0 = args['Cm0'] * 1e-2 # F/m2 Qm0 = args['Qm0'] * 1e-5 # C/m2 radii = np.array(args.get('radius', defaults['radius'])) * 1e-9 # m embeddings = np.array(args.get('embedding', defaults['embedding'])) * 1e-6 # m try: amps = parseUSAmps(args, defaults) except ValueError as err: logger.error(err) quit() stim_params = dict( freqs=np.array(args.get('freq', defaults['freq'])) * 1e3, # Hz amps=amps, # Pa charges=np.array(args.get('charge', defaults['charge'])) * 1e-5 # C/m2 ) # Run MECH batch pkl_filepaths = [] for a in radii: for d in embeddings: bls = BilayerSonophore(a, Cm0, Qm0, embedding_depth=d) - pkl_filepaths += runMechBatch(outdir, bls, stim_params, mpi=mpi) + pkl_filepaths += runMechBatch(outdir, bls, stim_params, mpi=mpi, loglevel=loglevel) pkl_dir, _ = os.path.split(pkl_filepaths[0]) # Plot resulting profiles if 'plot' in args: if args['plot'] == ['all']: pltscheme = None else: pltscheme = {x: [x] for x in args['plot']} plotBatch(pkl_filepaths, pltscheme=pltscheme) plt.show() if __name__ == '__main__': main()