UK-Biobank atlas#
The UK-Biobank provides an atlas for BiV meshes (see https://www.cardiacatlas.org/biventricular-modes/). We also provide a separate package to generate meshes from this atlas, see https://computationalphysiology.github.io/ukb-atlas.
However, if you want to use these meshes for cardiac simulations you probably need to generate fiber orientations using `fenicsx-ldrb as well. Here we will show how to do this programmatically from python
Select the mode you want to use (-1 will you the mean mode)
mode = -1
# Standard deviation of the mode
std = 1.5
Choose output directory
outdir = Path("ukb_mesh")
outdir.mkdir(exist_ok=True)
# Characteristic length of the mesh (smaller values will give finer meshes). Let us also pick the case from end-diastole (ED)
char_length = 10.0
case = "ED"
Generate surfaces
ukb.cli.main(["surf", str(outdir), "--mode", str(mode), "--std", str(std), "--case", case])
INFO:ukb.atlas:Generating points from /github/home/.ukb/UKBRVLV.h5
INFO:ukb.atlas:Using mode -1 and std 1.5
INFO:ukb.surface:Saved ukb_mesh/LV_ENDO_ED.stl
INFO:ukb.surface:Saved ukb_mesh/RV_SEPTUM_ED.stl
INFO:ukb.surface:Saved ukb_mesh/RV_FREEWALL_ED.stl
INFO:ukb.surface:Saved ukb_mesh/LV_EPI_ED.stl
INFO:ukb.surface:Saved ukb_mesh/RV_EPI_ED.stl
INFO:ukb.surface:Saved ukb_mesh/MV_ED.stl
INFO:ukb.surface:Saved ukb_mesh/AV_ED.stl
INFO:ukb.surface:Saved ukb_mesh/TV_ED.stl
INFO:ukb.surface:Saved ukb_mesh/PV_ED.stl
INFO:ukb.surface:Saved ukb_mesh/landmarks_ED.json
0
Generate mesh
ukb.cli.main([
"mesh",
str(outdir),
"--case",
case,
"--char_length_max",
str(char_length),
"--char_length_min",
str(char_length),
])
INFO:ukb.mesh:Creating mesh for ED with char_length_max=10.0, char_length_min=10.0, order=1
INFO:ukb.mesh:Created mesh ukb_mesh/ED.msh
0
# Choose the ED mesh
mesh_name = outdir / "ED.msh"
# Convert mesh to dolfinx
comm = MPI.COMM_WORLD
geometry = cg.utils.gmsh2dolfin(comm=comm, msh_file=mesh_name)
2026-11-12 21:38:16 [debug ] Convert file ukb_mesh/ED.msh to dolfin
Info : Reading 'ukb_mesh/ED.msh'...
Info : 30 entities
Info : 2038 nodes
Info : 10128 elements
Info : 9 parametrizations
Info : [ 0%] Processing parametrizations
Info : [ 10%] Processing parametrizations
Info : [ 20%] Processing parametrizations
Info : [ 30%] Processing parametrizations
Info : [ 40%] Processing parametrizations
Info : [ 50%] Processing parametrizations
Info : [ 60%] Processing parametrizations
Info : [ 70%] Processing parametrizations
Info : [ 80%] Processing parametrizations
Info : Done reading 'ukb_mesh/ED.msh'
Now we can plot the mesh
vtk_mesh = dolfinx.plot.vtk_mesh(geometry.mesh, geometry.mesh.topology.dim)
grid = pyvista.UnstructuredGrid(*vtk_mesh)
plotter = pyvista.Plotter()
plotter.add_mesh(grid, show_edges=True)
plotter.view_xy()
if not pyvista.OFF_SCREEN:
plotter.show()
else:
figure = plotter.screenshot("ukb_mesh.png")
INFO:trame_server.utils.namespace:Translator(prefix=None)
INFO:trame_rca.encoders.image_encoder:Turbo JPEG - NOT AVAILABLE (Python package)
INFO:trame_rca.encoders:VTKStreaming Video encoding is NOT AVAILABLE (missing Python package)
INFO:trame_rca.schedulers:cannot import name 'RcaVideoEncoder' from 'trame_rca.encoders' (/dolfinx-env/lib/python3.12/site-packages/trame_rca/encoders/__init__.py)
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 timeout 0
INFO:wslink.backends.aiohttp:awaiting running future
We also print the markers. The surfaces are the endocardium of each ventricle (LV_ENDO and
RV_ENDO), the epicardium (EPI) and the rims of the four valves (MV, AV, TV and PV),
and the volume is the myocardium (Wall)
print(geometry.markers)
{'LV_ENDO': (1, 2), 'RV_ENDO': (2, 2), 'EPI': (3, 2), 'MV': (4, 2), 'AV': (5, 2), 'TV': (6, 2), 'PV': (7, 2), 'Wall': (8, 3)}
And plot the facet tags
assert geometry.ffun is not None
vtk_bmesh = dolfinx.plot.vtk_mesh(
geometry.mesh, geometry.ffun.dim, geometry.ffun.indices
)
bgrid = pyvista.UnstructuredGrid(*vtk_bmesh)
bgrid.cell_data["Facet tags"] = geometry.ffun.values
bgrid.set_active_scalars("Facet tags")
p = pyvista.Plotter(window_size=[800, 800])
p.add_mesh(bgrid, show_edges=True)
if not pyvista.OFF_SCREEN:
p.show()
else:
figure = p.screenshot("facet_tags_ukb.png")
We could also combine all the valves into one entity, i.e the BASE
m = {name: value[0] for name, value in geometry.markers.items()}
entities = [
geometry.ffun.find(m["LV_ENDO"]),
geometry.ffun.find(m["RV_ENDO"]),
geometry.ffun.find(m["EPI"]),
np.hstack([geometry.ffun.find(m[valve]) for valve in ["MV", "AV", "TV", "PV"]]), # BASE
]
entity_values = [
np.full(e.shape, i + 1, dtype=np.int32) for i, e in enumerate(entities)
]
and create new mesh tags
ffun = dolfinx.mesh.meshtags(
geometry.mesh, 2, np.hstack(entities), np.hstack(entity_values)
)
and plot the new tags
vtk_bmesh_new = dolfinx.plot.vtk_mesh(geometry.mesh, ffun.dim, ffun.indices)
bgrid_new = pyvista.UnstructuredGrid(*vtk_bmesh_new)
bgrid_new.cell_data["Facet tags"] = ffun.values
bgrid_new.set_active_scalars("Facet tags")
p = pyvista.Plotter(window_size=[800, 800])
p.add_mesh(bgrid_new, show_edges=True)
if not pyvista.OFF_SCREEN:
p.show()
else:
figure = p.screenshot("new_facet_tags_ukb.png")
Let us also save the mesh and markers so that we can inspect them later. Let us save them in a adios2 file called geometry.bp
cg.geometry.save_geometry(
path=outdir / "geometry.bp",
mesh=geometry.mesh,
markers=geometry.markers,
ffun=ffun,
)
Now let us combine the markers as well so that they align with the markers needed in fenicsx-ldrb, where the valves together make up the base
markers = {
"lv": [m["LV_ENDO"]],
"rv": [m["RV_ENDO"]],
"epi": [m["EPI"]],
"base": [m[valve] for valve in ["MV", "AV", "TV", "PV"]],
}
And finally generate the fibers
fiber_space = "P_1"
fiber_angle_endo = 60
fiber_angle_epi = -60
system = ldrb.dolfinx_ldrb(
mesh=geometry.mesh,
ffun=geometry.ffun,
markers=markers,
alpha_endo_lv=fiber_angle_endo,
alpha_epi_lv=fiber_angle_epi,
beta_endo_lv=0,
beta_epi_lv=0,
fiber_space=fiber_space,
)
INFO:ldrb.ldrb:Calculating scalar fields
INFO:ldrb.ldrb:Compute scalar laplacian solutions with the markers:
lv: [1]
rv: [2]
epi: [3]
base: [4, 5, 6, 7]
INFO:ldrb.ldrb: Num vertices: 2038
INFO:ldrb.ldrb: Num cells: 6268
INFO:ldrb.ldrb: Apex coord: (43.74, -22.65, -28.26)
INFO:ldrb.ldrb:
Calculating gradients
INFO:ldrb.ldrb:Compute fiber-sheet system
INFO:ldrb.ldrb:Angles:
INFO:ldrb.ldrb:alpha:
endo_lv: 60
epi_lv: -60
endo_septum: 60
epi_septum: -60
endo_rv: 60
epi_rv: -60
INFO:ldrb.ldrb:beta:
endo_lv: 0
epi_lv: 0
endo_septum: 0
epi_septum: 0
endo_rv: 0
epi_rv: 0
and save them in the same file
cg.fibers.utils.save_microstructure(
mesh=geometry.mesh, functions=(system.f0, system.s0, system.n0), path=outdir / "geometry.bp"
)
# Let us also plot the fibers
topology_f0, cell_types_f0, geometry_f0 = dolfinx.plot.vtk_mesh(
system.f0.function_space
)
values = np.zeros((geometry_f0.shape[0], 3), dtype=np.float64)
values[:, : len(system.f0)] = system.f0.x.array.real.reshape(
(geometry_f0.shape[0], len(system.f0))
)
function_grid = pyvista.UnstructuredGrid(topology_f0, cell_types_f0, geometry_f0)
function_grid["u"] = values
glyphs = function_grid.glyph(orient="u", factor=1.0)
grid = pyvista.UnstructuredGrid(*vtk_mesh)
plotter = pyvista.Plotter()
plotter.add_mesh(grid, style="wireframe", color="r")
plotter.add_mesh(glyphs)
plotter.view_xy()
if not pyvista.OFF_SCREEN:
plotter.show()
else:
fig_as_array = plotter.screenshot("fiber_ukb.png")
You should now be able to load the geometry and the fibers using the following code
geo = cg.geometry.Geometry.from_folder(comm=comm, folder=outdir)
INFO:cardiac_geometries.geometry:Reading geometry from ukb_mesh
We have also implemented a wrapper around the ukb-atlas package that allows you to generate the
mesh and the fibers directly
outdir = Path("ukb_mesh2")
geo = cg.mesh.ukb(
outdir=outdir,
comm=comm,
char_length_max=2.0,
char_length_min=2.0,
fiber_angle_endo=60,
fiber_angle_epi=-60,
)
INFO:ukb.atlas:Generating points from /github/home/.ukb/UKBRVLV.h5
INFO:ukb.atlas:Using mode -1 and std 1.5
INFO:ukb.surface:Saved ukb_mesh2/LV_ENDO_ED.stl
INFO:ukb.surface:Saved ukb_mesh2/RV_SEPTUM_ED.stl
INFO:ukb.surface:Saved ukb_mesh2/RV_FREEWALL_ED.stl
INFO:ukb.surface:Saved ukb_mesh2/LV_EPI_ED.stl
INFO:ukb.surface:Saved ukb_mesh2/RV_EPI_ED.stl
INFO:ukb.surface:Saved ukb_mesh2/MV_ED.stl
INFO:ukb.surface:Saved ukb_mesh2/AV_ED.stl
INFO:ukb.surface:Saved ukb_mesh2/TV_ED.stl
INFO:ukb.surface:Saved ukb_mesh2/PV_ED.stl
INFO:ukb.surface:Saved ukb_mesh2/landmarks_ED.json
INFO:ukb.mesh:Creating mesh for ED with char_length_max=2.0, char_length_min=2.0, order=1
0
INFO:ukb.mesh:Created mesh ukb_mesh2/ED.msh
2026-11-12 21:38:28 [debug ] Convert file ukb_mesh2/ED.msh to dolfin
Info : Reading 'ukb_mesh2/ED.msh'...
Info : 30 entities
Info : 24728 nodes
Info : 128524 elements
Info : [ 0%] Reading elements
Info : [ 10%] Reading elements
Info : [ 20%] Reading elements
Info : [ 30%] Reading elements
Info : [ 40%] Reading elements
Info : [ 50%] Reading elements
Info : [ 60%] Reading elements
Info : [ 70%] Reading elements
Info : [ 80%] Reading elements
Info : [ 90%] Reading elements
Info : 9 parametrizations
Info : [ 0%] Processing parametrizations
Info : [ 10%] Processing parametrizations
Info : [ 20%] Processing parametrizations
Info : [ 30%] Processing parametrizations
Info : [ 40%] Processing parametrizations
Info : [ 50%] Processing parametrizations
Info : [ 60%] Processing parametrizations
Info : [ 70%] Processing parametrizations
Info : [ 80%] Processing parametrizations
Info : Done reading 'ukb_mesh2/ED.msh'
INFO:ldrb.doste:LV apex: [ 40.99441976 -19.62809528 -27.59830241]
INFO:ldrb.doste:RV apex: [ 28.02331751 -20.73926222 -34.63251664]
INFO:ldrb.doste:Calculating scalar fields
INFO:ldrb.doste:Calculating gradients
2026-11-12 21:38:41 [debug ] Write f0: f0
2026-11-12 21:38:41 [debug ] Write s0: s0
2026-11-12 21:38:41 [debug ] Write n0: n0
2026-11-12 21:38:41 [debug ] Write markers_scalar: markers_scalar
2026-11-12 21:38:41 [debug ] Write phi: phi
2026-11-12 21:38:41 [debug ] Write psi_lv_basal: psi_lv_basal
2026-11-12 21:38:41 [debug ] Write psi_lv_ot: psi_lv_ot
2026-11-12 21:38:41 [debug ] Write w_lv: w_lv
2026-11-12 21:38:41 [debug ] Write lv_rv: lv_rv
2026-11-12 21:38:41 [debug ] Write psi_rv_basal: psi_rv_basal
2026-11-12 21:38:41 [debug ] Write psi_rv_ot: psi_rv_ot
2026-11-12 21:38:41 [debug ] Write w_rv: w_rv
Since the UKB geometries include the outflow tracts, the fibers are by default generated with the
rule-based method of Doste et al. (2019) (see
ldrb.dolfinx_doste), which defines the longitudinal direction in each ventricle from the apex
to both the inflow and the outflow valve, and uses separate fiber angles in the RV and the
outflow tracts. Here fiber_angle_endo and fiber_angle_epi are the angles in the LV. To use
the method above instead, pass fiber_method="ldrb", which is also the default for clipped
meshes (clipped=True), as these have no outflow tracts
print(geo.info["fiber_method"])
doste
The wrapper can also create an LV-only geometry (geometry="lv"), and geometries where the
valves are closed by caps (geometry="biv-closed" and geometry="lv-closed"). In the closed
geometries the cavities are meshed as well, and are marked as LV_CAVITY and RV_CAVITY in the
cell tags, while the fibers are only created in the wall (Wall)
geo_closed = cg.mesh.ukb(
outdir=Path("ukb_mesh_lv_closed"),
comm=comm,
geometry="lv-closed",
char_length_max=5.0,
char_length_min=5.0,
)
print(geo_closed.markers)
INFO:ukb.atlas:Generating points from /github/home/.ukb/UKBRVLV.h5
INFO:ukb.atlas:Using mode -1 and std 1.5
INFO:ukb.surface:Saved ukb_mesh_lv_closed/LV_ENDO_ED.stl
INFO:ukb.surface:Saved ukb_mesh_lv_closed/LV_EPI_ED.stl
INFO:ukb.surface:Saved ukb_mesh_lv_closed/RV_SEPTUM_ED.stl
INFO:ukb.surface:Saved ukb_mesh_lv_closed/SEPTAL_BRIDGE_ED.stl
INFO:ukb.surface:Saved ukb_mesh_lv_closed/MV_ED.stl
INFO:ukb.surface:Saved ukb_mesh_lv_closed/AV_ED.stl
INFO:ukb.surface:Saved ukb_mesh_lv_closed/MV_CAP_ED.stl
INFO:ukb.surface:Saved ukb_mesh_lv_closed/AV_CAP_ED.stl
INFO:ukb.mesh:Creating mesh for ED with char_length_max=5.0, char_length_min=5.0, order=1
0
INFO:ukb.mesh:Created mesh ukb_mesh_lv_closed/ED.msh
2026-11-12 21:38:42 [debug ] Convert file ukb_mesh_lv_closed/ED.msh to dolfin
INFO:ldrb.doste:LV apex: [ 42.37254686 -16.72109744 -29.65610707]
INFO:ldrb.doste:Calculating scalar fields
INFO:ldrb.doste:Calculating gradients
Info : Reading 'ukb_mesh_lv_closed/ED.msh'...
Info : 22 entities
Info : 2479 nodes
Info : 15052 elements
Info : 8 parametrizations
Info : [ 0%] Processing parametrizations
Info : [ 10%] Processing parametrizations
Info : [ 20%] Processing parametrizations
Info : [ 30%] Processing parametrizations
Info : [ 40%] Processing parametrizations
Info : [ 60%] Processing parametrizations
Info : [ 70%] Processing parametrizations
Info : [ 80%] Processing parametrizations
Info : Done reading 'ukb_mesh_lv_closed/ED.msh'
2026-11-12 21:38:43 [debug ] Write f0: f0
2026-11-12 21:38:43 [debug ] Write s0: s0
2026-11-12 21:38:43 [debug ] Write n0: n0
2026-11-12 21:38:43 [debug ] Write markers_scalar: markers_scalar
2026-11-12 21:38:43 [debug ] Write phi: phi
2026-11-12 21:38:43 [debug ] Write psi_lv_basal: psi_lv_basal
2026-11-12 21:38:43 [debug ] Write psi_lv_ot: psi_lv_ot
2026-11-12 21:38:43 [debug ] Write w_lv: w_lv
{'LV_ENDO': (1, 2), 'EPI': (2, 2), 'RV_SEPTUM': (3, 2), 'SEPTAL_BRIDGE': (4, 2), 'MV': (5, 2), 'AV': (6, 2), 'MV_CAP': (7, 2), 'AV_CAP': (8, 2), 'Wall': (9, 3), 'LV_CAVITY': (10, 3)}
AHA segments for the UKB BiV#
With aha=True the cells are tagged with the 17 AHA segments of the LV (Cerqueira et al. 2002,
https://doi.org/10.1161/hc0402.102975), the RV free wall (RV) and the part of the LV above the
mitral plane (LVOT). The segments use anatomical landmarks that ukb-atlas writes next to the
surfaces (landmarks_ED.json): the RV insertion line, where the RV septum meets the RV free wall,
which every short-axis plane cuts at the anterior and the inferior insertion. The septum between
the insertions is split into two segments and the rest of the wall into four (basal and mid
levels), and the levels are thirds of the long axis from the apex to the mitral valve. This is
only supported for the open BiV geometry (geometry="biv", not clipped)
outdir_aha = Path("ukb_mesh_aha")
geo_aha = cg.mesh.ukb(
outdir=outdir_aha,
comm=comm,
char_length_max=3.0,
char_length_min=3.0,
create_fibers=False,
aha=True,
)
print(geo_aha.markers)
INFO:ukb.atlas:Generating points from /github/home/.ukb/UKBRVLV.h5
INFO:ukb.atlas:Using mode -1 and std 1.5
INFO:ukb.surface:Saved ukb_mesh_aha/LV_ENDO_ED.stl
INFO:ukb.surface:Saved ukb_mesh_aha/RV_SEPTUM_ED.stl
INFO:ukb.surface:Saved ukb_mesh_aha/RV_FREEWALL_ED.stl
INFO:ukb.surface:Saved ukb_mesh_aha/LV_EPI_ED.stl
INFO:ukb.surface:Saved ukb_mesh_aha/RV_EPI_ED.stl
INFO:ukb.surface:Saved ukb_mesh_aha/MV_ED.stl
INFO:ukb.surface:Saved ukb_mesh_aha/AV_ED.stl
INFO:ukb.surface:Saved ukb_mesh_aha/TV_ED.stl
INFO:ukb.surface:Saved ukb_mesh_aha/PV_ED.stl
INFO:ukb.surface:Saved ukb_mesh_aha/landmarks_ED.json
INFO:ukb.mesh:Creating mesh for ED with char_length_max=3.0, char_length_min=3.0, order=1
0
INFO:ukb.mesh:Created mesh ukb_mesh_aha/ED.msh
2026-11-12 21:38:46 [debug ] Convert file ukb_mesh_aha/ED.msh to dolfin
Info : Reading 'ukb_mesh_aha/ED.msh'...
Info : 30 entities
Info : 9873 nodes
Info : 50222 elements
Info : 9 parametrizations
Info : [ 0%] Processing parametrizations
Info : [ 10%] Processing parametrizations
Info : [ 20%] Processing parametrizations
Info : [ 30%] Processing parametrizations
Info : [ 40%] Processing parametrizations
Info : [ 50%] Processing parametrizations
Info : [ 60%] Processing parametrizations
Info : [ 70%] Processing parametrizations
Info : [ 80%] Processing parametrizations
Info : Done reading 'ukb_mesh_aha/ED.msh'
{'LV_ENDO': (1, 2), 'RV_ENDO': (2, 2), 'EPI': (3, 2), 'MV': (4, 2), 'AV': (5, 2), 'TV': (6, 2), 'PV': (7, 2), 'BASAL-ANTERIOR': (1, 3), 'BASAL-ANTEROSEPTAL': (2, 3), 'BASAL-SEPTAL': (3, 3), 'BASAL-INFERIOR': (4, 3), 'BASAL-POSTERIOR': (5, 3), 'BASAL-LATERAL': (6, 3), 'MID-ANTERIOR': (7, 3), 'MID-ANTEROSEPTAL': (8, 3), 'MID-SEPTAL': (9, 3), 'MID-INFERIOR': (10, 3), 'MID-POSTERIOR': (11, 3), 'MID-LATERAL': (12, 3), 'APICAL-ANTERIOR': (13, 3), 'APICAL-SEPTAL': (14, 3), 'APICAL-INFERIOR': (15, 3), 'APICAL-LATERAL': (16, 3), 'APEX': (17, 3), 'RV': (18, 3), 'LVOT': (19, 3)}
Let us plot the LV segments (hiding the RV free wall, tag 18) from the RV side and from the opposite side, together with the RV insertion line (in black)
landmarks = json.loads((outdir_aha / "landmarks_ED.json").read_text())
insertion = np.array(landmarks["insertion"])
insertion_line = pyvista.lines_from_points(np.vstack([insertion, insertion[:1]]))
vtk_mesh_aha = dolfinx.plot.vtk_mesh(geo_aha.mesh, geo_aha.mesh.topology.dim)
grid_aha = pyvista.UnstructuredGrid(*vtk_mesh_aha)
grid_aha.cell_data["AHA"] = geo_aha.cfun.values
lv_aha = grid_aha.threshold([0.5, 17.5], scalars="AHA")
# Number each segment at the cell centre nearest to the mean of its cell centres
centres = lv_aha.cell_centers().points
tags = np.asarray(lv_aha.cell_data["AHA"])
segment_numbers = [str(i) for i in range(1, 18) if np.any(tags == i)]
segment_centres = []
for i in map(int, segment_numbers):
c = centres[tags == i]
segment_centres.append(c[np.argmin(np.linalg.norm(c - c.mean(axis=0), axis=1))])
segment_centres = np.array(segment_centres)
rv_centre = np.mean(landmarks["rv_freewall"], axis=0)
lv_centre = np.array(lv_aha.center)
plotter = pyvista.Plotter(shape=(1, 2), window_size=[1600, 800])
for col, side in enumerate((1.0, -1.0)):
plotter.subplot(0, col)
plotter.add_mesh(
lv_aha, scalars="AHA", cmap="tab20", n_colors=17, clim=[0.5, 17.5], show_scalar_bar=False
)
plotter.add_mesh(insertion_line, color="black", line_width=6)
# Label the segments on the side facing the camera
facing = (segment_centres - lv_centre) @ (side * (rv_centre - lv_centre)) > 0
plotter.add_point_labels(
segment_centres[facing],
[n for n, f in zip(segment_numbers, facing) if f],
font_size=20,
shape_opacity=0.6,
always_visible=True,
)
plotter.camera_position = [
lv_centre + side * (rv_centre - lv_centre),
lv_centre,
np.array(landmarks["valves"]["MV"]) - lv_centre,
]
plotter.reset_camera()
if not pyvista.OFF_SCREEN:
plotter.show()
else:
figure = plotter.screenshot("aha_segments_ukb.png")
The same segments are also available from Python for any UKB BiV mesh, given the landmarks, with
cardiac_geometries.aha.biv_aha, and from the command line with geo ukb --aha
RV regions (Zhong et al. 2012)#
With rv_regions=True (together with aha=True) the RV is also divided, into the 15 regions of
Zhong et al. (2012, https://doi.org/10.1152/ajpheart.00679.2011): five sectors in the basal and
mid levels (anterior, lateral and inferior free wall, inferoseptal and anteroseptal), three in the
apical level, and the RV outlet and inlet above the basal level (tags 18-32; the LVOT is 33). The
septum is split between the LV and the RV by a transventricular Laplace solution (0 on the RV
endocardium, 1 on the LV endocardium): a septal cell belongs to the RV where it is below
rv_septum_fraction, by default 0.5, the middle of the septum. The levels lie in short-axis
planes of the LV long axis, from the lowest point of the RV to the centre of the tricuspid valve,
and above it a cell belongs to the outlet or the inlet by its nearest valve
outdir_rv = Path("ukb_mesh_rv_regions")
geo_rv = cg.mesh.ukb(
outdir=outdir_rv,
comm=comm,
char_length_max=3.0,
char_length_min=3.0,
create_fibers=False,
aha=True,
rv_regions=True,
)
print({k: v for k, v in geo_rv.markers.items() if v[1] == 3})
INFO:ukb.atlas:Generating points from /github/home/.ukb/UKBRVLV.h5
INFO:ukb.atlas:Using mode -1 and std 1.5
INFO:ukb.surface:Saved ukb_mesh_rv_regions/LV_ENDO_ED.stl
INFO:ukb.surface:Saved ukb_mesh_rv_regions/RV_SEPTUM_ED.stl
INFO:ukb.surface:Saved ukb_mesh_rv_regions/RV_FREEWALL_ED.stl
INFO:ukb.surface:Saved ukb_mesh_rv_regions/LV_EPI_ED.stl
INFO:ukb.surface:Saved ukb_mesh_rv_regions/RV_EPI_ED.stl
INFO:ukb.surface:Saved ukb_mesh_rv_regions/MV_ED.stl
INFO:ukb.surface:Saved ukb_mesh_rv_regions/AV_ED.stl
INFO:ukb.surface:Saved ukb_mesh_rv_regions/TV_ED.stl
INFO:ukb.surface:Saved ukb_mesh_rv_regions/PV_ED.stl
INFO:ukb.surface:Saved ukb_mesh_rv_regions/landmarks_ED.json
INFO:ukb.mesh:Creating mesh for ED with char_length_max=3.0, char_length_min=3.0, order=1
0
INFO:ukb.mesh:Created mesh ukb_mesh_rv_regions/ED.msh
2026-11-12 21:38:51 [debug ] Convert file ukb_mesh_rv_regions/ED.msh to dolfin
Info : Reading 'ukb_mesh_rv_regions/ED.msh'...
Info : 30 entities
Info : 9859 nodes
Info : 50146 elements
Info : 9 parametrizations
Info : [ 0%] Processing parametrizations
Info : [ 10%] Processing parametrizations
Info : [ 20%] Processing parametrizations
Info : [ 30%] Processing parametrizations
Info : [ 40%] Processing parametrizations
Info : [ 50%] Processing parametrizations
Info : [ 60%] Processing parametrizations
Info : [ 70%] Processing parametrizations
Info : [ 80%] Processing parametrizations
Info : Done reading 'ukb_mesh_rv_regions/ED.msh'
{'BASAL-ANTERIOR': (1, 3), 'BASAL-ANTEROSEPTAL': (2, 3), 'BASAL-SEPTAL': (3, 3), 'BASAL-INFERIOR': (4, 3), 'BASAL-POSTERIOR': (5, 3), 'BASAL-LATERAL': (6, 3), 'MID-ANTERIOR': (7, 3), 'MID-ANTEROSEPTAL': (8, 3), 'MID-SEPTAL': (9, 3), 'MID-INFERIOR': (10, 3), 'MID-POSTERIOR': (11, 3), 'MID-LATERAL': (12, 3), 'APICAL-ANTERIOR': (13, 3), 'APICAL-SEPTAL': (14, 3), 'APICAL-INFERIOR': (15, 3), 'APICAL-LATERAL': (16, 3), 'APEX': (17, 3), 'RV-BASAL-ANTERIOR': (18, 3), 'RV-BASAL-LATERAL': (19, 3), 'RV-BASAL-INFERIOR': (20, 3), 'RV-BASAL-INFEROSEPTAL': (21, 3), 'RV-BASAL-ANTEROSEPTAL': (22, 3), 'RV-MID-ANTERIOR': (23, 3), 'RV-MID-LATERAL': (24, 3), 'RV-MID-INFERIOR': (25, 3), 'RV-MID-INFEROSEPTAL': (26, 3), 'RV-MID-ANTEROSEPTAL': (27, 3), 'RV-APICAL-ANTERIOR': (28, 3), 'RV-APICAL-INFERIOR': (29, 3), 'RV-APICAL-SEPTAL': (30, 3), 'RV-OUTLET': (31, 3), 'RV-INLET': (32, 3), 'LVOT': (33, 3)}
Let us plot the RV regions (hiding the LV) from the RV free wall and from the LV side, where the RV half of the septum (regions 21, 22, 26, 27 and 30) shows through the cut-away LV
landmarks_rv = json.loads((outdir_rv / "landmarks_ED.json").read_text())
vtk_mesh_rv = dolfinx.plot.vtk_mesh(geo_rv.mesh, geo_rv.mesh.topology.dim)
grid_rv = pyvista.UnstructuredGrid(*vtk_mesh_rv)
grid_rv.cell_data["Region"] = geo_rv.cfun.values
rv_grid = grid_rv.threshold([17.5, 32.5], scalars="Region")
rv_centres = rv_grid.cell_centers().points
rv_tags = np.asarray(rv_grid.cell_data["Region"])
rv_numbers = [str(i) for i in range(18, 33) if np.any(rv_tags == i)]
rv_label_points = []
for i in map(int, rv_numbers):
c = rv_centres[rv_tags == i]
rv_label_points.append(c[np.argmin(np.linalg.norm(c - c.mean(axis=0), axis=1))])
rv_label_points = np.array(rv_label_points)
rv_mid = np.mean(landmarks_rv["rv_freewall"], axis=0)
lv_mid = np.array(grid_rv.threshold([0.5, 17.5], scalars="Region").center)
plotter = pyvista.Plotter(shape=(1, 2), window_size=[1600, 800])
for col, side in enumerate((1.0, -1.0)):
plotter.subplot(0, col)
plotter.add_mesh(
rv_grid, scalars="Region", cmap="tab20b", n_colors=15, clim=[17.5, 32.5],
show_scalar_bar=False,
)
# Label the free-wall regions in the view from the RV, the septal ones in the view from the LV
septal = np.isin(np.array(rv_numbers, dtype=int), [21, 22, 26, 27, 30])
facing = septal if side < 0 else ~septal
plotter.add_point_labels(
rv_label_points[facing],
[n for n, f in zip(rv_numbers, facing) if f],
font_size=20,
shape_opacity=0.6,
always_visible=True,
)
plotter.camera_position = [
lv_mid + side * 2.0 * (rv_mid - lv_mid),
lv_mid,
np.array(landmarks_rv["valves"]["MV"]) - lv_mid,
]
plotter.reset_camera()
if not pyvista.OFF_SCREEN:
plotter.show()
else:
figure = plotter.screenshot("rv_regions_ukb.png")
The same regions are available from Python with cardiac_geometries.aha.biv_regions, and from
the command line with geo ukb --aha --rv-regions [--rv-septum-fraction 0.5]