Kilohertz (kHz) Frequency Stimulation Block

This tutorial will use the fiber model created in the simulation tutorial to run a bisection search for the fiber’s block threshold (i.e., the minimum stimulation amplitude needed to stop the progression of a propagating action potential).

Create the fiber and set up simulation

As before, we create a fiber and electrical potentials.

import numpy as np
import matplotlib.pyplot as plt

import scipy.signal as sg
from pyfibers import build_fiber, FiberModel, ScaledStim

# create fiber model
n_nodes = 25
fiber = build_fiber(FiberModel.MRG_INTERPOLATION, diameter=10, n_nodes=n_nodes)

# define unscaled extracellular potentials (in response to a unitary current)
fiber.potentials = fiber.point_source_potentials(0, 250, fiber.length / 2, 1, 10)
plt.plot(fiber.longitudinal_coordinates, fiber.potentials)
plt.xlabel('Distance along fiber (μm)')
plt.ylabel('Electrical potential (mV)')
plt.title('Extracellular potentials')

# turn on saving of transmembrane potential (Vm) so that we can plot it later
fiber.record_vm()
[Vector[1],
 Vector[2],
 Vector[3],
 Vector[4],
 Vector[5],
 Vector[6],
 Vector[7],
 Vector[8],
 Vector[9],
 Vector[10],
 Vector[11],
 Vector[12],
 Vector[13],
 Vector[14],
 Vector[15],
 Vector[16],
 Vector[17],
 Vector[18],
 Vector[19],
 Vector[20],
 Vector[21],
 Vector[22],
 Vector[23],
 Vector[24],
 Vector[25]]
../_images/97cdaecc93cd131be171c316e42948cf2bb1a179fe013ae061cb1be194a3e774.png

We’ll create a 20 kHz square wave, using a longer time step than in previous tutorials so we have time to observe the fiber’s behavior before, during, and after block. We will set the block signal to only be on from t=50 to t=100 ms. Then we’ll add our waveform to a new ScaledStim instance.

# Setup for simulation
time_step = 0.001  # ms
time_stop = 150  # ms
frequency = 20  # kHz (because our time units are ms)

on, off = 50, 100


# create time vector to make waveform with scipy.signal
# blank out first and last thirds of simulation so we can see block turn on and off
def waveform(t):  # noqa: D
    if t > on and t < off:
        return sg.square(2 * np.pi * frequency * t)
    return 0


# Create stimulation object
blockstim = ScaledStim(waveform=waveform, dt=time_step, tstop=time_stop)

Let’s plot the whole waveform, as well as zoom in to show where it turns on and off.

# whole waveform (cannot see individual pulses)
time_steps = np.arange(0, time_stop, time_step)
plt.plot(time_steps, [waveform(t) for t in time_steps])
plt.xlim([0, 150])
plt.xlabel('Time (ms)')
plt.ylabel('Unscaled Stimulation Amplitude')
plt.gcf().set_size_inches((10, 5))

# create figure to show kilohertz waveform details (turning on and off)
fig, ax = plt.subplots(1, 2, figsize=(10, 5), sharey=True)

# zoom in: kHz turning on
ax[0].plot(time_steps, [waveform(t) for t in time_steps])
ax[0].set_xlim([49.8, 50.2])
ax[0].set_xlabel('Time (ms)\n\ntruncated x-limits to show waveform')
ax[0].set_ylabel('Unscaled Stimulation Amplitude')

# zoom in: kHz turning off
ax[1].plot(time_steps, [waveform(t) for t in time_steps])
ax[1].set_xlim([99.8, 100.2])
ax[1].set_xlabel('Time (ms)\n\ntruncated x-limits to show waveform')
Text(0.5, 0, 'Time (ms)\n\ntruncated x-limits to show waveform')
../_images/3564c21b0f0a6b82c0c88bc48fbd8ee596e79746e26230ce95d0adbeda69ee8f.png ../_images/f5e3b736006081ada87a6485cf7d73bcd1636d6dcba02ba73e89af4a5d755c86.png

