cardiac-geometriesx 0.5.1__tar.gz → 0.5.2__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 (27) hide show
  1. {cardiac_geometriesx-0.5.1/src/cardiac_geometriesx.egg-info → cardiac_geometriesx-0.5.2}/PKG-INFO +6 -2
  2. {cardiac_geometriesx-0.5.1 → cardiac_geometriesx-0.5.2}/README.md +5 -1
  3. {cardiac_geometriesx-0.5.1 → cardiac_geometriesx-0.5.2}/pyproject.toml +2 -2
  4. cardiac_geometriesx-0.5.2/src/cardiac_geometries/fibers/__init__.py +66 -0
  5. {cardiac_geometriesx-0.5.1 → cardiac_geometriesx-0.5.2}/src/cardiac_geometries/fibers/cylinder.py +1 -0
  6. {cardiac_geometriesx-0.5.1 → cardiac_geometriesx-0.5.2}/src/cardiac_geometries/fibers/lv_ellipsoid.py +1 -0
  7. {cardiac_geometriesx-0.5.1 → cardiac_geometriesx-0.5.2}/src/cardiac_geometries/fibers/slab.py +1 -1
  8. {cardiac_geometriesx-0.5.1 → cardiac_geometriesx-0.5.2}/src/cardiac_geometries/geometry.py +183 -1
  9. {cardiac_geometriesx-0.5.1 → cardiac_geometriesx-0.5.2}/src/cardiac_geometries/mesh.py +203 -78
  10. {cardiac_geometriesx-0.5.1 → cardiac_geometriesx-0.5.2}/src/cardiac_geometries/utils.py +1 -1
  11. {cardiac_geometriesx-0.5.1 → cardiac_geometriesx-0.5.2/src/cardiac_geometriesx.egg-info}/PKG-INFO +6 -2
  12. {cardiac_geometriesx-0.5.1 → cardiac_geometriesx-0.5.2}/src/cardiac_geometriesx.egg-info/SOURCES.txt +1 -0
  13. {cardiac_geometriesx-0.5.1 → cardiac_geometriesx-0.5.2}/tests/test_cli.py +5 -1
  14. cardiac_geometriesx-0.5.2/tests/test_refinement.py +96 -0
  15. cardiac_geometriesx-0.5.1/src/cardiac_geometries/fibers/__init__.py +0 -4
  16. {cardiac_geometriesx-0.5.1 → cardiac_geometriesx-0.5.2}/LICENSE +0 -0
  17. {cardiac_geometriesx-0.5.1 → cardiac_geometriesx-0.5.2}/setup.cfg +0 -0
  18. {cardiac_geometriesx-0.5.1 → cardiac_geometriesx-0.5.2}/src/cardiac_geometries/__init__.py +0 -0
  19. {cardiac_geometriesx-0.5.1 → cardiac_geometriesx-0.5.2}/src/cardiac_geometries/cli.py +0 -0
  20. {cardiac_geometriesx-0.5.1 → cardiac_geometriesx-0.5.2}/src/cardiac_geometries/fibers/utils.py +0 -0
  21. {cardiac_geometriesx-0.5.1 → cardiac_geometriesx-0.5.2}/src/cardiac_geometries/gui.py +0 -0
  22. {cardiac_geometriesx-0.5.1 → cardiac_geometriesx-0.5.2}/src/cardiac_geometriesx.egg-info/dependency_links.txt +0 -0
  23. {cardiac_geometriesx-0.5.1 → cardiac_geometriesx-0.5.2}/src/cardiac_geometriesx.egg-info/entry_points.txt +0 -0
  24. {cardiac_geometriesx-0.5.1 → cardiac_geometriesx-0.5.2}/src/cardiac_geometriesx.egg-info/not-zip-safe +0 -0
  25. {cardiac_geometriesx-0.5.1 → cardiac_geometriesx-0.5.2}/src/cardiac_geometriesx.egg-info/requires.txt +0 -0
  26. {cardiac_geometriesx-0.5.1 → cardiac_geometriesx-0.5.2}/src/cardiac_geometriesx.egg-info/top_level.txt +0 -0
  27. {cardiac_geometriesx-0.5.1 → cardiac_geometriesx-0.5.2}/tests/test_save_load.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: cardiac-geometriesx
3
- Version: 0.5.1
3
+ Version: 0.5.2
4
4
  Summary: A python library for cardiac geometries
5
5
  Author-email: Henrik Finsberg <henriknf@simula.no>
6
6
  License: MIT
@@ -77,7 +77,11 @@ To install the package you can use `pip`
77
77
  ```
78
78
  python3 -m pip install cardiac-geometriesx
79
79
  ```
80
- however, this assumes that you already have `dolfinx` pre-installed. You can also use the provided docker image e.g
80
+ however, this assumes that you already have `dolfinx` pre-installed. You can also use `conda`
81
+ ```
82
+ conda install -c conda-forge cardiac-geometriesx
83
+ ```
84
+ or the provided docker image
81
85
  ```
82
86
  docker pull ghcr.io/computationalphysiology/cardiac-geometriesx:latest
83
87
  ```
@@ -26,7 +26,11 @@ To install the package you can use `pip`
26
26
  ```
27
27
  python3 -m pip install cardiac-geometriesx
28
28
  ```
29
- however, this assumes that you already have `dolfinx` pre-installed. You can also use the provided docker image e.g
29
+ however, this assumes that you already have `dolfinx` pre-installed. You can also use `conda`
30
+ ```
31
+ conda install -c conda-forge cardiac-geometriesx
32
+ ```
33
+ or the provided docker image
30
34
  ```
31
35
  docker pull ghcr.io/computationalphysiology/cardiac-geometriesx:latest
32
36
  ```
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "cardiac-geometriesx"
7
- version = "0.5.1"
7
+ version = "0.5.2"
8
8
  description = "A python library for cardiac geometries"
