cardiac-geometriesx 0.4.8__tar.gz → 0.5.1__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 (26) hide show
  1. {cardiac_geometriesx-0.4.8/src/cardiac_geometriesx.egg-info → cardiac_geometriesx-0.5.1}/PKG-INFO +1 -1
  2. {cardiac_geometriesx-0.4.8 → cardiac_geometriesx-0.5.1}/pyproject.toml +2 -2
  3. {cardiac_geometriesx-0.4.8 → cardiac_geometriesx-0.5.1}/src/cardiac_geometries/cli.py +97 -0
  4. cardiac_geometriesx-0.5.1/src/cardiac_geometries/fibers/__init__.py +4 -0
  5. cardiac_geometriesx-0.5.1/src/cardiac_geometries/fibers/cylinder.py +124 -0
  6. {cardiac_geometriesx-0.4.8 → cardiac_geometriesx-0.5.1}/src/cardiac_geometries/fibers/utils.py +10 -4
  7. {cardiac_geometriesx-0.4.8 → cardiac_geometriesx-0.5.1}/src/cardiac_geometries/mesh.py +110 -0
  8. {cardiac_geometriesx-0.4.8 → cardiac_geometriesx-0.5.1}/src/cardiac_geometries/utils.py +9 -2
  9. {cardiac_geometriesx-0.4.8 → cardiac_geometriesx-0.5.1/src/cardiac_geometriesx.egg-info}/PKG-INFO +1 -1
  10. {cardiac_geometriesx-0.4.8 → cardiac_geometriesx-0.5.1}/src/cardiac_geometriesx.egg-info/SOURCES.txt +1 -0
  11. {cardiac_geometriesx-0.4.8 → cardiac_geometriesx-0.5.1}/tests/test_cli.py +34 -1
  12. cardiac_geometriesx-0.4.8/src/cardiac_geometries/fibers/__init__.py +0 -4
  13. {cardiac_geometriesx-0.4.8 → cardiac_geometriesx-0.5.1}/LICENSE +0 -0
  14. {cardiac_geometriesx-0.4.8 → cardiac_geometriesx-0.5.1}/README.md +0 -0
  15. {cardiac_geometriesx-0.4.8 → cardiac_geometriesx-0.5.1}/setup.cfg +0 -0
  16. {cardiac_geometriesx-0.4.8 → cardiac_geometriesx-0.5.1}/src/cardiac_geometries/__init__.py +0 -0
  17. {cardiac_geometriesx-0.4.8 → cardiac_geometriesx-0.5.1}/src/cardiac_geometries/fibers/lv_ellipsoid.py +0 -0
  18. {cardiac_geometriesx-0.4.8 → cardiac_geometriesx-0.5.1}/src/cardiac_geometries/fibers/slab.py +0 -0
  19. {cardiac_geometriesx-0.4.8 → cardiac_geometriesx-0.5.1}/src/cardiac_geometries/geometry.py +0 -0
  20. {cardiac_geometriesx-0.4.8 → cardiac_geometriesx-0.5.1}/src/cardiac_geometries/gui.py +0 -0
  21. {cardiac_geometriesx-0.4.8 → cardiac_geometriesx-0.5.1}/src/cardiac_geometriesx.egg-info/dependency_links.txt +0 -0
  22. {cardiac_geometriesx-0.4.8 → cardiac_geometriesx-0.5.1}/src/cardiac_geometriesx.egg-info/entry_points.txt +0 -0
  23. {cardiac_geometriesx-0.4.8 → cardiac_geometriesx-0.5.1}/src/cardiac_geometriesx.egg-info/not-zip-safe +0 -0
  24. {cardiac_geometriesx-0.4.8 → cardiac_geometriesx-0.5.1}/src/cardiac_geometriesx.egg-info/requires.txt +0 -0
  25. {cardiac_geometriesx-0.4.8 → cardiac_geometriesx-0.5.1}/src/cardiac_geometriesx.egg-info/top_level.txt +0 -0
  26. {cardiac_geometriesx-0.4.8 → cardiac_geometriesx-0.5.1}/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.4.8