Let’s try stimulating our fiber. We will use a stimulation amplitude low enough that the membrane voltage appears unaffected. This uses the run_sim() method.

ap, time = blockstim.run_sim(-0.5, fiber)
Running: -0.5
	N aps: 0, time None

Let’s create a function to plot our response to the block waveform, which we’ll use going forward. Call it now to plot the fiber response.

def block_plot():
    """Plot the response of a fiber to kHz stimulation."""
    plt.figure(figsize=(10, 5))
    ax = plt.gca()

    ax.plot(
        blockstim.time,
        fiber.vm[fiber.loc_index(0.9)],
        label=r"$V_m(t)$ at 90% fiber length",
    )

    ax.set_xlabel('Time (ms)')
    ax.set_ylabel('Transmembrane Potential (mV)')

    ax.axvspan(50, 100, alpha=0.5, color='red', label='block on')

    # label the synapse activation with vertical black lines
    if fiber.stim:
        for istim_time in np.arange(15, 150, 10):
            label = 'Intracellular stim' if istim_time == 15 else '__'
            ax.axvline(
                x=istim_time, color='black', alpha=0.5, label=label, linestyle='--'
            )

    ax.legend(loc='upper right')
    plt.xlim(-5, 155)


block_plot()
plt.ylim([-82, 20])
(-82.0, 20.0)
../_images/f23ca4745dbc34e8e27d25fb0e992e51a614a50d81a9ea9bd9adbab5f29b224f.png

kHz Excitation (below block threshold)

At higher stimulation amplitudes (but still below block threshold), the high frequency stimulation can cause repeated spiking. Let’s turn up the stimulus and watch this happen.

ap, time = blockstim.run_sim(
    -1.5, fiber
)  # NOTE: could happen either above or below threshold
block_plot()
Running: -1.5
	N aps: 27, time 99.386
../_images/71e89f350589ae0fe9a887695466c20bb7d640bfdf4d62cfa0e6cc79b8b59552.png

Transmission (with onset response to kHz)

We wanted to stop action potentials, but we are creating many of them! Let’s try turning up the stimulation. You will see an onset response of spiking caused by the stimulation which fades after some time.

ap, time = blockstim.run_sim(-2.5, fiber)
block_plot()
Running: -2.5
	N aps: 2, time 52.566
../_images/44c59ca98b2e32fbbdd29bbdb63d35060437bfdb852d8344da2a266a2d91f7b3.png

We need to know whether we are truly blocking. We have been recording from one end of the fiber (0.9). Let’s add spiking activity to the other end of the fiber (0.1). At t=15 ms, the activity turns on and a spike occurs every 10 ms. Note that, as we saw in the plot of our extracellular potentials, stimulation is occurring in the center of the fiber (0.5).

We add intrinsic activity using add_intrinsic_activity().

# add intrinsic activity, run with same kHz amplitude as above plot
# loc = 0.1 to avoid erroneous end excitation
fiber.add_intrinsic_activity(loc=0.1, start_time=15, avg_interval=10, num_stims=14)

# plot potentials along fiber, intrinsic activity location, and action potential monitoring location
plt.figure(figsize=(8, 4))
plt.plot(
    fiber.longitudinal_coordinates,
    fiber.potentials[0],
    label='Extracellular potentials',
)
intrinsic_activity_loc = fiber.longitudinal_coordinates[
    fiber.loc_index(0.1, target='sections')
]
monitoring_loc = fiber.longitudinal_coordinates[fiber.loc_index(0.9, target='sections')]
plt.axvline(
    intrinsic_activity_loc,
    color='r',
    linestyle='--',
    label='Intrinsic activity location',
)
plt.axvline(monitoring_loc, color='b', linestyle='--', label='AP monitoring location')
plt.legend()
plt.xlabel('Distance along fiber (μm)')
plt.ylabel('Electrical potential (mV)')
plt.title('Fiber potentials and activity locations')
Text(0.5, 1.0, 'Fiber potentials and activity locations')
../_images/2b28093fb2e0b398a08a322dc1009ca7c08d0650b2fa7eaecfb877158c96b5fe.png

