hexmb 0.2.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.
Files changed (84) hide show
  1. hexmb-0.2.1/LICENSE +21 -0
  2. hexmb-0.2.1/PKG-INFO +490 -0
  3. hexmb-0.2.1/README.md +452 -0
  4. hexmb-0.2.1/hexmb/__init__.py +7 -0
  5. hexmb-0.2.1/hexmb/assembly/__init__.py +0 -0
  6. hexmb-0.2.1/hexmb/assembly/ami.py +223 -0
  7. hexmb-0.2.1/hexmb/assembly/ogrid.py +144 -0
  8. hexmb-0.2.1/hexmb/assembly/splithex.py +701 -0
  9. hexmb-0.2.1/hexmb/assembly/sweep.py +74 -0
  10. hexmb-0.2.1/hexmb/cli.py +797 -0
  11. hexmb-0.2.1/hexmb/core/__init__.py +0 -0
  12. hexmb-0.2.1/hexmb/core/block.py +89 -0
  13. hexmb-0.2.1/hexmb/core/dedup.py +87 -0
  14. hexmb-0.2.1/hexmb/core/multiblock.py +219 -0
  15. hexmb-0.2.1/hexmb/core/tfi.py +140 -0
  16. hexmb-0.2.1/hexmb/core/topology.py +118 -0
  17. hexmb-0.2.1/hexmb/data/cap/GlobalHermiteParam.exelem +908 -0
  18. hexmb-0.2.1/hexmb/data/cap/README.md +26 -0
  19. hexmb-0.2.1/hexmb/data/cap/SCD0000101/SCD0000101_1.model.exnode +166 -0
  20. hexmb-0.2.1/hexmb/data/cap/SCD0000101/SCD0000101_10.model.exnode +166 -0
  21. hexmb-0.2.1/hexmb/data/cap/SCD0000101/SCD0000101_11.model.exnode +166 -0
  22. hexmb-0.2.1/hexmb/data/cap/SCD0000101/SCD0000101_12.model.exnode +166 -0
  23. hexmb-0.2.1/hexmb/data/cap/SCD0000101/SCD0000101_13.model.exnode +166 -0
  24. hexmb-0.2.1/hexmb/data/cap/SCD0000101/SCD0000101_14.model.exnode +166 -0
  25. hexmb-0.2.1/hexmb/data/cap/SCD0000101/SCD0000101_15.model.exnode +166 -0
  26. hexmb-0.2.1/hexmb/data/cap/SCD0000101/SCD0000101_16.model.exnode +166 -0
  27. hexmb-0.2.1/hexmb/data/cap/SCD0000101/SCD0000101_17.model.exnode +166 -0
  28. hexmb-0.2.1/hexmb/data/cap/SCD0000101/SCD0000101_18.model.exnode +166 -0
  29. hexmb-0.2.1/hexmb/data/cap/SCD0000101/SCD0000101_19.model.exnode +166 -0
  30. hexmb-0.2.1/hexmb/data/cap/SCD0000101/SCD0000101_2.model.exnode +166 -0
  31. hexmb-0.2.1/hexmb/data/cap/SCD0000101/SCD0000101_20.model.exnode +166 -0
  32. hexmb-0.2.1/hexmb/data/cap/SCD0000101/SCD0000101_3.model.exnode +166 -0
  33. hexmb-0.2.1/hexmb/data/cap/SCD0000101/SCD0000101_4.model.exnode +166 -0
  34. hexmb-0.2.1/hexmb/data/cap/SCD0000101/SCD0000101_5.model.exnode +166 -0
  35. hexmb-0.2.1/hexmb/data/cap/SCD0000101/SCD0000101_6.model.exnode +166 -0
  36. hexmb-0.2.1/hexmb/data/cap/SCD0000101/SCD0000101_7.model.exnode +166 -0
  37. hexmb-0.2.1/hexmb/data/cap/SCD0000101/SCD0000101_8.model.exnode +166 -0
  38. hexmb-0.2.1/hexmb/data/cap/SCD0000101/SCD0000101_9.model.exnode +166 -0
  39. hexmb-0.2.1/hexmb/io/__init__.py +0 -0
  40. hexmb-0.2.1/hexmb/io/cap.py +202 -0
  41. hexmb-0.2.1/hexmb/io/foam.py +91 -0
  42. hexmb-0.2.1/hexmb/io/stl_surface.py +44 -0
  43. hexmb-0.2.1/hexmb/io/surface.py +546 -0
  44. hexmb-0.2.1/hexmb/io/vtk.py +296 -0
  45. hexmb-0.2.1/hexmb/project/__init__.py +0 -0
  46. hexmb-0.2.1/hexmb/project/skeleton.py +173 -0
  47. hexmb-0.2.1/hexmb/project/stl_tube.py +225 -0
  48. hexmb-0.2.1/hexmb/services/__init__.py +0 -0
  49. hexmb-0.2.1/hexmb/services/cavity.py +109 -0
  50. hexmb-0.2.1/hexmb/services/export_foam.py +194 -0
  51. hexmb-0.2.1/hexmb/services/flow.py +261 -0
  52. hexmb-0.2.1/hexmb/services/morph.py +95 -0
  53. hexmb-0.2.1/hexmb/services/motion.py +600 -0
  54. hexmb-0.2.1/hexmb/services/quality.py +184 -0
  55. hexmb-0.2.1/hexmb/services/render.py +174 -0
  56. hexmb-0.2.1/hexmb/services/smooth.py +144 -0
  57. hexmb-0.2.1/hexmb/templates/__init__.py +0 -0
  58. hexmb-0.2.1/hexmb/templates/cavity.py +122 -0
  59. hexmb-0.2.1/hexmb/templates/conformal_lv.py +365 -0
  60. hexmb-0.2.1/hexmb/templates/lv.py +71 -0
  61. hexmb-0.2.1/hexmb/templates/stl.py +67 -0
  62. hexmb-0.2.1/hexmb/templates/telescoping_lv.py +209 -0
  63. hexmb-0.2.1/hexmb/templates/tube.py +80 -0
  64. hexmb-0.2.1/hexmb.egg-info/PKG-INFO +490 -0
  65. hexmb-0.2.1/hexmb.egg-info/SOURCES.txt +82 -0
  66. hexmb-0.2.1/hexmb.egg-info/dependency_links.txt +1 -0
  67. hexmb-0.2.1/hexmb.egg-info/entry_points.txt +2 -0
  68. hexmb-0.2.1/hexmb.egg-info/requires.txt +32 -0
  69. hexmb-0.2.1/hexmb.egg-info/top_level.txt +1 -0
  70. hexmb-0.2.1/pyproject.toml +46 -0
  71. hexmb-0.2.1/setup.cfg +4 -0
  72. hexmb-0.2.1/tests/test_ami.py +39 -0
  73. hexmb-0.2.1/tests/test_cap.py +104 -0
  74. hexmb-0.2.1/tests/test_cavity.py +165 -0
  75. hexmb-0.2.1/tests/test_cli_inputs.py +72 -0
  76. hexmb-0.2.1/tests/test_conformal_lv.py +98 -0
  77. hexmb-0.2.1/tests/test_core.py +99 -0
  78. hexmb-0.2.1/tests/test_export_flow.py +203 -0
  79. hexmb-0.2.1/tests/test_motion.py +284 -0
  80. hexmb-0.2.1/tests/test_ogrid_lv.py +66 -0
  81. hexmb-0.2.1/tests/test_splithex.py +190 -0
  82. hexmb-0.2.1/tests/test_surface.py +145 -0
  83. hexmb-0.2.1/tests/test_telescoping_lv.py +59 -0
  84. hexmb-0.2.1/tests/test_tube.py +83 -0
