cardiac-geometriesx 0.0.1__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of cardiac-geometriesx might be problematic. Click here for more details.

@@ -0,0 +1,6 @@
1
+ from . import lv_ellipsoid, slab
2
+ from . import utils
3
+
4
+ from .utils import Microstructure
5
+
6
+ __all__ = ["lv_ellipsoid", "slab", "utils", "Microstructure"]
@@ -0,0 +1,150 @@
1
+ from pathlib import Path
2
+ import dolfinx
3
+ import numpy as np
4
+ import basix
5
+
6
+
7
+ from . import utils
8
+
9
+
10
+ def compute_system(
11
+ t_func: dolfinx.fem.Function,
12
+ r_short_endo=0.025,
13
+ r_short_epi=0.035,
14
+ r_long_endo=0.09,
15
+ r_long_epi=0.097,
16
+ alpha_endo: float = -60,
17
+ alpha_epi: float = 60,
18
+ **kwargs,
19
+ ) -> utils.Microstructure:
20
+ V = t_func.function_space
21
+ element = V.ufl_element()
22
+ mesh = V.mesh
23
+
24
+ dof_coordinates = V.tabulate_dof_coordinates()
25
+
26
+ alpha = lambda x: (alpha_endo + (alpha_epi - alpha_endo) * x) * (np.pi / 180)
27
+ r_long = lambda x: r_long_endo + (r_long_epi - r_long_endo) * x
28
+ r_short = lambda x: r_short_endo + (r_short_epi - r_short_endo) * x
29
+
30
+ drl_dt = r_long_epi - r_long_endo
31
+ drs_dt = r_short_epi - r_short_endo
32
+
33
+ t = t_func.x.array
34
+
35
+ rl = r_long(t)
36
+ rs = r_short(t)
37
+ al = alpha(t)
38
+
39
+ x = dof_coordinates[:, 0]
40
+ y = dof_coordinates[:, 1]
41
+ z = dof_coordinates[:, 2]
42
+
43
+ a = np.sqrt(y**2 + z**2) / rs
44
+ b = x / rl
45
+ mu = np.arctan2(a, b)
46
+ theta = np.pi - np.arctan2(z, -y)
47
+ theta[mu < 1e-7] = 0.0
48
+
49
+ e_t = np.array(
50
+ [
51
+ drl_dt * np.cos(mu),
52
+ drs_dt * np.sin(mu) * np.cos(theta),
53
+ drs_dt * np.sin(mu) * np.sin(theta),
54
+ ],
55
+ )
56
+ e_t = utils.normalize(e_t)
57
+
58
+ e_mu = np.array(
59
+ [
60
+ -rl * np.sin(mu),
61
+ rs * np.cos(mu) * np.cos(theta),
62
+ rs * np.cos(mu) * np.sin(theta),
63
+ ],
64
+ )
65
+ e_mu = utils.normalize(e_mu)
66
+
67
+ e_theta = np.array(
68
+ [
69
+ np.zeros_like(t),
70
+ -rs * np.sin(mu) * np.sin(theta),
71
+ rs * np.sin(mu) * np.cos(theta),
72
+ ],
73
+ )
74
+ e_theta = utils.normalize(e_theta)
75
+
76
+ f0 = np.sin(al) * e_mu + np.cos(al) * e_theta
77
+ f0 = utils.normalize(f0)
78
+
79
+ n0 = np.cross(e_mu, e_theta, axis=0)
80
+ n0 = utils.normalize(n0)
81
+
82
+ s0 = np.cross(f0, n0, axis=0)
83
+ s0 = utils.normalize(s0)
84
+
85
+ el = basix.ufl.element(
86
+ element.family_name,
87
+ mesh.ufl_cell().cellname(),
88
+ degree=element.degree,
89
+ discontinuous=element.discontinuous,
90
+ shape=(mesh.geometry.dim,),
91
+ )
92
+ Vv = dolfinx.fem.functionspace(mesh, el)
93
+
94
+ fiber = dolfinx.fem.Function(Vv)
95
+ norm_f = np.linalg.norm(f0, axis=0)
96
+ fiber.x.array[:] = (f0 / norm_f).T.reshape(-1)
97
+ fiber.name = "f0"
98
+
99
+ sheet = dolfinx.fem.Function(Vv)
100
+ sheet.x.array[:] = s0.T.reshape(-1)
101
+ sheet.name = "s0"
102
+
103
+ sheet_normal = dolfinx.fem.Function(Vv)
104
+ sheet_normal.x.array[:] = n0.T.reshape(-1)
105
+ sheet_normal.name = "n0"
106
+
107
+ return utils.Microstructure(f0=fiber, s0=sheet, n0=sheet_normal)
108
+
109
+
110
+ def create_microstructure(
111
+ mesh,
112
+ ffun,
113
+ markers,
114
+ function_space="P_1",
115
+ r_short_endo=0.025,
116
+ r_short_epi=0.035,
117
+ r_long_endo=0.09,
118
+ r_long_epi=0.097,
119
+ alpha_endo: float = -60,
120
+ alpha_epi: float = 60,
121
+ outdir: str | Path | None = None,
122
+ ):
123
+ endo_marker = markers["ENDO"][0]
124
+ epi_marker = markers["EPI"][0]
125
+ t = utils.laplace(
126
+ mesh,
127
+ ffun,
128
+ endo_marker=endo_marker,
129
+ epi_marker=epi_marker,
130
+ function_space=function_space,
131
+ )
132
+
133
+ if outdir is not None:
134
+ with dolfinx.io.XDMFFile(mesh.comm, Path(outdir) / "laplace.xdmf", "w") as file:
135
+ file.write_mesh(mesh)
136
+ file.write_function(t)
137
+ system = compute_system(
138
+ t,
139
+ function_space=function_space,
140
+ r_short_endo=r_short_endo,
141
+ r_short_epi=r_short_epi,
142
+ r_long_endo=r_long_endo,
143
+ r_long_epi=r_long_epi,
144
+ alpha_endo=alpha_endo,
145
+ alpha_epi=alpha_epi,
146
+ )
147
+ if outdir is not None:
148
+ utils.save_microstructure(mesh, system, outdir)
149
+
150
+ return system
@@ -0,0 +1,150 @@
1
+ from pathlib import Path
2
+
3
+ import dolfinx
4
+ import numpy as np
5
+ import basix
6
+
7
+ from . import utils
8
+
9
+
10
+ def compute_system(
11
+ t_func: dolfinx.fem.Function,
12
+ alpha_endo: float = -60,
13
+ alpha_epi: float = 60,
14
+ **kwargs,
15
+ ) -> utils.Microstructure:
16
+ """Compute ldrb system for slab, assuming linear
17
+ angle between endo and epi
18
+
19
+ Parameters
20
+ ----------
21
+ t_func : dolfin.Function
22
+ Solution to laplace equation with 0 on endo
23
+ and 1 on epi
24
+ alpha_endo : float, optional
25
+ Angle on endocardium, by default -60
26
+ alpha_epi : float, optional
27
+ Angle on epicardium, by default 60
28
+
29
+ Returns
30
+ -------
31
+ Microstructure
32
+ Tuple with fiber, sheet and sheet normal
33
+ """
34
+
35
+ V = t_func.function_space
36
+ element = V.ufl_element()
37
+ mesh = V.mesh
38
+
39
+ alpha = lambda x: (alpha_endo + (alpha_epi - alpha_endo) * x) * (np.pi / 180)
40
+
41
+ t = t_func.x.array
42
+
43
+ f0 = np.array(
44
+ [
45
+ np.cos(alpha(t)),
46
+ np.zeros_like(t),
47
+ np.sin(alpha(t)),
48
+ ],
49
+ )
50
+
51
+ s0 = np.array(
52
+ [
53
+ np.zeros_like(t),
54
+ np.ones_like(t),
55
+ np.zeros_like(t),
56
+ ],
57
+ )
58
+
59
+ n0 = np.cross(f0, s0, axis=0)
60
+ n0 = utils.normalize(n0)
61
+
62
+ el = basix.ufl.element(
63
+ element.family_name,
64
+ mesh.ufl_cell().cellname(),
65
+ degree=element.degree,
66
+ discontinuous=element.discontinuous,
67
+ shape=(mesh.geometry.dim,),
68
+ )
69
+ Vv = dolfinx.fem.functionspace(mesh, el)
70
+
71
+ fiber = dolfinx.fem.Function(Vv)
72
+ norm_f = np.linalg.norm(f0, axis=0)
73
+ fiber.x.array[:] = (f0 / norm_f).T.reshape(-1)
74
+ fiber.name = "f0"
75
+
76
+ sheet = dolfinx.fem.Function(Vv)
77
+ sheet.x.array[:] = s0.T.reshape(-1)
78
+ sheet.name = "s0"
79
+
80
+ sheet_normal = dolfinx.fem.Function(Vv)
81
+ sheet_normal.x.array[:] = n0.T.reshape(-1)
82
+ sheet_normal.name = "n0"
83
+
84
+ return utils.Microstructure(f0=fiber, s0=sheet, n0=sheet_normal)
85
+
86
+
87
+ def create_microstructure(
88
+ mesh: dolfinx.mesh.Mesh,
89
+ ffun: dolfinx.mesh.MeshTags,
90
+ markers: dict[str, tuple[int, int]],
91
+ alpha_endo: float,
92
+ alpha_epi: float,
93
+ function_space: str = "P_1",
94
+ outdir: str | Path | None = None,
95
+ ) -> utils.Microstructure:
96
+ """Generate microstructure for slab using LDRB algorithm
97
+
98
+ Parameters
99
+ ----------
100
+ mesh : dolfinx.mesh.Mesh
101
+ A slab mesh
102
+ ffun : dolfinx.mesh.MeshTags
103
+ Facet function defining the boundaries
104
+ markers: Dict[str, Tuple[int, int]]
105
+ Markers with keys Y0 and Y1 representing the endo and
106
+ epi planes respectively. The values should be a tuple
107
+ whose first value is the value of the marker (corresponding
108
+ to ffun) and the second value is the dimension
109
+ alpha_endo : float
110
+ Angle on the endocardium
111
+ alpha_epi : float
112
+ Angle on the epicardium
113
+ function_space : str
114
+ Function space to interpolate the fibers, by default P_1
115
+ outdir : Optional[Union[str, Path]], optional
116
+ Output directory to store the results, by default None.
117
+ If no output directory is specified the results will not be stored,
118
+ but only returned.
119
+
120
+ Returns
121
+ -------
122
+ Microstructure
123
+ Tuple with fiber, sheet and sheet normal
124
+ """
125
+
126
+ endo_marker = markers["Y0"][0]
127
+ epi_marker = markers["Y1"][0]
128
+
129
+ t = utils.laplace(
130
+ mesh,
131
+ ffun,
132
+ endo_marker=endo_marker,
133
+ epi_marker=epi_marker,
134
+ function_space=function_space,
135
+ )
136
+ if outdir is not None:
137
+ with dolfinx.io.XDMFFile(mesh.comm, Path(outdir) / "laplace.xdmf", "w") as file:
138
+ file.write_mesh(mesh)
139
+ file.write_function(t)
140
+
141
+ system = compute_system(
142
+ t,
143
+ alpha_endo=alpha_endo,
144
+ alpha_epi=alpha_epi,
145
+ )
146
+
147
+ if outdir is not None:
148
+ utils.save_microstructure(mesh, system, outdir)
149
+
150
+ return system
@@ -0,0 +1,84 @@
1
+ from typing import NamedTuple
2
+ from pathlib import Path
3
+
4
+ import dolfinx
5
+ from dolfinx.fem.petsc import LinearProblem
6
+ import ufl
7
+ import numpy as np
8
+ import adios4dolfinx
9
+
10
+
11
+ class Microstructure(NamedTuple):
12
+ f0: dolfinx.fem.Function
13
+ s0: dolfinx.fem.Function
14
+ n0: dolfinx.fem.Function
15
+
16
+
17
+ def save_microstructure(
18
+ mesh: dolfinx.mesh.Mesh, system: Microstructure, outdir: str | Path
19
+ ) -> None:
20
+ from ..utils import element2array
21
+
22
+ # Save for paraview visualization
23
+ with dolfinx.io.XDMFFile(mesh.comm, Path(outdir) / "microstructure.xdmf", "w") as file:
24
+ file.write_mesh(mesh)
25
+ file.write_function(system.f0)
26
+ file.write_function(system.s0)
27
+ file.write_function(system.n0)
28
+
29
+ # Save with proper function space
30
+ filename = Path(outdir) / "microstructure.bp"
31
+ adios4dolfinx.write_function(u=system.f0, filename=filename)
32
+ adios4dolfinx.write_function(u=system.s0, filename=filename)
33
+ adios4dolfinx.write_function(u=system.n0, filename=filename)
34
+ arr = element2array(system.f0.ufl_element().basix_element)
35
+ adios4dolfinx.write_attributes(
36
+ comm=mesh.comm,
37
+ filename=filename,
38
+ name="function_space",
39
+ attributes={k: arr for k in ("f0", "s0", "n0")},
40
+ )
41
+
42
+
43
+ def normalize(u):
44
+ return u / np.linalg.norm(u, axis=0)
45
+
46
+
47
+ def laplace(
48
+ mesh: dolfinx.mesh.Mesh,
49
+ ffun: dolfinx.mesh.MeshTags,
50
+ endo_marker: int,
51
+ epi_marker: int,
52
+ function_space: str,
53
+ ):
54
+ V = dolfinx.fem.functionspace(mesh, ("Lagrange", 1))
55
+
56
+ u = ufl.TrialFunction(V)
57
+ v = ufl.TestFunction(V)
58
+ a = ufl.dot(ufl.grad(u), ufl.grad(v)) * ufl.dx
59
+ L = v * dolfinx.fem.Constant(mesh, dolfinx.default_scalar_type(0.0)) * ufl.dx
60
+
61
+ endo_facets = ffun.find(endo_marker)
62
+ endo_dofs = dolfinx.fem.locate_dofs_topological(V, 2, endo_facets)
63
+ epi_facets = ffun.find(epi_marker)
64
+ epi_dofs = dolfinx.fem.locate_dofs_topological(V, 2, epi_facets)
65
+ zero = dolfinx.fem.Constant(mesh, dolfinx.default_scalar_type(0.0))
66
+ one = dolfinx.fem.Constant(mesh, dolfinx.default_scalar_type(1.0))
67
+
68
+ endo_bc = dolfinx.fem.dirichletbc(zero, endo_dofs, V)
69
+ epi_bc = dolfinx.fem.dirichletbc(one, epi_dofs, V)
70
+
71
+ bcs = [endo_bc, epi_bc]
72
+
73
+ problem = LinearProblem(a, L, bcs=bcs, petsc_options={"ksp_type": "preonly", "pc_type": "lu"})
74
+ uh = problem.solve()
75
+
76
+ if function_space != "P_1":
77
+ family, degree = function_space.split("_")
78
+ W = dolfinx.fem.functionspace(mesh, (family, int(degree)))
79
+ t = dolfinx.fem.Function(W)
80
+ t.interpolate(uh)
81
+ else:
82
+ t = uh
83
+
84
+ return t
@@ -0,0 +1,126 @@
1
+ from dataclasses import dataclass, field
2
+ import json
3
+ from pathlib import Path
4
+ import dolfinx
5
+ from mpi4py import MPI
6
+ import shutil
7
+ import adios4dolfinx
8
+ import numpy as np
9
+
10
+ from . import utils
11
+
12
+
13
+ @dataclass(frozen=True, slots=True)
14
+ class Geometry:
15
+ mesh: dolfinx.mesh.Mesh
16
+ markers: dict[str, tuple[str, str]] = field(default_factory=dict)
17
+ ffun: dolfinx.mesh.MeshTags | None = None
18
+ cfun: dolfinx.mesh.MeshTags | None = None
19
+ f0: dolfinx.fem.Function | None = None
20
+ s0: dolfinx.fem.Function | None = None
21
+ n0: dolfinx.fem.Function | None = None
22
+
23
+ def save(self, path: str | Path) -> None:
24
+ path = Path(path)
25
+
26
+ if path.exists() and self.mesh.comm.rank == 0:
27
+ shutil.rmtree(path)
28
+ self.mesh.comm.barrier()
29
+ adios4dolfinx.write_mesh(mesh=self.mesh, filename=path)
30
+
31
+ adios4dolfinx.write_attributes(
32
+ comm=self.mesh.comm,
33
+ filename=path,
34
+ name="markers",
35
+ attributes={k: np.array(v, dtype=np.uint8) for k, v in self.markers.items()},
36
+ )
37
+
38
+ if self.ffun is not None:
39
+ adios4dolfinx.write_meshtags(
40
+ meshtags=self.ffun,
41
+ mesh=self.mesh,
42
+ filename=path,
43
+ )
44
+
45
+ if self.f0 is not None:
46
+ el = self.f0.ufl_element().basix_element
47
+ arr = utils.element2array(el)
48
+
49
+ adios4dolfinx.write_attributes(
50
+ comm=self.mesh.comm,
51
+ filename=path,
52
+ name="function_space",
53
+ attributes={k: arr for k in ("f0", "s0", "n0")},
54
+ )
55
+ adios4dolfinx.write_function(u=self.f0, filename=path, name="f0")
56
+ if self.s0 is not None:
57
+ adios4dolfinx.write_function(u=self.s0, filename=path, name="s0")
58
+ if self.n0 is not None:
59
+ adios4dolfinx.write_function(u=self.n0, filename=path, name="n0")
60
+
61
+ @classmethod
62
+ def from_file(cls, comm: MPI.Intracomm, path: str | Path) -> "Geometry":
63
+ path = Path(path)
64
+ mesh = adios4dolfinx.read_mesh(comm=comm, filename=path)
65
+ markers = adios4dolfinx.read_attributes(comm=comm, filename=path, name="markers")
66
+ ffun = adios4dolfinx.read_meshtags(mesh=mesh, meshtag_name="Facet tags", filename=path)
67
+ function_space = adios4dolfinx.read_attributes(
68
+ comm=comm, filename=path, name="function_space"
69
+ )
70
+ element = utils.array2element(function_space["f0"])
71
+ # Assume same function space for all functions
72
+ V = dolfinx.fem.functionspace(mesh, element)
73
+ f0 = dolfinx.fem.Function(V, name="f0")
74
+ s0 = dolfinx.fem.Function(V, name="s0")
75
+ n0 = dolfinx.fem.Function(V, name="n0")
76
+
77
+ adios4dolfinx.read_function(u=f0, filename=path, name="f0")
78
+ adios4dolfinx.read_function(u=s0, filename=path, name="s0")
79
+ adios4dolfinx.read_function(u=n0, filename=path, name="n0")
80
+ return cls(mesh=mesh, markers=markers, ffun=ffun, f0=f0, s0=s0, n0=n0)
81
+
82
+ @classmethod
83
+ def from_folder(cls, comm: MPI.Intracomm, folder: str | Path) -> "Geometry":
84
+ folder = Path(folder)
85
+
86
+ # Read mesh
87
+ if (folder / "mesh.xdmf").exists():
88
+ mesh, cfun = utils.read_mesh(comm=comm, filename=folder / "mesh.xdmf")
89
+ else:
90
+ raise ValueError("No mesh file found")
91
+
92
+ # Read meshtags
93
+ if (folder / "triangle_mesh.xdmf").exists():
94
+ ffun = utils.read_ffun(mesh=mesh, filename=folder / "triangle_mesh.xdmf")
95
+ else:
96
+ ffun = None
97
+
98
+ # Read markers
99
+ if (folder / "markers.json").exists():
100
+ if comm.rank == 0:
101
+ markers = json.loads((folder / "markers.json").read_text())
102
+ else:
103
+ markers = {}
104
+ markers = comm.bcast(markers, root=0)
105
+ else:
106
+ markers = {}
107
+
108
+ microstructure_path = folder / "microstructure.bp"
109
+ if microstructure_path.exists():
110
+ function_space = adios4dolfinx.read_attributes(
111
+ comm=MPI.COMM_WORLD, filename=microstructure_path, name="function_space"
112
+ )
113
+ # Assume same function space for all functions
114
+ element = utils.array2element(function_space["f0"])
115
+ V = dolfinx.fem.functionspace(mesh, element)
116
+ f0 = dolfinx.fem.Function(V, name="f0")
117
+ s0 = dolfinx.fem.Function(V, name="s0")
118
+ n0 = dolfinx.fem.Function(V, name="n0")
119
+
120
+ adios4dolfinx.read_function(u=f0, filename=microstructure_path, name="f0")
121
+ adios4dolfinx.read_function(u=s0, filename=microstructure_path, name="s0")
122
+ adios4dolfinx.read_function(u=n0, filename=microstructure_path, name="n0")
123
+ else:
124
+ f0 = s0 = n0 = None
125
+
126
+ return cls(mesh=mesh, ffun=ffun, cfun=cfun, markers=markers, f0=f0, s0=s0, n0=n0)