We will stimulate using the same amplitude as before.

ap, time = blockstim.run_sim(-2.5, fiber)
block_plot()
Running: -2.5
	N aps: 14, time 145.456
../_images/40e53b77d32e8e033cbac967c3bac9ba142b58795c55bd14129ee711e04938a7.png

This is called “transmission”, where the action potentials continue unhindered down the axon. The action potential takes time to propagate from one end of the fiber to the other, hence the offset between intracellular stimulation pulses and recorded action potentials.

kHz Block

Finally, if we stimulate at an even higher level, we will observe a complete cessation of spike transmission after the block onset response fades.

ap, time = blockstim.run_sim(-3, fiber)
block_plot()
Running: -3
	N aps: 10, time 145.455
../_images/4445a8eef8581a97b376a09a363bce05d3ecce4f71782c515ad10212b3fe248f.png

Search for kHz block threshold

If we want to determine the block threshold, we can use the find_threshold() method. This method returns the stimulation amplitude at which the fiber begins to block transmission and the number of generated action potentials. We can plot the response of the membrane potential and gating variables at threshold stimulation.

This uses the same algorithm as a search for activation threshold, but here suprathreshold stimulation is considered the absence (rather than the presence) of detected action potentials. Thus, intracellular stimulation is key to determining subthreshold stimulation.

The block_delay parameter defines when the function will start looking for action potentials to evaluate whether transmission is occurring. This should be far enough after block turns on to avoid any false positives due to the onset response. We will also truncate the end of the simulation so that it ends when block turns off.

# Stop simulation when block turns off
blockstim.tstop = 100

# stimamp_top and stimamp_bottom shown to be above and below block threshold, respectively, in previous cells
amp, _ = blockstim.find_threshold(
    fiber,
    condition="block",
    stimamp_top=-3,
    stimamp_bottom=-2,
    exit_t_shift=None,
    block_delay=65,
)
print(f'Block threshold: {amp} mA')
block_plot()
Running: -3
	N aps: 5, time 50.237
Running: -2
	N aps: 10, time 95.554
Search bounds: top=-3, bottom=-2
Beginning bisection search
Search bounds: top=-3, bottom=-2
Running: -2.5
	N aps: 9, time 95.74
Search bounds: top=-3, bottom=-2.5
Running: -2.75
	N aps: 6, time 95.945
Search bounds: top=-3, bottom=-2.75
Running: -2.875
	N aps: 5, time 50.242
Search bounds: top=-2.875, bottom=-2.75
Running: -2.8125
	N aps: 5, time 50.246
Search bounds: top=-2.8125, bottom=-2.75
Running: -2.78125
	N aps: 6, time 96.036
Search bounds: top=-2.8125, bottom=-2.78125
Running: -2.796875
	N aps: 6, time 96.134
Search bounds: top=-2.8125, bottom=-2.796875
Running: -2.804688
	N aps: 6, time 96.237
Threshold found at stimamp = -2.8125
Validating threshold...
Running: -2.8125
	N aps: 5, time 50.246
Block threshold: -2.8125 mA
../_images/247630258ebb4bcfcb919105dbd11515b8b1b3f078ff504a045d71abb1d6f6df.png

Re-excitation

It is important to be careful with the upper bound for your block threshold search, as “re-excitation” can occur at suprathreshold stimulation amplitudes. Thus, it is usually safer to set your initial search bounds subthreshold, to avoid any risk of starting with a top bound that causes re-excitation.

blockstim.tstop = 150
ap, time = blockstim.run_sim(
    -250, fiber
)  # NOTE: could happen either above or below threshold
block_plot()
Running: -250
	N aps: 22, time 145.471
../_images/5aed488c6a0cab845cb0395080b5f092a88ebe9010ea49711abccb10f96f1aa8.png