3
+ Version: 0.5.1
4
4
  Summary: A python library for cardiac geometries
5
5
  Author-email: Henrik Finsberg <henriknf@simula.no>
6
6
  License: MIT
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "cardiac-geometriesx"
7
- version = "0.4.8"
7
+ version = "0.5.1"
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.4.8"
180
+ current_version = "0.5.1"
181
181
 
182
182
 
183
183
  [[tool.bumpversion.files]]
@@ -973,6 +973,102 @@ def slab_in_bath(
973
973
  geo.save(outdir / "slab_in_bath.bp")
974
974
 
975
975
 
976
+ @click.command(help="Create BiV ellipsoidal geometry")
977
+ @click.argument(
978
+ "outdir",
979
+ required=True,
980
+ type=click.Path(
981
+ file_okay=False,
982
+ dir_okay=True,
983
+ writable=True,
984
+ readable=True,
985
+ resolve_path=True,
986
+ ),
987
+ )
988
+ @click.option(
989
+ "--char-length",
990
+ default=10.0,
991
+ type=float,
992
+ help="Characteristic length of mesh",
993
+ show_default=True,
994
+ )
995
+ @click.option(
996
+ "--r-inner",
997
+ default=10.0,
998
+ type=float,
999
+ help="Inner radius of the cylinder",
1000
+ show_default=True,
1001
+ )
1002
+ @click.option(
1003
+ "--r-outer",
1004
+ default=20.0,
1005
+ type=float,
1006
+ help="Outer radius of the cylinder",
1007
+ show_default=True,
1008
+ )
1009
+ @click.option(
1010
+ "--height",
1011
+ default=40.0,
1012
+ type=float,
1013
+ help="Height of the cylinder",
1014
+ show_default=True,
1015
+ )
1016
+ @click.option(
1017
+ "--create-fibers",
1018
+ default=False,
1019
+ is_flag=True,
1020
+ help="If True create analytic fibers",
1021
+ show_default=True,
1022
+ )
1023
+ @click.option(
1024
+ "--fiber-angle-endo",
1025
+ default=-60,
1026
+ type=float,
1027
+ help="Angle for the endocardium",
1028
+ show_default=True,
1029
+ )
1030
+ @click.option(
1031
+ "--fiber-angle-epi",
1032
+ default=+60,
1033
+ type=float,
1034
+ help="Angle for the epicardium",
1035
+ show_default=True,
1036
+ )
1037
+ @click.option(
1038
+ "--fiber-space",
1039
+ default="P_1",
1040
+ type=str,
1041
+ help="Function space for fibers of the form family_degree",
1042
+ show_default=True,
1043
+ )
1044
+ def cylinder(
1045
+ outdir: Path,
1046
+ char_length: float = 10.0,
1047
+ r_inner: float = 10.0,
1048
+ r_outer: float = 20.0,
1049
+ height: float = 40.0,
1050
+ create_fibers: bool = False,
1051
+ fiber_angle_endo: float = -60,
1052
+ fiber_angle_epi: float = +60,
1053
+ fiber_space: str = "P_1",
1054
+ ):
1055
+ outdir = Path(outdir)
1056
+ outdir.mkdir(exist_ok=True)
1057
+
1058
+ geo = mesh.cylinder(
1059
+ outdir=outdir,
1060
+ r_inner=r_inner,
1061
+ r_outer=r_outer,
1062
+ height=height,
1063
+ char_length=char_length,
1064
+ create_fibers=create_fibers,
1065
+ fiber_angle_endo=fiber_angle_endo,
1066
+ fiber_angle_epi=fiber_angle_epi,
1067
+ fiber_space=fiber_space,
1068
+ )
1069
+ geo.save(outdir / "cylinder.bp")
1070
+
1071
+
976
1072
  @click.command("gui")
977
1073
  def gui():
978
1074
  # Make sure we can import the required packages
@@ -991,3 +1087,4 @@ app.add_command(slab)
991
1087
  app.add_command(slab_in_bath)
992
1088
  app.add_command(gui)
993
1089
  app.add_command(ukb)
1090
+ app.add_command(cylinder)
@@ -0,0 +1,4 @@
1
+ from . import cylinder, lv_ellipsoid, slab, utils
2
+ from .utils import Microstructure
3
+
4
+ __all__ = ["lv_ellipsoid", "slab", "cylinder", "utils", "Microstructure"]
@@ -0,0 +1,124 @@
1
+ from pathlib import Path
2
+
3
+ import dolfinx
4
+ import numpy as np
5
+
6
+ from ..utils import space_from_string
7
+ from . import utils
8
+
9
+
10
+ def compute_system(
11
+ mesh: dolfinx.mesh.Mesh,
12
+ alpha_endo: float = -60,
13
+ alpha_epi: float = 60,
14
+ r_inner: float = 10.0,
15
+ r_outer: float = 20.0,
16
+ function_space: str = "P_1",
17
+ **kwargs,
18
+ ) -> utils.Microstructure:
19
+ """Compute ldrb system for cylinder, assuming linear
20
+ angle between endo and epi
21
+
22
+ Parameters
23
+ ----------
24
+ mesh : dolfinx.mesh.Mesh
25
+ A cylinder mesh
26
+ alpha_endo : float, optional
27
+ Angle on endocardium, by default -60
28
+ alpha_epi : float, optional
29
+ Angle on epicardium, by default 60
30
+ r_inner : float, optional
31
+ Inner radius, by default 10.0
32
+ r_outer : float, optional
33
+ Outer radius, by default 20.0
34
+ function_space : str, optional
35
+ Function space to interpolate the fibers, by default "P_1"
36
+ Returns
37
+ -------
38
+ Microstructure
39
+ Tuple with fiber, sheet and sheet normal
40
+ """
41
+
42
+ Vv = space_from_string(function_space, mesh, dim=3)
43
+
44
+ x, y, z = Vv.tabulate_dof_coordinates().T
45
+ r = np.sqrt(x**2 + y**2)
46
+
47
+ # Circumferential direction
48
+ e_r = np.array([x / r, y / r, np.zeros_like(r)])
49
+ e_theta = np.array([-y / r, x / r, np.zeros_like(r)])
50
+ e_z = np.array([np.zeros_like(r), np.zeros_like(r), np.ones_like(r)])
51
+
52
+ n0 = e_r
53
+ alpha = (alpha_endo + (alpha_epi - alpha_endo) * (r - r_inner) / (r_outer - r_inner)) * (
54
+ np.pi / 180
55
+ )
56
+
57
+ f0 = e_theta * np.cos(alpha) - e_z * np.sin(alpha)
58
+ s0 = e_theta * np.sin(alpha) + e_z * np.cos(alpha)
59
+
60
+ fiber = dolfinx.fem.Function(Vv)
61
+ norm_f = np.linalg.norm(f0, axis=0)
62
+ fiber.x.array[:] = (f0 / norm_f).T.reshape(-1)
63
+ fiber.name = "f0"
64
+
65
+ sheet = dolfinx.fem.Function(Vv)
66
+ sheet.x.array[:] = s0.T.reshape(-1)
67
+ sheet.name = "s0"
68
+
69
+ sheet_normal = dolfinx.fem.Function(Vv)
70
+ sheet_normal.x.array[:] = n0.T.reshape(-1)
71
+ sheet_normal.name = "n0"
72
+
73
+ return utils.Microstructure(f0=fiber, s0=sheet, n0=sheet_normal)
74
+
75
+
76
+ def create_microstructure(
77
+ mesh: dolfinx.mesh.Mesh,
78
+ alpha_endo: float,
79
+ alpha_epi: float,
80
+ r_inner: float,
81
+ r_outer: float,
82
+ function_space: str = "P_1",
83
+ outdir: str | Path | None = None,
84
+ ) -> utils.Microstructure:
85
+ """Generate microstructure for cylinder
86
+
87
+ Parameters
88
+ ----------
89
+ mesh : dolfinx.mesh.Mesh
90
+ A cylinder mesh
91
+ alpha_endo : float
92
+ Angle on the endocardium
93
+ alpha_epi : float
94
+ Angle on the epicardium
95
+ r_inner : float
96
+ Inner radius
97
+ r_outer : float
98
+ Outer radius
99
+ function_space : str
100
+ Function space to interpolate the fibers, by default P_1
101
+ outdir : Optional[Union[str, Path]], optional
102
+ Output directory to store the results, by default None.
103
+ If no output directory is specified the results will not be stored,
104
+ but only returned.
105
+
106
+ Returns
107
+ -------
108
+ Microstructure
109
+ Tuple with fiber, sheet and sheet normal
110
+ """
111
+
112
+ system = compute_system(
113
+ mesh=mesh,
114
+ function_space=function_space,
115
+ r_inner=r_inner,
116
+ r_outer=r_outer,
117
+ alpha_endo=alpha_endo,
118
+ alpha_epi=alpha_epi,
119
+ )
120
+
121
+ if outdir is not None:
122
+ utils.save_microstructure(mesh, system, outdir)
123
+
124
+ return system
@@ -1,4 +1,5 @@
1
1
  import json
2
+ import shutil
2
3
  from pathlib import Path
3
4
  from typing import NamedTuple, Sequence
4
5
 
@@ -32,20 +33,25 @@ def save_microstructure(
32
33
  if functions[0].function_space.ufl_element().family_name == "quadrature":
33
34
  from scifem.xdmf import XDMFFile
34
35
 
35
- with XDMFFile(Path(outdir) / "microstructure-viz.xdmf", functions) as xdmf:
36
+ fname = Path(outdir) / "microstructure-viz.xdmf"
37
+ fname.unlink(missing_ok=True)
38
+ fname.with_suffix(".h5").unlink(missing_ok=True)
39
+ with XDMFFile(fname, functions) as xdmf:
36
40
  xdmf.write(0.0)
37
41
 
38
42
  else:
43
+ fname = Path(outdir) / "microstructure-viz.bp"
44
+ shutil.rmtree(fname, ignore_errors=True)
39
45
  try:
40
- with dolfinx.io.VTXWriter(
41
- mesh.comm, Path(outdir) / "microstructure-viz.bp", functions, engine="BP4"
42
- ) as file:
46
+ with dolfinx.io.VTXWriter(mesh.comm, fname, functions, engine="BP4") as file:
43
47
  file.write(0.0)
44
48
  except RuntimeError as ex:
45
49
  print(f"Failed to write microstructure: {ex}")
46
50
 
47
51
  # Save with proper function space
52
+
48
53
  filename = Path(outdir) / "microstructure.bp"
54
+ shutil.rmtree(filename, ignore_errors=True)
49
55
  for function in functions:
50
56
  adios4dolfinx.write_function(u=function, filename=filename)
51
57
 
@@ -910,3 +910,113 @@ def slab_in_bath(
910
910
  geo = Geometry.from_folder(comm=comm, folder=outdir)
911
911
 
912
912
  return geo
913
+
914
+
915
+ def cylinder(
916
+ outdir: Path | str,
917
+ r_inner: float = 10.0,
918
+ r_outer: float = 20.0,
919
+ height: float = 40.0,
920
+ char_length: float = 10.0,
921
+ create_fibers: bool = False,
922
+ fiber_angle_endo: float = 60,
923
+ fiber_angle_epi: float = -60,
924
+ fiber_space: str = "P_1",
925
+ aha: bool = True,
926
+ verbose: bool = False,
927
+ comm: MPI.Comm = MPI.COMM_WORLD,
928
+ ) -> Geometry:
929
+ """Create an LV ellipsoidal geometry
930
+
931
+ Parameters
932
+ ----------
933
+ outdir : Optional[Path], optional
934
+ Directory where to save the results.
935
+ r_inner : float, optional
936
+ Radius on the endocardium layer, by default 10.0
937
+ r_outer : float, optional
938
+ Radius on the epicardium layer, by default 20.0
939
+ height : float, optional
940
+ Longest radius on the endocardium layer, by default 10.0
941
+ char_length : float, optional
942
+ Characteristic length of mesh, by default 10.0
943
+ create_fibers : bool, optional
944
+ If True create analytic fibers, by default False
945
+ fiber_angle_endo : float, optional
946
+ Angle for the endocardium, by default 60
947
+ fiber_angle_epi : float, optional
948
+ Angle for the epicardium, by default -60
949
+ fiber_space : str, optional
950
+ Function space for fibers of the form family_degree, by default "P_1"
951
+ aha : bool, optional
952
+ If True create 17-segment AHA regions
953
+ verbose : bool, optional
954
+ If True print information from gmsh, by default False
955
+ comm : MPI.Comm, optional
956
+ MPI communicator, by default MPI.COMM_WORLD
957
+
958
+ Returns
959
+ -------
960
+ cardiac_geometries.geometry.Geometry
961
+ A Geometry with the mesh, markers, markers functions and fibers.
962
+
963
+ """
964
+
965
+ outdir = Path(outdir)
966
+ mesh_name = outdir / "cylinder.msh"
967
+ if comm.rank == 0:
968
+ outdir.mkdir(exist_ok=True, parents=True)
969
+
970
+ with open(outdir / "info.json", "w") as f:
971
+ json.dump(
972
+ {
973
+ "r_inner": r_inner,
974
+ "r_outer": r_outer,
975
+ "height": height,
976
+ "char_length": char_length,
977
+ "create_fibers": create_fibers,
978
+ "fibers_angle_endo": fiber_angle_endo,
979
+ "fibers_angle_epi": fiber_angle_epi,
980
+ "fiber_space": fiber_space,
981
+ "aha": aha,
982
+ # "mesh_type": MeshTypes.lv_ellipsoid.value,
983
+ "cardiac_geometry_version": __version__,
984
+ "timestamp": datetime.datetime.now().isoformat(),
985
+ },
986
+ f,
987
+ indent=2,
988
+ default=utils.json_serial,
989
+ )
990
+
991
+ cgc.cylinder(
992
+ inner_radius=r_inner,
993
+ outer_radius=r_outer,
994
+ height=height,
995
+ mesh_name=mesh_name.as_posix(),
996
+ char_length=char_length,
997
+ verbose=verbose,
998
+ )
999
+ comm.barrier()
1000
+
1001
+ geometry = utils.gmsh2dolfin(comm=comm, msh_file=mesh_name)
1002
+
1003
+ if comm.rank == 0:
1004
+ with open(outdir / "markers.json", "w") as f:
1005
+ json.dump(geometry.markers, f, default=utils.json_serial)
1006
+
1007
+ if create_fibers:
1008
+ from .fibers.cylinder import create_microstructure
1009
+
1010
+ create_microstructure(
1011
+ mesh=geometry.mesh,
1012
+ function_space=fiber_space,
1013
+ r_inner=r_inner,
1014
+ r_outer=r_outer,
1015
+ alpha_endo=fiber_angle_endo,
1016
+ alpha_epi=fiber_angle_epi,
1017
+ outdir=outdir,
1018
+ )
1019
+
1020
+ geo = Geometry.from_folder(comm=comm, folder=outdir)
1021
+
1022
+ return geo
@@ -440,12 +440,19 @@ def gmsh2dolfin(comm: MPI.Intracomm, msh_file, rank: int = 0) -> GMshGeometry:
440
440
  ft = dolfinx.mesh.meshtags(
441
441
  mesh, tdim - 1, np.empty(0, dtype=np.int32), np.empty(0, dtype=np.int32)
442
442
  )
443
- et = mesh_data.edge_tags
443
+
444
+ if hasattr(mesh_data, "edge_tags"):
445
+ et = mesh_data.edge_tags
446
+ else:
447
+ et = mesh_data.ridge_tags
444
448
  if et is None:
445
449
  et = dolfinx.mesh.meshtags(
446
450
  mesh, tdim - 2, np.empty(0, dtype=np.int32), np.empty(0, dtype=np.int32)
447
451
  )
448
- vt = mesh_data.vertex_tags
452
+ if hasattr(mesh_data, "vertex_tags"):
453
+ vt = mesh_data.vertex_tags
454
+ else:
455
+ vt = mesh_data.peak_tags
449
456
  if vt is None:
450
457
  vt = dolfinx.mesh.meshtags(
451
458
  mesh, tdim - 3, np.empty(0, dtype=np.int32), np.empty(0, dtype=np.int32)
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: cardiac-geometriesx
3
- Version: 0.4.8
3
+ Version: 0.5.1
4
4
  Summary: A python library for cardiac geometries
5
5
  Author-email: Henrik Finsberg <henriknf@simula.no>
6
6
  License: MIT
@@ -8,6 +8,7 @@ src/cardiac_geometries/gui.py
8
8
  src/cardiac_geometries/mesh.py
9
9
  src/cardiac_geometries/utils.py
10
10
  src/cardiac_geometries/fibers/__init__.py
11
+ src/cardiac_geometries/fibers/cylinder.py
11
12
  src/cardiac_geometries/fibers/lv_ellipsoid.py
12
13
  src/cardiac_geometries/fibers/slab.py
13
14
  src/cardiac_geometries/fibers/utils.py
@@ -7,14 +7,29 @@ from click.testing import CliRunner
7
7
 
8
8
  from cardiac_geometries import Geometry, cli
9
9
 
10
+ try:
11
+ import ldrb # noqa: F401
12
+
13
+ HAS_LDRB = True
14
+ except ImportError:
15
+ HAS_LDRB = False
16
+
17
+ try:
18
+ import ukb.cli # noqa: F401
19
+
20
+ HAS_UKB = True
21
+ except ImportError:
22
+ HAS_UKB = False
23
+
10
24
 
11
25
  @pytest.mark.parametrize(
12
26
  "script",
13
27
  [
14
28
  cli.slab,
15
29
  cli.lv_ellipsoid,
30
+ cli.cylinder,
16
31
  ],
17
- ids=["slab", "lv_ellipsoid"],
32
+ ids=["slab", "lv_ellipsoid", "cylinder"],
18
33
  )
19
34
  @pytest.mark.parametrize("fiber_space", [None, "P_1", "P_2", "Quadrature_2", "DG_1"])
20
35
  def test_script(fiber_space, script, tmp_path: Path):
@@ -37,6 +52,23 @@ def test_script(fiber_space, script, tmp_path: Path):
37
52
  assert geo.f0 is not None
38
53
 
39
54
 
55
+ @pytest.mark.skipif(not HAS_LDRB, reason="LDRB atlas is not installed")
56
+ def test_biv_fibers(tmp_path: Path):
57
+ runner = CliRunner()
58
+
59
+ comm = MPI.COMM_WORLD
60
+ path = comm.bcast(tmp_path, root=0)
61
+
62
+ res = runner.invoke(
63
+ cli.biv_ellipsoid, [path.as_posix()], "--create-fibers", "--fiber-space", "P_1"
64
+ )
65
+ assert res.exit_code == 0
66
+ assert path.is_dir()
67
+
68
+ geo = Geometry.from_folder(comm=comm, folder=path)
69
+ assert geo.mesh.geometry.dim == 3
70
+
71
+
40
72
  @pytest.mark.parametrize(
41
73
  "script",
42
74
  [
@@ -62,6 +94,7 @@ def test_script_no_fibers(script, tmp_path: Path):
62
94
 
63
95
  @pytest.mark.parametrize("clipped", [True, False])
64
96
  @pytest.mark.parametrize("case", ["ED", "ES"])
97
+ @pytest.mark.skipif(not HAS_UKB, reason="UKB atlas is not installed")
65
98
  def test_ukb(tmp_path: Path, case: str, clipped: bool):
66
99
  runner = CliRunner()
67
100
 
@@ -1,4 +0,0 @@
1
- from . import lv_ellipsoid, slab, utils
2
- from .utils import Microstructure
3
-
4
- __all__ = ["lv_ellipsoid", "slab", "utils", "Microstructure"]