.. _topics-stimuli: ================== Electrical Stimuli ================== The :py:mod:`~pulse2percept.stimuli` module provides a number of common electrical stimulus types, which can be assigned to electrodes of a :py:class:`~pulse2percept.implants.ProsthesisSystem` object: ================================ ========================================== Stimulus Description -------------------------------- ------------------------------------------ `MonophasicPulse` single phase: cathodic or anodic `BiphasicPulse` biphasic: cathodic + anodic `AsymmetricBiphasicPulse` biphasic with unequal amplitude/duration `PulseTrain` combine any Stimulus into a pulse train `BiphasicPulseTrain` series of (symmetric) biphasic pulses `AsymmetricBiphasicPulseTrain` series of asymmetric biphasic pulses `BiphasicTripletTrain` series of biphasic pulse triplets ================================ ========================================== In addition, pulse2percept provides convenience functions to convert images and videos into :py:class:`~pulse2percept.stimuli.Stimulus` objects (see the :py:mod:`~pulse2percept.io` module). .. important :: Stimuli specify electrical currents in microamps (uA) and time in milliseconds (ms). When in doubt, check the docstring of the function or class you are trying to use. Understanding the Stimulus class --------------------------------- The :py:class:`~pulse2percept.stimuli.Stimulus` object defines a common interface for all electrical stimuli, consisting of a 2D data array with labeled axes, where rows denote electrodes and columns denote points in time. A stimulus can be created from a variety of source types. The number of electrodes and time points will be automatically extracted from the source type: ================ ========== ====== Source type electrodes time ---------------- ---------- ------ Scalar value 1 None Nx1 NumPy array N None NxM NumPy array N M ================ ========== ====== In addition, you can also pass a collection of source types (e.g., list, dictionary). .. note:: Depending on the source type, a stimulus might have a time component or not. .. ipython:: python :suppress: # Use defaults so we don't get gridlines in generated docs import matplotlib as mpl mpl.rcdefaults() Single-electrode stimuli ^^^^^^^^^^^^^^^^^^^^^^^^ The easiest way to create a stimulus is to specify the current amplitude (uA) to be delivered to an electrode: .. ipython:: python from pulse2percept.stimuli import Stimulus # Stimulate an unnamed electrode with -14uA: Stimulus(-14) You can also specify the name of the electrode to be stimulated: .. ipython:: python # Stimulate Electrode 'B7' with -14uA: Stimulus(-14, electrodes='B7') By default, this stimulus will not have a time component (``stim.time`` is None). Some models, such as :py:class:`~pulse2percept.models.ScoreboardModel`, cannot handle stimuli in time. To create stimuli in time, you can use one of the above mentioned stimulus types, such as :py:class:`~pulse2percept.stimuli.MonophasicPulse` or :py:class:`~pulse2percept.stimuli.BiphasicPulseTrain`: .. ipython:: python # Stimulate Electrode 'A001' with a 20Hz pulse train lasting 0.5s # (pulses: cathodic-first, 10uA amplitude, 0.45ms phase duration): from pulse2percept.stimuli import BiphasicPulseTrain pt = BiphasicPulseTrain(20, 10, 0.45, stim_dur=500) stim = Stimulus(pt) stim # This stimulus has a time component: stim.time You can specify not only the name of the electrode but also the time steps to be used: .. ipython:: python # Stimulate Electrode 'C7' with int time steps: Stimulus(pt, electrodes='C7', time=np.arange(pt.shape[-1])) Creating multi-electrode stimuli ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Stimuli can also be created from a list or dictionary of source types: .. ipython:: python # Stimulate three unnamed electrodes with -2uA, 14uA, and -100uA, # respectively: Stimulus([-2, 14, -100]) Electrode names can be passed in a list: .. ipython:: python Stimulus([-2, 14, -100], electrodes=['A1', 'B1', 'C1']) Alternatively, stimuli can be created from a dictionary: .. ipython:: python # Equivalent to the previous one: Stimulus({'A1': -2, 'B1': 14, 'C1': -100}) The same is true for a dictionary of pulse trains: .. ipython:: python from pulse2percept.stimuli import BiphasicPulse Stimulus({'A1': BiphasicPulse(10, 0.45, stim_dur=100), 'C9': BiphasicPulse(-30, 1, delay_dur=10, stim_dur=100)}) Interacting with stimuli ------------------------ Accessing individual data points ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ You can directly index into the :py:class:`~pulse2percept.stimuli.Stimulus` object to retrieve individual data points: ``stim[item]``. ``item`` can be an integer, string, slice, or tuple. For example, to retrieve all data points of the first electrode in a multi-electrode stimulus, use the following: .. ipython:: python stim = Stimulus(np.arange(10).reshape((2, 5))) stim[0] Here ``0`` is a valid electrode index, because we did not specify an electrode name. Analogously: .. ipython:: python stim = Stimulus(np.arange(10).reshape((2, 5)), electrodes=['B1', 'C2']) stim['B1'] Similarly, you can retrieve all data points at a particular time: .. ipython:: python stim = Stimulus(np.arange(10).reshape((2, 5))) stim[:, 3] .. important :: The second index or slice into ``stim`` is not a column index into ``stim.data``, but an exact time specified in ms! For example, ``stim[:, 3]`` translates to "retrieve all data points at time = 3 ms", not "retrieve stim.data[:, 3]". This works even when the specified time is not explicitly provided in the stimulus! In that case, the value is automatically interpolated (using SciPy's ``interp1d``): .. ipython:: python # A single-electrode ramp stimulus: stim = Stimulus(np.arange(10).reshape((1, -1))) stim # Retrieve stimulus at t=3: stim[0, 3] # Time point 3.45 is not in the data provided above, but can be # interpolated as follows: stim[0, 3.45] # This also works for multiple time points: stim[0, [3.45, 6.78]] # Time points above the valid range will return the largest stored value: stim[0, 123.45] Accessing the raw data ^^^^^^^^^^^^^^^^^^^^^^ The raw data is accessible as a 2D NumPy array (electrodes x time) stored in the ``data`` container of a Stimulus: .. ipython:: python stim = Stimulus(np.arange(10).reshape((2, 5))) stim.data You can index and slice the ``data`` container like any NumPy array. Assigning new coordinates to an existing stimulus ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ You can change the coordinates of an existing :py:class:`~pulse2percept.stimuli.Stimulus` object, but retain all its data, by wrapping it in a second Stimulus object: .. ipython:: python # Say you have a Stimulus object with unlabeled axes: stim = Stimulus(np.ones((2, 5))) stim # You can create a new object from it with named electrodes: Stimulus(stim, electrodes=['A1', 'F10']) # Same goes for time points: Stimulus(stim, time=[0, 0.1, 0.2, 0.3, 0.4]) Compressing a stimulus ^^^^^^^^^^^^^^^^^^^^^^ The :py:meth:`~pulse2percept.stimuli.Stimulus.compress` method automatically compresses the data in two ways: * Removes electrodes with all-zero activation. * Retains only the time points at which the stimulus changes. For example, only the signal edges of a pulse train are saved. That is, rather than saving the current amplitude at every 0.1ms time step, only the non-redundant values are retained. This drastically reduces the memory footprint of the stimulus. You can convince yourself of that by inspecting the size of a Stimulus object before and after compression: .. ipython:: python # An uncompressed stimulus: stim = Stimulus([[0, 0, 0, 1, 2, 0, 0, 0]], time=[0, 1, 2, 3, 4, 5, 6, 7]) stim # Now compress the data: stim.compress() # Notice how the time axis have changed: stim Accessing stimulus metadata ^^^^^^^^^^^^^^^^^^^^^^^^^^^ Stimuli can store additional relevant information in the ``metadata`` dictionary. Users can also pass their own metadata, which will be stored in ``metadata["user"]`` Stimuli built from a collection of sources store the metadata for each source in ``metadata["electrodes"][electrode]["metadata"]``: .. ipython:: python # Accessing metadata stim = Stimulus([[0, 1, 2, 3]], metadata='user_metadata') stim.metadata # Some objects store their own metadata stim = BiphasicPulseTrain(1,1,1, metadata='user_metadata') stim.metadata # Multiple source metadata stim = Stimulus({'A1' : BiphasicPulseTrain(1,1,1, metadata='A1 metadata'), 'B2' : BiphasicPulseTrain(1,1,1, metadata='B2 metadata')}, metadata='stimulus metadata') stim.metadata .. .. minigallery:: pulse2percept.stimuli.Stimulus .. :add-heading: Examples using ``Stimulus`` .. :heading-level: -