Endocardial stimulation

Endocardial stimulation#

In this demo we show how to load an ODE file and use it to simulate endocardial stimulation.

../_images/torso_electrodes.png

Fig. 1 Electrode positions#

from collections import defaultdict
from pathlib import Path
import cardiac_geometries
import numpy as np
import matplotlib.pyplot as plt
import dolfin
import pyvista


try:
    from tqdm import tqdm
except ImportError:
    tqdm = lambda x: x

import beat
import beat.viz
import gotranx
def get_data(datadir="data_endocardial_stimulation"):
    datadir = Path(datadir)
    msh_file = datadir / "biv_ellipsoid.msh"
    if not msh_file.is_file():
        cardiac_geometries.create_biv_ellipsoid(
            datadir,
            char_length=0.2,  # Reduce this value to get a finer mesh
            center_lv_y=0.2,
            center_lv_z=0.0,
            a_endo_lv=5.0,
            b_endo_lv=2.2,
            c_endo_lv=2.2,
            a_epi_lv=6.0,
            b_epi_lv=3.0,
            c_epi_lv=3.0,
            center_rv_y=1.0,
            center_rv_z=0.0,
            a_endo_rv=6.0,
            b_endo_rv=2.5,
            c_endo_rv=2.7,
            a_epi_rv=8.0,
            b_epi_rv=5.5,
            c_epi_rv=4.0,
            create_fibers=True,
        )

    return cardiac_geometries.geometry.Geometry.from_folder(datadir)
def load_from_file(heart_mesh, xdmffile, key="v", stop_index=None):
    V = dolfin.FunctionSpace(heart_mesh, "Lagrange", 1)
    v = dolfin.Function(V)

    timesteps = beat.postprocess.load_timesteps_from_xdmf(xdmffile)
    with dolfin.XDMFFile(Path(xdmffile).as_posix()) as f:
        for i, t in tqdm(timesteps.items()):
            f.read_checkpoint(v, key, i)
            yield v.copy(deepcopy=True), t
def compute_ecg_recovery():
    datadir = Path("data_endocardial_stimulation")
    xdmffile = datadir / "state.xdmf"
    data = get_data(datadir=datadir)

    # https://litfl.com/ecg-lead-positioning/
    vs = load_from_file(data.mesh, xdmffile, key="V")

    leads = dict(
        RA=(-15.0, 0.0, -10.0),
        LA=(4.0, -12.0, -7.0),
        RL=(0.0, 20.0, 3.0),
        LL=(17.0, 11.0, 7.0),
        V1=(-3.0, 4.0, -9.0),
        V2=(0.0, 2.0, -8.0),
        V3=(3.0, 1.0, -8.0),
        V4=(6.0, 1.0, -6.0),
        V5=(10.0, 2.0, 0.0),
        V6=(10.0, -6.0, 2.0),
    )

    fname = datadir / "extracellular_potential.npy"
    if not fname.is_file():
        phie = defaultdict(list)
        time = []
        for v, t in vs:
            time.append(t)
            for name, point in leads.items():
                phie[name].append(
                    beat.ecg.ecg_recovery(v=v, mesh=data.mesh, sigma_b=1.0, point=point)
                )
        np.save(fname, {"phie": phie, "time": time})

    phie_time = np.load(fname, allow_pickle=True).item()
    phie = phie_time["phie"]
    time = phie_time["time"]

    fig, ax = plt.subplots(2, 5, sharex=True, figsize=(12, 8))
    for i, (name, values) in enumerate(phie.items()):
        axi = ax.flatten()[i]
        axi.plot(time, values)
        axi.set_title(name)
    fig.savefig(datadir / "extracellular_potential.png")

    ecg = beat.ecg.Leads12(**{k: np.array(v) for k, v in phie.items()})
    fig, ax = plt.subplots(3, 4, sharex=True, figsize=(12, 8))
    for i, name in enumerate(
        [
            "I",
            "II",
            "III",
            "aVR",
            "aVL",
            "aVF",
            "V1_",
            "V2_",
            "V3_",
            "V4_",
            "V5_",
            "V6_",
        ]
    ):
        y = getattr(ecg, name)
        axi = ax.flatten()[i]
        axi.plot(time, y)
        axi.set_title(name)
    fig.savefig(datadir / "ecg_12_leads.png")
here = Path.cwd()
datadir = here / "data_endocardial_stimulation"
model_path = Path("ORdmm_Land.py")
if not model_path.is_file():
    ode = gotranx.load_ode(here / ".." / "odes" / "ORdmm_Land.ode")
    code = gotranx.cli.gotran2py.get_code(
        ode, scheme=[gotranx.schemes.Scheme.forward_generalized_rush_larsen]
    )
    model_path.write_text(code)
