from __future__ import annotations
import datetime
import json
import math
from importlib.metadata import metadata
from pathlib import Path
from mpi4py import MPI
import cardiac_geometries_core as cgc
import dolfinx
import numpy as np
from structlog import get_logger
from . import utils
from .fibers.utils import save_microstructure
from .geometry import Geometry
meta = metadata("cardiac-geometriesx")
__version__ = meta["Version"]
logger = get_logger()
def transform_markers(
markers: dict[str, tuple[int, int]], clipped: bool = False
) -> dict[str, list[int]]:
if clipped:
return {
"lv": [markers["LV"][0]],
"rv": [markers["RV"][0]],
"epi": [markers["EPI"][0]],
"base": [markers["BASE"][0]],
}
else:
return {
"lv": [markers["LV"][0]],
"rv": [markers["RV"][0]],
"epi": [markers["EPI"][0]],
"base": [
markers["PV"][0],
markers["TV"][0],
markers["AV"][0],
markers["MV"][0],
],
}
[docs]
def ukb(
outdir: str | Path,
mode: int = -1,
std: float = 1.5,
case: str = "ED",
char_length_max: float = 5.0,
char_length_min: float = 5.0,
fiber_angle_endo: float = 60,
fiber_angle_epi: float = -60,
create_fibers: bool = True,
fiber_space: str = "P_1",
clipped: bool = False,
use_burns: bool = False,
burns_path: Path | None = None,
comm: MPI.Comm = MPI.COMM_WORLD,
) -> Geometry:
"""Create a mesh from the UK-Biobank atlas using
the ukb-atlas package.
Parameters
----------
outdir : str | Path
Directory where to save the results.
mode : int, optional
Mode for the UKB mesh, by default -1
std : float, optional
Standard deviation for the UKB mesh, by default 1.5
case : str, optional
Case for the UKB mesh (either "ED" or "ES"), by default "ED"
char_length_max : float, optional
Maximum characteristic length of the mesh, by default 2.0
char_length_min : float, optional
Minimum characteristic length of the mesh, by default 2.0
fiber_angle_endo : float, optional
Fiber angle for the endocardium, by default 60
fiber_angle_epi : float, optional
Fiber angle for the epicardium, by default -60
create_fibers : bool, optional
If True create rule-based fibers, by default True
fiber_space : str, optional
Function space for fibers of the form family_degree, by default "P_1"
clipped : bool, optional
If True create a clipped mesh, by default False
use_burns : bool
If true, use the atlas from Richard Burns to generate the surfaces.
This will override the `all` parameter and use the burns atlas instead.
burns_path : Path | None
Path to the burns atlas file. This will be a .mat file which will be loaded
using scipy.io.loadmat. This needs to be specified if `use_burns`.
comm : MPI.Comm, optional
MPI communicator, by default MPI.COMM_WORLD
Returns
-------
cardiac_geometries.geometry.Geometry
A Geometry with the mesh, markers, markers functions and fibers.
"""
try:
import ukb.cli
except ImportError as e:
msg = (
"To create the UKB mesh you need to install the ukb package "
"which you can install with pip install ukb-atlas"
)
raise ImportError(msg) from e
if comm.rank == 0:
surf_args = ["surf", str(outdir), "--mode", str(mode), "--std", str(std), "--case", case]
if use_burns:
surf_args.extend(["--use-burns", "--burns-path", str(burns_path)])
ukb.cli.main(surf_args)
mesh_args = [
"mesh",
str(outdir),
"--case",
case,
"--char_length_max",
str(char_length_max),
"--char_length_min",
str(char_length_min),
]
if clipped:
ukb.cli.main(["clip", str(outdir), "--case", case, "--smooth"])
mesh_args.append("--clipped")
print(comm.rank)
ukb.cli.main(mesh_args)
comm.barrier()
outdir = Path(outdir)
if clipped:
mesh_name = outdir / f"{case}_clipped.msh"
else:
mesh_name = outdir / f"{case}.msh"
geometry = utils.gmsh2dolfin(comm=comm, msh_file=mesh_name)
if comm.rank == 0:
(outdir / "markers.json").write_text(
json.dumps(geometry.markers, default=utils.json_serial)
)
(outdir / "info.json").write_text(
json.dumps(
{
"mode": mode,
"std": std,
"case": case,
"char_length_max": char_length_max,
"char_length_min": char_length_min,
"fiber_angle_endo": fiber_angle_endo,
"fiber_angle_epi": fiber_angle_epi,
"fiber_space": fiber_space,
"cardiac_geometry_version": __version__,
"mesh_type": "ukb",
"clipped": clipped,
"timestamp": datetime.datetime.now().isoformat(),
}
)
)
if create_fibers:
try:
import ldrb
except ImportError as ex:
msg = (
"To create fibers you need to install the ldrb package "
"which you can install with pip install fenicsx-ldrb"
)
raise ImportError(msg) from ex
markers = transform_markers(geometry.markers, clipped=clipped)
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,
)
save_microstructure(
mesh=geometry.mesh,
functions=(system.f0, system.s0, system.n0),
outdir=outdir,
)
for k, v in system._asdict().items():
if v is None:
continue
if fiber_space.startswith("Q"):
# Cannot visualize Quadrature spaces yet
continue
logger.debug(f"Write {k}: {v}")
with dolfinx.io.VTXWriter(comm, outdir / f"{k}-viz.bp", [v], engine="BP4") as vtx:
vtx.write(0.0)
geo = Geometry.from_folder(comm=comm, folder=outdir)
return geo
[docs]
def biv_ellipsoid(
outdir: str | Path,
char_length: float = 0.5,
center_lv_x: float = 0.0,
center_lv_y: float = 0.0,
center_lv_z: float = 0.0,
a_endo_lv: float = 2.5,
b_endo_lv: float = 1.0,
c_endo_lv: float = 1.0,
a_epi_lv: float = 3.0,
b_epi_lv: float = 1.5,
c_epi_lv: float = 1.5,
center_rv_x: float = 0.0,
center_rv_y: float = 0.5,
center_rv_z: float = 0.0,
a_endo_rv: float = 3.0,
b_endo_rv: float = 1.5,
c_endo_rv: float = 1.5,
a_epi_rv: float = 4.0,
b_epi_rv: float = 2.5,
c_epi_rv: float = 2.0,
create_fibers: bool = False,
fiber_angle_endo: float = 60,
fiber_angle_epi: float = -60,
fiber_space: str = "P_1",
verbose: bool = False,
comm: MPI.Comm = MPI.COMM_WORLD,
) -> Geometry:
"""Create BiV ellipsoidal geometry
Parameters
----------
outdir : str | Path
Directory where to save the results.
char_length : float, optional
Characteristic length of mesh, by default 0.5
center_lv_y : float, optional
X-coordinate for the center of the lv, by default 0.0
center_lv_y : float, optional
Y-coordinate for the center of the lv, by default 0.0
center_lv_z : float, optional
Z-coordinate for the center of the lv, by default 0.0
a_endo_lv : float, optional
Dilation of lv endo ellipsoid in the x-direction, by default 2.5
b_endo_lv : float, optional
Dilation of lv endo ellipsoid in the y-direction, by default 1.0
c_endo_lv : float, optional
Dilation of lv endo ellipsoid in the z-direction, by default 1.0
a_epi_lv : float, optional
Dilation of lv epi ellipsoid in the x-direction, by default 3.0
b_epi_lv : float, optional
Dilation of lv epi ellipsoid in the y-direction, by default 1.5
c_epi_lv : float, optional
Dilation of lv epi ellipsoid in the z-direction, by default 1.5
center_rv_x : float, optional
X-coordinate for the center of the rv, by default 0.0
center_rv_y : float, optional
Y-coordinate for the center of the rv, by default 0.5
center_rv_z : float, optional
Z-coordinate for the center of the rv, by default 0.0
a_endo_rv : float, optional
Dilation of rv endo ellipsoid in the x-direction, by default 3.0
b_endo_rv : float, optional
Dilation of rv endo ellipsoid in the y-direction, by default 1.5
c_endo_rv : float, optional
Dilation of rv endo ellipsoid in the z-direction, by default 1.5
a_epi_rv : float, optional
Dilation of rv epi ellipsoid in the x-direction, by default 4.0
b_epi_rv : float, optional
Dilation of rv epi ellipsoid in the y-direction, by default 2.5
c_epi_rv : float, optional
Dilation of rv epi ellipsoid in the z-direction, by default 2.0
create_fibers : bool, optional
If True create analytic fibers, by default False
fiber_angle_endo : float, optional
Angle for the endocardium, by default 60
fiber_angle_epi : float, optional
Angle for the epicardium, by default -60
fiber_space : str, optional
Function space for fibers of the form family_degree, by default "P_1"
verbose : bool, optional
If True print information from gmsh, by default False
comm : MPI.Comm, optional
MPI communicator, by default MPI.COMM_WORLD
Returns
-------
cardiac_geometries.geometry.Geometry
A Geometry with the mesh, markers, markers functions and fibers.
"""
outdir = Path(outdir)
mesh_name = outdir / "biv_ellipsoid.msh"
if comm.rank == 0:
outdir.mkdir(exist_ok=True, parents=True)
with open(outdir / "info.json", "w") as f:
json.dump(
{
"char_length": char_length,
"center_lv_x": center_lv_x,
"center_lv_y": center_lv_y,
"center_lv_z": center_lv_z,
"a_endo_lv": a_endo_lv,
"b_endo_lv": b_endo_lv,
"c_endo_lv": c_endo_lv,
"a_epi_lv": a_epi_lv,
"b_epi_lv": b_epi_lv,
"c_epi_lv": c_epi_lv,
"center_rv_x": center_rv_x,
"center_rv_y": center_rv_y,
"center_rv_z": center_rv_z,
"a_endo_rv": a_endo_rv,
"b_endo_rv": b_endo_rv,
"c_endo_rv": c_endo_rv,
"a_epi_rv": a_epi_rv,
"b_epi_rv": b_epi_rv,
"c_epi_rv": c_epi_rv,
"create_fibers": create_fibers,
"fiber_angle_endo": fiber_angle_endo,
"fiber_angle_epi": fiber_angle_epi,
"fiber_space": fiber_space,
"mesh_type": "biv_ellipsoid",
"cardiac_geometry_version": __version__,
"timestamp": datetime.datetime.now().isoformat(),
},
f,
indent=2,
default=utils.json_serial,
)
cgc.biv_ellipsoid(
mesh_name=mesh_name.as_posix(),
char_length=char_length,
center_lv_x=center_lv_x,
center_lv_y=center_lv_y,
center_lv_z=center_lv_z,
a_endo_lv=a_endo_lv,
b_endo_lv=b_endo_lv,
c_endo_lv=c_endo_lv,
a_epi_lv=a_epi_lv,
b_epi_lv=b_epi_lv,
c_epi_lv=c_epi_lv,
center_rv_x=center_rv_x,
center_rv_y=center_rv_y,
center_rv_z=center_rv_z,
a_endo_rv=a_endo_rv,
b_endo_rv=b_endo_rv,
c_endo_rv=c_endo_rv,
a_epi_rv=a_epi_rv,
b_epi_rv=b_epi_rv,
c_epi_rv=c_epi_rv,
verbose=verbose,
)
comm.barrier()
geometry = utils.gmsh2dolfin(comm=comm, msh_file=mesh_name)
if comm.rank == 0:
with open(outdir / "markers.json", "w") as f:
json.dump(geometry.markers, f, default=utils.json_serial)
comm.barrier()
if create_fibers:
try:
import ldrb
except ImportError:
msg = (
"To create fibers you need to install the ldrb package "
"which you can install with pip install fenicsx-ldrb"
)
raise ImportError(msg)
system = ldrb.dolfinx_ldrb(
mesh=geometry.mesh,
ffun=geometry.ffun,
markers=geometry.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,
)
save_microstructure(
mesh=geometry.mesh,
functions=(system.f0, system.s0, system.n0),
outdir=outdir,
)
geo = Geometry.from_folder(comm=comm, folder=outdir)
return geo
[docs]
def biv_ellipsoid_torso(
outdir: str | Path,
char_length: float = 0.5,
heart_as_surface: bool = False,
torso_length: float = 20.0,
torso_width: float = 20.0,
torso_height: float = 20.0,
rotation_angle: float = math.pi / 6,
center_lv_x: float = 0.0,
center_lv_y: float = 0.0,
center_lv_z: float = 0.0,
a_endo_lv: float = 2.5,
b_endo_lv: float = 1.0,
c_endo_lv: float = 1.0,
a_epi_lv: float = 3.0,
b_epi_lv: float = 1.5,
c_epi_lv: float = 1.5,
center_rv_x: float = 0.0,
center_rv_y: float = 0.5,
center_rv_z: float = 0.0,
a_endo_rv: float = 3.0,
b_endo_rv: float = 1.5,
c_endo_rv: float = 1.5,
a_epi_rv: float = 4.0,
b_epi_rv: float = 2.5,
c_epi_rv: float = 2.0,
create_fibers: bool = False,
fiber_angle_endo: float = 60,
fiber_angle_epi: float = -60,
fiber_space: str = "P_1",
verbose: bool = False,
comm: MPI.Comm = MPI.COMM_WORLD,
) -> Geometry:
"""Create BiV ellipsoidal geometry
Parameters
----------
outdir : str | Path
Directory where to save the results.
char_length : float, optional
Characteristic length of mesh, by default 0.5
heart_as_surface: bool
If true, create the heart as a a surface inside the torso,
otherwise let the heart be a volume, by default True.
torso_length : float, optional
Length of torso in the x-direction, by default 20.0
torso_width : float, optional
Length of torso in the y-direction, by default 20.0
torso_height : float, optional
Length of torso in the z-direction, by default 20.0
rotation_angle: float, optional
Angle to rotate the torso in order to object realistic position of
the heart in a torso, by default pi / 6
center_lv_x : float, optional
X-coordinate for the center of the lv, by default 0.0
center_lv_y : float, optional
Y-coordinate for the center of the lv, by default 0.0
center_lv_z : float, optional
Z-coordinate for the center of the lv, by default 0.0
a_endo_lv : float, optional
Dilation of lv endo ellipsoid in the x-direction, by default 2.5
b_endo_lv : float, optional
Dilation of lv endo ellipsoid in the y-direction, by default 1.0
c_endo_lv : float, optional
Dilation of lv endo ellipsoid in the z-direction, by default 1.0
a_epi_lv : float, optional
Dilation of lv epi ellipsoid in the x-direction, by default 3.0
b_epi_lv : float, optional
Dilation of lv epi ellipsoid in the y-direction, by default 1.5
c_epi_lv : float, optional
Dilation of lv epi ellipsoid in the z-direction, by default 1.5
center_rv_x : float, optional
X-coordinate for the center of the rv, by default 0.0
center_rv_y : float, optional
Y-coordinate for the center of the rv, by default 0.5
center_rv_z : float, optional
Z-coordinate for the center of the rv, by default 0.0
a_endo_rv : float, optional
Dilation of rv endo ellipsoid in the x-direction, by default 3.0
b_endo_rv : float, optional
Dilation of rv endo ellipsoid in the y-direction, by default 1.5
c_endo_rv : float, optional
Dilation of rv endo ellipsoid in the z-direction, by default 1.5
a_epi_rv : float, optional
Dilation of rv epi ellipsoid in the x-direction, by default 4.0
b_epi_rv : float, optional
Dilation of rv epi ellipsoid in the y-direction, by default 2.5
c_epi_rv : float, optional
Dilation of rv epi ellipsoid in the z-direction, by default 2.0
create_fibers : bool, optional
If True create analytic fibers, by default False
fiber_angle_endo : float, optional
Angle for the endocardium, by default 60
fiber_angle_epi : float, optional
Angle for the epicardium, by default -60
fiber_space : str, optional
Function space for fibers of the form family_degree, by default "P_1"
verbose : bool, optional
If True print information from gmsh, by default False
comm : MPI.Comm, optional
MPI communicator, by default MPI.COMM_WORLD
Returns
-------
cardiac_geometries.geometry.Geometry
A Geometry with the mesh, markers, markers functions and fibers.
"""
outdir = Path(outdir)
mesh_name = outdir / "biv_ellipsoid_torso.msh"
if comm.rank == 0:
outdir.mkdir(exist_ok=True, parents=True)
with open(outdir / "info.json", "w") as f:
json.dump(
{
"char_length": char_length,
"heart_as_surface": heart_as_surface,
"torso_length": torso_length,
"torso_width": torso_width,
"torso_height": torso_height,
"rotation_angle": rotation_angle,
"center_lv_x": center_lv_x,
"center_lv_y": center_lv_y,
"center_lv_z": center_lv_z,
"a_endo_lv": a_endo_lv,
"b_endo_lv": b_endo_lv,
"c_endo_lv": c_endo_lv,
"a_epi_lv": a_epi_lv,
"b_epi_lv": b_epi_lv,
"c_epi_lv": c_epi_lv,
"center_rv_x": center_rv_x,
"center_rv_y": center_rv_y,
"center_rv_z": center_rv_z,
"a_endo_rv": a_endo_rv,
"b_endo_rv": b_endo_rv,
"c_endo_rv": c_endo_rv,
"a_epi_rv": a_epi_rv,
"b_epi_rv": b_epi_rv,
"c_epi_rv": c_epi_rv,
"create_fibers": create_fibers,
"fiber_angle_endo": fiber_angle_endo,
"fiber_angle_epi": fiber_angle_epi,
"fiber_space": fiber_space,
"mesh_type": "biv",
"cardiac_geometry_version": __version__,
"timestamp": datetime.datetime.now().isoformat(),
},
f,
indent=2,
default=utils.json_serial,
)
cgc.biv_ellipsoid_torso(
mesh_name=mesh_name.as_posix(),
char_length=char_length,
heart_as_surface=heart_as_surface,
torso_length=torso_length,
torso_height=torso_height,
torso_width=torso_width,
rotation_angle=rotation_angle,
center_lv_x=center_lv_x,
center_lv_y=center_lv_y,
center_lv_z=center_lv_z,
a_endo_lv=a_endo_lv,
b_endo_lv=b_endo_lv,
c_endo_lv=c_endo_lv,
a_epi_lv=a_epi_lv,
b_epi_lv=b_epi_lv,
c_epi_lv=c_epi_lv,
center_rv_x=center_rv_x,
center_rv_y=center_rv_y,
center_rv_z=center_rv_z,
a_endo_rv=a_endo_rv,
b_endo_rv=b_endo_rv,
c_endo_rv=c_endo_rv,
a_epi_rv=a_epi_rv,
b_epi_rv=b_epi_rv,
c_epi_rv=c_epi_rv,
verbose=verbose,
)
comm.barrier()
geometry = utils.gmsh2dolfin(comm=comm, msh_file=mesh_name)
if comm.rank == 0:
with open(outdir / "markers.json", "w") as f:
json.dump(geometry.markers, f, default=utils.json_serial)
comm.barrier()
if create_fibers:
if heart_as_surface:
logger.warning("Can only create fibers when heart is a volume.")
else:
raise NotImplementedError("Fibers not implemented yet for biv ellipsoid.")
# from .fibers._biv_ellipsoid import create_biv_in_torso_fibers
# create_biv_in_torso_fibers(
# mesh=geometry.mesh,
# ffun=geometry.marker_functions.ffun,
# cfun=geometry.marker_functions.cfun,
# markers=geometry.markers,
# fiber_space=fiber_space,
# alpha_endo=fiber_angle_endo,
# alpha_epi=fiber_angle_epi,
# outdir=outdir,
# )
geo = Geometry.from_folder(comm=comm, folder=outdir)
return geo
[docs]
def lv_ellipsoid(
outdir: Path | str,
r_short_endo: float = 7.0,
r_short_epi: float = 10.0,
r_long_endo: float = 17.0,
r_long_epi: float = 20.0,
psize_ref: float = 3,
mu_apex_endo: float = -math.pi,
mu_base_endo: float = -math.acos(5 / 17),
mu_apex_epi: float = -math.pi,
mu_base_epi: float = -math.acos(5 / 20),
create_fibers: bool = False,
fiber_angle_endo: float = 60,
fiber_angle_epi: float = -60,
fiber_space: str = "P_1",
aha: bool = True,
verbose: bool = False,
comm: MPI.Comm = MPI.COMM_WORLD,
) -> Geometry:
"""Create an LV ellipsoidal geometry
Parameters
----------
outdir : Optional[Path], optional
Directory where to save the results.
r_short_endo : float, optional
Shortest radius on the endocardium layer, by default 7.0
r_short_epi : float, optional
Shortest radius on the epicardium layer, by default 10.0
r_long_endo : float, optional
Longest radius on the endocardium layer, by default 17.0
r_long_epi : float, optional
Longest radius on the epicardium layer, by default 20.0
psize_ref : float, optional
The reference point size (smaller values yield as finer mesh, by default 3
mu_apex_endo : float, optional
Angle for the endocardial apex, by default -math.pi
mu_base_endo : float, optional
Angle for the endocardial base, by default -math.acos(5 / 17)
mu_apex_epi : float, optional
Angle for the epicardial apex, by default -math.pi
mu_base_epi : float, optional
Angle for the epicardial apex, by default -math.acos(5 / 20)
create_fibers : bool, optional
If True create analytic fibers, by default False
fiber_angle_endo : float, optional
Angle for the endocardium, by default 60
fiber_angle_epi : float, optional
Angle for the epicardium, by default -60
fiber_space : str, optional
Function space for fibers of the form family_degree, by default "P_1"
aha : bool, optional
If True create 17-segment AHA regions
verbose : bool, optional
If True print information from gmsh, by default False
comm : MPI.Comm, optional
MPI communicator, by default MPI.COMM_WORLD
Returns
-------
cardiac_geometries.geometry.Geometry
A Geometry with the mesh, markers, markers functions and fibers.
"""
outdir = Path(outdir)
mesh_name = outdir / "lv_ellipsoid.msh"
if comm.rank == 0:
outdir.mkdir(exist_ok=True, parents=True)
with open(outdir / "info.json", "w") as f:
json.dump(
{
"r_short_endo": r_short_endo,
"r_short_epi": r_short_epi,
"r_long_endo": r_long_endo,
"r_long_epi": r_long_epi,
"psize_ref": psize_ref,
"mu_apex_endo": mu_apex_endo,
"mu_base_endo": mu_base_endo,
"mu_apex_epi": mu_apex_epi,
"mu_base_epi": mu_base_epi,
"create_fibers": create_fibers,
"fiber_angle_endo": fiber_angle_endo,
"fiber_angle_epi": fiber_angle_epi,
"fiber_space": fiber_space,
"aha": aha,
"mesh_type": "lv_ellipsoid",
"cardiac_geometry_version": __version__,
"timestamp": datetime.datetime.now().isoformat(),
},
f,
indent=2,
default=utils.json_serial,
)
cgc.lv_ellipsoid(
mesh_name=mesh_name.as_posix(),
r_short_endo=r_short_endo,
r_short_epi=r_short_epi,
r_long_endo=r_long_endo,
r_long_epi=r_long_epi,
mu_base_endo=mu_base_endo,
mu_base_epi=mu_base_epi,
mu_apex_endo=mu_apex_endo,
mu_apex_epi=mu_apex_epi,
psize_ref=psize_ref,
verbose=verbose,
)
comm.barrier()
geometry = utils.gmsh2dolfin(comm=comm, msh_file=mesh_name)
# if aha:
# from .aha import lv_aha
# geometry = lv_aha(
# geometry=geometry,
# r_long_endo=r_long_endo,
# r_short_endo=r_short_endo,
# mu_base=mu_base_endo,
# )
# from dolfin import XDMFFile
# with XDMFFile((outdir / "cfun.xdmf").as_posix()) as xdmf:
# xdmf.write(geometry.marker_functions.cfun)
if comm.rank == 0:
with open(outdir / "markers.json", "w") as f:
json.dump(geometry.markers, f, default=utils.json_serial)
if create_fibers:
from .fibers.lv_ellipsoid import create_microstructure
create_microstructure(
mesh=geometry.mesh,
ffun=geometry.ffun,
markers=geometry.markers,
function_space=fiber_space,
r_short_endo=r_short_endo,
r_short_epi=r_short_epi,
r_long_endo=r_long_endo,
r_long_epi=r_long_epi,
alpha_endo=fiber_angle_endo,
alpha_epi=fiber_angle_epi,
outdir=outdir,
)
geo = Geometry.from_folder(comm=comm, folder=outdir)
# if aha:
# # Update schema
# from .geometry import H5Path
# cfun = geo.schema["cfun"].to_dict()
# cfun["fname"] = "cfun.xdmf:f"
# geo.schema["cfun"] = H5Path(**cfun)
return geo
def slab_dolfinx(
comm, outdir: Path, lx: float = 20.0, ly: float = 7.0, lz: float = 3.0, dx: float = 1.0
) -> utils.GMshGeometry:
mesh = dolfinx.mesh.create_box(
comm,
[[0.0, 0.0, 0.0], [lx, ly, lz]],
[int(lx / dx), int(ly / dx), int(lz / dx)],
dolfinx.mesh.CellType.tetrahedron,
ghost_mode=dolfinx.mesh.GhostMode.none,
)
mesh.name = "Mesh"
fdim = mesh.topology.dim - 1
x0_facets = dolfinx.mesh.locate_entities_boundary(mesh, fdim, lambda x: np.isclose(x[0], 0))
x1_facets = dolfinx.mesh.locate_entities_boundary(mesh, fdim, lambda x: np.isclose(x[0], lx))
y0_facets = dolfinx.mesh.locate_entities_boundary(mesh, fdim, lambda x: np.isclose(x[1], 0))
y1_facets = dolfinx.mesh.locate_entities_boundary(mesh, fdim, lambda x: np.isclose(x[1], ly))
z0_facets = dolfinx.mesh.locate_entities_boundary(mesh, fdim, lambda x: np.isclose(x[2], 0))
z1_facets = dolfinx.mesh.locate_entities_boundary(mesh, fdim, lambda x: np.isclose(x[2], lz))
# Concatenate and sort the arrays based on facet indices.
# Left facets marked with 1, right facets with two
marked_facets = np.hstack([x0_facets, x1_facets, y0_facets, y1_facets, z0_facets, z1_facets])
marked_values = np.hstack(
[
np.full_like(x0_facets, 1),
np.full_like(x1_facets, 2),
np.full_like(y0_facets, 3),
np.full_like(y1_facets, 4),
np.full_like(z0_facets, 5),
np.full_like(z1_facets, 6),
],
)
sorted_facets = np.argsort(marked_facets)
ft = dolfinx.mesh.meshtags(
mesh,
fdim,
marked_facets[sorted_facets],
marked_values[sorted_facets],
)
ft.name = "Facet tags"
markers = {
"X0": (1, 2),
"X1": (2, 2),
"Y0": (3, 2),
"Y1": (4, 2),
"Z0": (5, 2),
"Z1": (6, 2),
}
with dolfinx.io.XDMFFile(comm, outdir / "mesh.xdmf", "w") as xdmf:
xdmf.write_mesh(mesh)
xdmf.write_meshtags(ft, mesh.geometry)
return utils.GMshGeometry(
mesh=mesh,
markers=markers,
cfun=None,
ffun=ft.indices,
efun=None,
vfun=None,
)
[docs]
def slab(
outdir: Path | str,
lx: float = 20.0,
ly: float = 7.0,
lz: float = 3.0,
dx: float = 1.0,
create_fibers: bool = True,
fiber_angle_endo: float = 60,
fiber_angle_epi: float = -60,
fiber_space: str = "P_1",
verbose: bool = False,
use_dolfinx: bool = False,
comm: MPI.Comm = MPI.COMM_WORLD,
) -> Geometry:
"""Create slab geometry
Parameters
----------
outdir : Optional[Path], optional
Directory where to save the results.
lx : float, optional
Length of slab the x-direction, by default 20.0
ly : float, optional
Length of slab the x-direction, by default 7.0
lz : float, optional
Length of slab the z-direction, by default 3.0
dx : float, optional
Element size, by default 1.0
create_fibers : bool, optional
If True create analytic fibers, by default True
fiber_angle_endo : float, optional
Angle for the endocardium, by default 60
fiber_angle_epi : float, optional
Angle for the epicardium, by default -60
fiber_space : str, optional
Function space for fibers of the form family_degree, by default "P_1"
verbose : bool, optional
If True print information from gmsh, by default False
use_dolfinx : bool, optional
If True use dolfinx to create the mesh, by default False (gmsh)
comm : MPI.Comm, optional
MPI communicator, by default MPI.COMM_WORLD
Returns
-------
cardiac_geometries.geometry.Geometry
A Geometry with the mesh, markers, markers functions and fibers.
"""
outdir = Path(outdir)
mesh_name = outdir / "slab.msh"
if comm.rank == 0:
outdir.mkdir(exist_ok=True, parents=True)
with open(outdir / "info.json", "w") as f:
json.dump(
{
"Lx": lx,
"Ly": ly,
"Lz": lz,
"dx": dx,
"create_fibers": create_fibers,
"fiber_angle_endo": fiber_angle_endo,
"fiber_angle_epi": fiber_angle_epi,
"fiber_space": fiber_space,
"mesh_type": "slab",
"cardiac_geometry_version": __version__,
"timestamp": datetime.datetime.now().isoformat(),
},
f,
indent=2,
default=utils.json_serial,
)
if not use_dolfinx:
cgc.slab(
mesh_name=mesh_name.as_posix(),
lx=lx,
ly=ly,
lz=lz,
dx=dx,
verbose=verbose,
)
comm.barrier()
if use_dolfinx:
geometry = slab_dolfinx(
comm=comm,
outdir=outdir,
lx=lx,
ly=ly,
lz=lz,
dx=dx,
)
else:
geometry = utils.gmsh2dolfin(comm=comm, msh_file=mesh_name)
if comm.rank == 0:
with open(outdir / "markers.json", "w") as f:
json.dump(geometry.markers, f, default=utils.json_serial)
if create_fibers:
from .fibers.slab import create_microstructure
create_microstructure(
mesh=geometry.mesh,
ffun=geometry.ffun,
markers=geometry.markers,
function_space=fiber_space,
alpha_endo=fiber_angle_endo,
alpha_epi=fiber_angle_epi,
outdir=outdir,
)
geo = Geometry.from_folder(comm=comm, folder=outdir)
return geo
[docs]
def slab_in_bath(
outdir: Path | str,
lx: float = 1.0,
ly: float = 0.01,
lz: float = 0.5,
bx: float = 0.0,
by: float = 0.0,
bz: float = 0.1,
dx: float = 0.001,
verbose: bool = False,
comm: MPI.Comm = MPI.COMM_WORLD,
) -> Geometry:
"""Create slab geometry
Parameters
----------
outdir : Path
Directory where to save the results.
lx : float, optional
Length of slab the x-direction, by default 1.0
ly : float, optional
Length of slab the x-direction, by default 0.5
lz : float, optional
Length of slab the z-direction, by default 0.01
bx : float, optional
Thickness of bath the x-direction, by default 0.0
by : float, optional
Thickness of bath the x-direction, by default 0.0
bz : float, optional
Thickness of bath the z-direction, by default 0.1
dx : float, optional
Element size, by default 0.001
verbose : bool, optional
If True print information from gmsh, by default False
comm : MPI.Comm, optional
MPI communicator, by default MPI.COMM_WORLD
Returns
-------
cardiac_geometries.geometry.Geometry
A Geometry with the mesh, markers, markers functions and fibers.
"""
outdir = Path(outdir)
mesh_name = outdir / "slab_in_bath.msh"
if comm.rank == 0:
outdir.mkdir(exist_ok=True, parents=True)
with open(outdir / "info.json", "w") as f:
json.dump(
{
"lx": lx,
"ly": ly,
"lz": lz,
"bx": bx,
"by": by,
"bz": bz,
"dx": dx,
"mesh_type": "slab-bath",
"cardiac_geometry_version": __version__,
"timestamp": datetime.datetime.now().isoformat(),
},
f,
indent=2,
default=utils.json_serial,
)
cgc.slab_in_bath(
mesh_name=mesh_name.as_posix(),
lx=lx,
ly=ly,
lz=lz,
bx=bx,
by=by,
bz=bz,
dx=dx,
verbose=verbose,
)
geometry = utils.gmsh2dolfin(comm=comm, msh_file=mesh_name)
if comm.rank == 0:
with open(outdir / "markers.json", "w") as f:
json.dump(geometry.markers, f, default=utils.json_serial)
geo = Geometry.from_folder(comm=comm, folder=outdir)
return geo
[docs]
def cylinder(
outdir: Path | str,
r_inner: float = 10.0,
r_outer: float = 20.0,
height: float = 40.0,
char_length: float = 10.0,
create_fibers: bool = False,
fiber_angle_endo: float = 60,
fiber_angle_epi: float = -60,
fiber_space: str = "P_1",
aha: bool = True,
verbose: bool = False,
comm: MPI.Comm = MPI.COMM_WORLD,
) -> Geometry:
"""Create a cylindrical geometry
Parameters
----------
outdir : Optional[Path], optional
Directory where to save the results.
r_inner : float, optional
Radius on the endocardium layer, by default 10.0
r_outer : float, optional
Radius on the epicardium layer, by default 20.0
height : float, optional
Longest radius on the endocardium layer, by default 10.0
char_length : float, optional
Characteristic length of mesh, by default 10.0
create_fibers : bool, optional
If True create analytic fibers, by default False
fiber_angle_endo : float, optional
Angle for the endocardium, by default 60
fiber_angle_epi : float, optional
Angle for the epicardium, by default -60
fiber_space : str, optional
Function space for fibers of the form family_degree, by default "P_1"
aha : bool, optional
If True create 17-segment AHA regions
verbose : bool, optional
If True print information from gmsh, by default False
comm : MPI.Comm, optional
MPI communicator, by default MPI.COMM_WORLD
Returns
-------
cardiac_geometries.geometry.Geometry
A Geometry with the mesh, markers, markers functions and fibers.
"""
outdir = Path(outdir)
mesh_name = outdir / "cylinder.msh"
if comm.rank == 0:
outdir.mkdir(exist_ok=True, parents=True)
with open(outdir / "info.json", "w") as f:
json.dump(
{
"r_inner": r_inner,
"r_outer": r_outer,
"height": height,
"char_length": char_length,
"create_fibers": create_fibers,
"fiber_angle_endo": fiber_angle_endo,
"fiber_angle_epi": fiber_angle_epi,
"fiber_space": fiber_space,
"aha": aha,
"mesh_type": "cylinder",
"cardiac_geometry_version": __version__,
"timestamp": datetime.datetime.now().isoformat(),
},
f,
indent=2,
default=utils.json_serial,
)
cgc.cylinder(
inner_radius=r_inner,
outer_radius=r_outer,
height=height,
mesh_name=mesh_name.as_posix(),
char_length=char_length,
verbose=verbose,
)
comm.barrier()
geometry = utils.gmsh2dolfin(comm=comm, msh_file=mesh_name)
if comm.rank == 0:
with open(outdir / "markers.json", "w") as f:
json.dump(geometry.markers, f, default=utils.json_serial)
if create_fibers:
from .fibers.cylinder import create_microstructure
create_microstructure(
mesh=geometry.mesh,
function_space=fiber_space,
r_inner=r_inner,
r_outer=r_outer,
alpha_endo=fiber_angle_endo,
alpha_epi=fiber_angle_epi,
outdir=outdir,
)
geo = Geometry.from_folder(comm=comm, folder=outdir)
return geo
[docs]
def cylinder_racetrack(
outdir: Path | str,
r_inner: float = 13.0,
r_outer: float = 20.0,
height: float = 40.0,
inner_flat_face_distance: float = 10.0,
outer_flat_face_distance: float = 17.0,
char_length: float = 10.0,
create_fibers: bool = False,
fiber_angle_endo: float = 60,
fiber_angle_epi: float = -60,
fiber_space: str = "P_1",
aha: bool = True,
verbose: bool = False,
comm: MPI.Comm = MPI.COMM_WORLD,
) -> Geometry:
"""Create a racetrack-shaped thick cylindrical shell mesh using GMSH.
Both the inner and outer surfaces have two flat faces on opposite sides.
Parameters
----------
outdir : Optional[Path], optional
Directory where to save the results.
r_inner : float, optional
Radius on the endocardium layer, by default 13.0
r_outer : float, optional
Radius on the epicardium layer, by default 20.0
height : float, optional
Longest radius on the endocardium layer, by default 10.0
inner_flat_face_distance : float
The distance of the inner flat face from the center (along the x-axis).
This value must be less than inner_radius. Default is 10.0.
outer_flat_face_distance : float
The distance of the outer flat face from the center (along the x-axis).
This value must be less than outer_radius. Default is 17.0.
char_length : float, optional
Characteristic length of mesh, by default 10.0
create_fibers : bool, optional
If True create analytic fibers, by default False
fiber_angle_endo : float, optional
Angle for the endocardium, by default 60
fiber_angle_epi : float, optional
Angle for the epicardium, by default -60
fiber_space : str, optional
Function space for fibers of the form family_degree, by default "P_1"
aha : bool, optional
If True create 17-segment AHA regions
verbose : bool, optional
If True print information from gmsh, by default False
comm : MPI.Comm, optional
MPI communicator, by default MPI.COMM_WORLD
Returns
-------
cardiac_geometries.geometry.Geometry
A Geometry with the mesh, markers, markers functions and fibers.
"""
outdir = Path(outdir)
mesh_name = outdir / "cylinder_racetrack.msh"
if comm.rank == 0:
outdir.mkdir(exist_ok=True, parents=True)
with open(outdir / "info.json", "w") as f:
json.dump(
{
"r_inner": r_inner,
"r_outer": r_outer,
"height": height,
"inner_flat_face_distance": inner_flat_face_distance,
"outer_flat_face_distance": outer_flat_face_distance,
"char_length": char_length,
"create_fibers": create_fibers,
"fiber_angle_endo": fiber_angle_endo,
"fiber_angle_epi": fiber_angle_epi,
"fiber_space": fiber_space,
"aha": aha,
"mesh_type": "cylinder",
"cardiac_geometry_version": __version__,
"timestamp": datetime.datetime.now().isoformat(),
},
f,
indent=2,
default=utils.json_serial,
)
cgc.cylinder_racetrack(
inner_radius=r_inner,
outer_radius=r_outer,
inner_flat_face_distance=inner_flat_face_distance,
outer_flat_face_distance=outer_flat_face_distance,
height=height,
mesh_name=mesh_name.as_posix(),
char_length=char_length,
verbose=verbose,
)
comm.barrier()
geometry = utils.gmsh2dolfin(comm=comm, msh_file=mesh_name)
if comm.rank == 0:
with open(outdir / "markers.json", "w") as f:
json.dump(geometry.markers, f, default=utils.json_serial)
if create_fibers:
from .fibers.cylinder_flat import create_microstructure
create_microstructure(
mesh=geometry.mesh,
function_space=fiber_space,
r_inner=r_inner,
r_outer=r_outer,
inner_flat_face_distance=inner_flat_face_distance,
ffun=geometry.ffun,
markers=geometry.markers,
alpha_endo=fiber_angle_endo,
alpha_epi=fiber_angle_epi,
outdir=outdir,
)
geo = Geometry.from_folder(comm=comm, folder=outdir)
return geo
[docs]
def cylinder_D_shaped(
outdir: Path | str,
r_inner: float = 13.0,
r_outer: float = 20.0,
height: float = 40.0,
inner_flat_face_distance: float = 10.0,
outer_flat_face_distance: float = 17.0,
char_length: float = 10.0,
create_fibers: bool = False,
fiber_angle_endo: float = 60,
fiber_angle_epi: float = -60,
fiber_space: str = "P_1",
aha: bool = True,
verbose: bool = False,
comm: MPI.Comm = MPI.COMM_WORLD,
) -> Geometry:
"""Create a D-shaped thick cylindrical shell mesh using GMSH.
Both the inner and outer surfaces have two flat faces on opposite sides.
Parameters
----------
outdir : Optional[Path], optional
Directory where to save the results.
r_inner : float, optional
Radius on the endocardium layer, by default 13.0
r_outer : float, optional
Radius on the epicardium layer, by default 20.0
height : float, optional
Longest radius on the endocardium layer, by default 10.0
inner_flat_face_distance : float
The distance of the inner flat face from the center (along the x-axis).
This value must be less than inner_radius. Default is 10.0.
outer_flat_face_distance : float
The distance of the outer flat face from the center (along the x-axis).
This value must be less than outer_radius. Default is 17.0.
char_length : float, optional
Characteristic length of mesh, by default 10.0
create_fibers : bool, optional
If True create analytic fibers, by default False
fiber_angle_endo : float, optional
Angle for the endocardium, by default 60
fiber_angle_epi : float, optional
Angle for the epicardium, by default -60
fiber_space : str, optional
Function space for fibers of the form family_degree, by default "P_1"
aha : bool, optional
If True create 17-segment AHA regions
verbose : bool, optional
If True print information from gmsh, by default False
comm : MPI.Comm, optional
MPI communicator, by default MPI.COMM_WORLD
Returns
-------
cardiac_geometries.geometry.Geometry
A Geometry with the mesh, markers, markers functions and fibers.
"""
outdir = Path(outdir)
mesh_name = outdir / "cylinder_D_shaped.msh"
if comm.rank == 0:
outdir.mkdir(exist_ok=True, parents=True)
with open(outdir / "info.json", "w") as f:
json.dump(
{
"r_inner": r_inner,
"r_outer": r_outer,
"height": height,
"inner_flat_face_distance": inner_flat_face_distance,
"outer_flat_face_distance": outer_flat_face_distance,
"char_length": char_length,
"create_fibers": create_fibers,
"fiber_angle_endo": fiber_angle_endo,
"fiber_angle_epi": fiber_angle_epi,
"fiber_space": fiber_space,
"aha": aha,
"mesh_type": "cylinder",
"cardiac_geometry_version": __version__,
"timestamp": datetime.datetime.now().isoformat(),
},
f,
indent=2,
default=utils.json_serial,
)
cgc.cylinder_D_shaped(
inner_radius=r_inner,
outer_radius=r_outer,
inner_flat_face_distance=inner_flat_face_distance,
outer_flat_face_distance=outer_flat_face_distance,
height=height,
mesh_name=mesh_name.as_posix(),
char_length=char_length,
verbose=verbose,
)
comm.barrier()
geometry = utils.gmsh2dolfin(comm=comm, msh_file=mesh_name)
if comm.rank == 0:
with open(outdir / "markers.json", "w") as f:
json.dump(geometry.markers, f, default=utils.json_serial)
if create_fibers:
from .fibers.cylinder_flat import create_microstructure
create_microstructure(
mesh=geometry.mesh,
function_space=fiber_space,
r_inner=r_inner,
r_outer=r_outer,
inner_flat_face_distance=inner_flat_face_distance,
ffun=geometry.ffun,
markers=geometry.markers,
alpha_endo=fiber_angle_endo,
alpha_epi=fiber_angle_epi,
outdir=outdir,
)
geo = Geometry.from_folder(comm=comm, folder=outdir)
return geo