Problem 2: Inflation of a ventricle

Problem 2: Inflation of a ventricle#

In the second problem we will solve the inflation of a ventricle. First we import the necessary libraries

from pathlib import Path
from mpi4py import MPI
from dolfinx import log
import logging
import dolfinx
import numpy as np
import math
import cardiac_geometries
import fenicsx_pulse
logging.basicConfig(level=logging.INFO)
# Next we will create the geometry and save it in the folder called `lv_ellipsoid`.
comm = MPI.COMM_WORLD
geodir = Path("lv_ellipsoid-problem2")
if not geodir.exists():
    comm.barrier()
    cardiac_geometries.mesh.lv_ellipsoid(
        outdir=geodir,
        r_short_endo=7.0,
        r_short_epi=10.0,
        r_long_endo=17.0,
        r_long_epi=20.0,
        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,
    )
2025-03-05 19:41:41 [debug    ] Convert file lv_ellipsoid-problem2/lv_ellipsoid.msh to dolfin
INFO:cardiac_geometries.geometry:Reading geometry from lv_ellipsoid-problem2
Info    : Reading 'lv_ellipsoid-problem2/lv_ellipsoid.msh'...
Info    : 53 entities
Info    : 771 nodes
Info    : 4026 elements
Info    : Done reading 'lv_ellipsoid-problem2/lv_ellipsoid.msh'

If the folder already exist, then we just load the geometry

geo = cardiac_geometries.geometry.Geometry.from_folder(
    comm=comm,
    folder=geodir,
)
INFO:cardiac_geometries.geometry:Reading geometry from lv_ellipsoid-problem2

Now, lets convert the geometry to a fenicsx_pulse.Geometry object.

geometry = fenicsx_pulse.HeartGeometry.from_cardiac_geometries(geo, metadata={"quadrature_degree": 4})

The material model used in this benchmark is the Guccione model.

material_params = {
    "C": dolfinx.fem.Constant(geometry.mesh, dolfinx.default_scalar_type(10.0)),
    "bf": dolfinx.fem.Constant(geometry.mesh, dolfinx.default_scalar_type(1.0)),
    "bt": dolfinx.fem.Constant(geometry.mesh, dolfinx.default_scalar_type(1.0)),
    "bfs": dolfinx.fem.Constant(geometry.mesh, dolfinx.default_scalar_type(1.0)),
}
material = fenicsx_pulse.Guccione(**material_params)
WARNING:fenicsx_pulse.material_models.guccione:Setting C to c_0 kPa

There are now active contraction, so we choose a pure passive model

active_model = fenicsx_pulse.active_model.Passive()

and the model should be incompressible

comp_model = fenicsx_pulse.Incompressible()

and assembles the CardiacModel

model = fenicsx_pulse.CardiacModel(
    material=material,
    active=active_model,
    compressibility=comp_model,
    decouple_deviatoric_volumetric=True,
)

We apply a traction in endocardium

traction = dolfinx.fem.Constant(geometry.mesh, dolfinx.default_scalar_type(0.0))
neumann = fenicsx_pulse.NeumannBC(
    traction=fenicsx_pulse.Variable(traction, "kPa"),
    marker=geo.markers["ENDO"][0],
)

and finally combine all the boundary conditions

bcs = fenicsx_pulse.BoundaryConditions(neumann=(neumann,))

and create a Mixed problem

problem = fenicsx_pulse.StaticProblem(
    model=model, geometry=geometry, bcs=bcs, parameters={"base_bc": fenicsx_pulse.BaseBC.fixed},
)
# Now we can solve the problem
log.set_log_level(log.LogLevel.INFO)
problem.solve()
INFO:scifem.solvers:Newton iteration 1: r (abs) = 0.0 (tol=1e-06), r (rel) = 0.0 (tol=1e-10)
1
# Now step up the pressure to 10 kPa starting with an increment of 1 kPa
target_value = 10.0
incr = 1.0

Here we use a continuation strategy to speed up the convergence

use_continuation = True
old_u = [problem.u.copy()]
old_p = [problem.p.copy()]
old_tractions = [traction.value.copy()]
while traction.value < target_value:
    value = min(traction.value + incr, target_value)
    print(f"Solving problem for traction={value}")

    if use_continuation and len(old_tractions) > 1:
        # Better initial guess
        d = (value - old_tractions[-2]) / (old_tractions[-1] - old_tractions[-2])
        problem.u.x.array[:] = (1 - d) * old_u[-2].x.array + d * old_u[-1].x.array
        problem.p.x.array[:] = (1 - d) * old_p[-2].x.array + d * old_p[-1].x.array

    traction.value = value

    try:
        nit = problem.solve()
    except RuntimeError:
        print("Convergence failed, reducing increment")

        # Reset state and half the increment
        traction.value = old_tractions[-1]
        problem.u.x.array[:] = old_u[-1].x.array
        problem.p.x.array[:] = old_p[-1].x.array
        incr *= 0.5
        problem._init_forms()
    else:
        print(f"Converged in {nit} iterations")
        if nit < 3:
            print("Increasing increment")
            # Increase increment
            incr *= 1.5
        old_u.append(problem.u.copy())
        old_p.append(problem.p.copy())
        old_tractions.append(traction.value.copy())