9
9
  authors = [{name = "Henrik Finsberg", email = "henriknf@simula.no"}]
10
10
  license = {text = "MIT"}
@@ -177,7 +177,7 @@ tag = true
177
177
  sign_tags = false
178
178
  tag_name = "v{new_version}"
179
179
  tag_message = "Bump version: {current_version} → {new_version}"
180
- current_version = "0.5.1"
180
+ current_version = "0.5.2"
181
181
 
182
182
 
183
183
  [[tool.bumpversion.files]]
@@ -0,0 +1,66 @@
1
+ import logging
2
+
3
+ import dolfinx
4
+
5
+ from . import cylinder, lv_ellipsoid, slab, utils
6
+ from .utils import Microstructure
7
+
8
+ __all__ = ["lv_ellipsoid", "slab", "cylinder", "utils", "Microstructure"]
9
+
10
+ logger = logging.getLogger(__name__)
11
+
12
+
13
+ supported_mesh_types = ("slab", "cylinder", "lv_ellipsoid", "biv_ellipsoid", "ukb", "lv", "biv")
14
+
15
+
16
+ def generate_fibers(mesh_type: str, mesh: dolfinx.mesh.Mesh, **kwargs) -> Microstructure:
17
+ """Generate fibers based on the mesh type."""
18
+
19
+ kwargs = kwargs.copy()
20
+
21
+ # Map fiber_angle_endo and fiber_angle_epi to alpha_endo and alpha_epi
22
+ if "fiber_angle_endo" in kwargs:
23
+ kwargs["alpha_endo"] = kwargs.pop("fiber_angle_endo")
24
+ if "fiber_angle_epi" in kwargs:
25
+ kwargs["alpha_epi"] = kwargs.pop("fiber_angle_epi")
26
+ if "fiber_space" in kwargs:
27
+ kwargs["function_space"] = kwargs.pop("fiber_space")
28
+
29
+ if mesh_type == "slab":
30
+ return slab.create_microstructure(mesh, **kwargs)
31
+ elif mesh_type == "cylinder":
32
+ return cylinder.create_microstructure(mesh, **kwargs)
33
+ elif mesh_type == "lv_ellipsoid":
34
+ return lv_ellipsoid.create_microstructure(mesh, **kwargs)
35
+ else:
36
+ if mesh_type not in supported_mesh_types:
37
+ logger.warning(
38
+ f"Mesh type {mesh_type!r} is not recognized. "
39
+ f"Supported mesh types are: {supported_mesh_types!r}. "
40
+ "Lets try with LDRB algorithm to generate fibers.",
41
+ )
42
+
43
+ # Try with LDRB
44
+ try:
45
+ import ldrb
46
+ except ImportError as ex:
47
+ msg = (
48
+ "To create fibers you need to install the ldrb package "
49
+ "which you can install with pip install fenicsx-ldrb"
50
+ )
51
+ raise ImportError(msg) from ex
52
+
53
+ markers = kwargs.pop("markers", None)
54
+ clipped = kwargs.pop("clipped", False)
55
+
56
+ if mesh_type == "ukb" and markers is not None:
57
+ from ..mesh import transform_markers
58
+
59
+ markers = transform_markers(markers, clipped=clipped)
60
+
61
+ system = ldrb.dolfinx_ldrb(mesh=mesh, markers=markers, **kwargs)
62
+ return Microstructure(
63
+ f0=system.f0,
64
+ s0=system.s0,
65
+ n0=system.n0,
66
+ )
@@ -81,6 +81,7 @@ def create_microstructure(
81
81
  r_outer: float,
82
82
  function_space: str = "P_1",
83
83
  outdir: str | Path | None = None,
84
+ **kwargs,
84
85
  ) -> utils.Microstructure:
85
86
  """Generate microstructure for cylinder
86
87
 
@@ -203,6 +203,7 @@ def create_microstructure(
203
203
  alpha_epi: float = 60,
204
204
  long_axis: int = 0,
205
205
  outdir: str | Path | None = None,
206
+ **kwargs,
206
207
  ):
207
208
  endo_marker = markers["ENDO"][0]
208
209
  epi_marker = markers["EPI"][0]
@@ -11,7 +11,6 @@ def compute_system(
11
11
  t_func: dolfinx.fem.Function,
12
12
  alpha_endo: float = -60,
13
13
  alpha_epi: float = 60,
14
- **kwargs,
15
14
  ) -> utils.Microstructure:
16
15
  """Compute ldrb system for slab, assuming linear
17
16
  angle between endo and epi
@@ -90,6 +89,7 @@ def create_microstructure(
90
89
  alpha_epi: float,
91
90
  function_space: str = "P_1",
92
91
  outdir: str | Path | None = None,
92
+ **kwargs: dict,
93
93
  ) -> utils.Microstructure:
94
94
  """Generate microstructure for slab using LDRB algorithm
95
95
 
@@ -3,6 +3,7 @@ import logging
3
3
  import shutil
4
4
  from dataclasses import dataclass, field
5
5
  from pathlib import Path
6
+ from typing import Any
6
7
 
7
8
  from mpi4py import MPI
8
9
 
@@ -10,6 +11,7 @@ import adios2
10
11
  import adios4dolfinx
11
12
  import dolfinx
12
13
  import numpy as np
14
+ import ufl
13
15
  from packaging.version import Version
14
16
 
15
17
  from . import utils
@@ -17,7 +19,7 @@ from . import utils
17
19
  logger = logging.getLogger(__name__)
18
20
 
19
21
 
20
- @dataclass # (frozen=True, slots=True)
22
+ @dataclass
21
23
  class Geometry:
22
24
  mesh: dolfinx.mesh.Mesh
23
25
  markers: dict[str, tuple[int, int]] = field(default_factory=dict)
@@ -28,8 +30,17 @@ class Geometry:
28
30
  f0: dolfinx.fem.Function | None = None
29
31
  s0: dolfinx.fem.Function | None = None
30
32
  n0: dolfinx.fem.Function | None = None
33
+ info: dict[str, Any] = field(default_factory=dict)
31
34
 
32
35
  def save(self, path: str | Path) -> None:
36
+ """Save the geometry to a file using adios4dolfinx.
37
+
38
+ Parameters
39
+ ----------
40
+ path : str | Path
41
+ The path to the file where the geometry will be saved.
42
+ The file will be created if it does not exist, or overwritten if it does.
43
+ """
33
44
  path = Path(path)
34
45
 
35
46
  shutil.rmtree(path, ignore_errors=True)
@@ -97,6 +108,123 @@ class Geometry:
97
108
 
98
109
  self.mesh.comm.barrier()
99
110
 
111
+ @property
112
+ def dx(self):
113
+ """Volume measure for the mesh using
114
+ the cell function `cfun` if it exists as subdomain data.
115
+ """
116
+ return ufl.Measure("dx", domain=self.mesh, subdomain_data=self.cfun)
117
+
118
+ @property
119
+ def ds(self):
120
+ """Surface measure for the mesh using
121
+ the facet function `ffun` if it exists as subdomain data.
122
+ """
123
+ return ufl.Measure("ds", domain=self.mesh, subdomain_data=self.ffun)
124
+
125
+ @property
126
+ def facet_normal(self) -> ufl.FacetNormal:
127
+ """Facet normal vector for the mesh."""
128
+ return ufl.FacetNormal(self.mesh)
129
+
130
+ def refine(self, n=1, outdir: Path | None = None) -> "Geometry":
131
+ """
132
+ Refine the mesh and transfer the meshtags to new geometry.
133
+ Also regenerate fibers if `self.info` is found.
134
+ If `self.info` is not found, it currently raises a
135
+ NotImplementedError, however fiber could be interpolated
136
+ from the old mesh to the new mesh but this will result in a
137
+ loss of information about the fiber orientation.
138
+
139
+ Parameters
140
+ ----------
141
+ n : int, optional
142
+ Number of times to refine the mesh, by default 1
143
+ outdir : Path | None, optional
144
+ Output directory to save the refined mesh and meshtags,
145
+ by default None in which case the mesh is not saved.
146
+
147
+ Returns
148
+ -------
149
+ Geometry
150
+ A new Geometry object with the refined mesh and updated meshtags.
151
+
152
+ Raises
153
+ ------
154
+ NotImplementedError
155
+ If `self.info` is not found, indicating that fiber
156
+ interpolation after refinement is not implemented yet.
157
+ """
158
+ mesh = self.mesh
159
+ cfun = self.cfun
160
+ ffun = self.ffun
161
+
162
+ for _ in range(n):
163
+ new_mesh, parent_cell, parent_facet = dolfinx.mesh.refine(
164
+ mesh,
165
+ partitioner=None,
166
+ option=dolfinx.mesh.RefinementOption.parent_cell_and_facet,
167
+ )
168
+ new_mesh.name = mesh.name
169
+ mesh = new_mesh
170
+ new_mesh.topology.create_entities(1)
171
+ new_mesh.topology.create_connectivity(2, 3)
172
+ if cfun is not None:
173
+ new_cfun = dolfinx.mesh.transfer_meshtag(cfun, new_mesh, parent_cell, parent_facet)
174
+ new_cfun.name = cfun.name
175
+ cfun = new_cfun
176
+ else:
177
+ new_cfun = None
178
+ if ffun is not None:
179
+ new_ffun = dolfinx.mesh.transfer_meshtag(ffun, new_mesh, parent_cell, parent_facet)
180
+ new_ffun.name = ffun.name
181
+ ffun = new_ffun
182
+ else:
183
+ new_ffun = None
184
+
185
+ if outdir is not None:
186
+ outdir = Path(outdir)
187
+ outdir.mkdir(parents=True, exist_ok=True)
188
+ with dolfinx.io.XDMFFile(new_mesh.comm, outdir / "mesh.xdmf", "w") as xdmf:
189
+ xdmf.write_mesh(new_mesh)
190
+ xdmf.write_meshtags(
191
+ cfun,
192
+ new_mesh.geometry,
193
+ geometry_xpath=f"/Xdmf/Domain/Grid[@Name='{new_mesh.name}']/Geometry",
194
+ )
195
+ mesh.topology.create_connectivity(2, 3)
196
+ xdmf.write_meshtags(
197
+ ffun,
198
+ new_mesh.geometry,
199
+ geometry_xpath=f"/Xdmf/Domain/Grid[@Name='{new_mesh.name}']/Geometry",
200
+ )
201
+
202
+ if self.info is not None:
203
+ info = self.info.copy()
204
+ info["refinement"] = n
205
+ info.pop("outdir", None)
206
+ if outdir is not None:
207
+ info["outdir"] = str(outdir)
208
+ from .fibers import generate_fibers
209
+
210
+ f0, s0, n0 = generate_fibers(mesh=new_mesh, ffun=new_ffun, markers=self.markers, **info)
211
+ else:
212
+ info = None
213
+ # Interpolate fibers
214
+ raise NotImplementedError(
215
+ "Interpolating fibers after refinement is not implemented yet."
216
+ )
217
+
218
+ return Geometry(
219
+ mesh=new_mesh,
220
+ markers=self.markers,
221
+ cfun=new_cfun,
222
+ ffun=new_ffun,
223
+ f0=f0,
224
+ s0=s0,
225
+ n0=n0,
226
+ )
227
+
100
228
  @classmethod
101
229
  def from_file(
102
230
  cls,
@@ -104,6 +232,24 @@ class Geometry:
104
232
  path: str | Path,
105
233
  function_space_data: dict[str, np.ndarray] | None = None,
106
234
  ) -> "Geometry":
235
+ """Read geometry from a file using adios4dolfinx.
236
+
237
+ Parameters
238
+ ----------
239
+ comm : MPI.Intracomm
240
+ The MPI communicator to use for reading the mesh.
241
+ path : str | Path
242
+ The path to the file containing the geometry data.
243
+ function_space_data : dict[str, np.ndarray] | None, optional
244
+ A dictionary containing function space data for the functions to be read.
245
+ If None, it will be read from the file.
246
+
247
+ Returns
248
+ -------
249
+ Geometry
250
+ An instance of the Geometry class containing the mesh, markers, and functions.
251
+ """
252
+
107
253
  path = Path(path)
108
254
 
109
255
  mesh = adios4dolfinx.read_mesh(comm=comm, filename=path)
@@ -148,6 +294,31 @@ class Geometry:
148
294
 
149
295
  @classmethod
150
296
  def from_folder(cls, comm: MPI.Intracomm, folder: str | Path) -> "Geometry":
297
+ """Read geometry from a folder containing mesh and markers files.
298
+
299
+ Parameters
300
+ ----------
301
+ comm : MPI.Intracomm
302
+ The MPI communicator to use for reading the mesh and markers.
303
+ folder : str | Path
304
+ The path to the folder containing the geometry data.
305
+ The folder should contain the following files:
306
+ - mesh.xdmf: The mesh file in XDMF format.
307
+ - markers.json: A JSON file containing markers.
308
+ - microstructure.json: A JSON file containing microstructure data (optional).
309
+ - microstructure.bp: A BP file containing microstructure functions (optional).
310
+ - info.json: A JSON file containing additional information (optional).
311
+
312
+ Returns
313
+ -------
314
+ Geometry
315
+ An instance of the Geometry class containing the mesh, markers, and functions.
316
+
317
+ Raises
318
+ ------
319
+ ValueError
320
+ If the required mesh file is not found in the specified folder.
321
+ """
151
322
  folder = Path(folder)
152
323
  logger.info(f"Reading geometry from {folder}")
153
324
  # Read mesh
@@ -196,9 +367,20 @@ class Geometry:
196
367
  else:
197
368
  functions[name] = f
198
369
 
370
+ if (folder / "info.json").exists():
371
+ logger.debug("Reading info")
372
+ if comm.rank == 0:
373
+ info = json.loads((folder / "info.json").read_text())
374
+ else:
375
+ info = {}
376
+ info = comm.bcast(info, root=0)
377
+ else:
378
+ info = {}
379
+
199
380
  return cls(
200
381
  mesh=mesh,
201
382
  markers=markers,
383
+ info=info,
202
384
  **functions,
203
385
  **tags,
204
386
  )
@@ -10,6 +10,7 @@ from mpi4py import MPI
10
10
 
11
11
  import cardiac_geometries_core as cgc
12
12
  import dolfinx
13
+ import numpy as np
13
14
  from structlog import get_logger
14
15
 
15
16
  from . import utils
@@ -22,19 +23,80 @@ __version__ = meta["Version"]
22
23
  logger = get_logger()
23
24
 
24
25
 
26
+ def transform_markers(
27
+ markers: dict[str, tuple[int, int]], clipped: bool = False
28
+ ) -> dict[str, list[int]]:
29
+ if clipped:
30
+ return {
31
+ "lv": [markers["LV"][0]],
32
+ "rv": [markers["RV"][0]],
33
+ "epi": [markers["EPI"][0]],
34
+ "base": [markers["BASE"][0]],
35
+ }
36
+ else:
37
+ return {
38
+ "lv": [markers["LV"][0]],
39
+ "rv": [markers["RV"][0]],
40
+ "epi": [markers["EPI"][0]],
41
+ "base": [
42
+ markers["PV"][0],
43
+ markers["TV"][0],
44
+ markers["AV"][0],
45
+ markers["MV"][0],
46
+ ],
47
+ }
48
+
49
+
25
50
  def ukb(
26
51
  outdir: str | Path,
27
52
  mode: int = -1,
28
53
  std: float = 1.5,
29
54
  case: str = "ED",
30
- char_length_max: float = 2.0,
31
- char_length_min: float = 2.0,
55
+ char_length_max: float = 5.0,
56
+ char_length_min: float = 5.0,
32
57
  fiber_angle_endo: float = 60,
33
58
  fiber_angle_epi: float = -60,
59
+ create_fibers: bool = True,
34
60
  fiber_space: str = "P_1",
35
61
  clipped: bool = False,
36
62
  comm: MPI.Comm = MPI.COMM_WORLD,
37
63
  ) -> Geometry:
64
+ """Create a mesh from the UK-Biobank atlas using
65
+ the ukb-atlas package.
66
+
67
+ Parameters
68
+ ----------
69
+ outdir : str | Path
70
+ Directory where to save the results.
71
+ mode : int, optional
72
+ Mode for the UKB mesh, by default -1
73
+ std : float, optional
74
+ Standard deviation for the UKB mesh, by default 1.5
75
+ case : str, optional
76
+ Case for the UKB mesh (either "ED" or "ES"), by default "ED"
77
+ char_length_max : float, optional
78
+ Maximum characteristic length of the mesh, by default 2.0
79
+ char_length_min : float, optional
80
+ Minimum characteristic length of the mesh, by default 2.0
81
+ fiber_angle_endo : float, optional
82
+ Fiber angle for the endocardium, by default 60
83
+ fiber_angle_epi : float, optional
84
+ Fiber angle for the epicardium, by default -60
85
+ create_fibers : bool, optional
86
+ If True create rule-based fibers, by default True
87
+ fiber_space : str, optional
88
+ Function space for fibers of the form family_degree, by default "P_1"
89
+ clipped : bool, optional
90
+ If True create a clipped mesh, by default False
91
+ comm : MPI.Comm, optional
92
+ MPI communicator, by default MPI.COMM_WORLD
93
+
94
+ Returns
95
+ -------
96
+ cardiac_geometries.geometry.Geometry
97
+ A Geometry with the mesh, markers, markers functions and fibers.
98
+
99
+ """
38
100
  try:
39
101
  import ukb.cli
40
102
  except ImportError as e:
@@ -87,67 +149,51 @@ def ukb(
87
149
  "fiber_angle_epi": fiber_angle_epi,
88
150
  "fiber_space": fiber_space,
89
151
  "cardiac_geometry_version": __version__,
90
- "type": "ukb",
152
+ "mesh_type": "ukb",
153
+ "clipped": clipped,
91
154
  "timestamp": datetime.datetime.now().isoformat(),
92
155
  }
93
156
  )
94
157
  )
95
158
 
96
- try:
97
- import ldrb
98
- except ImportError:
99
- msg = (
100
- "To create fibers you need to install the ldrb package "
101
- "which you can install with pip install fenicsx-ldrb"
102
- )
103
- raise ImportError(msg)
159
+ if create_fibers:
160
+ try:
161
+ import ldrb
162
+ except ImportError as ex:
163
+ msg = (
164
+ "To create fibers you need to install the ldrb package "
165
+ "which you can install with pip install fenicsx-ldrb"
166
+ )
167
+ raise ImportError(msg) from ex
104
168
 
105
- if clipped:
106
- markers = {
107
- "lv": [geometry.markers["LV"][0]],
108
- "rv": [geometry.markers["RV"][0]],
109
- "epi": [geometry.markers["EPI"][0]],
110
- "base": [geometry.markers["BASE"][0]],
111
- }
112
- else:
113
- markers = {
114
- "lv": [geometry.markers["LV"][0]],
115
- "rv": [geometry.markers["RV"][0]],
116
- "epi": [geometry.markers["EPI"][0]],
117
- "base": [
118
- geometry.markers["PV"][0],
119
- geometry.markers["TV"][0],
120
- geometry.markers["AV"][0],
121
- geometry.markers["MV"][0],
122
- ],
123
- }
124
- system = ldrb.dolfinx_ldrb(
125
- mesh=geometry.mesh,
126
- ffun=geometry.ffun,
127
- markers=markers,
128
- alpha_endo_lv=fiber_angle_endo,
129
- alpha_epi_lv=fiber_angle_epi,
130
- beta_endo_lv=0,
131
- beta_epi_lv=0,
132
- fiber_space=fiber_space,
133
- )
169
+ markers = transform_markers(geometry.markers, clipped=clipped)
170
+ system = ldrb.dolfinx_ldrb(
171
+ mesh=geometry.mesh,
172
+ ffun=geometry.ffun,
173
+ markers=markers,
174
+ alpha_endo_lv=fiber_angle_endo,
175
+ alpha_epi_lv=fiber_angle_epi,
176
+ beta_endo_lv=0,
177
+ beta_epi_lv=0,
178
+ fiber_space=fiber_space,
179
+ )
134
180
 
135
- save_microstructure(
136
- mesh=geometry.mesh,
137
- functions=(system.f0, system.s0, system.n0),
138
- outdir=outdir,
139
- )
181
+ save_microstructure(
182
+ mesh=geometry.mesh,
183
+ functions=(system.f0, system.s0, system.n0),
184
+ outdir=outdir,
185
+ )
140
186
 
141
- for k, v in system._asdict().items():
142
- if v is None:
143
- continue
144
- if fiber_space.startswith("Q"):
145
- # Cannot visualize Quadrature spaces yet
146
- continue
187
+ for k, v in system._asdict().items():
188
+ if v is None:
189
+ continue
190
+ if fiber_space.startswith("Q"):
191
+ # Cannot visualize Quadrature spaces yet
192
+ continue
147
193
 
148
- logger.debug(f"Write {k}: {v}")
149
- with dolfinx.io.VTXWriter(comm, outdir / f"{k}-viz.bp", [v], engine="BP4") as vtx:
150
- vtx.write(0.0)
194
+ logger.debug(f"Write {k}: {v}")
195
+ with dolfinx.io.VTXWriter(comm, outdir / f"{k}-viz.bp", [v], engine="BP4") as vtx:
196
+ vtx.write(0.0)
151
197
 
152
198
  geo = Geometry.from_folder(comm=comm, folder=outdir)
153
199
  return geo
@@ -272,10 +318,10 @@ def biv_ellipsoid(
272
318
  "b_epi_rv": b_epi_rv,
273
319
  "c_epi_rv": c_epi_rv,
274
320
  "create_fibers": create_fibers,
275
- "fibers_angle_endo": fiber_angle_endo,
276
- "fibers_angle_epi": fiber_angle_epi,
321
+ "fiber_angle_endo": fiber_angle_endo,
322
+ "fiber_angle_epi": fiber_angle_epi,
277
323
  "fiber_space": fiber_space,
278
- # "mesh_type": MeshTypes.biv_ellipsoid.value,
324
+ "mesh_type": "biv_ellipsoid",
279
325
  "cardiac_geometry_version": __version__,
280
326
  "timestamp": datetime.datetime.now().isoformat(),
281
327
  },
@@ -486,10 +532,10 @@ def biv_ellipsoid_torso(
486
532
  "b_epi_rv": b_epi_rv,
487
533
  "c_epi_rv": c_epi_rv,
488
534
  "create_fibers": create_fibers,
489
- "fibers_angle_endo": fiber_angle_endo,
490
- "fibers_angle_epi": fiber_angle_epi,
535
+ "fiber_angle_endo": fiber_angle_endo,
536
+ "fiber_angle_epi": fiber_angle_epi,
491
537
  "fiber_space": fiber_space,
492
- # "mesh_type": MeshTypes.biv_ellipsoid.value,
538
+ "mesh_type": "biv",
493
539
  "cardiac_geometry_version": __version__,
494
540
  "timestamp": datetime.datetime.now().isoformat(),
495
541
  },
@@ -640,11 +686,11 @@ def lv_ellipsoid(
640
686
  "mu_apex_epi": mu_apex_epi,
641
687
  "mu_base_epi": mu_base_epi,
642
688
  "create_fibers": create_fibers,
643
- "fibers_angle_endo": fiber_angle_endo,
644
- "fibers_angle_epi": fiber_angle_epi,
689
+ "fiber_angle_endo": fiber_angle_endo,
690
+ "fiber_angle_epi": fiber_angle_epi,
645
691
  "fiber_space": fiber_space,
646
692
  "aha": aha,
647
- # "mesh_type": MeshTypes.lv_ellipsoid.value,
693
+ "mesh_type": "lv_ellipsoid",
648
694
  "cardiac_geometry_version": __version__,
649
695
  "timestamp": datetime.datetime.now().isoformat(),
650
696
  },
@@ -717,6 +763,70 @@ def lv_ellipsoid(
717
763
  return geo
718
764
 
719
765
 
766
+ def slab_dolfinx(
767
+ comm, outdir: Path, lx: float = 20.0, ly: float = 7.0, lz: float = 3.0, dx: float = 1.0
768
+ ) -> utils.GMshGeometry:
769
+ mesh = dolfinx.mesh.create_box(
770
+ comm,
771
+ [[0.0, 0.0, 0.0], [lx, ly, lz]],
772
+ [int(lx / dx), int(ly / dx), int(lz / dx)],
773
+ dolfinx.mesh.CellType.tetrahedron,
774
+ ghost_mode=dolfinx.mesh.GhostMode.none,
775
+ )
776
+ mesh.name = "Mesh"
777
+ fdim = mesh.topology.dim - 1
778
+ x0_facets = dolfinx.mesh.locate_entities_boundary(mesh, fdim, lambda x: np.isclose(x[0], 0))
779
+ x1_facets = dolfinx.mesh.locate_entities_boundary(mesh, fdim, lambda x: np.isclose(x[0], lx))
780
+ y0_facets = dolfinx.mesh.locate_entities_boundary(mesh, fdim, lambda x: np.isclose(x[1], 0))
781
+ y1_facets = dolfinx.mesh.locate_entities_boundary(mesh, fdim, lambda x: np.isclose(x[1], ly))
782
+ z0_facets = dolfinx.mesh.locate_entities_boundary(mesh, fdim, lambda x: np.isclose(x[2], 0))
783
+ z1_facets = dolfinx.mesh.locate_entities_boundary(mesh, fdim, lambda x: np.isclose(x[2], lz))
784
+
785
+ # Concatenate and sort the arrays based on facet indices.
786
+ # Left facets marked with 1, right facets with two
787
+ marked_facets = np.hstack([x0_facets, x1_facets, y0_facets, y1_facets, z0_facets, z1_facets])
788
+
789
+ marked_values = np.hstack(
790
+ [
791
+ np.full_like(x0_facets, 1),
792
+ np.full_like(x1_facets, 2),
793
+ np.full_like(y0_facets, 3),
794
+ np.full_like(y1_facets, 4),
795
+ np.full_like(z0_facets, 5),
796
+ np.full_like(z1_facets, 6),
797
+ ],
798
+ )
799
+ sorted_facets = np.argsort(marked_facets)
800
+ ft = dolfinx.mesh.meshtags(
801
+ mesh,
802
+ fdim,
803
+ marked_facets[sorted_facets],
804
+ marked_values[sorted_facets],
805
+ )
806
+ ft.name = "Facet tags"
807
+ markers = {
808
+ "X0": (1, 2),
809
+ "X1": (2, 2),
810
+ "Y0": (3, 2),
811
+ "Y1": (4, 2),
812
+ "Z0": (5, 2),
813
+ "Z1": (6, 2),
814
+ }
815
+
816
+ with dolfinx.io.XDMFFile(comm, outdir / "mesh.xdmf", "w") as xdmf:
817
+ xdmf.write_mesh(mesh)
818
+ xdmf.write_meshtags(ft, mesh.geometry)
819
+
820
+ return utils.GMshGeometry(
821
+ mesh=mesh,
822
+ markers=markers,
823
+ cfun=None,
824
+ ffun=ft.indices,
825
+ efun=None,
826
+ vfun=None,
827
+ )
828
+
829
+
720
830
  def slab(
721
831
  outdir: Path | str,
722
832
  lx: float = 20.0,
@@ -728,6 +838,7 @@ def slab(
728
838
  fiber_angle_epi: float = -60,
729
839
  fiber_space: str = "P_1",
730
840
  verbose: bool = False,
841
+ use_dolfinx: bool = False,
731
842
  comm: MPI.Comm = MPI.COMM_WORLD,
732
843
  ) -> Geometry:
733
844
  """Create slab geometry
