Features#

Here we will go through the different features that we can compute with ap_features.

import numpy as np
import matplotlib.pyplot as plt
import ap_features as apf

Single beat features#

To start with we will go through some features that we can derive from a single beat. To illustrate this we will use a synthetic calcium transient

time = np.linspace(0, 1, 101)
tstart = 0.05
y = apf.testing.ca_transient(t=time, tstart=tstart)

fig, ax = plt.subplots()
ax.plot(time, y)
plt.show()
../_images/c2c1728f7dffd20ca556fb44e2044d75aa3f1ac493571a38692b5d200053ff37.png

Notice that the trace starts at t=0.05 and we could use a pacing amplitude to indicate this, e.g

pacing = np.zeros_like(time)
pacing[np.isclose(time, tstart)]  = 1

fig, ax = plt.subplots()
ax.plot(time, y)
ax.plot(time, pacing, color="red")
plt.show()
../_images/5d9d51a8c8cff6e6ca74b2e3547895a6efcb58a85e7afabef06b11af67ec63c6.png

Another way to work with ap_feautres is to convert the trace into a beat, in which case you can do

beat = apf.Beat(y=y, t=time, pacing=pacing)

beat.plot(include_pacing=True)
(<Figure size 640x480 with 2 Axes>, <Axes: xlabel='Time (ms)'>)
../_images/234d3abe33e43f730a5336436590bfec58a1f7ee0cfbe8355012e0b04a7073a4.png
Hide code cell source
def arrow_annotate(ax, y1, y2, t1, t2, label, add_bbox=False):
    mid_t = 0.5 * (t1 + t2)
    mid_y = 0.5 * (y1 + y2)
    ax.annotate(
        text="",
        xy=(t1, y1),
        xytext=(t2, y2),
        arrowprops=dict(arrowstyle="<->"),
    )
    ax.text(
        mid_t,
        mid_y,
        label,
        size="large",
        bbox=dict(boxstyle="circle", fc="w", ec="k") if add_bbox else None,
    )

Time to peak#

Time to peak is the time to the maximum attained value of the trace

If no more information than the time stamps and the trace is available, the algorithm will compute the time from the first time stamp (here 0.0) to the time of the maximum attained value

Hide code cell source
idx_max = np.argmax(y)
t_max = time[idx_max]
y_max = y[idx_max]
y_start = 0.0

fig, ax = plt.subplots()
ax.plot(time, y)
ax.plot([t_max, t_max],[y_max, y_start], "k:")
arrow_annotate(ax=ax, y1=0.0, y2=0.0, t1=0.0, t2=t_max, label="")
plt.show()
../_images/eed99f08ff6a7d383ceb6103e500c69361e1e3f61940c1c1bb2116b2fac293b4.png

In this case we have

print(apf.features.time_to_peak(y=y, x=time))
0.12

However, if we have pacing information available we can extract information about when the upstroke starts

Hide code cell source
fig, ax = plt.subplots()
ax.plot(time, y)
ax.plot([t_max, t_max],[y_max, y_start], "k:")
arrow_annotate(ax=ax, y1=0.0, y2=0.0, t1=tstart, t2=t_max, label="")
../_images/114ff781e370268cbed0f4321994e55fd2619d996ed713e97bc5ef074f3e81e5.png

In which case we have for this trace

print(apf.features.time_to_peak(y=y, x=time, pacing=pacing))
0.06999999999999999

or using the beat object

beat.ttp(use_pacing=True)
np.float64(0.06999999999999999)

Action potential duration (APD)#

To compute the action potential duration, commonly abbreviated as APD, you first need to find the points when your trace intersects with the APD\(p\) - line and then compute the time difference between those two intersections. For example, say if you want to compute APD30, then you need to find the two intersecting points of the APD30-line which is where you are 30% from the peak of the trace. Once you have found those two intersections you can compute the time difference between those two points.

Hide code cell source
apd_coords30 = apf.features.apd_coords(30, V=y, t=time)
apd_coords80 = apf.features.apd_coords(80, V=y, t=time)


fig, ax = plt.subplots()
ax.plot(time, y)
ax.plot([apd_coords30.x1], [apd_coords30.y1], "r*")
ax.plot([apd_coords30.x2], [apd_coords30.y2], "r*")
ax.plot([apd_coords80.x1], [apd_coords80.y1], "b*")
ax.plot([apd_coords80.x2], [apd_coords80.y2], "b*")
ax.plot([0.0, 0.5], [1, 1], "k:")
arrow_annotate(
    ax=ax, 
    y1=apd_coords30.y1, 
    y2=apd_coords30.y2, 
    t1=apd_coords30.x1,
    t2=apd_coords30.x2, 
    label=""
)
arrow_annotate(
    ax=ax, 
    y1=apd_coords80.y1, 
    y2=apd_coords80.y2, 
    t1=apd_coords80.x1,
    t2=apd_coords80.x2, 
    label=""
)
ax.text(
    0.09,
    0.63,
    "APD30",
    size="large",
)
ax.text(
    0.15,
    0.15,
    "APD80",
    size="large",
)
arrow_annotate(ax=ax, y1=1.0, y2=0.7, t1=0.0, t2=0.0, label="30%")
arrow_annotate(ax=ax, y1=1.0, y2=0.2, t1=0.5, t2=0.5, label="80%")
../_images/a2e0a532e78899fd39f22eb43b0fe2f5526bc2d7196e2d79d01286b008d92d75.png

