Patient meshes from another tool#
Meshes of patient hearts are often made with other tools and shared as a tetrahedral volume mesh
together with the boundary surfaces (endocardium, epicardium, base, valves) as separate files. With
cardiac_geometries.mesh.from_volume such a mesh becomes a geometry with facet markers, and
optionally fibers, like the other geometries in this package. Every boundary face of the volume must
lie on exactly one of the surfaces; the surfaces are matched to the volume by their nodes. The volume
is read with the pyvista backend of io4dolfinx
(any format pyvista reads, e.g. VTU or legacy VTK), already distributed over the processes.
Use the marker names of the UK Biobank geometries (LV_ENDO, RV_ENDO, EPI, BASE, MV, AV,
TV, PV), so that the fiber methods find their boundaries. Here we make a small example: a thick
spherical shell, cut at the top, standing in for an LV.
from pathlib import Path
import cardiac_geometries as cg
from cardiac_geometries.labelled import boundary_faces
comm = MPI.COMM_WORLD
outdir = Path("patient_mesh")
outdir.mkdir(exist_ok=True)
First we make the “patient” files: a volume mesh and three surfaces, as another tool would give them
if comm.rank == 0:
gmsh.initialize()
gmsh.option.setNumber("General.Terminal", 0)
outer = gmsh.model.occ.addSphere(0, 0, 0, 20.0)
inner = gmsh.model.occ.addSphere(0, 0, 0, 10.0)
shell, _ = gmsh.model.occ.cut([(3, outer)], [(3, inner)])
top = gmsh.model.occ.addBox(-30, -30, 5.0, 60, 60, 30)
gmsh.model.occ.cut(shell, [(3, top)])
gmsh.model.occ.synchronize()
gmsh.option.setNumber("Mesh.CharacteristicLengthMax", 2.5)
gmsh.model.mesh.generate(3)
tags, coords, _ = gmsh.model.mesh.getNodes()
index = np.empty(int(tags.max()) + 1, dtype=np.int64)
index[tags.astype(np.int64)] = np.arange(len(tags))
points = coords.reshape(-1, 3)
tets = index[gmsh.model.mesh.getElementsByType(4)[1].astype(np.int64)].reshape(-1, 4)
gmsh.finalize()
pyvista.UnstructuredGrid({pyvista.CellType.TETRA: tets}, points).save(outdir / "volume.vtu")
faces = boundary_faces(tets)
centre = points[faces].mean(axis=1)
names = np.where(
np.isclose(centre[:, 2], 5.0),
"BASE",
np.where(np.linalg.norm(centre, axis=1) < 15.0, "LV_ENDO", "EPI"),
)
for name in ("LV_ENDO", "EPI", "BASE"):
pyvista.PolyData.from_regular_faces(points, faces[names == name]).save(
outdir / f"{name.lower()}.ply"
)
comm.barrier()
Now we import it, with fibers by the method of Bayer et al.
geo = cg.mesh.from_volume(
outdir / "geometry",
outdir / "volume.vtu",
surfaces={name: outdir / f"{name.lower()}.ply" for name in ("LV_ENDO", "EPI", "BASE")},
create_fibers=True,
fiber_space="P_1",
)
print(geo.markers)
{'LV_ENDO': (1, 2), 'EPI': (3, 2), 'BASE': (4, 2), 'Wall': (1, 3)}
The same from the command line:
geo from-volume patient_mesh/geometry patient_mesh/volume.vtu \
--surface LV_ENDO=patient_mesh/lv_endo.ply --surface EPI=patient_mesh/epi.ply \
--surface BASE=patient_mesh/base.ply --create-fibers
A mesh in other units can be scaled with scale (--scale), e.g. scale=1e-3 from micrometres to
millimetres, and surfaces whose nodes do not coincide exactly with the volume’s can be matched within
a distance tol (--tol). Instead of surface files, a rule on the boundary faces can label them
(rule=..., from Python only).
Let us plot the facet markers and the fibers
vtk_mesh = dolfinx.plot.vtk_mesh(geo.mesh, geo.mesh.topology.dim - 1, geo.ffun.indices)
grid = pyvista.UnstructuredGrid(*vtk_mesh)
grid.cell_data["Marker"] = geo.ffun.values
V = geo.f0.function_space
topology, cell_types, x = dolfinx.plot.vtk_mesh(V)
fibers = pyvista.UnstructuredGrid(topology, cell_types, x)
fibers["f0"] = geo.f0.x.array.reshape(-1, 3)
glyphs = fibers.glyph(orient="f0", factor=1.5, tolerance=0.02)
plotter = pyvista.Plotter(shape=(1, 2))
plotter.subplot(0, 0)
plotter.add_mesh(grid, scalars="Marker", cmap="tab10", show_edges=False)
plotter.subplot(0, 1)
plotter.add_mesh(glyphs, color="firebrick")
if not pyvista.OFF_SCREEN:
plotter.show()
else:
plotter.screenshot("patient_mesh.png")