Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 17 additions & 0 deletions .cspell_dict.txt
Original file line number Diff line number Diff line change
@@ -1,17 +1,21 @@
Alfio
allclose
allreduce
Arens
argsort
arostica
astype
atol
Barnafi
Bartolucci
basix
bcast
behaviour
bestel
Bestel
booktitle
Britton
Bucelli
Bursi
caplog
cardiomyocytes
Expand All @@ -29,6 +33,7 @@ dofmap
dofs
dolfinx
dvlp
elastodynamics
Elife
Elsevier
endo
Expand All @@ -41,11 +46,13 @@ ffun
finsberg
Francesco
functionspace
Gebauer
geodir
gmsh
gradu
Grice
Guccione
Gurev
Holohan
Holzapfel
holzapfelogden
Expand All @@ -58,6 +65,10 @@ isclose
isochoric
isscalar
Jakub
Javiera
Jilberto
Karabelas
Kasra
Kluwer
Laszlo
levelname
Expand All @@ -80,30 +91,36 @@ ndarray
neohookean
Neumann
Niederer
Nolte
numpy
Oreste
Orovio
Osouli
outdir
Paraview
Passini
pendo
petsc
Piersanti
Piola
preonly
PYVISTA
Quarteroni
regazzoni
Regazzoni
Reidmen
Remedios
Riccobelli
rique
rspa
rtol
scifem
Severi
Shrier
Silvano
Sorine
Stefano
stica
subplus
Taras
tomek
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/build_docs.yml
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ jobs:

build:
runs-on: ubuntu-22.04
container: ghcr.io/fenics/dolfinx/lab:v0.9.0
container: ghcr.io/fenics/dolfinx/lab:v0.10.0
env:
PUBLISH_DIR: ./build
PYVISTA_OFF_SCREEN: false
Expand Down
Binary file added _static/cylinder.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added _static/cylinder_d.mp4
Binary file not shown.
2 changes: 1 addition & 1 deletion _toc.yml
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ parts:
- file: "demo/time_dependent_bestel_biv"
- file: "demo/time_dependent_land_circ_lv"
- file: "demo/time_dependent_land_circ_biv"
- file: "demo/bleeding_biv.py"
- file: "demo/cylinder.py"
- file: "demo/prestress.py"
- caption: Community
chapters:
Expand Down
19 changes: 17 additions & 2 deletions demo/benchmark/README.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,18 @@
# Cardiac Mechanics benchmark
# Cardiac Mechanics Benchmark

TBW
This folder contains the implementation of the benchmark problems for cardiac mechanics described in the paper:

> Land, S., Gurev, V., Arens, S. et al. Verification of cardiac mechanics software: benchmark problems and solutions for testing active and passive material behaviour. > Proc. R. Soc. A 471, 20150641 (2015).DOI: 10.1098/rspa.2015.0641

The benchmark suite consists of three problems of increasing complexity designed to verify the implementation of passive and active cardiac mechanics solvers.

## Problems

### Problem 1: Deformation of a Beam
Deflection of a beam made of anisotropic material under a pressure load. This problem tests the implementation of the transversely isotropic constitutive law (Guccione model) and the handling of Neumann boundary conditions (pressure).

### Problem 2: Inflation of a Ventricle
Inflation of an idealized Left Ventricle (truncated ellipsoid) made of isotropic material. This problem tests the handling of curvilinear geometries and large deformations under pressure loading.

### Problem 3: Inflation and Active Contraction
Inflation and active contraction of the idealized Left Ventricle with a spatially varying fiber architecture. This problem tests the coupling between the passive anisotropic material, the active stress model, and the fiber field definition.
154 changes: 101 additions & 53 deletions demo/benchmark/problem1.py
Original file line number Diff line number Diff line change
@@ -1,125 +1,172 @@
# # Problem 1: deformation of a beam
# # Problem 1: Deformation of a Beam
#
# In the first problem we will solve the deformation of a beam. First we import the necessary libraries
# This example implements Problem 1 from the cardiac mechanics benchmark suite [Land et al. 2015].
#
# ## Problem Description
#
# **Geometry**:
# A cuboid beam with dimensions $10 \times 1 \times 1$ mm.
# The domain is defined as $\Omega = [0, 10] \times [0, 1] \times [0, 1]$.
#
# **Material**:
# Transversely isotropic Guccione material.
# * Fiber direction $\mathbf{f}_0 = (1, 0, 0)$ (aligned with the long axis).
# * Constitutive parameters: $C = 2.0$ kPa, $b_f = 8.0$, $b_t = 2.0$, $b_{fs} = 4.0$.
# * The material is incompressible.
#
# **Boundary Conditions**:
# * **Dirichlet**: The face at $X=0$ is fully clamped ($\mathbf{u} = \mathbf{0}$).
# * **Neumann**: A pressure load $P$ is applied to the bottom face ($Z=0$). The pressure increases linearly from 0 to 0.004 kPa.
#
# **Target Quantity**:
# The deflection (vertical displacement) of the point $(10, 0.5, 1.0)$ at the maximum load.
# The benchmark reference value is approximately **4.0 - 4.2 mm**.
#
# ---