Solving problem for traction=1.0
INFO:scifem.solvers:Newton iteration 1: r (abs) = 23626.244411701242 (tol=1e-06), r (rel) = 1.0 (tol=1e-10)
INFO:scifem.solvers:Newton iteration 2: r (abs) = 4005.750527508181 (tol=1e-06), r (rel) = 0.1695466472667266 (tol=1e-10)
INFO:scifem.solvers:Newton iteration 3: r (abs) = 93.06415070374997 (tol=1e-06), r (rel) = 0.003939015828417427 (tol=1e-10)
INFO:scifem.solvers:Newton iteration 4: r (abs) = 0.06329548858772499 (tol=1e-06), r (rel) = 2.679033006040392e-06 (tol=1e-10)
INFO:scifem.solvers:Newton iteration 5: r (abs) = 5.777026628947666e-08 (tol=1e-06), r (rel) = 2.4451734809306e-12 (tol=1e-10)
Converged in 5 iterations
Solving problem for traction=2.0
INFO:scifem.solvers:Newton iteration 1: r (abs) = 30372.591563072612 (tol=1e-06), r (rel) = 1.0 (tol=1e-10)
INFO:scifem.solvers:Newton iteration 2: r (abs) = 1825.5635880719399 (tol=1e-06), r (rel) = 0.06010562464783162 (tol=1e-10)
INFO:scifem.solvers:Newton iteration 3: r (abs) = 54.12934758791407 (tol=1e-06), r (rel) = 0.0017821774436174629 (tol=1e-10)
INFO:scifem.solvers:Newton iteration 4: r (abs) = 0.07896978813910814 (tol=1e-06), r (rel) = 2.6000345731156056e-06 (tol=1e-10)
INFO:scifem.solvers:Newton iteration 5: r (abs) = 3.9048948542960816e-07 (tol=1e-06), r (rel) = 1.2856640323849424e-11 (tol=1e-10)
Converged in 5 iterations
Solving problem for traction=3.0
INFO:scifem.solvers:Newton iteration 1: r (abs) = 40306.002325679634 (tol=1e-06), r (rel) = 1.0 (tol=1e-10)
INFO:scifem.solvers:Newton iteration 2: r (abs) = 35134.91654353103 (tol=1e-06), r (rel) = 0.8717043248207719 (tol=1e-10)
INFO:scifem.solvers:Newton iteration 3: r (abs) = 4916.066118701996 (tol=1e-06), r (rel) = 0.12196858619168707 (tol=1e-10)
INFO:scifem.solvers:Newton iteration 4: r (abs) = 227.628357537395 (tol=1e-06), r (rel) = 0.005647505195333379 (tol=1e-10)
INFO:scifem.solvers:Newton iteration 5: r (abs) = 1.519410268439401 (tol=1e-06), r (rel) = 3.769687343741751e-05 (tol=1e-10)
INFO:scifem.solvers:Newton iteration 6: r (abs) = 0.00011788422453350133 (tol=1e-06), r (rel) = 2.9247312492312168e-09 (tol=1e-10)
INFO:scifem.solvers:Newton iteration 7: r (abs) = 8.6428538550554e-11 (tol=1e-06), r (rel) = 2.1443093723906454e-15 (tol=1e-10)
Converged in 7 iterations
Solving problem for traction=4.0
INFO:scifem.solvers:Newton iteration 1: r (abs) = 20422.60652284542 (tol=1e-06), r (rel) = 1.0 (tol=1e-10)
INFO:scifem.solvers:Newton iteration 2: r (abs) = 8379.660769812626 (tol=1e-06), r (rel) = 0.4103129911668647 (tol=1e-10)
INFO:scifem.solvers:Newton iteration 3: r (abs) = 673.1241547142881 (tol=1e-06), r (rel) = 0.03295975731409742 (tol=1e-10)
INFO:scifem.solvers:Newton iteration 4: r (abs) = 9.112164288803987 (tol=1e-06), r (rel) = 0.0004461802796137119 (tol=1e-10)
INFO:scifem.solvers:Newton iteration 5: r (abs) = 0.002458977865296588 (tol=1e-06), r (rel) = 1.2040470262921102e-07 (tol=1e-10)
INFO:scifem.solvers:Newton iteration 6: r (abs) = 2.680276835498316e-10 (tol=1e-06), r (rel) = 1.3124068333295592e-14 (tol=1e-10)
Converged in 6 iterations
Solving problem for traction=5.0
INFO:scifem.solvers:Newton iteration 1: r (abs) = 15537.378747403713 (tol=1e-06), r (rel) = 1.0 (tol=1e-10)
INFO:scifem.solvers:Newton iteration 2: r (abs) = 2195.5410477483642 (tol=1e-06), r (rel) = 0.14130704306318323 (tol=1e-10)
INFO:scifem.solvers:Newton iteration 3: r (abs) = 63.98316768407319 (tol=1e-06), r (rel) = 0.004118015575488545 (tol=1e-10)
INFO:scifem.solvers:Newton iteration 4: r (abs) = 0.08259278772889403 (tol=1e-06), r (rel) = 5.315747853716653e-06 (tol=1e-10)
INFO:scifem.solvers:Newton iteration 5: r (abs) = 1.638119472179985e-07 (tol=1e-06), r (rel) = 1.0543087729348902e-11 (tol=1e-10)
Converged in 5 iterations
Solving problem for traction=6.0
INFO:scifem.solvers:Newton iteration 1: r (abs) = 12303.37944490715 (tol=1e-06), r (rel) = 1.0 (tol=1e-10)
INFO:scifem.solvers:Newton iteration 2: r (abs) = 860.3455332163771 (tol=1e-06), r (rel) = 0.06992757860300795 (tol=1e-10)
INFO:scifem.solvers:Newton iteration 3: r (abs) = 9.165681268587791 (tol=1e-06), r (rel) = 0.0007449726564664983 (tol=1e-10)
INFO:scifem.solvers:Newton iteration 4: r (abs) = 0.001537502383423525 (tol=1e-06), r (rel) = 1.2496585920220135e-07 (tol=1e-10)
INFO:scifem.solvers:Newton iteration 5: r (abs) = 3.4147937093681795e-10 (tol=1e-06), r (rel) = 2.775492477216653e-14 (tol=1e-10)
Converged in 5 iterations
Solving problem for traction=7.0
INFO:scifem.solvers:Newton iteration 1: r (abs) = 10087.082182729808 (tol=1e-06), r (rel) = 1.0 (tol=1e-10)
INFO:scifem.solvers:Newton iteration 2: r (abs) = 416.04880397325 (tol=1e-06), r (rel) = 0.041245703805761716 (tol=1e-10)
INFO:scifem.solvers:Newton iteration 3: r (abs) = 1.8941819691494566 (tol=1e-06), r (rel) = 0.00018778294206747955 (tol=1e-10)
INFO:scifem.solvers:Newton iteration 4: r (abs) = 5.81173510192871e-05 (tol=1e-06), r (rel) = 5.761562161037053e-09 (tol=1e-10)
INFO:scifem.solvers:Newton iteration 5: r (abs) = 4.048984321041039e-10 (tol=1e-06), r (rel) = 4.0140292779346483e-14 (tol=1e-10)
Converged in 5 iterations
Solving problem for traction=8.0
INFO:scifem.solvers:Newton iteration 1: r (abs) = 8506.388361606121 (tol=1e-06), r (rel) = 1.0 (tol=1e-10)
INFO:scifem.solvers:Newton iteration 2: r (abs) = 229.37611181174762 (tol=1e-06), r (rel) = 0.026965158662052704 (tol=1e-10)
INFO:scifem.solvers:Newton iteration 3: r (abs) = 0.5111504096780219 (tol=1e-06), r (rel) = 6.009018022091691e-05 (tol=1e-10)
INFO:scifem.solvers:Newton iteration 4: r (abs) = 3.736647527207666e-06 (tol=1e-06), r (rel) = 4.392754443323037e-10 (tol=1e-10)
INFO:scifem.solvers:Newton iteration 5: r (abs) = 5.12861135246978e-10 (tol=1e-06), r (rel) = 6.029129090341024e-14 (tol=1e-10)
Converged in 5 iterations
Solving problem for traction=9.0
INFO:scifem.solvers:Newton iteration 1: r (abs) = 7330.422209848763 (tol=1e-06), r (rel) = 1.0 (tol=1e-10)
INFO:scifem.solvers:Newton iteration 2: r (abs) = 138.49085203148363 (tol=1e-06), r (rel) = 0.018892616014042785 (tol=1e-10)
INFO:scifem.solvers:Newton iteration 3: r (abs) = 0.16757857483509547 (tol=1e-06), r (rel) = 2.286069888443068e-05 (tol=1e-10)
INFO:scifem.solvers:Newton iteration 4: r (abs) = 3.55862492632172e-07 (tol=1e-06), r (rel) = 4.854597490360844e-11 (tol=1e-10)
Converged in 4 iterations
Solving problem for traction=10.0
INFO:scifem.solvers:Newton iteration 1: r (abs) = 6424.6876697761745 (tol=1e-06), r (rel) = 1.0 (tol=1e-10)
INFO:scifem.solvers:Newton iteration 2: r (abs) = 89.41446624488565 (tol=1e-06), r (rel) = 0.013917324987722041 (tol=1e-10)
INFO:scifem.solvers:Newton iteration 3: r (abs) = 0.0635887498920717 (tol=1e-06), r (rel) = 9.897562832698296e-06 (tol=1e-10)
INFO:scifem.solvers:Newton iteration 4: r (abs) = 4.574304350199894e-08 (tol=1e-06), r (rel) = 7.119885954486026e-12 (tol=1e-10)
Converged in 4 iterations
log.set_log_level(log.LogLevel.INFO)

