LV ellipsoid with time dependent pressure and activation#
In this example we will solve a time dependent mechanics problem for the left ventricle ellipsoid geometry.
The equations of motion can be expressed using the following notation in the reference configuration
If we introduce the notation \(a= \ddot{u}, v=\dot{u}\) for the acceleration and velocity, respectively, a weak form of (1) can be written as
In order to integrate (2) in time, we need to express \(a\) and \(v\) in terms of the displacement \(u\). For this we use the Generalized \(\alpha\)-method
The generalized \(\alpha\) method#
The generalized \(\alpha\) or G-\(\alpha\) methods introduce additional parameters \(\alpha_f\) and \(\alpha_m\) to approximate \(v\) and \(a\) by evaluating the terms of the weak form at times \(t_{i+1}-\Delta t\alpha_m\) and \(t_{i+1}-\Delta t\alpha_f\). Specifically, the inertia term is evaluated at \(t_{i+1}-\Delta t\alpha_m\), and the other terms at \(t_{i+1}-\Delta t\alpha_f\). The weak form becomes
with
\(v_{i+1},a_{i+1}\) given by
and
Different choices of the four parameters \(\alpha_m, \alpha_f, \beta, \gamma\) yield methods with different accuracy and stability properties. Tables 1–3 in [EBB02] provides an overview of parameter choices for methods in the literature, as well as conditions for stability and convergence. We have used the choice \(\alpha_m =0.2, \alpha_f=0.4\), and
For this choice the solver converges through the time interval of interest, and the convergence is second order.
Pressure and activation model#
We will use a pressure and activation model from [BClementS01] to drive the simulation. We consider a time-dependent pressure derived from the Bestel model. The solution \(p = p(t)\) is characterized as solution to the evolution equation
with \(b(\cdot)\) being the activation function described below:
with \(S^{\pm}\) given by
Similarly, the active stress is characterized through a time-dependent stress function \(\tau\) solution to the evolution equation
with \(a(\cdot)\) being the activation function and \sigma_0 contractility, where each remaining term is described below:
Constitutive model#
We will use a nearly incompressible, orthotropic and viscoelastic version of the Holzapfel Ogden model. The material parameters are taken from [HO09]. The anistropic material strain energy function is given by
The viscoelastic energy is given by
where \(\dot{\mathbf{E}}\) is temporal derivative the Green-Lagrange strain tensor and \(\eta\) is the viscosity parameter. Here the temporal derivative is computed by first computing the temporal deformation gradient
and then
see [Hol02] Equation 2.139 and 2.163 for more details.
For compressibility we use the following strain energy function
where \(J\) is the determinant of the deformation gradient.
To solve the problem we first import the necessary packages
from pathlib import Path
import logging
import math
import os
from mpi4py import MPI
import dolfinx
from dolfinx import log
import numpy as np
from scipy.integrate import solve_ivp
import matplotlib.pyplot as plt
import circulation.bestel
import cardiac_geometries
import cardiac_geometries.geometry
import fenicsx_pulse
Next we set up the logging and the MPI communicator
logging.basicConfig(level=logging.INFO)
comm = MPI.COMM_WORLD
and create an output directory
outdir = Path("time-dependent-bestel-lv")
outdir.mkdir(parents=True, exist_ok=True)
Next we create the geometry, which is an ellipsoid with a fiber field varying from -60 to 60 degrees in the endocardium and epicardium and interpolated into a Quadrature_6 function space
geodir = outdir / "geometry"
if not geodir.exists():
comm.barrier()
cardiac_geometries.mesh.lv_ellipsoid(
outdir=geodir,
create_fibers=True,
fiber_space="Quadrature_6",
r_short_endo=0.025,
r_short_epi=0.035,
r_long_endo=0.09,
r_long_epi=0.097,
psize_ref=0.03,
mu_apex_endo=-math.pi,
mu_base_endo=-math.acos(5 / 17),
mu_apex_epi=-math.pi,
mu_base_epi=-math.acos(5 / 20),
comm=comm,
fiber_angle_epi=-60,
fiber_angle_endo=60,
)
2025-03-05 19:48:31 [debug ] Convert file time-dependent-bestel-lv/geometry/lv_ellipsoid.msh to dolfin
INFO:cardiac_geometries.geometry:Reading geometry from time-dependent-bestel-lv/geometry
Info : Reading 'time-dependent-bestel-lv/geometry/lv_ellipsoid.msh'...
Info : 54 entities
Info : 191 nodes
Info : 990 elements
Info : Done reading 'time-dependent-bestel-lv/geometry/lv_ellipsoid.msh'
We load the geometry and convert it to the format used by fenicsx-pulse
.
geo = cardiac_geometries.geometry.Geometry.from_folder(
comm=comm,
folder=geodir,
)
geometry = fenicsx_pulse.HeartGeometry.from_cardiac_geometries(geo, metadata={"quadrature_degree": 6})
INFO:cardiac_geometries.geometry:Reading geometry from time-dependent-bestel-lv/geometry
Next we create the material object using the class Holzapfel Ogden model
material_params = fenicsx_pulse.HolzapfelOgden.orthotropic_parameters()
material = fenicsx_pulse.HolzapfelOgden(f0=geo.f0, s0=geo.s0, **material_params) # type: ignore
print(material)
a/2b (exp(b(I1 - 3)) - 1) + af/2bf H(I4f - 1) (exp(bf (I4f - 1)_+^2) - 1) + as/2bs H(I4s - 1) (exp(bs (I4s - 1)_+^2) - 1) + afs/2bfs (exp(bfs I8fs^2) - 1)
We set up the active stress model
Ta = fenicsx_pulse.Variable(dolfinx.fem.Constant(geometry.mesh, dolfinx.default_scalar_type(0.0)), "Pa")
active_model = fenicsx_pulse.ActiveStress(geo.f0, activation=Ta)
print(active_model)
Ta (I4f - 1 + η ((I1 - 3) - (I4f - 1)))
and define the compressibility
comp_model = fenicsx_pulse.compressibility.Compressible2()
print(comp_model)
κ (J ** 2 - 1 - 2 ln(J))
and viscoelasticity model
viscoelastic_model = fenicsx_pulse.viscoelasticity.Viscous()
print(viscoelastic_model)
0.5η tr (E_dot E_dot)
Finally we assembles the CardiacModel
model = fenicsx_pulse.CardiacModel(
material=material,
active=active_model,
compressibility=comp_model,
viscoelasticity=viscoelastic_model,
)
Next we define the boundary conditions. First we define a traction on the endocardium
traction = fenicsx_pulse.Variable(dolfinx.fem.Constant(geometry.mesh, dolfinx.default_scalar_type(0.0)), "Pa")
neumann = fenicsx_pulse.NeumannBC(traction=traction, marker=geometry.markers["ENDO"][0])
and Robin boundary conditions on the epicardium and base
alpha_epi = fenicsx_pulse.Variable(
dolfinx.fem.Constant(geometry.mesh, dolfinx.default_scalar_type(1e8)), "Pa / m",
)
robin_epi_u = fenicsx_pulse.RobinBC(value=alpha_epi, marker=geometry.markers["EPI"][0])
beta_epi = fenicsx_pulse.Variable(
dolfinx.fem.Constant(geometry.mesh, dolfinx.default_scalar_type(5e3)), "Pa s/ m",
)
robin_epi_v = fenicsx_pulse.RobinBC(value=beta_epi, marker=geometry.markers["EPI"][0], damping=True)
alpha_base = fenicsx_pulse.Variable(
dolfinx.fem.Constant(geometry.mesh, dolfinx.default_scalar_type(1e5)), "Pa / m",
)
robin_base_u = fenicsx_pulse.RobinBC(value=alpha_base, marker=geometry.markers["BASE"][0])
beta_base = fenicsx_pulse.Variable(
dolfinx.fem.Constant(geometry.mesh, dolfinx.default_scalar_type(5e3)), "Pa s/ m",
)
robin_base_v = fenicsx_pulse.RobinBC(value=beta_base, marker=geometry.markers["BASE"][0], damping=True)
We assemble the boundary conditions
bcs = fenicsx_pulse.BoundaryConditions(robin=(robin_epi_u, robin_epi_v, robin_base_u, robin_base_v), neumann=(neumann,))
Finally we create a DynamicProblem
problem = fenicsx_pulse.problem.DynamicProblem(model=model, geometry=geometry, bcs=bcs, parameters={"base_bc": fenicsx_pulse.problem.BaseBC.free})
Note that we also specify that the base is free to move, meaning that there will be no Dirichlet boundary conditions on the base. Now we can do an initial solve the problem
log.set_log_level(log.LogLevel.INFO)
problem.solve()
INFO:scifem.solvers:Newton iteration 1: r (abs) = 1.0145062213561107e-05 (tol=1e-06), r (rel) = 1.0 (tol=1e-10)
INFO:scifem.solvers:Newton iteration 2: r (abs) = 4.5704973235210315e-08 (tol=1e-06), r (rel) = 0.004505144697300679 (tol=1e-10)
2
The next step is to get the activation and pressure as a function of time. For this we use the time step from the problem parameters
dt = problem.parameters["dt"].to_base_units()
times = np.arange(0.0, 1.0, dt)
We solve the Bestel model for the pressure and activation which is already implemented in the circulation
package
pressure_model = circulation.bestel.BestelPressure()
res = solve_ivp(
pressure_model,
[0.0, 1.0],
[0.0],
t_eval=times,
method="Radau",
)
# Convert the pressure from Pa to kPa
pressure = res.y[0]
INFO:circulation.bestel:
Bestel pressure model
parameters
┏━━━━━━━━━━━━┳━━━━━━━━━┓
┃ Parameter ┃ Value ┃
┡━━━━━━━━━━━━╇━━━━━━━━━┩
│ t_sys_pre │ 0.17 │
│ t_dias_pre │ 0.484 │
│ gamma │ 0.005 │
│ a_max │ 5.0 │
│ a_min │ -30.0 │
│ alpha_pre │ 5.0 │
│ alpha_mid │ 1.0 │
│ sigma_pre │ 7000.0 │
│ sigma_mid │ 16000.0 │
└────────────┴─────────┘
activation_model = circulation.bestel.BestelActivation()
res = solve_ivp(
activation_model,
[0.0, 1.0],
[0.0],
t_eval=times,
method="Radau",
)
# Convert the pressure from Pa to kPa
activation = res.y[0]
INFO:circulation.bestel:
Bestel activation model
parameters
┏━━━━━━━━━━━┳━━━━━━━━━━┓
┃ Parameter ┃ Value ┃
┡━━━━━━━━━━━╇━━━━━━━━━━┩
│ t_sys │ 0.16 │
│ t_dias │ 0.484 │
│ gamma │ 0.005 │
│ a_max │ 5.0 │
│ a_min │ -30.0 │
│ sigma_0 │ 150000.0 │
└───────────┴──────────┘
Let us also plot the profiles of the pressure and activation
fig, ax = plt.subplots(2, 1, sharex=True, figsize=(10, 10))
ax[0].plot(times, pressure)
ax[0].set_title("Pressure")
ax[1].plot(times, activation)
ax[1].set_title("Activation")
fig.savefig(outdir / "pressure_activation.png")