Here we show the APD30 and APD80 which in this case is

print(f"ADP30 = {apf.features.apd(30, y, time)}")
print(f"ADP80 = {apf.features.apd(80, y, time)}")
ADP30 = 0.12940912122129322
ADP80 = 0.3059229823735669

or using the beat object

print(f"ADP30 = {beat.apd(30)}")
print(f"ADP80 = {beat.apd(80)}")
ADP30 = 0.12940912122129322
ADP80 = 0.3059229823735669

Decay time (\(\tau\))#

The decay time, also referred to as \(\tau_p\) (for some \(p\)) is the time from the attained peak value to the intersection of the APD \(p\)-line occurring after the peak value (i.e during the repolarization)

Hide code cell source
idx_max = np.argmax(y)
t_max = time[idx_max]
y_max = y[idx_max]
y_start = 0.0

apd_coords30 = apf.features.apd_coords(30, V=y, t=time)
apd_coords80 = apf.features.apd_coords(80, V=y, t=time)


fig, ax = plt.subplots()
ax.plot(time, y)
ax.plot([t_max], [y_max], "g*")
ax.plot([apd_coords30.x2], [apd_coords30.y2], "r*")
ax.plot([apd_coords80.x2], [apd_coords80.y2], "b*")
ax.plot([t_max, t_max], [y_max, 0], "k:")
arrow_annotate(
    ax=ax, 
    y1=apd_coords30.y1, 
    y2=apd_coords30.y2, 
    t1=t_max,
    t2=apd_coords30.x2, 
    label=""
)
arrow_annotate(
    ax=ax, 
    y1=apd_coords80.y1, 
    y2=apd_coords80.y2, 
    t1=t_max,
    t2=apd_coords80.x2, 
    label=""
)
ax.text(
    0.15,
    0.64,
    r"$\tau_{30}$",
    size="large",
)
ax.text(
    0.18,
    0.15,
    r"$\tau_{80}$",
    size="large",
)
arrow_annotate(ax=ax, y1=1.0, y2=0.7, t1=0.0, t2=0.0, label="30%")
arrow_annotate(ax=ax, y1=1.0, y2=0.2, t1=0.5, t2=0.5, label="80%")
../_images/35c121bdc18d5301b04f9dc0130dbe6b5a991adc1dd2df953b61dcd63d35ae8e.png

Here you see the \(\tau_{30}\) and \(\tau_{80}\) which in this case is

print(f"\u03C4_30 = {apf.features.tau(a=30, y=y, x=time)}")
print(f"\u03C4_80 = {apf.features.tau(a=80, y=y, x=time)}")
τ_30 = 0.1951680427731784
τ_80 = 0.06484277207692196

or using the beat object

print(f"\u03C4_30 = {beat.tau(30)}")
print(f"\u03C4_80 = {beat.tau(80)}")
τ_30 = 0.1951680427731784
τ_80 = 0.06484277207692196

Upstroke time#

The upstroke time is the time from the first intersection of the APD \(p\)-line occurring before the peak value (i.e during the depolarization) to the attained peak value

Hide code cell source
idx_max = np.argmax(y)
t_max = time[idx_max]
y_max = y[idx_max]
y_start = 0.0

apd_coords30 = apf.features.apd_coords(30, V=y, t=time)
apd_coords80 = apf.features.apd_coords(80, V=y, t=time)


fig, ax = plt.subplots()
ax.plot(time, y)
ax.plot([t_max], [y_max], "g*")
ax.plot([apd_coords30.x1], [apd_coords30.y1], "r*")
ax.plot([apd_coords80.x1], [apd_coords80.y1], "b*")
ax.plot([t_max, t_max], [y_max, 0], "k:")
arrow_annotate(
    ax=ax, 
    y1=apd_coords30.y1, 
    y2=apd_coords30.y2, 
    t1=t_max,
    t2=apd_coords30.x1, 
    label=""
)
arrow_annotate(
    ax=ax, 
    y1=apd_coords80.y1, 
    y2=apd_coords80.y2, 
    t1=t_max,
    t2=apd_coords80.x1, 
    label=""
)
ax.text(
    0.07,
    0.64,
    "",
    size="large",
)
ax.text(
    0.06,
    0.15,
    r"",
    size="large",
)
arrow_annotate(ax=ax, y1=1.0, y2=0.7, t1=0.0, t2=0.0, label="30%")
arrow_annotate(ax=ax, y1=1.0, y2=0.2, t1=0.5, t2=0.5, label="80%")
../_images/884b06ad697ce72ae715934412cee66fd0dadf6a5a5440eb44edc95324c0b994.png