Now save the displacement to a file that we can view in Paraview

with dolfinx.io.VTXWriter(geometry.mesh.comm, "problem2.bp", [problem.u], engine="BP4") as vtx:
    vtx.write(0.0)
try:
    import pyvista
except ImportError:
    print("Pyvista is not installed")
else:
    pyvista.start_xvfb()
    V = dolfinx.fem.functionspace(geometry.mesh, ("Lagrange", 1, (geometry.mesh.geometry.dim,)))
    uh = dolfinx.fem.Function(V)
    uh.interpolate(problem.u)
    # Create plotter and pyvista grid
    p = pyvista.Plotter()
    topology, cell_types, geometry = dolfinx.plot.vtk_mesh(V)
    grid = pyvista.UnstructuredGrid(topology, cell_types, geometry)

    # Attach vector values to grid and warp grid by vector
    grid["u"] = uh.x.array.reshape((geometry.shape[0], 3))
    actor_0 = p.add_mesh(grid, style="wireframe", color="k")
    warped = grid.warp_by_vector("u", factor=1.5)
    actor_1 = p.add_mesh(warped, show_edges=True)
    p.show_axes()
    if not pyvista.OFF_SCREEN:
        p.show()
    else:
        figure_as_array = p.screenshot("problem2.png")
[2025-03-05 19:42:28.590] [info] Checking required entities per dimension
[2025-03-05 19:42:28.590] [info] Cell type: 0 dofmap: 2838x4
[2025-03-05 19:42:28.590] [info] Global index computation
[2025-03-05 19:42:28.590] [info] Got 1 index_maps
[2025-03-05 19:42:28.590] [info] Get global indices
INFO:trame_server.utils.namespace:Translator(prefix=None)
INFO:wslink.backends.aiohttp:awaiting runner setup
INFO:wslink.backends.aiohttp:awaiting site startup
INFO:wslink.backends.aiohttp:Print WSLINK_READY_MSG
INFO:wslink.backends.aiohttp:Schedule auto shutdown with timout 0
INFO:wslink.backends.aiohttp:awaiting running future
error: XDG_RUNTIME_DIR is invalid or not set in the environment.