2025-04-02 20:54:47 [info     ] Load ode /__w/fenics-beat/fenics-beat/demos/../odes/ORdmm_Land.ode
2025-04-02 20:54:48 [info     ] Num states 48                 
2025-04-02 20:54:48 [info     ] Num parameters 139            
import ORdmm_Land
model = ORdmm_Land.__dict__
data = get_data(datadir=datadir)
Calculating scalar fields
Compute scalar laplacian solutions with the markers: 
base: [1]
lv: [2]
rv: [3]
epi: [4]
  Num vertices: 29407
  Num cells: 139200
Calling FFC just-in-time (JIT) compiler, this may take some time.
Calling FFC just-in-time (JIT) compiler, this may take some time.
Calling FFC just-in-time (JIT) compiler, this may take some time.
Calling FFC just-in-time (JIT) compiler, this may take some time.
Calling FFC just-in-time (JIT) compiler, this may take some time.
Calling FFC just-in-time (JIT) compiler, this may take some time.
Calling FFC just-in-time (JIT) compiler, this may take some time.
  Apex coord: (7.96, 1.72, -0.05)
Solving Laplace equation
 lv = 1, rv, epi = 0
 rv = 1, lv, epi = 0
 epi = 1, lv, rv = 0
Calling FFC just-in-time (JIT) compiler, this may take some time.

Calculating gradients
Calling FFC just-in-time (JIT) compiler, this may take some time.
Calling FFC just-in-time (JIT) compiler, this may take some time.
Calling FFC just-in-time (JIT) compiler, this may take some time.
Calling FFC just-in-time (JIT) compiler, this may take some time.
Compute fiber-sheet system
Angles: 
alpha: 
 endo_lv: -60
 epi_lv: 60
 endo_septum: -60
 epi_septum: 60
 endo_rv: -60
 epi_rv: 60
beta: 
 endo_lv: -65
 epi_lv: 25
 endo_septum: -65
 epi_septum: 25
 endo_rv: -65
 epi_rv: 25
mesh_unit = "mm"
V = dolfin.FunctionSpace(data.mesh, "Lagrange", 1)

markers = beat.utils.expand_layer_biv(
    V=V,
    mfun=data.ffun,
    endo_lv_marker=data.markers["ENDO_LV"][0],
    endo_rv_marker=data.markers["ENDO_RV"][0],
    epi_marker=data.markers["EPI"][0],
    endo_size=0.3,
    epi_size=0.3,
)


pyvista.start_xvfb()
plotter_markers = pyvista.Plotter()
topology, cell_types, x = beat.viz.create_vtk_structures(V)
grid = pyvista.UnstructuredGrid(topology, cell_types, x)
grid["markers"] = markers.vector().get_local()
plotter_markers.add_mesh(grid, show_edges=True)
Actor (0x7fa533439de0)
  Center:                     (3.998753816684164, 1.199999999999994, 0.0)
  Pickable:                   True
  Position:                   (0.0, 0.0, 0.0)
  Scale:                      (1.0, 1.0, 1.0)
  Visible:                    True
  X Bounds                    0.000E+00, 7.998E+00
  Y Bounds                    -4.300E+00, 6.700E+00
  Z Bounds                    -4.000E+00, 4.000E+00
  User matrix:                Identity
  Has mapper:                 True

Property (0x7fa53343a140)
  Ambient:                     0.0
  Ambient color:               Color(name='lightblue', hex='#add8e6ff', opacity=255)
  Anisotropy:                  0.0
  Color:                       Color(name='lightblue', hex='#add8e6ff', opacity=255)
  Culling:                     "none"
  Diffuse:                     1.0
  Diffuse color:               Color(name='lightblue', hex='#add8e6ff', opacity=255)
  Edge color:                  Color(name='black', hex='#000000ff', opacity=255)
  Edge opacity:                1.0
  Interpolation:               0
  Lighting:                    True
  Line width:                  1.0
  Metallic:                    0.0
  Opacity:                     1.0
  Point size:                  5.0
  Render lines as tubes:       False
  Render points as spheres:    False
  Roughness:                   0.5
  Show edges:                  True
  Specular:                    0.0
  Specular color:              Color(name='lightblue', hex='#add8e6ff', opacity=255)
  Specular power:              100.0
  Style:                       "Surface"

DataSetMapper (0x7fa5334399c0)
  Scalar visibility:           True
  Scalar range:                (0.0, 2.0)
  Interpolate before mapping:  True
  Scalar map mode:             point
  Color mode:                  map

Attached dataset:
UnstructuredGrid (0x7fa533439a80)
  N Cells:    139200
  N Points:   29407
  X Bounds:   0.000e+00, 7.998e+00
  Y Bounds:   -4.300e+00, 6.700e+00
  Z Bounds:   -4.000e+00, 4.000e+00
  N Arrays:   1
plotter_markers.view_zy()
plotter_markers.camera.zoom(4)
if not pyvista.OFF_SCREEN:
    plotter_markers.show()