New let us write the displacement to a file using the VTXWriter
from dolfinx
vtx = dolfinx.io.VTXWriter(geometry.mesh.comm, outdir / "displacement.bp", [problem.u], engine="BP4")
vtx.write(0.0)
We can also calculate the volume of the left ventricle at each time step, so let us first define the volume form
volume_form = dolfinx.fem.form(geometry.volume_form(u=problem.u) * geometry.ds(geometry.markers["ENDO"][0]))
initial_volume = geo.mesh.comm.allreduce(dolfinx.fem.assemble_scalar(volume_form))
print(f"Initial volume: {initial_volume}")
Initial volume: 0.00013552928610385148[2025-03-05 19:48:39.251] [info] Requesting connectivity (2, 0) - (3, 0)
[2025-03-05 19:48:39.251] [info] Requesting connectivity (3, 0) - (2, 0)
[2025-03-05 19:48:39.251] [info] Requesting connectivity (2, 0) - (3, 0)
[2025-03-05 19:48:39.251] [info] Requesting connectivity (3, 0) - (2, 0)
and then loop over the time steps and solve the problem for each time step
volumes = []
for i, (tai, pi, ti) in enumerate(zip(activation, pressure, times)):
print(f"Solving for time {ti}, activation {tai}, pressure {pi}")
traction.assign(pi)
Ta.assign(tai)
problem.solve()
vtx.write((i + 1) * dt)
volumes.append(geo.mesh.comm.allreduce(dolfinx.fem.assemble_scalar(volume_form)))
if geo.mesh.comm.rank == 0:
# Plot data as we go
fig, ax = plt.subplots(4, 1, figsize=(10, 10))
ax[0].plot(times[:i + 1], pressure[:i + 1])
ax[0].set_title("Pressure")
ax[1].plot(times[:i + 1], activation[:i + 1])
ax[1].set_title("Activation")
ax[2].plot(times[:i + 1], volumes)
ax[2].set_title("Volume")
ax[3].plot(volumes, pressure[:i + 1])
fig.savefig(outdir / "lv_ellipsoid_time_dependent_bestel.png")
plt.close(fig)
if os.getenv("CI") and i > 2:
# Early stopping for CI
break
INFO:scifem.solvers:Newton iteration 1: r (abs) = 1.051666490739738e-05 (tol=1e-06), r (rel) = 1.0 (tol=1e-10)
Solving for time 0.0, activation 0.0, pressure 0.0
INFO:scifem.solvers:Newton iteration 2: r (abs) = 7.111185719151402e-09 (tol=1e-06), r (rel) = 0.0006761825903713469 (tol=1e-10)
INFO:scifem.solvers:Newton iteration 1: r (abs) = 4.3792581856585495e-06 (tol=1e-06), r (rel) = 1.0 (tol=1e-10)
Solving for time 0.001, activation 0.0, pressure 6.916668004930999
INFO:scifem.solvers:Newton iteration 2: r (abs) = 6.831272542970362e-09 (tol=1e-06), r (rel) = 0.001559915459047793 (tol=1e-10)
INFO:scifem.solvers:Newton iteration 1: r (abs) = 7.400240303774599e-06 (tol=1e-06), r (rel) = 1.0 (tol=1e-10)
INFO:scifem.solvers:Newton iteration 2: r (abs) = 7.220696891292544e-09 (tol=1e-06), r (rel) = 0.0009757381645579163 (tol=1e-10)
Solving for time 0.002, activation 0.0, pressure 13.668630239081294
INFO:scifem.solvers:Newton iteration 1: r (abs) = 1.4723190123813685e-05 (tol=1e-06), r (rel) = 1.0 (tol=1e-10)
Solving for time 0.003, activation 0.0, pressure 20.261093815707657
INFO:scifem.solvers:Newton iteration 2: r (abs) = 1.4469828017039121e-08 (tol=1e-06), r (rel) = 0.0009827916297593163 (tol=1e-10)
References#
Julie Bestel, Frédérique Clément, and Michel Sorine. A biomechanical model of muscle contraction. In Medical Image Computing and Computer-Assisted Intervention–MICCAI 2001: 4th International Conference Utrecht, The Netherlands, October 14–17, 2001 Proceedings 4, 1159–1161. Springer, 2001.
Silvano Erlicher, Luca Bonaventura, and Oreste S Bursi. The analysis of the generalized-α method for non-linear dynamic problems. Computational mechanics, 28(2):83–104, 2002.
Gerhard A Holzapfel. Nonlinear solid mechanics: a continuum approach for engineering science. 2002.
Gerhard A Holzapfel and Ray W Ogden. Constitutive modelling of passive myocardium: a structurally based framework for material characterization. Philosophical Transactions of the Royal Society A: Mathematical, Physical and Engineering Sciences, 367(1902):3445–3475, 2009.