from mpi4py import MPI
import numpy as np
import dolfinx
from dolfinx import log
import pulse

# Next we create the beam, which should have a length og 10 mm and a width of 1 mm
# ## 1. Geometry and Mesh
# We create a box mesh of dimensions $10 \times 1 \times 1$.

L = 10.0
W = 1.0
mesh = dolfinx.mesh.create_box(MPI.COMM_WORLD, [[0.0, 0.0, 0.0], [L, W, W]], [30, 3, 3], dolfinx.mesh.CellType.hexahedron)
mesh = dolfinx.mesh.create_box(
MPI.COMM_WORLD, [[0.0, 0.0, 0.0], [L, W, W]], [30, 3, 3], dolfinx.mesh.CellType.hexahedron,
)


# We define markers for the boundary conditions.
# * `left` (Marker 1): Face at X=0.
# * `bottom` (Marker 2): Face at Z=0.

# There will be two boundaries. On the left boundary ($x = 0$) we will have a fixed Dirichlet condition and on the bottom boundary we will have a traction force pushing the beam upwards.
#
# We create markers for the two boundaries
left = 1
bottom = 2
boundaries = [
pulse.Marker(name="left", marker=left, dim=2, locator=lambda x: np.isclose(x[0], 0)),
pulse.Marker(name="bottom", marker=bottom, dim=2, locator=lambda x: np.isclose(x[2], 0)),
]

# and assemble the geometry

geo = pulse.Geometry(
mesh=mesh,
boundaries=boundaries,
metadata={"quadrature_degree": 4},
)

# The material model used in this benchmark is the {py:class}`Guccione <pulse.material_models.guccione.Guccione>` model.
# ## 2. Constitutive Model
#
# We use the **Guccione** model as specified in the benchmark.
#
# $$
# \Psi = \frac{C}{2} (e^Q - 1), \quad Q = b_f E_{ff}^2 + b_t (E_{ss}^2 + E_{nn}^2 + E_{sn}^2 + E_{ns}^2) + b_{fs} (E_{fs}^2 + E_{sf}^2 + E_{fn}^2 + E_{nf}^2)
# $$
#
# Since the problem defines a fixed fiber direction along $x$, we set $\mathbf{f}_0, \mathbf{s}_0, \mathbf{n}_0$ to align with the coordinate axes.

material_params = {
"C": pulse.Variable(dolfinx.fem.Constant(mesh, dolfinx.default_scalar_type(2.0)), "kPa"),
"bf": dolfinx.fem.Constant(mesh, dolfinx.default_scalar_type(8.0)),
"bt": dolfinx.fem.Constant(mesh, dolfinx.default_scalar_type(2.0)),
"bfs": dolfinx.fem.Constant(mesh, dolfinx.default_scalar_type(4.0)),
}

f0 = dolfinx.fem.Constant(mesh, dolfinx.default_scalar_type((1.0, 0.0, 0.0)))
s0 = dolfinx.fem.Constant(mesh, dolfinx.default_scalar_type((0.0, 1.0, 0.0)))
n0 = dolfinx.fem.Constant(mesh, dolfinx.default_scalar_type((0.0, 0.0, 1.0)))

material = pulse.Guccione(f0=f0, s0=s0, n0=n0, **material_params)

# There are now active contraction, so we choose a pure passive model
# The problem is purely passive (no active contraction) and incompressible.

active_model = pulse.active_model.Passive()

# and the model should be incompressible

comp_model = pulse.Incompressible()

# We can now assemble the `CardiacModel`
#

model = pulse.CardiacModel(
material=material,
active=active_model,
compressibility=comp_model,
)

# ## 3. Boundary Conditions
#
# **Dirichlet BC**: Fix all displacement components on the 'left' face.

# Next we define the Dirichlet BC

def dirichlet_bc(
V: dolfinx.fem.FunctionSpace,
) -> list[dolfinx.fem.bcs.DirichletBC]:
facets = geo.facet_tags.find(left) # Specify the marker used on the boundary
mesh.topology.create_connectivity(mesh.topology.dim - 1, mesh.topology.dim)
dofs = dolfinx.fem.locate_dofs_topological(V, 2, facets)
def dirichlet_bc(V: dolfinx.fem.FunctionSpace):
facets = geo.facet_tags.find(left)
dofs = dolfinx.fem.locate_dofs_topological(V, geo.facet_dimension, facets)
u_fixed = dolfinx.fem.Function(V)
u_fixed.x.array[:] = 0.0
return [dolfinx.fem.dirichletbc(u_fixed, dofs)]


