cardiac-geometriesx 0.1.3__tar.gz → 0.3.0__tar.gz

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.

Files changed (24) hide show
  1. {cardiac_geometriesx-0.1.3/src/cardiac_geometriesx.egg-info → cardiac_geometriesx-0.3.0}/PKG-INFO +8 -1
  2. {cardiac_geometriesx-0.1.3 → cardiac_geometriesx-0.3.0}/pyproject.toml +11 -2
  3. {cardiac_geometriesx-0.1.3 → cardiac_geometriesx-0.3.0}/src/cardiac_geometries/cli.py +12 -0
  4. {cardiac_geometriesx-0.1.3 → cardiac_geometriesx-0.3.0}/src/cardiac_geometries/fibers/lv_ellipsoid.py +96 -20
  5. {cardiac_geometriesx-0.1.3 → cardiac_geometriesx-0.3.0}/src/cardiac_geometries/fibers/slab.py +10 -12
  6. {cardiac_geometriesx-0.1.3 → cardiac_geometriesx-0.3.0}/src/cardiac_geometries/fibers/utils.py +13 -8
  7. {cardiac_geometriesx-0.1.3 → cardiac_geometriesx-0.3.0}/src/cardiac_geometries/geometry.py +1 -1
  8. cardiac_geometriesx-0.3.0/src/cardiac_geometries/gui.py +272 -0
  9. {cardiac_geometriesx-0.1.3 → cardiac_geometriesx-0.3.0}/src/cardiac_geometries/utils.py +82 -14
  10. {cardiac_geometriesx-0.1.3 → cardiac_geometriesx-0.3.0/src/cardiac_geometriesx.egg-info}/PKG-INFO +8 -1
  11. {cardiac_geometriesx-0.1.3 → cardiac_geometriesx-0.3.0}/src/cardiac_geometriesx.egg-info/SOURCES.txt +1 -0
  12. {cardiac_geometriesx-0.1.3 → cardiac_geometriesx-0.3.0}/src/cardiac_geometriesx.egg-info/requires.txt +8 -0
  13. {cardiac_geometriesx-0.1.3 → cardiac_geometriesx-0.3.0}/tests/test_cli.py +1 -1
  14. {cardiac_geometriesx-0.1.3 → cardiac_geometriesx-0.3.0}/LICENSE +0 -0
  15. {cardiac_geometriesx-0.1.3 → cardiac_geometriesx-0.3.0}/README.md +0 -0
  16. {cardiac_geometriesx-0.1.3 → cardiac_geometriesx-0.3.0}/setup.cfg +0 -0
  17. {cardiac_geometriesx-0.1.3 → cardiac_geometriesx-0.3.0}/src/cardiac_geometries/__init__.py +0 -0
  18. {cardiac_geometriesx-0.1.3 → cardiac_geometriesx-0.3.0}/src/cardiac_geometries/fibers/__init__.py +0 -0
  19. {cardiac_geometriesx-0.1.3 → cardiac_geometriesx-0.3.0}/src/cardiac_geometries/mesh.py +0 -0
  20. {cardiac_geometriesx-0.1.3 → cardiac_geometriesx-0.3.0}/src/cardiac_geometriesx.egg-info/dependency_links.txt +0 -0
  21. {cardiac_geometriesx-0.1.3 → cardiac_geometriesx-0.3.0}/src/cardiac_geometriesx.egg-info/entry_points.txt +0 -0
  22. {cardiac_geometriesx-0.1.3 → cardiac_geometriesx-0.3.0}/src/cardiac_geometriesx.egg-info/not-zip-safe +0 -0
  23. {cardiac_geometriesx-0.1.3 → cardiac_geometriesx-0.3.0}/src/cardiac_geometriesx.egg-info/top_level.txt +0 -0
  24. {cardiac_geometriesx-0.1.3 → cardiac_geometriesx-0.3.0}/tests/test_save_load.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: cardiac-geometriesx
3
- Version: 0.1.3
3
+ Version: 0.3.0
4
4
  Summary: A python library for cardiac geometries
5
5
  Author-email: Henrik Finsberg <henriknf@simula.no>
6
6
  License: MIT
@@ -36,6 +36,13 @@ Provides-Extra: test
36
36
  Requires-Dist: pre-commit; extra == "test"
37
37
  Requires-Dist: pytest; extra == "test"
38
38
  Requires-Dist: pytest-cov; extra == "test"