Finally we can extract the longitudinal position of the endocardial and epicardial apex First we create a function to get the coordinates of the apex

U = dolfinx.fem.Function(problem.u.function_space)
U.interpolate(lambda x: (x[0], x[1], x[2]))
endo_apex_coord = fenicsx_pulse.utils.evaluate_at_vertex_tag(U, geo.vfun, geo.markers["ENDOPT"][0])
u_endo_apex = fenicsx_pulse.utils.evaluate_at_vertex_tag(problem.u, geo.vfun, geo.markers["ENDOPT"][0])
endo_apex_pos = fenicsx_pulse.utils.gather_broadcast_array(geo.mesh.comm, endo_apex_coord + u_endo_apex)
print(f"\nGet longitudinal position of endocardial apex: {endo_apex_pos[0, 0]:4f} mm")
Get longitudinal position of endocardial apex: -26.523660 mm
epi_apex_coord = fenicsx_pulse.utils.evaluate_at_vertex_tag(U, geo.vfun, geo.markers["EPIPT"][0])
u_epi_apex = fenicsx_pulse.utils.evaluate_at_vertex_tag(problem.u, geo.vfun, geo.markers["EPIPT"][0])
epi_apex_pos = fenicsx_pulse.utils.gather_broadcast_array(geo.mesh.comm, epi_apex_coord + u_epi_apex)
print(f"\nGet longitudinal position of epicardial apex: {epi_apex_pos[0, 0]:4f} mm")
Get longitudinal position of epicardial apex: -28.172577 mm