diff --git a/PySONIC/core/drives.py b/PySONIC/core/drives.py index ea70c2d..62f835b 100644 --- a/PySONIC/core/drives.py +++ b/PySONIC/core/drives.py @@ -1,475 +1,475 @@ # -*- coding: utf-8 -*- # @Author: Theo Lemaire # @Email: theo.lemaire@epfl.ch # @Date: 2020-01-30 11:46:47 # @Last Modified by: Theo Lemaire -# @Last Modified time: 2020-03-31 15:58:02 +# @Last Modified time: 2020-04-13 12:19:46 import abc import numpy as np from ..utils import si_format, StimObject from ..constants import * from .batches import Batch class Drive(StimObject): ''' Generic interface to drive object. ''' @abc.abstractmethod def __repr__(self): ''' String representation. ''' raise NotImplementedError @abc.abstractmethod def __eq__(self, other): ''' Equality operator. ''' raise NotImplementedError @abc.abstractmethod def copy(self): ''' String representation. ''' raise NotImplementedError @property @abc.abstractmethod def meta(self): raise NotImplementedError @property @abc.abstractmethod def desc(self): raise NotImplementedError @property @abc.abstractmethod def filecodes(self): raise NotImplementedError @abc.abstractmethod def compute(self, t): ''' Compute the input drive at a specific time. :param t: time (s) :return: specific input drive ''' raise NotImplementedError @classmethod def createQueue(cls, *args): ''' Create a list of Drive objects for combinations of input parameters. ''' if len(args) == 1: return [cls(item) for item in args[0]] else: return [cls(*item) for item in Batch.createQueue(*args)] @property def is_searchable(self): return False class XDrive(Drive): ''' Drive object that can be titrated to find the threshold value of one of its inputs. ''' xvar_initial = None xvar_rel_thr = None xvar_thr = None xvar_precheck = False @property @abc.abstractmethod def xvar(self): raise NotImplementedError @xvar.setter @abc.abstractmethod def xvar(self, value): raise NotImplementedError def updatedX(self, value): other = self.copy() other.xvar = value return other @property def is_searchable(self): return True @property def is_resolved(self): return self.xvar is not None class ElectricDrive(XDrive): ''' Electric drive object with constant amplitude. ''' xkey = 'I' xvar_initial = ESTIM_AMP_INITIAL xvar_rel_thr = ESTIM_REL_CONV_THR xvar_range = (0., ESTIM_AMP_UPPER_BOUND) def __init__(self, I): ''' Constructor. :param A: current density (mA/m2) ''' self.I = I @property def I(self): return self._I @I.setter def I(self, value): if value is not None: value = self.checkFloat('I', value) self._I = value def __eq__(self, other): if not isinstance(other, self.__class__): return False return self.I == other.I def __repr__(self): params = [] if self.I is not None: params.append(f'{si_format(self.I * 1e-3, 1, space="")}A/m2') return f'{self.__class__.__name__}({", ".join(params)})' @property def xvar(self): return self.I @xvar.setter def xvar(self, value): self.I = value def copy(self): return self.__class__(self.I) @staticmethod def inputs(): return { 'I': { 'desc': 'current density amplitude', 'label': 'I', 'unit': 'mA/m2', 'factor': 1e0, 'precision': 1 } } @property def meta(self): return {'I': self.I} @property def desc(self): return f'I = {si_format(self.I * 1e-3, 2)}A/m2' @property def filecodes(self): return {'I': f'{self.I:.2f}mAm2'} def compute(self, t): return self.I class VoltageDrive(Drive): ''' Voltage drive object with a held potential and a step potential. ''' def __init__(self, Vhold, Vstep): ''' Constructor. :param Vhold: held voltage (mV) :param Vstep: step voltage (mV) ''' self.Vhold = Vhold self.Vstep = Vstep @property def Vhold(self): return self._Vhold @Vhold.setter def Vhold(self, value): value = self.checkFloat('Vhold', value) self._Vhold = value @property def Vstep(self): return self._Vstep @Vstep.setter def Vstep(self, value): value = self.checkFloat('Vstep', value) self._Vstep = value def __eq__(self, other): if not isinstance(other, self.__class__): return False return self.Vhold == other.Vhold and self.Vstep == other.Vstep def __repr__(self): return f'{self.__class__.__name__}({self.desc})' def copy(self): return self.__class__(self.Vhold, self.Vstep) @staticmethod def inputs(): return { 'Vhold': { 'desc': 'held voltage', 'label': 'V_{hold}', 'unit': 'mV', 'precision': 0 }, 'Vstep': { 'desc': 'step voltage', 'label': 'V_{step}', 'unit': 'mV', 'precision': 0 } } @property def meta(self): return { 'Vhold': self.Vhold, 'Vstep': self.Vstep, } @property def desc(self): return f'Vhold = {self.Vhold:.1f}mV, Vstep = {self.Vstep:.1f}mV' @property def filecodes(self): return { 'Vhold': f'{self.Vhold:.1f}mV', 'Vstep': f'{self.Vstep:.1f}mV', } def compute(self, t): return self.Vstep class AcousticDrive(XDrive): ''' Acoustic drive object with intrinsic frequency and amplitude. ''' xkey = 'A' xvar_initial = ASTIM_AMP_INITIAL xvar_rel_thr = ASTIM_REL_CONV_THR xvar_thr = ASTIM_ABS_CONV_THR xvar_precheck = True def __init__(self, f, A=None, phi=np.pi): ''' Constructor. :param f: carrier frequency (Hz) :param A: peak pressure amplitude (Pa) :param phi: phase (rad) ''' self.f = f self.A = A self.phi = phi @property def f(self): return self._f @f.setter def f(self, value): value = self.checkFloat('f', value) self.checkStrictlyPositive('f', value) self._f = value @property def A(self): return self._A @A.setter def A(self, value): if value is not None: value = self.checkFloat('A', value) self.checkPositiveOrNull('A', value) self._A = value @property def phi(self): return self._phi @phi.setter def phi(self, value): value = self.checkFloat('phi', value) self._phi = value @property def xvar(self): return self.A @xvar.setter def xvar(self, value): self.A = value def __repr__(self): params = [f'{si_format(self.f, 1, space="")}Hz'] if self.A is not None: params.append(f'{si_format(self.A, 1, space="")}Pa') return f'{self.__class__.__name__}({", ".join(params)})' def __eq__(self, other): if not isinstance(other, self.__class__): return False return self.f == other.f and self.A == other.A and self.phi == other.phi def copy(self): return self.__class__(self.f, self.A, phi=self.phi) @staticmethod def inputs(): return { 'f': { 'desc': 'US drive frequency', 'label': 'f', 'unit': 'kHz', 'factor': 1e-3, 'precision': 0 }, 'A': { 'desc': 'US pressure amplitude', 'label': 'A', 'unit': 'kPa', 'factor': 1e-3, 'precision': 2 }, 'phi': { 'desc': 'US drive phase', 'label': '\Phi', 'unit': 'rad', 'precision': 2 } } @property def meta(self): return { 'f': self.f, 'A': self.A } @property def desc(self): return 'f = {}Hz, A = {}Pa'.format(*si_format([self.f, self.A], 2)) @property def filecodes(self): - return { - 'f': f'{self.f * 1e-3:.0f}kHz', - 'A': f'{self.A * 1e-3:.2f}kPa' - } + codes = {'f': f'{self.f * 1e-3:.0f}kHz'} + if self.A is not None: + codes['A'] = f'{self.A * 1e-3:.2f}kPa' + return codes @property def dt(self): ''' Determine integration time step. ''' return 1 / (NPC_DENSE * self.f) @property def dt_sparse(self): return 1 / (NPC_SPARSE * self.f) @property def periodicity(self): ''' Determine drive periodicity. ''' return 1. / self.f @property def nPerCycle(self): return NPC_DENSE @property def modulationFrequency(self): return self.f def compute(self, t): return self.A * np.sin(2 * np.pi * self.f * t - self.phi) class AcousticDriveArray(Drive): def __init__(self, drives): self.drives = {f'source {i + 1}': s for i, s in enumerate(drives)} def __eq__(self, other): if not isinstance(other, self.__class__): return False if self.ndrives != other.ndrives: return False if list(self.drives.keys()) != list(other.drives.keys()): return False for k, v in self.drives.items(): if other.drives[k] != v: return False return True def __repr__(self): params = [repr(drive) for drive in self.drives.values()] return f'{self.__class__.__name__}({", ".join(params)})' @staticmethod def inputs(): return self.drives.values()[0].inputs() def copy(self): return self.__class__([x.copy() for x in self.drives.values()]) @property def ndrives(self): return len(self.drives) @property def meta(self): return {k: s.meta for k, s in self.drives.items()} @property def desc(self): descs = [f'[{s.desc}]' for k, s in self.drives.items()] return ', '.join(descs) @property def filecodes(self): return {k: s.filecodes for k, s in self.drives.items()} @property def fmax(self): return max(s.f for s in self.drives.values()) @property def fmin(self): return min(s.f for s in self.drives.values()) @property def dt(self): return 1 / (NPC_DENSE * self.fmax) @property def dt_sparse(self): return 1 / (NPC_SPARSE * self.fmax) @property def periodicity(self): if self.ndrives > 2: raise ValueError('cannot compute periodicity for more than two drives') return 1 / (self.fmax - self.fmin) @property def nPerCycle(self): return int(self.periodicity // self.dt) @property def modulationFrequency(self): return np.mean([s.f for s in self.drives.values()]) def compute(self, t): return sum(s.compute(t) for s in self.drives.values()) diff --git a/PySONIC/plt/actmap.py b/PySONIC/plt/actmap.py index 4755b4f..238eb9e 100644 --- a/PySONIC/plt/actmap.py +++ b/PySONIC/plt/actmap.py @@ -1,420 +1,425 @@ # -*- coding: utf-8 -*- # @Author: Theo Lemaire # @Email: theo.lemaire@epfl.ch # @Date: 2019-06-04 18:24:29 # @Last Modified by: Theo Lemaire -# @Last Modified time: 2020-04-09 15:48:44 +# @Last Modified time: 2020-04-13 12:31:25 import abc import pandas as pd import csv from itertools import product import numpy as np import matplotlib.pyplot as plt from matplotlib.ticker import FormatStrFormatter from ..core import NeuronalBilayerSonophore, PulsedProtocol, AcousticDrive, LogBatch from ..utils import logger, si_format, isIterable from .pltutils import cm2inch, setNormalizer from ..postpro import detectSpikes class XYMap(LogBatch): ''' Generic 2D map object interface. ''' def __init__(self, root, xvec, yvec): self.root = root self.xvec = xvec self.yvec = yvec super().__init__([list(pair) for pair in product(self.xvec, self.yvec)], root=root) def checkVector(self, name, value): if not isIterable(value): raise ValueError(f'{name} vector must be an iterable') if not isinstance(value, np.ndarray): value = np.asarray(value) if len(value.shape) > 1: raise ValueError(f'{name} vector must be one-dimensional') return value @property def in_key(self): return self.xkey @property def unit(self): return self.xunit @property def xvec(self): return self._xvec @xvec.setter def xvec(self, value): self._xvec = self.checkVector('x', value) @property def yvec(self): return self._yvec @yvec.setter def yvec(self, value): self._yvec = self.checkVector('x', value) @property @abc.abstractmethod def xkey(self): raise NotImplementedError @property @abc.abstractmethod def xfactor(self): raise NotImplementedError @property @abc.abstractmethod def xunit(self): raise NotImplementedError @property @abc.abstractmethod def ykey(self): raise NotImplementedError @property @abc.abstractmethod def yfactor(self): raise NotImplementedError @property @abc.abstractmethod def yunit(self): raise NotImplementedError @property @abc.abstractmethod def zkey(self): raise NotImplementedError @property @abc.abstractmethod def zunit(self): raise NotImplementedError @property @abc.abstractmethod def zfactor(self): raise NotImplementedError @property def out_keys(self): return [f'{self.zkey} ({self.zunit})'] @property def in_labels(self): return [f'{self.xkey} ({self.xunit})', f'{self.ykey} ({self.yunit})'] def getLogData(self): ''' Retrieve the batch log file data (inputs and outputs) as a dataframe. ''' return pd.read_csv(self.fpath, sep=self.delimiter).sort_values(self.in_labels) def getInput(self): ''' Retrieve the logged batch inputs as an array. ''' return self.getLogData()[self.in_labels].values def getOutput(self): return np.reshape(super().getOutput(), (self.xvec.size, self.yvec.size)).T def writeLabels(self): with open(self.fpath, 'w') as csvfile: writer = csv.writer(csvfile, delimiter=self.delimiter) writer.writerow([*self.in_labels, *self.out_keys]) def writeEntry(self, entry, output): with open(self.fpath, 'a', newline='') as csvfile: writer = csv.writer(csvfile, delimiter=self.delimiter) writer.writerow([*entry, output]) def isEntry(self, comb): ''' Check if a given input is logged in the batch log file. ''' inputs = self.getInput() if len(inputs) == 0: return False imatches_x = np.where(np.isclose(inputs[:, 0], comb[0], rtol=self.rtol, atol=self.atol))[0] imatches_y = np.where(np.isclose(inputs[:, 1], comb[1], rtol=self.rtol, atol=self.atol))[0] imatches = list(set(imatches_x).intersection(imatches_y)) if len(imatches) == 0: return False return True @property def inputscode(self): ''' String describing the batch inputs. ''' xcode = self.rangecode(self.xvec * self.xfactor, self.xkey, self.xunit) ycode = self.rangecode(self.yvec * self.yfactor, self.ykey, self.yunit) return '_'.join([xcode, ycode]) @staticmethod def getScaleType(x): xmin, xmax, nx = x.min(), x.max(), x.size if np.all(np.isclose(x, np.logspace(np.log10(xmin), np.log10(xmax), nx))): return 'log' else: return 'lin' # elif np.all(np.isclose(x, np.linspace(xmin, xmax, nx))): # return 'lin' # else: # raise ValueError('Unknown distribution type') @property def xscale(self): return self.getScaleType(self.xvec) @property def yscale(self): return self.getScaleType(self.yvec) @staticmethod def computeMeshEdges(x, scale): ''' Compute the appropriate edges of a mesh that quads a linear or logarihtmic distribution. :param x: the input vector :param scale: the type of distribution ('lin' for linear, 'log' for logarihtmic) :return: the edges vector ''' if scale == 'log': x = np.log10(x) range_func = np.logspace else: range_func = np.linspace dx = x[1] - x[0] n = x.size + 1 return range_func(x[0] - dx / 2, x[-1] + dx / 2, n) @abc.abstractmethod def compute(self, x): ''' Compute the necessary output(s) for a given inputs combination. ''' raise NotImplementedError def getOnClickXY(self, event): ''' Get x and y values from from x and y click event coordinates. ''' x = self.xvec[np.searchsorted(self.xedges, event.xdata / self.xfactor) - 1] y = self.yvec[np.searchsorted(self.yedges, event.ydata / self.yfactor) - 1] return x, y def onClick(self, event): ''' Exexecute specific action when the user clicks on a cell in the 2D map. ''' pass @property @abc.abstractmethod def title(self): raise NotImplementedError def getZBounds(self): matrix = self.getOutput() zmin, zmax = np.nanmin(matrix), np.nanmax(matrix) logger.info( f'{self.zkey} range: {zmin * self.zfactor:.0f} - {zmax * self.zfactor:.0f} {self.zunit}') return zmin, zmax def checkZbounds(self, zbounds): zmin, zmax = self.getZBounds() if zmin < zbounds[0]: logger.warning( f'Minimal {self.zkey} ({zmin:.0f} {self.zunit}) is below defined lower bound ({zbounds[0]:.0f} {self.zunit})') if zmax > zbounds[1]: logger.warning( f'Maximal {self.zkey} ({zmax:.0f} {self.zunit}) is above defined upper bound ({zbounds[1]:.0f} {self.zunit})') def render(self, xscale='lin', yscale='lin', zscale='lin', zbounds=None, fs=8, cmap='viridis', interactive=False, figsize=None): # Get figure size if figsize is None: figsize = cm2inch(12, 7) # Compute Z normalizer mymap = plt.get_cmap(cmap) mymap.set_bad('silver') if zbounds is None: zbounds = self.getZBounds() else: self.checkZbounds(zbounds) norm, sm = setNormalizer(mymap, zbounds, zscale) # Compute mesh edges self.xedges = self.computeMeshEdges(self.xvec, xscale) self.yedges = self.computeMeshEdges(self.yvec, yscale) # Create figure fig, ax = plt.subplots(figsize=figsize) fig.subplots_adjust(left=0.15, bottom=0.15, right=0.8, top=0.92) ax.set_title(self.title, fontsize=fs) ax.set_xlabel(f'{self.xkey} ({self.xunit})', fontsize=fs, labelpad=-0.5) ax.set_ylabel(f'{self.ykey} ({self.yunit})', fontsize=fs) for item in ax.get_xticklabels() + ax.get_yticklabels(): item.set_fontsize(fs) if xscale == 'log': ax.set_xscale('log') if yscale == 'log': ax.set_yscale('log') # Plot map with specific color code data = self.getOutput() ax.pcolormesh(self.xedges * self.xfactor, self.yedges * self.yfactor, data, cmap=mymap, norm=norm) # Plot z-scale colorbar pos1 = ax.get_position() # get the map axis position cbarax = fig.add_axes([pos1.x1 + 0.02, pos1.y0, 0.03, pos1.height]) fig.colorbar(sm, cax=cbarax) cbarax.set_ylabel(f'{self.zkey} ({self.zunit})', fontsize=fs) for item in cbarax.get_yticklabels(): item.set_fontsize(fs) if interactive: fig.canvas.mpl_connect('button_press_event', lambda event: self.onClick(event)) return fig class ActivationMap(XYMap): xkey = 'Duty cycle' xfactor = 1e2 xunit = '%' ykey = 'Amplitude' yfactor = 1e-3 yunit = 'kPa' zkey = 'Firing rate' zunit = 'Hz' zfactor = 1e0 suffix = 'actmap' - def __init__(self, root, pneuron, a, f, tstim, PRF, amps, DCs): + def __init__(self, root, pneuron, a, fs, f, tstim, PRF, amps, DCs): self.nbls = NeuronalBilayerSonophore(a, pneuron) self.drive = AcousticDrive(f, None) self.pp = PulsedProtocol(tstim, 0., PRF, 1.) + self.fs = fs super().__init__(root, DCs, amps) @property def sim_args(self): - return [self.drive, self.pp, 1., 'sonic', None] + return [self.drive, self.pp, self.fs, 'sonic', None] @property def title(self): - return '{} neuron @ {}Hz, {}Hz PRF ({}m sonophore)'.format( + s = '{} neuron @ {}Hz, {}Hz PRF ({}m sonophore'.format( self.nbls.pneuron.name, *si_format([self.drive.f, self.pp.PRF, self.nbls.a])) + if self.fs < 1: + s = f'{s}, {self.fs * 1e2:.0f}% coverage' + return f'{s})' def corecode(self): - return '{}_{}Hz_PRF{}Hz_{}s'.format( - self.nbls.pneuron.name, - *si_format([self.drive.f, self.pp.PRF, self.pp.tstim], space='')) + corecodes = self.nbls.filecodes(*self.sim_args) + corecodes['PRF'] = f'PRF{self.pp.PRF:.0f}Hz' + del corecodes['nature'] + return '_'.join(filter(lambda x: x is not None, corecodes.values())) def compute(self, x): ''' Compute firing rate from simulation output ''' # Adapt drive and pulsed protocol self.pp.DC = x[0] self.drive.A = x[1] # Get model output, running simulation if needed data, meta = self.nbls.getOutput(*self.sim_args, outputdir=self.root) # Detect spikes in data ispikes, _ = detectSpikes(data) # Compute firing metrics if ispikes.size > 1: t = data['t'].values sr = 1 / np.diff(t[ispikes]) return np.mean(sr) else: return np.nan def addThresholdCurve(self, ax, fs): Athrs = [] for DC in self.xvec: self.pp.DC = DC Athrs.append(self.nbls.titrate(*self.sim_args)) Athrs = np.array(Athrs) ax.plot(self.xvec * self.xfactor, Athrs * self.yfactor, '-', color='#F26522', linewidth=3, label='threshold amplitudes') ax.legend(loc='lower center', frameon=False, fontsize=fs) def render(self, Ascale='log', FRscale='log', FRbounds=None, thresholds=False, **kwargs): kwargs['yscale'] = Ascale kwargs['zscale'] = FRscale kwargs['zbounds'] = FRbounds fig = super().render(**kwargs) if thresholds: self.addThresholdCurve(fig.axes[0], fs=8) return fig def onClick(self, event): ''' Retrieve the specific input parameters of the x and y dimensions when the user clicks on a cell in the 2D map, and define filename from it. ''' # Define filepath DC, A = self.getOnClickXY(event) self.drive.A = A self.pp.DC = DC # Get model output, running simulation if needed data, meta = self.nbls.getOutput(*self.sim_args, outputdir=self.root) # Plot Q-trace self.plotQVeff(data, meta) plt.show() def plotQVeff(self, data, meta, tonset=10e-3, trange=None, ybounds=None, fs=8, lw=1): ''' Plot superimposed profiles of membrane charge density and effective membrane potential. :param data: simulation output dataframe :param tonset: pre-stimulus onset to add to profiles (s) :param trange: time lower and upper bounds on graph (s) :param ybounds: y-axis bounds (mV / nC/cm2) :return: handle to the generated figure ''' # Bound time if needede if trange is not None: tmin, tmax = trange data = data.loc[(data['t'] >= tmin) & (data['t'] <= tmax)] # Load variables, add onset and rescale t, Qm, Vm = [data[k].values for k in ['t', 'Qm', 'Vm']] t = np.hstack((np.array([-tonset, t[0]]), t)) # s Vm = np.hstack((np.array([self.nbls.pneuron.Vm0] * 2), Vm)) # mV Qm = np.hstack((np.array([self.nbls.pneuron.Qm0] * 2), Qm)) # C/m2 t *= 1e3 # ms Qm *= 1e5 # nC/cm2 # Determine axes bounds if ybounds is None: ybounds = (min(Vm.min(), Qm.min()), max(Vm.max(), Qm.max())) # Create figure fig, ax = plt.subplots(figsize=cm2inch(12, 5)) fig.canvas.set_window_title(self.nbls.desc(meta)) plt.subplots_adjust(left=0.2, bottom=0.2, right=0.95, top=0.95) for key in ['top', 'right']: ax.spines[key].set_visible(False) for key in ['bottom', 'left']: ax.spines[key].set_position(('axes', -0.03)) ax.spines[key].set_linewidth(2) ax.yaxis.set_tick_params(width=2) ax.yaxis.set_major_formatter(FormatStrFormatter('%.0f')) ax.set_xticks([]) ax.set_xlabel(f'{si_format(np.ptp(t))}s', fontsize=fs) ax.set_ylabel('mV - $\\rm nC/cm^2$', fontsize=fs, labelpad=-15) ax.set_ylim(ybounds) ax.set_yticks(ybounds) for item in ax.get_yticklabels(): item.set_fontsize(fs) # Plot Qm and Vmeff profiles ax.plot(t, Vm, color='darkgrey', linewidth=lw) ax.plot(t, Qm, color='k', linewidth=lw) # fig.tight_layout() return fig diff --git a/scripts/plot_activation_map.py b/scripts/plot_activation_map.py index 4924831..19c7822 100644 --- a/scripts/plot_activation_map.py +++ b/scripts/plot_activation_map.py @@ -1,61 +1,63 @@ # -*- coding: utf-8 -*- # @Author: Theo Lemaire # @Email: theo.lemaire@epfl.ch # @Date: 2018-09-26 09:51:43 # @Last Modified by: Theo Lemaire -# @Last Modified time: 2020-04-09 16:04:43 +# @Last Modified time: 2020-04-13 12:12:48 ''' Plot (duty-cycle x amplitude) US activation map of a neuron at a given frequency and PRF. ''' import numpy as np import matplotlib.pyplot as plt from PySONIC.utils import logger from PySONIC.plt import ActivationMap from PySONIC.parsers import AStimParser def main(): # Parse command line arguments parser = AStimParser() parser.defaults['amp'] = np.logspace(np.log10(10), np.log10(600), 30) # kPa parser.defaults['DC'] = np.arange(1, 101) # % parser.defaults['tstim'] = 200. # ms parser.defaults['toffset'] = 0. # ms parser.addInputDir() parser.addThresholdCurve() parser.addInteractive() parser.addAscale() parser.addTimeRange(default=(0., 200.)) parser.addFiringRateBounds((1e0, 1e3)) parser.addFiringRateScale() parser.addPotentialBounds(default=(-150, 50)) parser.outputdir_dep_key = 'save' args = parser.parse() logger.setLevel(args['loglevel']) for pneuron in args['neuron']: for a in args['radius']: - for f in args['freq']: - for tstim in args['tstim']: - for PRF in args['PRF']: - actmap = ActivationMap(args['inputdir'], pneuron, a, f, tstim, PRF, - args['amp'], args['DC']) - actmap.run(mpi=args['mpi']) - actmap.render( - cmap=args['cmap'], - Ascale=args['Ascale'], - FRscale=args['FRscale'], - FRbounds=args['FRbounds'], - interactive=args['interactive'], - # Vbounds=args['Vbounds'], - # trange=args['trange'], - thresholds=args['threshold'], - ) + for fs in args['fs']: + for f in args['freq']: + for tstim in args['tstim']: + for PRF in args['PRF']: + actmap = ActivationMap( + args['inputdir'], pneuron, a, fs, + f, tstim, PRF, args['amp'], args['DC']) + actmap.run(mpi=args['mpi']) + actmap.render( + cmap=args['cmap'], + Ascale=args['Ascale'], + FRscale=args['FRscale'], + FRbounds=args['FRbounds'], + interactive=args['interactive'], + # Vbounds=args['Vbounds'], + # trange=args['trange'], + thresholds=args['threshold'], + ) plt.show() if __name__ == '__main__': main()