# and the traction force

traction = dolfinx.fem.Constant(mesh, dolfinx.default_scalar_type(0.0))
neumann = pulse.NeumannBC(traction=traction, marker=bottom)
# **Neumann BC**: Apply a pressure load to the 'bottom' face.
# Note: In `fenicsx-pulse`, a `NeumannBC` with a scalar traction represents a pressure load $P$ acting normal to the deformed surface: $\mathbf{t} = P \mathbf{n}$.
# Since the pressure is pushing *up* against the bottom face (normal pointing down), we define a positive pressure.

# We assemble all the boundary conditions
pressure = pulse.Variable(dolfinx.fem.Constant(mesh, dolfinx.default_scalar_type(0.0)), "kPa")
neumann = pulse.NeumannBC(traction=pressure, marker=bottom)

bcs = pulse.BoundaryConditions(dirichlet=(dirichlet_bc,), neumann=(neumann,))

# and create a mechanics problem
# ## 4. Solving the Problem
#
# We initialize the static problem and solve it incrementally up to the target pressure of 0.004 kPa.

problem = pulse.StaticProblem(model=model, geometry=geo, bcs=bcs)

# Now let us turn on some more logging

log.set_log_level(log.LogLevel.INFO)

# and step up the traction to the target value (which is 0.004 kPa)
target_pressure = 0.004
steps = [0.0005, 0.001, 0.002, 0.003, 0.004]

for t in [0.0, 0.001, 0.002, 0.003, 0.004]:
print(f"Solving problem for traction={t}")
traction.value = t
for p in steps:
print(f"Solving for pressure = {p} kPa")
pressure.assign(p)
problem.solve()

# Now let us turn off the logging again

log.set_log_level(log.LogLevel.WARNING)

# Save the displacement to a file
# ## 5. Post-processing
#
# We save the final displacement and evaluate the deflection at the specific target point $(10, 0.5, 1)$.

with dolfinx.io.VTXWriter(mesh.comm, "problem1_displacement.bp", [problem.u], engine="BP4") as vtx:
vtx.write(0.0)

# and we find the deflection of the given point in the benchmark

# Evaluate displacement at the target point
point = np.array([10.0, 0.5, 1.0])
tree = dolfinx.geometry.bb_tree(mesh, 3)

# Use bounding box tree for point collision
tree = dolfinx.geometry.bb_tree(mesh, mesh.topology.dim)
cell_candidates = dolfinx.geometry.compute_collisions_points(tree, point)
cell = dolfinx.geometry.compute_colliding_cells(mesh, cell_candidates, point)
uz = mesh.comm.allreduce(problem.u.eval(point, cell.array)[2], op=MPI.MAX)

# Get the z-displacement
if len(cell.array) > 0:
# Evaluate only if the point is found on this process
u_val = problem.u.eval(point, cell.array)[2]
else:
u_val = -np.inf # Placeholder for reduction

# Reduce across all processes (take the max to get the actual value)
uz = mesh.comm.allreduce(u_val, op=MPI.MAX)
result = point[2] + uz
print(f"Get z-position of point {point}: {result:.2f} mm")
assert np.isclose(result, 4.17, atol=1.0e-2)
# Finally, let us plot the deflected beam using pyvista

print(f"Target Point: {point}")
print(f"Vertical Deflection (Uz): {uz:.4f} mm")
print(f"Final Z Position: {result:.4f} mm")

# Verify against benchmark tolerance (approx 4.0 - 4.2 mm deflection)
# Note: Result is typically around 4.17 mm for the settings described.
if mesh.comm.rank == 0:
print(f"Benchmark Ref: ~4.17 mm")

# Visualization with PyVista
try:
import pyvista
except ImportError:
Expand All @@ -128,18 +175,19 @@ def dirichlet_bc(
V = dolfinx.fem.functionspace(mesh, ("Lagrange", 1, (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.add_mesh(grid, style="wireframe", color="k", opacity=0.5, label="Reference")
warped = grid.warp_by_vector("u", factor=1.0)
p.add_mesh(warped, show_edges=True, label="Deformed")

p.add_legend()
p.show_axes()
if not pyvista.OFF_SCREEN:
p.show()
else:
figure_as_array = p.screenshot("deflection.png")
p.screenshot("deflection.png")
Loading