39
+ Provides-Extra: gui
40
+ Requires-Dist: streamlit; extra == "gui"
41
+ Requires-Dist: stpyvista; extra == "gui"
42
+ Requires-Dist: pyvista[all]>=0.43.0; extra == "gui"
43
+ Requires-Dist: trame-vuetify; extra == "gui"
44
+ Requires-Dist: ipywidgets; extra == "gui"
45
+ Requires-Dist: fenicsx-ldrb; extra == "gui"
39
46
 
40
47
  ![_](docs/_static/logo.png)
41
48
 
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "cardiac-geometriesx"
7
- version = "0.1.3"
7
+ version = "0.3.0"
8
8
  description = "A python library for cardiac geometries"
9
9
  authors = [{name = "Henrik Finsberg", email = "henriknf@simula.no"}]
10
10
  license = {text = "MIT"}
@@ -51,6 +51,15 @@ test = [
51
51
  "pytest",
52
52
  "pytest-cov",
53
53
  ]
54
+ gui = [
55
+ "streamlit",
56
+ "stpyvista",
57
+ "pyvista[all]>=0.43.0",
58
+ "trame-vuetify",
59
+ "ipywidgets",
60
+ "fenicsx-ldrb",
61
+
62
+ ]
54
63
 
55
64
  [project.scripts]
56
65
  geox = "cardiac_geometries.cli:app"
@@ -164,7 +173,7 @@ tag = true
164
173
  sign_tags = false
165
174
  tag_name = "v{new_version}"
166
175
  tag_message = "Bump version: {current_version} → {new_version}"
167
- current_version = "0.1.3"
176
+ current_version = "0.3.0"
168
177
 
169
178
 
170
179
  [[tool.bumpversion.files]]