@@ -754,6 +865,8 @@ def slab(
754
865
  Function space for fibers of the form family_degree, by default "P_1"
755
866
  verbose : bool, optional
756
867
  If True print information from gmsh, by default False
868
+ use_dolfinx : bool, optional
869
+ If True use dolfinx to create the mesh, by default False (gmsh)
757
870
  comm : MPI.Comm, optional
758
871
  MPI communicator, by default MPI.COMM_WORLD
759
872
 
@@ -771,15 +884,15 @@ def slab(
771
884
  with open(outdir / "info.json", "w") as f:
772
885
  json.dump(
773
886
  {
774
- "lx": lx,
775
- "ly": ly,
776
- "lz": lz,
887
+ "Lx": lx,
888
+ "Ly": ly,
889
+ "Lz": lz,
777
890
  "dx": dx,
778
891
  "create_fibers": create_fibers,
779
- "fibers_angle_endo": fiber_angle_endo,
780
- "fibers_angle_epi": fiber_angle_epi,
892
+ "fiber_angle_endo": fiber_angle_endo,
893
+ "fiber_angle_epi": fiber_angle_epi,
781
894
  "fiber_space": fiber_space,
782
- # "mesh_type": MeshTypes.slab.value,
895
+ "mesh_type": "slab",
783
896
  "cardiac_geometry_version": __version__,
784
897
  "timestamp": datetime.datetime.now().isoformat(),
785
898
  },
@@ -788,17 +901,29 @@ def slab(
788
901
  default=utils.json_serial,
789
902
  )
790
903
 
791
- cgc.slab(
792
- mesh_name=mesh_name.as_posix(),
904
+ if not use_dolfinx:
905
+ cgc.slab(
906
+ mesh_name=mesh_name.as_posix(),
907
+ lx=lx,
908
+ ly=ly,
909
+ lz=lz,
910
+ dx=dx,
911
+ verbose=verbose,
912
+ )
913
+ comm.barrier()
914
+
915
+ if use_dolfinx:
916
+ geometry = slab_dolfinx(
917
+ comm=comm,
918
+ outdir=outdir,
793
919
  lx=lx,
794
920
  ly=ly,
795
921
  lz=lz,
796
922
  dx=dx,
797
- verbose=verbose,
798
923
  )
799
- comm.barrier()
800
924
 
801
- geometry = utils.gmsh2dolfin(comm=comm, msh_file=mesh_name)
925
+ else:
926
+ geometry = utils.gmsh2dolfin(comm=comm, msh_file=mesh_name)
802
927
 
803
928
  if comm.rank == 0:
804
929
  with open(outdir / "markers.json", "w") as f:
@@ -880,7 +1005,7 @@ def slab_in_bath(
880
1005
  "by": by,
881
1006
  "bz": bz,
882
1007
  "dx": dx,
883
- # "mesh_type": MeshTypes.slab.value,
1008
+ "mesh_type": "slab-bath",
884
1009
  "cardiac_geometry_version": __version__,
885
1010
  "timestamp": datetime.datetime.now().isoformat(),
886
1011
  },
@@ -975,11 +1100,11 @@ def cylinder(
975
1100
  "height": height,
976
1101
  "char_length": char_length,
977
1102
  "create_fibers": create_fibers,
978
- "fibers_angle_endo": fiber_angle_endo,
979
- "fibers_angle_epi": fiber_angle_epi,
1103
+ "fiber_angle_endo": fiber_angle_endo,
1104
+ "fiber_angle_epi": fiber_angle_epi,
980
1105
  "fiber_space": fiber_space,
981
1106
  "aha": aha,
982
- # "mesh_type": MeshTypes.lv_ellipsoid.value,
1107
+ "mesh_type": "cylinder",
983
1108
  "cardiac_geometry_version": __version__,
984
1109
  "timestamp": datetime.datetime.now().isoformat(),
985
1110
  },
@@ -403,7 +403,7 @@ def read_mesh(
403
403
  ) -> tuple[dolfinx.mesh.Mesh, dict[str, dolfinx.mesh.MeshTags]]:
404
404
  tags = {}
405
405
  with dolfinx.io.XDMFFile(comm, filename, "r") as xdmf:
406
- mesh = xdmf.read_mesh(name="Mesh")
406
+ mesh = xdmf.read_mesh(name="Mesh", ghost_mode=dolfinx.mesh.GhostMode.none)
407
407
  for var, name, dim in [
408
408
  ("cfun", "Cell tags", mesh.topology.dim),
409
409
  ("ffun", "Facet tags", mesh.topology.dim - 1),
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: cardiac-geometriesx
3
- Version: 0.5.1
3
+ Version: 0.5.2
4
4
  Summary: A python library for cardiac geometries
5
5
  Author-email: Henrik Finsberg <henriknf@simula.no>
6
6
  License: MIT
@@ -77,7 +77,11 @@ To install the package you can use `pip`
77
77
  ```
78
78
  python3 -m pip install cardiac-geometriesx
79
79
  ```
80
- however, this assumes that you already have `dolfinx` pre-installed. You can also use the provided docker image e.g
80
+ however, this assumes that you already have `dolfinx` pre-installed. You can also use `conda`
81
+ ```
82
+ conda install -c conda-forge cardiac-geometriesx
83
+ ```
84
+ or the provided docker image
81
85
  ```
82
86
  docker pull ghcr.io/computationalphysiology/cardiac-geometriesx:latest
83
87
  ```
@@ -20,4 +20,5 @@ src/cardiac_geometriesx.egg-info/not-zip-safe
20
20
  src/cardiac_geometriesx.egg-info/requires.txt
21
21
  src/cardiac_geometriesx.egg-info/top_level.txt
22
22
  tests/test_cli.py
23
+ tests/test_refinement.py
23
24
  tests/test_save_load.py
@@ -21,6 +21,8 @@ try:
21
21
  except ImportError:
22
22
  HAS_UKB = False
23
23
 
24
+ MPI_SIZE = MPI.COMM_WORLD.size
25
+
24
26
 
25
27
  @pytest.mark.parametrize(
26
28
  "script",
@@ -60,7 +62,8 @@ def test_biv_fibers(tmp_path: Path):
60
62
  path = comm.bcast(tmp_path, root=0)
61
63
 
62
64
  res = runner.invoke(
63
- cli.biv_ellipsoid, [path.as_posix()], "--create-fibers", "--fiber-space", "P_1"
65
+ cli.biv_ellipsoid,
66
+ [path.as_posix(), "--create-fibers", "--fiber-space", "P_1"],
64
67
  )
65
68
  assert res.exit_code == 0
66
69
  assert path.is_dir()
@@ -95,6 +98,7 @@ def test_script_no_fibers(script, tmp_path: Path):
95
98
  @pytest.mark.parametrize("clipped", [True, False])
96
99
  @pytest.mark.parametrize("case", ["ED", "ES"])
97
100
  @pytest.mark.skipif(not HAS_UKB, reason="UKB atlas is not installed")
101
+ @pytest.mark.skipif(MPI.COMM_WORLD.size > 1, reason="Pyvista operations is not parallelized yet")
98
102
  def test_ukb(tmp_path: Path, case: str, clipped: bool):
99
103
  runner = CliRunner()
100
104
 
@@ -0,0 +1,96 @@
1
+ from pathlib import Path
2
+
3
+ from mpi4py import MPI
4
+
5
+ import pytest
6
+
7
+ import cardiac_geometries as cg
8
+
9
+ try:
10
+ import ldrb # noqa: F401
11
+
12
+ HAS_LDRB = True
13
+ except ImportError:
14
+ HAS_LDRB = False
15
+
16
+ try:
17
+ import ukb.cli # noqa: F401
18
+
19
+ HAS_UKB = True
20
+ except ImportError:
21
+ HAS_UKB = False
22
+
23
+ MPI_SIZE = MPI.COMM_WORLD.size
24
+
25
+
26
+ @pytest.mark.parametrize(
27
+ "script",
28
+ [
29
+ cg.mesh.slab,
30
+ cg.mesh.lv_ellipsoid,
31
+ cg.mesh.cylinder,
32
+ ],
33
+ ids=["slab", "lv_ellipsoid", "cylinder"],
34
+ )
35
+ def test_refine_analytic_fibers(script, tmp_path: Path):
36
+ comm = MPI.COMM_WORLD
37
+ path = comm.bcast(tmp_path, root=0)
38
+ outdir = path / "mesh"
39
+ script(outdir=outdir, create_fibers=True, comm=comm)
40
+ geo = cg.Geometry.from_folder(comm=comm, folder=outdir)
41
+ assert (outdir / "mesh.xdmf").is_file()
42
+ assert geo.f0 is not None
43
+ # assert geo.mesh.geometry.dim == 3
44
+ refined_outdir = path / "refined"
45
+ refined = geo.refine(outdir=refined_outdir, n=1)
46
+ assert refined.f0 is not None
47
+ assert (refined_outdir / "mesh.xdmf").is_file()
48
+ assert (
49
+ refined.f0.function_space.dofmap.index_map.size_global
50
+ > geo.f0.function_space.dofmap.index_map.size_global
51
+ )
52
+ assert refined.mesh.geometry.index_map().size_global > geo.mesh.geometry.index_map().size_global
53
+
54
+
55
+ @pytest.mark.skipif(not HAS_LDRB, reason="LDRB atlas is not installed")
56
+ def test_refine_biv(tmp_path: Path):
57
+ comm = MPI.COMM_WORLD
58
+ path = comm.bcast(tmp_path, root=0)
59
+ outdir = path / "mesh"
60
+ cg.mesh.biv_ellipsoid(outdir=outdir, create_fibers=True, comm=comm)
61
+ geo = cg.Geometry.from_folder(comm=comm, folder=outdir)
62
+ assert geo.f0 is not None
63
+ assert (outdir / "mesh.xdmf").is_file()
64
+ # assert geo.mesh.geometry.dim == 3
65
+ refined_outdir = path / "refined"
66
+ refined = geo.refine(outdir=refined_outdir, n=1)
67
+ assert refined.f0 is not None
68
+ assert (refined_outdir / "mesh.xdmf").is_file()
69
+ assert (
70
+ refined.f0.function_space.dofmap.index_map.size_global
71
+ > geo.f0.function_space.dofmap.index_map.size_global
72
+ )
73
+ assert refined.mesh.geometry.index_map().size_global > geo.mesh.geometry.index_map().size_global
74
+
75
+
76
+ @pytest.mark.skipif(not HAS_LDRB, reason="LDRB atlas is not installed")
77
+ @pytest.mark.skipif(not HAS_UKB, reason="UKB atlas is not installed")
78
+ @pytest.mark.skipif(MPI.COMM_WORLD.size > 1, reason="Pyvista operations is not parallelized yet")
79
+ def test_refine_ukb(tmp_path: Path):
80
+ comm = MPI.COMM_WORLD
81
+ path = comm.bcast(tmp_path, root=0)
82
+ outdir = path / "mesh"
83
+ cg.mesh.ukb(outdir=outdir, create_fibers=True, comm=comm)
84
+ geo = cg.Geometry.from_folder(comm=comm, folder=outdir)
85
+ assert geo.f0 is not None
86
+ assert (outdir / "mesh.xdmf").is_file()
87
+ # assert geo.mesh.geometry.dim == 3
88
+ refined_outdir = path / "refined"
89
+ refined = geo.refine(outdir=refined_outdir, n=1)
90
+ assert refined.f0 is not None
91
+ assert (refined_outdir / "mesh.xdmf").is_file()
92
+ assert (
93
+ refined.f0.function_space.dofmap.index_map.size_global
94
+ > geo.f0.function_space.dofmap.index_map.size_global
95
+ )
96
+ assert refined.mesh.geometry.index_map().size_global > geo.mesh.geometry.index_map().size_global
@@ -1,4 +0,0 @@
1
- from . import cylinder, lv_ellipsoid, slab, utils
2
- from .utils import Microstructure
3
-
4
- __all__ = ["lv_ellipsoid", "slab", "cylinder", "utils", "Microstructure"]