hexmb-0.2.1/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 hexmb contributors
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
hexmb-0.2.1/PKG-INFO ADDED
@@ -0,0 +1,490 @@
1
+ Metadata-Version: 2.4
2
+ Name: hexmb
3
+ Version: 0.2.1
4
+ Summary: Structured O-grid (butterfly) all-hex mesh engine for tubes and beating heart chambers, with OpenFOAM export and moving-mesh cases.
5
+ Author: Jie Wang
6
+ License-Expression: MIT
7
+ Project-URL: Repository, https://github.com/JieWangnk/hexmb
8
+ Keywords: mesh,hexahedral,O-grid,OpenFOAM,CFD,left ventricle,moving mesh
9
+ Requires-Python: >=3.10
10
+ Description-Content-Type: text/markdown
11
+ License-File: LICENSE
12
+ Requires-Dist: numpy>=1.23
13
+ Requires-Dist: scipy>=1.9
14
+ Provides-Extra: scipy
15
+ Requires-Dist: scipy>=1.9; extra == "scipy"
16
+ Provides-Extra: surface
17
+ Requires-Dist: trimesh>=3.20; extra == "surface"
18
+ Requires-Dist: rtree>=1.0; extra == "surface"
19
+ Requires-Dist: meshio>=5.0; extra == "surface"
20
+ Provides-Extra: stl
21
+ Requires-Dist: trimesh>=3.20; extra == "stl"
22
+ Requires-Dist: rtree>=1.0; extra == "stl"
23
+ Provides-Extra: image
24
+ Requires-Dist: nibabel>=5.0; extra == "image"
25
+ Requires-Dist: scikit-image>=0.21; extra == "image"
26
+ Provides-Extra: render
27
+ Requires-Dist: matplotlib>=3.6; extra == "render"
28
+ Provides-Extra: all
29
+ Requires-Dist: trimesh>=3.20; extra == "all"
30
+ Requires-Dist: rtree>=1.0; extra == "all"
31
+ Requires-Dist: meshio>=5.0; extra == "all"
32
+ Requires-Dist: nibabel>=5.0; extra == "all"
33
+ Requires-Dist: scikit-image>=0.21; extra == "all"
34
+ Requires-Dist: matplotlib>=3.6; extra == "all"
35
+ Provides-Extra: dev
36
+ Requires-Dist: pytest>=7; extra == "dev"
37
+ Dynamic: license-file
38
+
39
+ # hexmb — structured O-grid hex mesher for tubes and beating heart chambers
40
+
41
+ `hexmb` builds all-hexahedral **O-grid (butterfly)** meshes for CFD and writes
42
+ them directly as an OpenFOAM `constant/polyMesh`. An O-grid is a square "H" core
43
+ block surrounded by boundary-fitted petal blocks — all-hex, boundary-conforming,
44
+ and with **no singular polar axis**, so it can be rebuilt as one iso-topological
45
+ mesh through large deformation (a beating heart chamber) without remeshing.
46
+
47
+ **hexmb is the mesher: you give it a surface, it gives you the hex mesh.** The
48
+ surface can be a tubular STL, a closed or open left-ventricle surface in any
49
+ common mesh format, a segmentation mask, a Cardiac Atlas model, or an ordered
50
+ ring grid `(stations, angle, 3)` — from any segmentation, whoever produced it.
51
+ For a moving chamber you give one surface per phase and get back the moving-mesh
52
+ case.
53
+
54
+ One subject of the **public-domain** Cardiac Atlas Project (Sunnybrook Cardiac
55
+ Data, CC0) ships inside the package, so the whole left-ventricle pipeline runs
56
+ with no private data and no download: `hexmb demo`. See [`DATA.md`](DATA.md).
57
+
58
+ ## What it produces
59
+
60
+ <p align="center">
61
+ <img src="examples/img/lv_motion_slice.gif" width="34%" alt="Conformal LV mesh morphing through the cardiac cycle (long-axis cut)" />
62
+ <img src="examples/img/lv_conformal_cut.png" width="40%" alt="Conformal split-hex LV at end-diastole, long-axis cut: coarse apex cap, 2x-finer body, coarse valve cap, butterfly core" />
63
+ </p>
64
+ <p align="center">
65
+ <img src="examples/img/pipe.png" width="30%" alt="Pipe O-grid mesh with butterfly core cap" />
66
+ <img src="examples/img/lv_endo_stl.png" width="30%" alt="CAP endocardial blood-pool surface as an STL/CAD file" />
67
+ </p>
68
+
69
+ <p align="center"><em>Left to right, top to bottom: the conformal LV mesh of the bundled heart morphing through
70
+ its 20 cardiac phases (one iso-topological mesh, no remeshing); the same mesh cut along the long axis at
71
+ end-diastole (coarse apex cap, 2x-finer body, coarse valve cap, butterfly core, 2:1 split-hex transitions
72
+ at the two zone boundaries); a pipe O-grid (note the butterfly core cap, no polar axis); and the
73
+ endocardial CAD surface the mesh is built from. All in <a href="examples/">examples/</a>.</em></p>
74
+
75
+ <p align="center">
76
+ <img src="examples/img/lv_demo_cycle.png" width="92%" alt="hexmb demo: the bundled heart at end-diastole and end-systole, and its cavity-volume curve with every phase passing the gate" />
77
+ </p>
78
+ <p align="center"><em>What <code>hexmb demo --render</code> writes: the bundled heart's surface at its largest and
79
+ smallest cavity and the volume curve over the 20 phases, every phase passing the quality gate.</em></p>
80
+
81
+ ## What it does, and where each part stands
82
+
83
+ | Track | What it does | Status |
84
+ |---|---|---|
85
+ | **Engine** — swept tubes + single chamber | O-grid meshes for a pipe, bend, tapered/STL vessel, or a heart chamber; near-wall boundary layers; whole-cycle rebuild by transfinite-interpolation resurfacing | **Verified.** `checkMesh`-clean; the LV rebuilds across the cardiac cycle with 0 inverted cells; assembly vectorised to millions of cells. |
86
+ | **Conformal telescoping LV** — multi-resolution, one region | Coarse apex cap + 2x-finer body + coarse valve cap joined by conformal split-hex 2:1 transitions: ONE flux-conservative region, no interpolated coupling | **Verified.** `checkMesh` Mesh OK at end-diastole and end-systole on the bundled heart; three-level refinement with grid-convergence indices, and the same template rebuilt through all 20 phases at six levels up to 2.57 M cells with every phase passing the gate, identical topology and no face over the 70 deg non-orthogonality warning ([docs/VERIFICATION.md](docs/VERIFICATION.md)); in production use as the mesh behind the author's moving-ventricle CFD runs. |
87
+ | **Bring your own surface** | STL/OBJ/PLY/OFF, VTK/VTP/VTU, NIfTI mask (thick-slice stack or isotropic), Cardiac Atlas model, saved grid -> the ring grid, with automatic apex/base landmarks | **Working.** The STL route reproduces the model route's cavity volume on the bundled heart to 0.1 %; synthetic masks at scanner resolution come back within 1-3 % of the exact surface ([docs/VERIFICATION.md](docs/VERIFICATION.md)); please report what your own surfaces do. |
88
+ | **Moving mesh** | Per-phase surfaces -> the same mesh rebuilt at every phase, gated, written as an OpenFOAM 12 `interpolator` case | **Working.** The bundled heart runs through `foamRun -solver movingMesh` with positive volumes at every written time. |
89
+ | **Quality metrics, export, flow case** | checkMesh-style non-orthogonality / skewness / aspect ratio computed without OpenFOAM; VTK (`.vtu` per phase + `.pvd`) and Abaqus `.inp` export; a wall-driven `incompressibleFluid` flow case on the moving mesh | **Working.** Metrics match `checkMesh` to the printed precision; the flow case runs on the bundled heart at three mesh levels ([docs/VERIFICATION.md](docs/VERIFICATION.md)). |
90
+ | **Telescoping LV (AMI)** — multi-resolution, coupled zones | Coarse apex / fine body / coarse base as three zones joined by non-conformal AMI couplings, body to ~1M cells | **Working** for static meshes; the interpolated coupling is not flux-conservative and stalls a moving filling solve — use the conformal template for moving CFD. |
91
+
92
+ Branches, junctions, and multi-branch trees are **out of scope** — conforming
93
+ meshing through a junction is unsolved here.
94
+
95
+ ## Install
96
+
97
+ Use a virtual environment (modern Linux blocks `pip` into the system Python):
98
+
99
+ ```bash
100
+ python3 -m venv .venv && source .venv/bin/activate
101
+ pip install "hexmb[all] @ git+https://github.com/JieWangnk/hexmb" # everything: surfaces, masks, figures
102
+ ```
103
+
104
+ Or from a clone: `pip install -e ".[all,dev]"`, or with uv in a fresh venv:
105
+ `uv venv .venv && uv pip install -e ".[all,dev]"` (2 s with a warm cache). The core (numpy + scipy) builds
106
+ every mesh; the extras add `trimesh`/`rtree`/`meshio` (surface files),
107
+ `nibabel`/`scikit-image` (masks) and `matplotlib` (figures). OpenFOAM 12 is only
108
+ needed to run `checkMesh`, move the mesh, or run a solver on the output.
109
+
110
+ No install at all: open
111
+ [`examples/hexmb_quickstart.ipynb`](examples/hexmb_quickstart.ipynb) in Colab.
112
+
113
+ ## Quick start
114
+
115
+ Every command prints a quality line and says what it wrote. The outputs below are
116
+ the real results of running each command.
117
+
118
+ **1. The bundled heart, all 20 phases — no data, no OpenFOAM, about a minute.**
119
+
120
+ (A longer walkthrough with every output, the per-phase STL route, the cut-plane
121
+ animation and the OpenFOAM moving-mesh run is in [docs/DEMO.md](docs/DEMO.md).)
122
+
123
+ ```bash
124
+ hexmb demo -o out_demo --render
125
+ ```
126
+ ```text
127
+ demo: Cardiac Atlas SCD0000101, 20 phases (CC0), template conformal
128
+ wrote out_demo/lv_endo_phase1.stl (4368 triangles, mm)
129
+ template conformal: 14688 cells per phase, inlet rule 'sectors'
130
+ phase 0 volume 172.7 mL min cell vol +1.671e+00 mm3 failing cells 0
131
+ ...
132
+ phase 19 volume 173.1 mL min cell vol +1.645e+00 mm3 failing cells 0
133
+ reference phase 8 (t 0.4000 s, 123.1 mL); EDV 173.1 ESV 123.1 mL EF 29 % -> VALID
134
+ wrote out_demo/lv_moving: constant/polyMesh at phase 8, 29 wantedDisplacement time folders (t 0 to 1.4), motion_quality.csv
135
+ wrote out_demo/demo.png
136
+ ```
137
+ `out_demo/lv_moving` is an OpenFOAM 12 case: `constant/polyMesh` at the reference
138
+ phase (the smallest cavity), one `wantedDisplacement` point field per phase time
139
+ (zero at the reference), and the `interpolator` mover already set up (spline
140
+ interpolation of the table in time, so the wall velocity is continuous), so
141
+ `foamRun -solver movingMesh` carries the mesh round the cycle.
142
+ `motion_quality.csv` is the per-phase gate. `demo.png` shows the surface at
143
+ end-diastole and end-systole and the volume curve.
144
+
145
+ **2. Your own ventricle — any surface file, one command.**
146
+
147
+ ```bash
148
+ hexmb lv --surface lv_endo.stl -o case_lv # also .obj .ply .off .vtk .vtp
149
+ hexmb lv --surface lv_mask.nii.gz --label 1 -o case_lv # a segmentation mask (thick short-axis slices or isotropic)
150
+ hexmb lv --surface SCD0000101_1.model.exnode -o case_lv # a Cardiac Atlas model
151
+ hexmb lv --surface lv_endo_cm.stl --units cm -o case_lv # a surface saved in cm (or --units m)
152
+ ```
153
+ ```text
154
+ lv: lv_endo.stl
155
+ surface lv_endo.stl: mesh -> grid 40 x 56, landmarks open: apex (2.0 0.3 0.0) base (0.0 0.0 80.0), length 80.0 mm, interpolated rays max 0/level
156
+ template conformal: coarse ring NT 24 NR 4, body 2x, levels 10/30/12 -> 14688 cells, inlet rule 'sectors'
157
+ cells 14688 (14472 hex + 216 split-hex) points 16130 min pyramid +6.832e-02 mm3 failing cells 0 zero-area faces 0 cavity 103.3 mL (VALID)
158
+ wrote case_lv/constant/polyMesh (one region, 43344 internal faces, patches ['wall', 'outlet1', 'inlet'])
159
+ ```
160
+ Surfaces are read in millimetres unless `--units` says otherwise. A surface
161
+ whose apex-to-base length is outside 20-250 mm is stopped with a message that
162
+ names the likely unit, since a silent unit slip would build a valid mesh of the
163
+ wrong size; `--any-size` lets a non-human heart through.
164
+ The apex and the valve-plane centre are detected (an open surface: the largest
165
+ boundary loop is the base; a closed one: the narrower end of the longest axis is
166
+ the apex); give them with `--apex x,y,z --base x,y,z` when you know better.
167
+ `--template single` builds one O-grid instead, `--template telescoping` the
168
+ AMI-coupled zones. `--body-levels 60` doubles the body resolution; that axial
169
+ refinement is the skew-safe way to add cells on a moving ventricle.
170
+
171
+ **3. A beating ventricle from per-phase surfaces.**
172
+
173
+ No surfaces of your own yet? `hexmb demo -o out_demo --phases-stl phases` writes
174
+ the bundled heart's 20 phases as STL files to try this route on.
175
+
176
+ ```bash
177
+ hexmb motion --surfaces phase_*.vtk --period 0.9 -o case_moving # files sorted naturally
178
+ hexmb motion --surfaces phases.npz --times 0,0.05,0.1,... -o case_moving # or one npz with 'grids'
179
+ ```
180
+ Every phase is cast with the frame of the first file so the grids correspond
181
+ point-for-point, meshed with the same template, gated, and written as in the
182
+ demo. `--reference ed` builds the mesh on the largest cavity instead of the
183
+ smallest. The builder screens every ring first (a ring that passes through the
184
+ long axis, overlaps itself or collapses to a point cannot be meshed) and acts on
185
+ what it finds: bad rings at the base or apex move `base_cut` / `apex_trim` to
186
+ the last good row for every phase; cells that fail the gate at a rim trim that
187
+ rim one step and rebuild (at most three times); a phase whose surface is
188
+ degenerate in the middle is replaced by the interpolation of its neighbours in
189
+ time and gated again. Every step is printed and written to
190
+ `motion_adjustments.txt`; the quality table marks such a phase `filled`.
191
+ `--no-auto-trim` and `--no-fill` switch the two behaviours off
192
+ (`--base-cut` / `--apex-trim` then set the knobs by hand).
193
+
194
+ ```bash
195
+ hexmb render -o case_moving --gif motion_slice.gif --png motion_slice.png # see it move
196
+ ```
197
+ ```text
198
+ render: 21 frames from t 0.4 to 1.4 s, cavity 123.1-173.3 mL
199
+ ```
200
+ <p align="center"><img src="examples/img/lv_motion_slice.png" width="900"></p>
201
+ <p align="center"><em>Long-axis cut of the moving mesh at four phases of the cycle (the bundled heart
202
+ through the per-phase STL route); the GIF has one frame per table time.</em></p>
203
+
204
+ **4. See it in ParaView, run flow on it.**
205
+
206
+ ```bash
207
+ hexmb export -o case_moving # or --vtk on lv / motion / demo
208
+ hexmb flow -o case_moving --window diastole # mitral open, ES -> ED (systole: build with --reference ed)
209
+ case_moving/Allrun 4 # decomposePar + foamRun -solver incompressibleFluid + reconstructPar
210
+ ```
211
+ ```text
212
+ wrote case_moving/vtk: 29 .vtu (14688 cells), 3 patch .vtp, motion.pvd
213
+ flow case (diastole): t 0.4 -> 0.95 s, open valve inlet, closed outlet1; nu 3.77e-06 m2/s, maxCo 1.0, maxDeltaT 0.001 s; run with case_moving/Allrun [n_procs]
214
+ ```
215
+ Open `motion.pvd` in ParaView for the moving mesh with its displacement and
216
+ cell volumes (no OpenFOAM reader needed). The flow case is the usual
217
+ prescribed-motion model: the wall motion drives the flow, the open valve is a
218
+ zero-total-pressure opening, the closed one a moving wall; function objects log
219
+ the valve flow rate, peak speed, kinetic energy, wall shear stress and
220
+ `checkMesh` at every write time. `hexmb export --inp lv.inp --scale 1000`
221
+ writes an all-hex mesh (pipe, elbow, `--template single`) as Abaqus / CalculiX
222
+ C3D8 elements with node sets per patch.
223
+
224
+ **5. Check a case (`checkMesh` runs too when OpenFOAM 12 is present).**
225
+
226
+ ```bash
227
+ hexmb verify -o case_lv
228
+ ```
229
+ ```text
230
+ readback: 16130 pts, 45432 faces (43344 internal), owner<neighbour OK, max |sum Sf| per cell 1.31e-20
231
+ metrics: non-orthogonality max 58.0 deg (average 12.8, 0 faces > 70), skewness max 2.31 (0 faces > 4), aspect ratio max 3.5, 0 cells with non-positive volume
232
+ [checkMesh] hexahedra: 14472
233
+ [checkMesh] polyhedra: 216
234
+ [checkMesh] Number of regions: 1 (OK).
235
+ [checkMesh] Min volume = 1.115531e-09. Max volume = 7.426447e-08. Total volume = 0.0001230599. Cell volumes OK.
236
+ [checkMesh] Mesh non-orthogonality Max: 57.99516 average: 12.78606
237
+ [checkMesh] Max skewness = 2.308924 OK.
238
+ [checkMesh] Mesh OK.
239
+ ```
240
+ `verify` uses `checkMesh` from the PATH, else sources `$HEXMB_FOAM_BASHRC`,
241
+ `$WM_PROJECT_DIR/etc/bashrc` or `/opt/openfoam12/etc/bashrc`, in that order.
242
+ (the demo's end-systolic reference mesh; the `metrics:` line is hexmb's own,
243
+ computed with OpenFOAM's definitions, and agrees with `checkMesh` to the
244
+ printed precision.)
245
+
246
+ **6. A pipe and a U-bend — the engine on its own.**
247
+
248
+ ```bash
249
+ hexmb pipe --length 100 --radius 10 -o case_pipe
250
+ hexmb elbow --radius 10 --bend-radius 35 --angle 180 -o case_ubend
251
+ ```
252
+ ```text
253
+ cells 8736 points 9640 min scaled-Jac +0.7071 median +0.991 inverted 0 (VALID)
254
+ wrote case_pipe/constant/polyMesh (nFaces 27056, patches ['outlet', 'inlet', 'wall'])
255
+ ```
256
+
257
+ **7. From Python.**
258
+
259
+ ```python
260
+ from hexmb.io.surface import grid_from_file
261
+ from hexmb.templates.conformal_lv import ConformalLVConfig, build_conformal_lv, polymesh_quality
262
+
263
+ G, axis, e1, e2, mu, info = grid_from_file("lv_endo.stl", NL=40, NT=56) # any format above
264
+ mesh = build_conformal_lv(G, axis, e1, e2, ConformalLVConfig(body_levels=60), mu=mu)
265
+ print(polymesh_quality(mesh)) # cells, min pyramid, failing cells, zero-area faces, mL
266
+ mesh.write("case_lv") # constant/polyMesh: wall / inlet / outlet1
267
+
268
+ from hexmb.services.motion import build_lv_motion
269
+ res = build_lv_motion(grids, times, builder="conformal", period=0.9, out="case_moving")
270
+ ```
271
+
272
+ The single O-grid chamber (`hexmb.templates.lv.LVChamberTemplate`), the tubes
273
+ (`hexmb.templates.tube`) and the AMI telescoping zones
274
+ (`hexmb.templates.telescoping_lv`) keep their v0.1 interfaces.
275
+
276
+ ## Additional utility: static cavities
277
+
278
+ The secondary `hexmb cavity` command builds static rectangular or smooth single
279
+ cavities with explicit units and boundary choices. It does not fit a sequence of
280
+ measured surfaces or preserve correspondence through motion. For example, a 2D cavity mesh with wall clustering:
281
+
282
+ ```bash
283
+ hexmb cavity --size 1 1 0.01 --units m --cells 64 64 1 --two-d \
284
+ --stretch 1.5 1.5 0 --vtk -o cavity_2d
285
+ hexmb verify -o cavity_2d
286
+ ```
287
+
288
+ The command exports metres, records quality and provenance in `mesh_report.json`,
289
+ and refuses poor-quality meshes or a non-empty output directory. It supplies
290
+ mesh boundaries, not solver fields. See [the cavity guide](docs/CAVITIES.md) for
291
+ smooth surfaces, cap rules and limitations, and [the engineering review](docs/ENGINEERING_REVIEW.md)
292
+ for findings and remaining verification work. The O-grid route is still limited
293
+ to suitable single cavities; the box template handles rectangular corners.
294
+
295
+ ## The quality gate
296
+
297
+ 1. Build a mesh from a template.
298
+ 2. **Gate**: all-hex meshes need `scaled_jacobian().min() > 0`; the conformal
299
+ split-hex mesh needs every signed face-pyramid volume positive (the pyramid
300
+ from the cell centre to each face, faces oriented outward by construction, so
301
+ an inverted cell and a cell folded past its centre both fail) AND no
302
+ zero-area face (`polymesh_quality`). The moving-mesh builder trims a rim
303
+ that fails and rebuilds, or fills a degenerate phase from its neighbours
304
+ (see `hexmb motion` above); a phase that still fails makes the case INVALID,
305
+ and the message says where the bad cells sit.
306
+ 3. **Export**: `write_polymesh(...)` / `mesh.write(...)`.
307
+ 4. **Verify**: `hexmb verify -o <case>` — owner/neighbour order, closed cells,
308
+ and `checkMesh` as an independent cross-check (it cannot see a folded cell
309
+ once the writer has turned every face outward, which is why the gate above
310
+ is the hard condition). "Mesh OK" is ideal, a skewness warning at the
311
+ split-hex transition is non-fatal.
312
+
313
+ ### Run the test suite
314
+
315
+ ```bash
316
+ python -m pytest tests/ -q
317
+ ```
318
+ ```text
319
+ 97 passed
320
+ ```
321
+
322
+ Optional surface/image tests are skipped when their dependencies are unavailable.
323
+ The cavity regressions cover units, exact volumes, boundary types, quality
324
+ rejection, overwrite protection and verification exit codes.
325
+
326
+ The suite needs no data files beyond the bundled heart: `core` (dedup, TFI,
327
+ million-cell assembly), `tubes`, `ogrid_lv` and `telescoping_lv` (synthetic
328
+ prolate cavity), `ami`, `splithex` (exactness of the 2:1 transition, one-region
329
+ telescoping tube), `conformal_lv` (gate, patches, inlet rules), `cap` (exact evaluation of the Atlas model: nodes reproduced, rings wind once, axis-crossing warning), `surface` (STL
330
+ round trip, closed ellipsoid landmarks, mask to grid, dispatch) and `motion`
331
+ (reference phase, zero displacement, time folders).
332
+
333
+ ## Conditions of use (applicability domain)
334
+
335
+ `hexmb` applies to a geometry that is **one connected lumen describable as a stack
336
+ of cross-sections swept along a centreline**. Concretely, use it when:
337
+
338
+ 1. **Topology** — a single-lumen **tube** (two open ends) or a **sac** (one open
339
+ end + a closed cap, like the LV). Genus 0, one connected region.
340
+ 2. **Description** — a surface mesh or mask of that lumen, a structured surface
341
+ grid `G[station, angle]` (shape `(ns, NT, 3)`), or a centreline path + radius.
342
+ `NT` (points around the section) must be **divisible by 4**.
343
+ 3. **Cross-sections are star-shaped about their centre** — every boundary point is
344
+ visible from the section centroid, so the petals do not fold and every ray of
345
+ the surface reader hits the wall once. Mild non-convexity is fine (the LV base
346
+ is); strongly re-entrant sections are not.
347
+ 4. **Gentle bends** — a bend radius smaller than the tube radius distorts the
348
+ inner cells (a limit of any structured O-grid sweep).
349
+ 5. **Ends → patches** — each open end becomes one patch; a closed cap becomes wall.
350
+ The LV valve plane is partitioned into `inlet` (mitral) / `outlet1` (aortic) /
351
+ `wall` by angular sectors about the cap centre, the whole cap (`--inlet fullcap`),
352
+ or a centred ellipse of a given area (`--inlet centred`).
353
+ 6. **Moving meshes** — supply one surface per phase. The rebuild stays valid while
354
+ the surface stays star-shaped and non-self-intersecting; on a contracting
355
+ ventricle keep the apex ring coarse (the conformal template's coarse ring is also
356
+ its apex ring) and add cells axially.
357
+
358
+ 7. **Masks** — a short-axis cine stack with thick slices is read slice by slice
359
+ (one ring per slice outline, holes filled, the clinical Simpson's-rule
360
+ volume); an isotropic mask (CT) through marching cubes and the ray cast.
361
+ `--mask-method` forces either.
362
+
363
+ **Within the heart: the left-ventricular blood pool only.** The right ventricle
364
+ is crescent-shaped about any axis, so its sections are not star-shaped and the
365
+ petals of an O-grid fold; the myocardial wall is a shell between two surfaces
366
+ and needs a different block topology; the atria carry an appendage and four to
367
+ six pulmonary veins, which are branches. None of these is produced here. Mesh
368
+ points correspond across phases by ring index and angle in a fixed frame, so the
369
+ mesh follows the wall's normal motion (which drives the flow) and not its
370
+ torsion; a solver that needs material-point wall motion needs a different
371
+ correspondence.
372
+
373
+ **Out of scope.** These produce no single conforming, flow-verified domain here:
374
+ - Branches / bifurcations / junctions (Y, T) and multi-branch trees.
375
+ - Multi-lumen geometries; arbitrary genus (holes/handles).
376
+ - Sharp features/corners that must stay crisp (the O-grid is smooth boundary-fitted).
377
+ - Right ventricle, myocardial wall, atria (above).
378
+
379
+ ### Pass rate on an open cohort
380
+
381
+ The default template was run on all 45 subjects of the CC0 Cardiac Atlas
382
+ Sunnybrook models (20 phases each, nothing set per subject): **40 of 45 pass
383
+ the gate on every phase exactly as given, the builder's own rim trimming takes
384
+ that to 44, and the last subject, whose fitted model crosses its own long axis
385
+ a third of the way up from the apex on one phase, has that phase filled from
386
+ its neighbours and reported, so all 45 moving-mesh cases are written and
387
+ valid.** The four trims cut 2-8 % off the base, where the fitted model folds
388
+ through its own axis or narrows past the ring below at end-systole, and remove
389
+ 2.5-10 mL (1.5-5.4 % of the cavity, 14 % of the smallest heart's 17 mL at
390
+ end-systole); the builder reports the removed volume per phase. Over the whole
391
+ cohort the worst skewness is 3.06 (no face reaches OpenFOAM's warning level of
392
+ 4) and the worst non-orthogonality 80 deg, with 8 subjects above 70 deg on a
393
+ few core faces where a small, curved ventricle's centreline bends.
394
+ Per-subject table with the metrics, and the figure, in [DATA.md](DATA.md);
395
+ the survey of other open per-phase LV data is
396
+ [docs/OPEN_LV_DATA.md](docs/OPEN_LV_DATA.md).
397
+
398
+ <p align="center"><img src="examples/img/sunnybrook_cohort.png" width="900"></p>
399
+
400
+ ## Architecture
401
+
402
+ ```
403
+ core/ Block, MultiBlockMesh (union-find node dedup), topology, tfi3d (Coons)
404
+ assembly/ ogrid (5-block butterfly + 8 joins), sweep (frames, rings), ami (non-conformal AMI assembler),
405
+ splithex (conformal 2:1 split-hex transition, PolyMesh polyhedral writer)
406
+ services/ quality (scaled-Jacobian gate + checkMesh-style metrics), export_foam (polyMesh), smooth, morph (RBF),
407
+ motion (per-phase rebuild + interpolator case), flow (wall-driven incompressibleFluid case),
408
+ render (figures, cut-plane GIF)
409
+ io/ cap (CAP .exnode + .exelem -> grid, exact bicubic Hermite), surface (STL/OBJ/PLY/VTK/mask/npz -> grid,
410
+ landmarks, slice-by-slice mask reader), stl_surface (grid -> STL/CAD), vtk (.vtu/.vtp/.pvd + Abaqus .inp),
411
+ foam (readers for what hexmb writes)
412
+ templates/ tube (pipe/elbow), stl (STL tube), lv (single chamber),
413
+ conformal_lv (one-region telescoping LV), telescoping_lv (AMI zones)
414
+ data/cap/ the bundled CC0 Cardiac Atlas subject (20 phases)
415
+ cli.py hexmb pipe | elbow | lv | cap | motion | demo | stl | export | flow | verify | render
416
+ ```
417
+
418
+ The block cell order matches the scaled-Jacobian gate and the OpenFOAM face
419
+ template; faces are oriented geometrically (outward from the cell centroid), so
420
+ export is correct regardless of a block's index handedness. Shared block faces are
421
+ merged topologically (union-find) and verified by coordinate coincidence. The
422
+ split-hex transition keeps the coarse nodes an exact subset of the fine ones
423
+ (bilinear core, linear petals, subdivided rings), which is what makes the join
424
+ watertight.
425
+
426
+ ## Scale
427
+
428
+ Node deduplication / assembly is vectorised via connected components (SciPy
429
+ `csgraph`, with a numpy fallback), so it builds **millions of cells** fast —
430
+ ≈1.2 M hex in ~2 s and ≈4 M hex in ~9 s on one core (a boundary-layer straight
431
+ pipe, 0 inverted). The conformal LV at its default 14.7k cells builds in about
432
+ 1.5 s per phase; the per-geometry ceiling (the LV apex ring, ~50 across) is why
433
+ the body is refined axially rather than circumferentially.
434
+
435
+ ## Related tools, and what is different here
436
+
437
+ A structured hexahedral LV mesh moved as one body is an old idea: Imperial
438
+ College built one per MRI phase from 24 hand-made blocks (Long et al., Proc
439
+ IMechE H 2008) and Karlsruhe projected two O-grids onto the apex (Schenkel et
440
+ al., Ann Biomed Eng 2009), and both groups left it for tetrahedra with
441
+ remeshing or immersed boundaries because the blocking was manual in a
442
+ commercial tool, quality could not be held above a few hundred thousand cells,
443
+ and valve detail was lost. The groups that publish image-based LV CFD today
444
+ move tetrahedra by a lifting equation (lifex, CARP, OasisMove, svFSI), remesh
445
+ them (YALES2BIO, Fluent, CFX), or keep a fixed Cartesian grid (immersed
446
+ boundary). Cut-cell and hex-dominant meshers (`snappyHexMesh`, cfMesh) mesh one
447
+ static surface; blocking tools (`blockMesh`, Gmsh transfinite blocks, ICEM,
448
+ Pointwise) build O-grids from a hand-made block layout; the cardiac image-to-mesh
449
+ chains (SimVascular, VMTK, meshtool) give tetrahedral or hybrid meshes. Closest
450
+ in spirit are the template models of cardiac modelling: the Cardiac Atlas
451
+ Project fits one 16-element cubic-Hermite left-ventricle template to every
452
+ subject and phase; Lamata et al. (Medical Image Analysis 2011) warp a
453
+ cubic-Hermite template mesh to a segmentation by image registration; Canè et
454
+ al. (BioMed Res Int 2018) build structured quadrilateral LV surfaces with the
455
+ same vertices at every phase and list the hexahedral volume as future work;
456
+ Kong and Shadden (IEEE TMI 2023) learn the deformation of a whole-heart
457
+ template to a patient's images. `hexmb` is the automated, open version of the
458
+ structured route: it reads such a model, a segmentation mask or an STL, builds
459
+ the all-hexahedral flow mesh of the cavity inside it at any resolution with a
460
+ coarse apex cap instead of a tapered pole, rebuilds it with the same
461
+ connectivity at every phase with no lifting equation, gates each phase, and
462
+ writes the OpenFOAM case with the metrics, the export and the flow case a CFD
463
+ study needs. The verification evidence (three-level refinement with the exact
464
+ answer known, grid-convergence indices, synthetic-mask reader test) is in
465
+ [docs/VERIFICATION.md](docs/VERIFICATION.md); it is calculation verification
466
+ only, and no comparison with measured flow or with a tetrahedral mesh on the
467
+ same surfaces is claimed. The full survey behind this paragraph, with
468
+ citations, is in the paper's "State of the field" section (`paper.md`).
469
+
470
+ ## Roadmap
471
+
472
+ The development priority is measured-surface, moving-LV CFD with the same mesh
473
+ connectivity across phases:
474
+
475
+ 1. Support the cine-to-CFD-to-4D-flow study with reproducible production cases
476
+ and spatial/temporal convergence evidence.
477
+ 2. Compare tetrahedral and hexahedral meshes on the same moving LV surfaces,
478
+ matching geometry, wall motion, boundary conditions and accuracy targets;
479
+ report error, cost and robustness rather than cell count alone.
480
+ 3. Add a reader for an external group's shape model, selected with an identified
481
+ user and tested on their data.
482
+ 4. Publish a reproducible 45-heart cohort evidence table: time per case,
483
+ intervention frequency, quality distributions and volume preservation,
484
+ distinguishing original inputs from trimmed or interpolated cases.
485
+
486
+ Release packaging, a PyPI release and a Zenodo DOI support this work. Generic
487
+ static cavities are a secondary utility; junction meshing and a snappyHexMesh
488
+ backend are not current development priorities.
489
+
490
+ MIT-licensed (see `LICENSE`). Changes per version in [`CHANGELOG.md`](CHANGELOG.md).