@@ -869,8 +869,20 @@ def slab_in_bath(
869
869
  geo.save(outdir / "slab_in_bath.bp")
870
870
 
871
871
 
872
+ @click.command("gui")
873
+ def gui():
874
+ # Make sure we can import the required packages
875
+ from . import gui # noqa: F401
876
+
877
+ gui_path = Path(__file__).parent.joinpath("gui.py")
878
+ import subprocess as sp
879
+
880
+ sp.run(["streamlit", "run", gui_path.as_posix()])
881
+
882
+
872
883
  app.add_command(lv_ellipsoid)
873
884
  app.add_command(biv_ellipsoid)
874
885
  app.add_command(biv_ellipsoid_torso)
875
886
  app.add_command(slab)
876
887
  app.add_command(slab_in_bath)
888
+ app.add_command(gui)
@@ -1,12 +1,64 @@
1
1
  from pathlib import Path
2
2
 
3
- import basix
4
3
  import dolfinx
5
4
  import numpy as np
6
5
 
6
+ from ..utils import space_from_string
7
7
  from . import utils
8
8
 
9
9
 
10
+ def mu_theta(
11
+ x: np.ndarray, y: np.ndarray, z: np.ndarray, long_axis: int = 0
12
+ ) -> tuple[np.ndarray, np.ndarray, list[int]]:
13
+ """Get the angles mu and theta from the coordinates x, y, z
14
+ given the long axis.
15
+
16
+ Parameters
17
+ ----------
18
+ x : np.ndarray
19
+ The x-coordinates
20
+ y : np.ndarray
21
+ The y-coordinates
22
+ z : np.ndarray
23
+ The z-coordinates
24
+ long_axis : int, optional
25
+ The long axis, by default 0 (x-axis)
26
+
27
+ Returns
28
+ -------
29
+ tuple[np.ndarray, np.ndarray, list[int]]
30
+ The angles mu and theta and the permutation of the axes
31
+
32
+ Raises
33
+ ------
34
+ ValueError
35
+ If the long axis is not 0, 1 or 2
36
+ """
37
+ if long_axis == 0:
38
+ a = np.sqrt(y**2 + z**2)
39
+ b = x
40
+ theta = np.pi - np.arctan2(z, -y)
41
+ perm = [0, 1, 2]
42
+ elif long_axis == 1:
43
+ a = np.sqrt(x**2 + z**2)
44
+ b = y
45
+ theta = np.pi - np.arctan2(z, -x)
46
+ perm = [1, 0, 2]
47
+ elif long_axis == 2:
48
+ a = np.sqrt(x**2 + y**2)
49
+ b = z
50
+ theta = np.pi - np.arctan2(x, -y)
51
+ perm = [2, 1, 0]
52
+ else:
53
+ raise ValueError("Invalid long_axis")
54
+
55
+ mu = np.arctan2(a, b)
56
+
57
+ theta[mu < 1e-7] = 0.0
58
+
59
+ return mu, theta, perm
60
+
61
+
10
62
  def compute_system(
11
63
  t_func: dolfinx.fem.Function,
12
64
  r_short_endo=0.025,
@@ -15,8 +67,36 @@ def compute_system(
15
67
  r_long_epi=0.097,
16
68
  alpha_endo: float = -60,
17
69
  alpha_epi: float = 60,
70
+ long_axis: int = 0,
18
71
  **kwargs,
19
72
  ) -> utils.Microstructure:
73
+ """Compute the microstructure for the given time function.
74
+
75
+ Parameters
76
+ ----------
77
+ t_func : dolfinx.fem.Function
78
+ Solution of the Laplace equation
79
+ r_short_endo : float, optional
80
+ Short radius at the endocardium, by default 0.025
81
+ r_short_epi : float, optional
82
+ Short radius at the epicardium, by default 0.035
83
+ r_long_endo : float, optional
84
+ Long radius at the endocardium, by default 0.09
85
+ r_long_epi : float, optional
86
+ Long radius at the epicardium, by default 0.097
87
+ alpha_endo : float, optional
88
+ Angle at the endocardium, by default -60
89
+ alpha_epi : float, optional
90
+ Angle at the epicardium, by default 60
91
+ long_axis : int, optional
92
+ Long axis, by default 0 (x-axis)
93
+
94
+ Returns
95
+ -------
96
+ utils.Microstructure
97
+ The microstructure
98
+ """
99
+
20
100
  V = t_func.function_space
21
101
  element = V.ufl_element()
22
102
  mesh = V.mesh
@@ -40,11 +120,7 @@ def compute_system(
40
120
  y = dof_coordinates[:, 1]
41
121
  z = dof_coordinates[:, 2]
42
122
 
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
123
+ mu, theta, perm = mu_theta(x, y, z, long_axis=long_axis)
48
124
 
49
125
  e_t = np.array(
50
126
  [
@@ -52,7 +128,7 @@ def compute_system(
52
128
  drs_dt * np.sin(mu) * np.cos(theta),
53
129
  drs_dt * np.sin(mu) * np.sin(theta),
54
130
  ],
55
- )
131
+ )[perm]
56
132
  e_t = utils.normalize(e_t)
57
133
 
58
134
  e_mu = np.array(
@@ -61,7 +137,7 @@ def compute_system(
61
137
  rs * np.cos(mu) * np.cos(theta),
62
138
  rs * np.cos(mu) * np.sin(theta),
63
139
  ],
64
- )
140
+ )[perm]
65
141
  e_mu = utils.normalize(e_mu)
66
142
 
67
143
  e_theta = np.array(
@@ -70,7 +146,7 @@ def compute_system(
70
146
  -rs * np.sin(mu) * np.sin(theta),
71
147
  rs * np.sin(mu) * np.cos(theta),
72
148
  ],
73
- )
149
+ )[perm]
74
150
  e_theta = utils.normalize(e_theta)
75
151
 
76
152
  f0 = np.sin(al) * e_mu + np.cos(al) * e_theta
@@ -82,14 +158,9 @@ def compute_system(
82
158
  s0 = np.cross(f0, n0, axis=0)
83
159
  s0 = utils.normalize(s0)
84
160
 
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,),
161
+ Vv = space_from_string(
162
+ space_string=f"{element.family_name}_{element.degree}", mesh=mesh, dim=mesh.geometry.dim
91
163
  )
92
- Vv = dolfinx.fem.functionspace(mesh, el)
93
164
 
94
165
  fiber = dolfinx.fem.Function(Vv)
95
166
  norm_f = np.linalg.norm(f0, axis=0)
@@ -118,6 +189,7 @@ def create_microstructure(
118
189
  r_long_epi=0.097,
119
190
  alpha_endo: float = -60,
120
191
  alpha_epi: float = 60,
192
+ long_axis: int = 0,
121
193
  outdir: str | Path | None = None,
122
194
  ):
123
195
  endo_marker = markers["ENDO"][0]
@@ -131,10 +203,13 @@ def create_microstructure(
131
203
  )
132
204
 
133
205
  if outdir is not None:
134
- with dolfinx.io.VTXWriter(
135
- mesh.comm, Path(outdir) / "laplace.bp", [t], engine="BP4"
136
- ) as file:
137
- file.write(0.0)
206
+ try:
207
+ with dolfinx.io.VTXWriter(
208
+ mesh.comm, Path(outdir) / "laplace.bp", [t], engine="BP4"
209
+ ) as file:
210
+ file.write(0.0)
211
+ except RuntimeError:
212
+ pass
138
213
 
139
214
  system = compute_system(
140
215
  t,
@@ -145,6 +220,7 @@ def create_microstructure(
145
220
  r_long_epi=r_long_epi,
146
221
  alpha_endo=alpha_endo,
147
222
  alpha_epi=alpha_epi,
223
+ long_axis=long_axis,
148
224
  )
149
225
  if outdir is not None:
150
226
  utils.save_microstructure(mesh, system, outdir)
@@ -1,9 +1,9 @@
1
1
  from pathlib import Path
2
2
 
3
- import basix
4
3
  import dolfinx
5
4
  import numpy as np
6
5
 
6
+ from ..utils import space_from_string
7
7
  from . import utils
8
8
 
9
9
 
@@ -59,14 +59,9 @@ def compute_system(
59
59
  n0 = np.cross(f0, s0, axis=0)
60
60
  n0 = utils.normalize(n0)
61
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,),
62
+ Vv = space_from_string(
63
+ space_string=f"{element.family_name}_{element.degree}", mesh=mesh, dim=mesh.geometry.dim
68
64
  )
69
- Vv = dolfinx.fem.functionspace(mesh, el)
70
65
 
71
66
  fiber = dolfinx.fem.Function(Vv)
72
67
  norm_f = np.linalg.norm(f0, axis=0)
@@ -134,10 +129,13 @@ def create_microstructure(
134
129
  function_space=function_space,
135
130
  )
136
131
  if outdir is not None:
137
- with dolfinx.io.VTXWriter(
138
- mesh.comm, Path(outdir) / "laplace.bp", [t], engine="BP4"
139
- ) as file:
140
- file.write(0.0)
132
+ try:
133
+ with dolfinx.io.VTXWriter(
134
+ mesh.comm, Path(outdir) / "laplace.bp", [t], engine="BP4"
135
+ ) as file:
136
+ file.write(0.0)
137
+ except RuntimeError:
138
+ pass
141
139
 
142
140
  system = compute_system(
143
141
  t,
@@ -7,6 +7,8 @@ import numpy as np
7
7
  import ufl
8
8
  from dolfinx.fem.petsc import LinearProblem
9
9
 
10
+ from ..utils import space_from_string
11
+
10
12
 
11
13
  class Microstructure(NamedTuple):
12
14
  f0: dolfinx.fem.Function
@@ -20,17 +22,20 @@ def save_microstructure(
20
22
  from ..utils import element2array
21
23
 
22
24
  # Save for paraview visualization
23
- with dolfinx.io.VTXWriter(
24
- mesh.comm, Path(outdir) / "microstructure-viz.bp", functions, engine="BP4"
25
- ) as file:
26
- file.write(0.0)
25
+ try:
26
+ with dolfinx.io.VTXWriter(
27
+ mesh.comm, Path(outdir) / "microstructure-viz.bp", functions, engine="BP4"
28
+ ) as file:
29
+ file.write(0.0)
30
+ except RuntimeError:
31
+ pass
27
32
 
28
33
  # Save with proper function space
29
34
  filename = Path(outdir) / "microstructure.bp"
30
35
  for function in functions:
31
36
  adios4dolfinx.write_function(u=function, filename=filename)
32
37
 
33
- attributes = {f.name: element2array(f.ufl_element().basix_element) for f in functions}
38
+ attributes = {f.name: element2array(f.ufl_element()) for f in functions}
34
39
  adios4dolfinx.write_attributes(
35
40
  comm=mesh.comm,
36
41
  filename=filename,
@@ -73,10 +78,10 @@ def laplace(
73
78
  uh = problem.solve()
74
79
 
75
80
  if function_space != "P_1":
76
- family, degree = function_space.split("_")
77
- W = dolfinx.fem.functionspace(mesh, (family, int(degree)))
81
+ W = space_from_string(function_space, mesh, dim=1)
78
82
  t = dolfinx.fem.Function(W)
79
- t.interpolate(uh)
83
+ expr = dolfinx.fem.Expression(uh, W.element.interpolation_points())
84
+ t.interpolate(expr)
80
85
  else:
81
86
  t = uh
82
87
 
@@ -68,7 +68,7 @@ class Geometry:
68
68
  )
69
69
 
70
70
  if self.f0 is not None:
71
- el = self.f0.ufl_element().basix_element
71
+ el = self.f0.ufl_element()
72
72
  arr = utils.element2array(el)
73
73
 
74
74
  adios4dolfinx.write_attributes(
@@ -0,0 +1,272 @@
1
+ try:
2
+ import streamlit as st
3
+ except ImportError:
4
+ print("Please install streamlit - python3 -m pip install streamlit")
5
+ exit(1)
6
+
7
+ try:
8
+ import pyvista as pv
9
+ except ImportError:
10
+ msg = (
11
+ "Please install pyvista - python3 -m pip install pyvista. "
12
+ "Note if you using ARM Mac, then check out the following link "
13
+ "on how to install vtk: https://github.com/KitwareMedical/VTKPythonPackage/issues/42"
14
+ )
15
+ print(msg)
16
+ exit(1)
17
+
18
+ try:
19
+ from stpyvista import stpyvista
20
+ except ImportError:
21
+ print("Please install stpyvista - python3 -m pip install stpyvista")
22
+ exit(1)
23
+
24
+ import os
25
+
26
+ os.environ["GMSH_INTERRUPTIBLE"] = "0"
27
+
28
+ import math
29
+ from pathlib import Path
30
+
31
+ import mpi4py
32
+
33
+ import dolfinx
34
+
35
+ import cardiac_geometries
36
+
37
+
38
+ def return_none(*args, **kwargs):
39
+ return None
40
+
41
+
42
+ def load_geometry(folder: str):
43
+ comm = mpi4py.MPI.COMM_WORLD
44
+ try:
45
+ return cardiac_geometries.geometry.Geometry.from_folder(comm, folder)
46
+ except Exception as e:
47
+ st.error(f"Error loading geometry: {e}")
48
+ return None
49
+
50
+
51
+ def plot_geometry(geo):
52
+ V = dolfinx.fem.functionspace(geo.mesh, ("Lagrange", 1))
53
+
54
+ pv.start_xvfb()
55
+
56
+ # Plot the mesh with cell tags
57
+ mesh_plotter = pv.Plotter()
58
+ mesh_plotter.background_color = "white"
59
+ mesh_plotter.window_size = [600, 400]
60
+
61
+ topology, cell_types, geometry = dolfinx.plot.vtk_mesh(V)
62
+
63
+ grid = pv.UnstructuredGrid(topology, cell_types, geometry)
64
+ if geo.cfun is not None:
65
+ grid.cell_data["Cell tags"] = geo.cfun.values
66
+ grid.set_active_scalars("Cell tags")
67
+ mesh_plotter.add_mesh(grid, show_edges=True)
68
+
69
+ mesh_plotter.view_isometric()
70
+ st.header("Mesh and cell tags")
71
+ stpyvista(mesh_plotter)
72
+
73
+ if geo.ffun is not None:
74
+ vtk_bmesh = dolfinx.plot.vtk_mesh(geo.mesh, geo.ffun.dim, geo.ffun.indices)
75
+ bgrid = pv.UnstructuredGrid(*vtk_bmesh)
76
+ bgrid.cell_data["Facet tags"] = geo.ffun.values
77
+ bgrid.set_active_scalars("Facet tags")
78
+ facet_plotter = pv.Plotter()
79
+ facet_plotter.background_color = "white"
80
+ facet_plotter.window_size = [600, 400]
81
+ facet_plotter.add_mesh(bgrid, show_edges=True)
82
+ facet_plotter.view_isometric()
83
+ st.header("Facet tags")
84
+ stpyvista(facet_plotter)
85
+
86
+ if geo.f0 is not None:
87
+ st.header("Fibers")
88
+ size_arrows_fibers = st.slider(
89
+ "Arrow size fibers", min_value=0.1, max_value=10.0, value=2.0
90
+ )
91
+ topology, cell_types, geometry = dolfinx.plot.vtk_mesh(geo.f0.function_space)
92
+ values = geo.f0.x.array.real.reshape((geometry.shape[0], len(geo.f0)))
93
+ function_grid = pv.UnstructuredGrid(topology, cell_types, geometry)
94
+ function_grid["u"] = values
95
+ glyphs = function_grid.glyph(orient="u", factor=size_arrows_fibers)
96
+ fiber_plotter = pv.Plotter()
97
+ fiber_plotter.background_color = "white"
98
+ fiber_plotter.window_size = [600, 400]
99
+ fiber_plotter.add_mesh(glyphs, show_edges=True)
100
+ fiber_plotter.view_isometric()
101
+ stpyvista(fiber_plotter)
102
+
103
+ if geo.s0 is not None:
104
+ st.header("Sheets")
105
+ size_arrows_sheets = st.slider(
106
+ "Arrow size sheets", min_value=0.1, max_value=10.0, value=2.0
107
+ )
108
+ topology, cell_types, geometry = dolfinx.plot.vtk_mesh(geo.s0.function_space)
109
+ values = geo.s0.x.array.real.reshape((geometry.shape[0], len(geo.s0)))
110
+ function_grid = pv.UnstructuredGrid(topology, cell_types, geometry)
111
+ function_grid["u"] = values
112
+ glyphs = function_grid.glyph(orient="u", factor=size_arrows_sheets)
113
+ sheet_plotter = pv.Plotter()
114
+ sheet_plotter.background_color = "white"
115
+ sheet_plotter.window_size = [600, 400]
116
+ sheet_plotter.add_mesh(glyphs, show_edges=True)
117
+ sheet_plotter.view_isometric()
118
+ stpyvista(sheet_plotter)
119
+
120
+ if geo.n0 is not None:
121
+ st.header("Sheet Normal")
122
+ size_arrows_normal = st.slider(
123
+ "Arrow size sheet normal", min_value=0.1, max_value=10.0, value=2.0
124
+ )
125
+ topology, cell_types, geometry = dolfinx.plot.vtk_mesh(geo.n0.function_space)
126
+ values = geo.n0.x.array.real.reshape((geometry.shape[0], len(geo.n0)))
127
+ function_grid = pv.UnstructuredGrid(topology, cell_types, geometry)
128
+ function_grid["u"] = values
129
+ glyphs = function_grid.glyph(orient="u", factor=size_arrows_normal)
130
+ normal_plotter = pv.Plotter()
131
+ normal_plotter.background_color = "white"
132
+ normal_plotter.window_size = [600, 400]
133
+ normal_plotter.add_mesh(glyphs, show_edges=True)
134
+ normal_plotter.view_isometric()
135
+ stpyvista(normal_plotter)
136
+
137
+
138
+ def load():
139
+ st.title("Load existing geometries")
140
+
141
+ cwd = Path.cwd()
142
+ folders = [f.name for f in cwd.iterdir() if f.is_dir()]
143
+ # Select a folder
144
+
145
+ folder = st.selectbox("Select a folder", folders)
146
+ geo = load_geometry(folder)
147
+
148
+ if geo is not None:
149
+ plot_geometry(geo)
150
+
151
+ return
152
+
153
+
154
+ def create_lv():
155
+ st.title("Create lv geometry")
156
+
157
+ outdir = st.text_input("Output directory", value="lv_ellipsoid")
158
+ col1, col2, col3 = st.columns(3)
159
+
160
+ with col1:
161
+ st.header("Radius")
162
+ r_short_endo = st.number_input("r_short_endo", value=7.0)
163
+ r_short_epi = st.number_input("r_short_epi", value=10.0)
164
+ r_long_endo = st.number_input("r_long_endo", value=17.0)
165
+ r_long_epi = st.number_input("r_long_epi", value=20.0)
166
+ p_size_ref = st.number_input("p_size_ref", value=3.0)
167
+
168
+ with col2:
169
+ st.header("Angles")
170
+ mu_apex_endo = st.number_input("mu_apex_endo", value=-math.pi)
171
+ mu_base_endo = st.number_input("mu_base_endo", value=-math.acos(5 / 17))
172
+ mu_apex_epi = st.number_input("mu_apex_epi", value=-math.pi)
173
+ mu_base_epi = st.number_input("mu_base_epi", value=-math.acos(5 / 20))
174
+
175
+ with col3:
176
+ st.header("Fibers")
177
+ create_fibers = st.checkbox("Create fibers", value=True)
178
+ if create_fibers:
179
+ fiber_space = st.selectbox("Fiber space", ["P_1", "P_2"])
180
+ fiber_angle_endo = st.number_input("fiber_angle_endo", value=60)
181
+ fiber_angle_epi = st.number_input("fiber_angle_epi", value=-60)
182
+
183
+ if st.button("Create"):
184
+ args = [
185
+ "geox",
186
+ "lv-ellipsoid",
187
+ "--r-short-endo",
188
+ str(r_short_endo),
189
+ "--r-short-epi",
190
+ str(r_short_epi),
191
+ "--r-long-endo",
192
+ str(r_long_endo),
193
+ "--r-long-epi",
194
+ str(r_long_epi),
195
+ "--mu-apex-endo",
196
+ str(mu_apex_endo),
197
+ "--mu-base-endo",
198
+ str(mu_base_endo),
199
+ "--mu-apex-epi",
200
+ str(mu_apex_epi),
201
+ "--mu-base-epi",
202
+ str(mu_base_epi),
203
+ "--psize-ref",
204
+ str(p_size_ref),
205
+ ]
206
+
207
+ if create_fibers:
208
+ args.extend(
209
+ [
210
+ "--create-fibers",
211
+ "--fiber-space",
212
+ fiber_space,
213
+ "--fiber-angle-endo",
214
+ str(fiber_angle_endo),
215
+ "--fiber-angle-epi",
216
+ str(fiber_angle_epi),
217
+ ]
218
+ )
219
+
220
+ args.append(str(outdir))
221
+ st.markdown(f"```{' '.join(args)}```")
222
+
223
+ if Path(outdir).exists():
224
+ st.warning(f"Folder {outdir} already exists. Overwriting...")
225
+ import shutil
226
+
227
+ shutil.rmtree(outdir)
228
+
229
+ import subprocess as sp
230
+
231
+ ret = sp.run(
232
+ args,
233
+ capture_output=True,
234
+ )
235
+
236
+ st.markdown(f"```{ret.stdout.decode()}```")
237
+
238
+ if st.button("Visualize folder"):
239
+ geo = load_geometry(outdir)
240
+
241
+ if geo is not None:
242
+ plot_geometry(geo)
243
+
244
+ return
245
+
246
+
247
+ # Page settings
248
+ st.set_page_config(page_title="simcardems")
249
+
250
+ # Sidebar settings
251
+ pages = {
252
+ "Load": load,
253
+ "Create LV geometry": create_lv,
254
+ }
255
+
256
+ st.sidebar.title("Cardiac geometries")
257
+
258
+ # Radio buttons to select desired option
259
+ page = st.sidebar.radio("Pages", tuple(pages.keys()))
260
+
261
+ pages[page]()
262
+
263
+ # About
264
+ st.sidebar.markdown(
265
+ """
266
+ - [Source code](https://github.com/ComputationalPhysiology/cardiac-geometriesx)
267
+ - [Documentation](http://computationalphysiology.github.io/cardiac-geometriesx)
268
+
269
+
270
+ Copyright © 2024 Henrik Finsberg @ Simula Research Laboratory
271
+ """,
272
+ )
@@ -13,6 +13,9 @@ from structlog import get_logger
13
13
 
14
14
  logger = get_logger()
15
15
 
16
+ quads = ("Quadrature", "Q", "Quad", "quadrature", "q", "quad")
17
+ QUADNR = 42
18
+
16
19
 
17
20
  class GMshModel(NamedTuple):
18
21
  mesh: dolfinx.mesh.Mesh
@@ -246,11 +249,70 @@ def model_to_mesh(
246
249
  return GMshModel(mesh, ct, ft, et, vt)
247
250
 
248
251
 
249
- def element2array(el: basix.finite_element.FiniteElement) -> np.ndarray:
250
- return np.array(
251
- [int(el.family), int(el.cell_type), int(el.degree), int(el.discontinuous)],
252
- dtype=np.uint8,
253
- )
252
+ def parse_element(space_string: str, mesh: dolfinx.mesh.Mesh, dim: int) -> basix.ufl._ElementBase:
253
+ """
254
+ Parse a string representation of a basix element family
255
+ """
256
+
257
+ family_str, degree_str = space_string.split("_")
258
+ kwargs = {"degree": int(degree_str), "cell": mesh.ufl_cell().cellname()}
259
+ if dim > 1:
260
+ if family_str in quads:
261
+ kwargs["value_shape"] = (dim,)
262
+ else:
263
+ kwargs["shape"] = (dim,)
264
+
265
+ if family_str in ["Lagrange", "P", "CG"]:
266
+ el = basix.ufl.element(family=basix.ElementFamily.P, discontinuous=False, **kwargs)
267
+ elif family_str in ["Discontinuous Lagrange", "DG", "dP"]:
268
+ el = basix.ufl.element(family=basix.ElementFamily.P, discontinuous=True, **kwargs)
269
+
270
+ elif family_str in quads:
271
+ el = basix.ufl.quadrature_element(scheme="default", **kwargs)
272
+ else:
273
+ families = list(basix.ElementFamily.__members__.keys())
274
+ msg = f"Unknown element family: {family_str!r}, available families: {families}"
275
+ raise ValueError(msg)
276
+ return el
277
+
278
+
279
+ def space_from_string(
280
+ space_string: str, mesh: dolfinx.mesh.Mesh, dim: int
281
+ ) -> dolfinx.fem.functionspace:
282
+ """
283
+ Constructed a finite elements space from a string
284
+ representation of the space
285
+
286
+ Arguments
287
+ ---------
288
+ space_string : str
289
+ A string on the form {family}_{degree} which
290
+ determines the space. Example 'Lagrange_1'.
291
+ mesh : df.Mesh
292
+ The mesh
293
+ dim : int
294
+ 1 for scalar space, 3 for vector space.
295
+ """
296
+ el = parse_element(space_string, mesh, dim)
297
+ return dolfinx.fem.functionspace(mesh, el)
298
+
299
+
300
+ def element2array(el: basix.ufl._BlockedElement) -> np.ndarray:
301
+ if el.family_name in quads:
302
+ return np.array(
303
+ [QUADNR, int(el.cell_type), int(el.degree), 0],
304
+ dtype=np.uint8,
305
+ )
306
+ else:
307
+ return np.array(
308
+ [
309
+ int(el.basix_element.family),
310
+ int(el.cell_type),
311
+ int(el.degree),
312
+ int(el.basix_element.discontinuous),
313
+ ],
314
+ dtype=np.uint8,
315
+ )
254
316
 
255
317
 
256
318
  def number2Enum(num: int, enum: Iterable) -> Enum:
@@ -261,18 +323,24 @@ def number2Enum(num: int, enum: Iterable) -> Enum:
261
323
 
262
324
 
263
325
  def array2element(arr: np.ndarray) -> basix.finite_element.FiniteElement:
264
- family = number2Enum(arr[0], basix.ElementFamily)
265
326
  cell_type = number2Enum(arr[1], basix.CellType)
266
327
  degree = int(arr[2])
267
328
  discontinuous = bool(arr[3])
268
- # TODO: Shape is hardcoded to (3,) for now, but this should also be stored
269
- return basix.ufl.element(
270
- family=family,
271
- cell=cell_type,
272
- degree=degree,
273
- discontinuous=discontinuous,
274
- shape=(3,),
275
- )
329
+ if arr[0] == QUADNR:
330
+ return basix.ufl.quadrature_element(
331
+ scheme="default", degree=degree, value_shape=(3,), cell=cell_type
332
+ )
333
+ else:
334
+ family = number2Enum(arr[0], basix.ElementFamily)
335
+
336
+ # TODO: Shape is hardcoded to (3,) for now, but this should also be stored
337
+ return basix.ufl.element(
338
+ family=family,
339
+ cell=cell_type,
340
+ degree=degree,
341
+ discontinuous=discontinuous,
342
+ shape=(3,),
343
+ )
276
344
 
277
345
 
278
346
  def handle_mesh_name(mesh_name: str = "") -> Path:
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: cardiac-geometriesx
3
- Version: 0.1.3
3
+ Version: 0.3.0
4
4
  Summary: A python library for cardiac geometries
5
5
  Author-email: Henrik Finsberg <henriknf@simula.no>
6
6
  License: MIT
@@ -36,6 +36,13 @@ Provides-Extra: test
36
36
  Requires-Dist: pre-commit; extra == "test"
37
37
  Requires-Dist: pytest; extra == "test"
38
38
  Requires-Dist: pytest-cov; extra == "test"
39
+ Provides-Extra: gui
40
+ Requires-Dist: streamlit; extra == "gui"
41
+ Requires-Dist: stpyvista; extra == "gui"
42
+ Requires-Dist: pyvista[all]>=0.43.0; extra == "gui"
43
+ Requires-Dist: trame-vuetify; extra == "gui"
44
+ Requires-Dist: ipywidgets; extra == "gui"
45
+ Requires-Dist: fenicsx-ldrb; extra == "gui"
39
46
 
40
47
  ![_](docs/_static/logo.png)
41
48
 
@@ -4,6 +4,7 @@ pyproject.toml
4
4
  src/cardiac_geometries/__init__.py
5
5
  src/cardiac_geometries/cli.py
6
6
  src/cardiac_geometries/geometry.py
7
+ src/cardiac_geometries/gui.py
7
8
  src/cardiac_geometries/mesh.py
8
9
  src/cardiac_geometries/utils.py
9
10
  src/cardiac_geometries/fibers/__init__.py
@@ -21,6 +21,14 @@ trame-vuetify
21
21
  ipywidgets
22
22
  fenicsx-ldrb
23
23
 
24
+ [gui]
25
+ streamlit
26
+ stpyvista
27
+ pyvista[all]>=0.43.0
28
+ trame-vuetify
29
+ ipywidgets
30
+ fenicsx-ldrb
31
+
24
32
  [test]
25
33
  pre-commit
26
34
  pytest
@@ -16,7 +16,7 @@ from cardiac_geometries import Geometry, cli
16
16
  ],
17
17
  ids=["slab", "lv_ellipsoid"],
18
18
  )
19
- @pytest.mark.parametrize("fiber_space", [None, "P_1", "P_2"])
19
+ @pytest.mark.parametrize("fiber_space", [None, "P_1", "P_2", "Quadrature_2"])
20
20
  def test_script(fiber_space, script, tmp_path: Path):
21
21
  runner = CliRunner()
22
22