The upstroke time in this case is

print(f"Upstroke 30 = {apf.features.upstroke(a=30, y=y, x=time)}")
print(f"Upstroke 80 = {apf.features.upstroke(a=80, y=y, x=time)}")
Upstroke 30 = 0.04328364176313941
Upstroke 80 = 0.06358780310936087

or equivalently

print(f"Upstroke 30 = {beat.upstroke(a=30)}")
print(f"Upstroke 80 = {beat.upstroke(a=80)}")
Upstroke 30 = 0.04328364176313941
Upstroke 80 = 0.06358780310936087

Triangulation#

For \(\tau_p\) we take the time from the peak value to the intersection of the APD \(p\) line during the repolarization. Instead of starting from the \(p\) value we can start from another APD \(q\)-line, e.g we can compute the time from the APD30 line to the APD 80 line during the repolarization. This is called the triangulation 80-30

Hide code cell source
idx_max = np.argmax(y)
t_max = time[idx_max]
y_max = y[idx_max]
y_start = 0.0

apd_coords30 = apf.features.apd_coords(30, V=y, t=time)
apd_coords80 = apf.features.apd_coords(80, V=y, t=time)


fig, ax = plt.subplots()
ax.plot(time, y)
ax.plot([apd_coords30.x2], [apd_coords30.y2], "r*")
ax.plot([apd_coords80.x2], [apd_coords80.y2], "b*")
ax.plot([apd_coords30.x2, apd_coords30.x2], [apd_coords30.y2, 0], "k:")

arrow_annotate(
    ax=ax, 
    y1=apd_coords80.y1, 
    y2=apd_coords80.y2, 
    t1=apd_coords30.x2,
    t2=apd_coords80.x2, 
    label=""
)
arrow_annotate(ax=ax, y1=1.0, y2=0.7, t1=0.0, t2=0.0, label="30%")
arrow_annotate(ax=ax, y1=1.0, y2=0.2, t1=0.5, t2=0.5, label="80%")
../_images/808897498561e8d4e515b289afcf8f056079a0cbaf3d3fe7e433a047bf37770b.png
print(f"Triangulation 30-80 = {apf.features.triangulation(low=30, high=80, V=y, t=time)}")
Triangulation 30-80 = 0.15620969980605223

or using the beat object

print(f"Triangulation 30-80 = {beat.triangulation(low=30, high=80)}")
Triangulation 30-80 = 0.15620969980605223

APD up xy#

This feature takes two factors \(p_1\) and \(p_2\) and report the time from the first intersection of the \(APD\) \(p_1\) line to the first intersection of the \(p_2\)-line. This is equivalent to the upstroke time when \(p_2 = 0\).

Hide code cell source
idx_max = np.argmax(y)
t_max = time[idx_max]
y_max = y[idx_max]
y_start = 0.0

apd_coords30 = apf.features.apd_coords(30, V=y, t=time)
apd_coords80 = apf.features.apd_coords(80, V=y, t=time)


fig, ax = plt.subplots()
ax.plot(time, y)
ax.plot([apd_coords30.x1], [apd_coords30.y1], "r*")
ax.plot([apd_coords80.x1], [apd_coords80.y1], "b*")
ax.plot([apd_coords30.x1, apd_coords30.x1], [apd_coords30.y1, 0], "k:")

arrow_annotate(
    ax=ax, 
    y1=apd_coords80.y1, 
    y2=apd_coords80.y2, 
    t1=apd_coords30.x1,
    t2=apd_coords80.x1, 
    label=""
)
arrow_annotate(ax=ax, y1=1.0, y2=0.7, t1=0.0, t2=0.0, label="30%")
arrow_annotate(ax=ax, y1=1.0, y2=0.2, t1=0.5, t2=0.5, label="80%")
../_images/fdd863cb2b98b317acfe97179c273f9958132ca467e54e35c4a28ffe4483a424.png
print(f"APD upstroke 30-80 = {apf.features.apd_up_xy(low=30, high=80, y=y, t=time)}")
APD upstroke 30-80 = 0.024500577259997706

or using the beat object

print(f"APD upstroke 30-80 = {beat.apd_up_xy(low=30, high=80)}")
APD upstroke 30-80 = 0.024500577259997706