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.
- hexmb-0.2.1/LICENSE +21 -0
- hexmb-0.2.1/PKG-INFO +490 -0
- hexmb-0.2.1/README.md +452 -0
- hexmb-0.2.1/hexmb/__init__.py +7 -0
- hexmb-0.2.1/hexmb/assembly/__init__.py +0 -0
- hexmb-0.2.1/hexmb/assembly/ami.py +223 -0
- hexmb-0.2.1/hexmb/assembly/ogrid.py +144 -0
- hexmb-0.2.1/hexmb/assembly/splithex.py +701 -0
- hexmb-0.2.1/hexmb/assembly/sweep.py +74 -0
- hexmb-0.2.1/hexmb/cli.py +797 -0
- hexmb-0.2.1/hexmb/core/__init__.py +0 -0
- hexmb-0.2.1/hexmb/core/block.py +89 -0
- hexmb-0.2.1/hexmb/core/dedup.py +87 -0
- hexmb-0.2.1/hexmb/core/multiblock.py +219 -0
- hexmb-0.2.1/hexmb/core/tfi.py +140 -0
- hexmb-0.2.1/hexmb/core/topology.py +118 -0
- hexmb-0.2.1/hexmb/data/cap/GlobalHermiteParam.exelem +908 -0
- hexmb-0.2.1/hexmb/data/cap/README.md +26 -0
- hexmb-0.2.1/hexmb/data/cap/SCD0000101/SCD0000101_1.model.exnode +166 -0
- hexmb-0.2.1/hexmb/data/cap/SCD0000101/SCD0000101_10.model.exnode +166 -0
- hexmb-0.2.1/hexmb/data/cap/SCD0000101/SCD0000101_11.model.exnode +166 -0
- hexmb-0.2.1/hexmb/data/cap/SCD0000101/SCD0000101_12.model.exnode +166 -0
- hexmb-0.2.1/hexmb/data/cap/SCD0000101/SCD0000101_13.model.exnode +166 -0
- hexmb-0.2.1/hexmb/data/cap/SCD0000101/SCD0000101_14.model.exnode +166 -0
- hexmb-0.2.1/hexmb/data/cap/SCD0000101/SCD0000101_15.model.exnode +166 -0
- hexmb-0.2.1/hexmb/data/cap/SCD0000101/SCD0000101_16.model.exnode +166 -0
- hexmb-0.2.1/hexmb/data/cap/SCD0000101/SCD0000101_17.model.exnode +166 -0
- hexmb-0.2.1/hexmb/data/cap/SCD0000101/SCD0000101_18.model.exnode +166 -0
- hexmb-0.2.1/hexmb/data/cap/SCD0000101/SCD0000101_19.model.exnode +166 -0
- hexmb-0.2.1/hexmb/data/cap/SCD0000101/SCD0000101_2.model.exnode +166 -0
- hexmb-0.2.1/hexmb/data/cap/SCD0000101/SCD0000101_20.model.exnode +166 -0
- hexmb-0.2.1/hexmb/data/cap/SCD0000101/SCD0000101_3.model.exnode +166 -0
- hexmb-0.2.1/hexmb/data/cap/SCD0000101/SCD0000101_4.model.exnode +166 -0
- hexmb-0.2.1/hexmb/data/cap/SCD0000101/SCD0000101_5.model.exnode +166 -0
- hexmb-0.2.1/hexmb/data/cap/SCD0000101/SCD0000101_6.model.exnode +166 -0
- hexmb-0.2.1/hexmb/data/cap/SCD0000101/SCD0000101_7.model.exnode +166 -0
- hexmb-0.2.1/hexmb/data/cap/SCD0000101/SCD0000101_8.model.exnode +166 -0
- hexmb-0.2.1/hexmb/data/cap/SCD0000101/SCD0000101_9.model.exnode +166 -0
- hexmb-0.2.1/hexmb/io/__init__.py +0 -0
- hexmb-0.2.1/hexmb/io/cap.py +202 -0
- hexmb-0.2.1/hexmb/io/foam.py +91 -0
- hexmb-0.2.1/hexmb/io/stl_surface.py +44 -0
- hexmb-0.2.1/hexmb/io/surface.py +546 -0
- hexmb-0.2.1/hexmb/io/vtk.py +296 -0
- hexmb-0.2.1/hexmb/project/__init__.py +0 -0
- hexmb-0.2.1/hexmb/project/skeleton.py +173 -0
- hexmb-0.2.1/hexmb/project/stl_tube.py +225 -0
- hexmb-0.2.1/hexmb/services/__init__.py +0 -0
- hexmb-0.2.1/hexmb/services/cavity.py +109 -0
- hexmb-0.2.1/hexmb/services/export_foam.py +194 -0
- hexmb-0.2.1/hexmb/services/flow.py +261 -0
- hexmb-0.2.1/hexmb/services/morph.py +95 -0
- hexmb-0.2.1/hexmb/services/motion.py +600 -0
- hexmb-0.2.1/hexmb/services/quality.py +184 -0
- hexmb-0.2.1/hexmb/services/render.py +174 -0
- hexmb-0.2.1/hexmb/services/smooth.py +144 -0
- hexmb-0.2.1/hexmb/templates/__init__.py +0 -0
- hexmb-0.2.1/hexmb/templates/cavity.py +122 -0
- hexmb-0.2.1/hexmb/templates/conformal_lv.py +365 -0
- hexmb-0.2.1/hexmb/templates/lv.py +71 -0
- hexmb-0.2.1/hexmb/templates/stl.py +67 -0
- hexmb-0.2.1/hexmb/templates/telescoping_lv.py +209 -0
- hexmb-0.2.1/hexmb/templates/tube.py +80 -0
- hexmb-0.2.1/hexmb.egg-info/PKG-INFO +490 -0
- hexmb-0.2.1/hexmb.egg-info/SOURCES.txt +82 -0
- hexmb-0.2.1/hexmb.egg-info/dependency_links.txt +1 -0
- hexmb-0.2.1/hexmb.egg-info/entry_points.txt +2 -0
- hexmb-0.2.1/hexmb.egg-info/requires.txt +32 -0
- hexmb-0.2.1/hexmb.egg-info/top_level.txt +1 -0
- hexmb-0.2.1/pyproject.toml +46 -0
- hexmb-0.2.1/setup.cfg +4 -0
- hexmb-0.2.1/tests/test_ami.py +39 -0
- hexmb-0.2.1/tests/test_cap.py +104 -0
- hexmb-0.2.1/tests/test_cavity.py +165 -0
- hexmb-0.2.1/tests/test_cli_inputs.py +72 -0
- hexmb-0.2.1/tests/test_conformal_lv.py +98 -0
- hexmb-0.2.1/tests/test_core.py +99 -0
- hexmb-0.2.1/tests/test_export_flow.py +203 -0
- hexmb-0.2.1/tests/test_motion.py +284 -0
- hexmb-0.2.1/tests/test_ogrid_lv.py +66 -0
- hexmb-0.2.1/tests/test_splithex.py +190 -0
- hexmb-0.2.1/tests/test_surface.py +145 -0
- hexmb-0.2.1/tests/test_telescoping_lv.py +59 -0
- 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).
|