else:
    figure = plotter_markers.screenshot("fundamentals_mesh.png")
init_states = {
    0: model["init_state_values"](),
    1: model["init_state_values"](),
    2: model["init_state_values"](),
}
# endo = 0, epi = 1, M = 2
parameters = {
    0: model["init_parameter_values"](amp=0.0, celltype=2),
    1: model["init_parameter_values"](amp=0.0, celltype=0),
    2: model["init_parameter_values"](amp=0.0, celltype=1),
}
fun = {
    0: model["forward_generalized_rush_larsen"],
    1: model["forward_generalized_rush_larsen"],
    2: model["forward_generalized_rush_larsen"],
}
v_index = {
    0: model["state_index"]("v"),
    1: model["state_index"]("v"),
    2: model["state_index"]("v"),
}

# Surface to volume ratio
chi = 140.0 * beat.units.ureg("mm**-1")
# Membrane capacitance
C_m = 0.01 * beat.units.ureg("uF/mm**2")

time = dolfin.Constant(0.0)

subdomain_data = dolfin.MeshFunction("size_t", data.mesh, 2)
subdomain_data.set_all(0)
marker = 1
subdomain_data.array()[data.ffun.array() == data.markers["ENDO_LV"][0]] = 1
subdomain_data.array()[data.ffun.array() == data.markers["ENDO_RV"][0]] = 1
I_s = beat.stimulation.define_stimulus(
    mesh=data.mesh,
    chi=chi,
    mesh_unit=mesh_unit,
    time=time,
    subdomain_data=subdomain_data,
    marker=marker,
    amplitude=500.0,
)

M = beat.conductivities.define_conductivity_tensor(
    chi=chi, f0=data.f0, g_il=0.17, g_it=0.019, g_el=0.62, g_et=0.24
)

params = {"preconditioner": "sor", "use_custom_preconditioner": False}
pde = beat.MonodomainModel(
    time=time,
    C_m=C_m.to(f"uF/{mesh_unit}**2").magnitude,
    mesh=data.mesh,
    M=M,
    I_s=I_s,
    params=params,
)

ode = beat.odesolver.DolfinMultiODESolver(
    v_ode=dolfin.Function(V),
    v_pde=pde.state,
    markers=markers,
    num_states={i: len(s) for i, s in init_states.items()},
    fun=fun,
    init_states=init_states,
    parameters=parameters,
    v_index=v_index,
)
Calling FFC just-in-time (JIT) compiler, this may take some time.
Calling FFC just-in-time (JIT) compiler, this may take some time.
Calling FFC just-in-time (JIT) compiler, this may take some time.
T = 1
# Change to 500 to simulate the full cardiac cycle
# T = 500
t = 0.0
dt = 0.05
solver = beat.MonodomainSplittingSolver(pde=pde, ode=ode)

fname = (datadir / "state.xdmf").as_posix()
i = 0
while t < T + 1e-12:
    if i % 20 == 0:
        v = solver.pde.state.vector().get_local()
        print(f"Solve for {t=:.2f}, {v.max() =}, {v.min() = }")
        with dolfin.XDMFFile(dolfin.MPI.comm_world, fname) as xdmf:
            xdmf.parameters["functions_share_mesh"] = True
            xdmf.parameters["rewrite_function_mesh"] = False
            xdmf.write_checkpoint(
                solver.pde.state,
                "V",
                float(t),
                dolfin.XDMFFile.Encoding.HDF5,
                True,
            )
    solver.step((t, t + dt))
    i += 1
    t += dt
Calling FFC just-in-time (JIT) compiler, this may take some time.
Calling FFC just-in-time (JIT) compiler, this may take some time.
Solve for t=0.00, v.max() =-86.99999999999982, v.min() = -87.00000000000013
Calling FFC just-in-time (JIT) compiler, this may take some time.
Calling FFC just-in-time (JIT) compiler, this may take some time.
Calling FFC just-in-time (JIT) compiler, this may take some time.
Calling FFC just-in-time (JIT) compiler, this may take some time.
Solve for t=1.00, v.max() =-49.021796610832325, v.min() = -91.90071327251552
compute_ecg_recovery()
  0%|          | 0/2 [00:00<?, ?it/s]
Calling FFC just-in-time (JIT) compiler, this may take some time.
Calling FFC just-in-time (JIT) compiler, this may take some time.
 50%|█████     | 1/2 [00:01<00:01,  1.73s/it]
100%|██████████| 2/2 [00:02<00:00,  1.27s/it]
100%|██████████| 2/2 [00:02<00:00,  1.34s/it]

../_images/54e8ad25214515acffd0c5124dfe9b175cee83ec6b8631d5d530a443359fe855.png ../_images/004b2328d384a2f1e090aca661767a32961738b74d4ce5e6725a0490d91335dc.png
../_images/ecg_12_leads.png

Fig. 2 Precomputed 12-lead ECG#