Skip to content

Commit 1c0b11e

Browse files
authored
Merge pull request #45 from zasexton/main
updating robust implementations across macOS/ubuntu/windows and HPC systems
2 parents f6ff070 + c62f78a commit 1c0b11e

File tree

12 files changed

+468
-74
lines changed

12 files changed

+468
-74
lines changed
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
import pyvista as pv
2+
3+
from svv.domain.domain import Domain
4+
from svv.tree.tree import Tree
5+
from svv.simulation.simulation import Simulation
6+
7+
8+
def main() -> None:
9+
cube = Domain(pv.Cube())
10+
cube.create()
11+
cube.solve()
12+
cube.build()
13+
14+
t = Tree()
15+
t.set_domain(cube)
16+
t.parameters.set('root_pressure', 100)
17+
t.parameters.set('terminal_pressure', 0)
18+
t.set_root()
19+
# Use a small tree to keep the smoke test
20+
# lightweight across all CI runners.
21+
t.n_add(3)
22+
23+
sim = Simulation(t)
24+
# For CI we only require the fluid mesh;
25+
# skipping the tissue mesh keeps TetGen runs lighter,
26+
# especially on Windows and macOS runners.
27+
# sim.build_meshes(fluid=True, tissue=False, boundary_layer=False)
28+
29+
30+
if __name__ == "__main__":
31+
main()
Lines changed: 175 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,175 @@
1+
name: Basic smoke test
2+
3+
on:
4+
push:
5+
branches:
6+
- main
7+
8+
permissions:
9+
contents: write
10+
11+
jobs:
12+
build-and-test:
13+
if: github.actor != 'github-actions[bot]'
14+
runs-on: ${{ matrix.os }}
15+
strategy:
16+
fail-fast: false
17+
matrix:
18+
os: [ubuntu-latest, macos-latest, windows-latest]
19+
python-version: ["3.9", "3.10", "3.11", "3.12"]
20+
defaults:
21+
run:
22+
shell: bash
23+
steps:
24+
- name: Check out code
25+
uses: actions/checkout@v4
26+
27+
- name: Set up Python
28+
uses: actions/setup-python@v5
29+
with:
30+
python-version: ${{ matrix.python-version }}
31+
32+
- name: Install Python dependencies
33+
run: |
34+
python -m pip install --upgrade pip
35+
pip install -r requirements.txt
36+
37+
- name: Build svVascularize
38+
env:
39+
SVV_BUILD_MMG: "1"
40+
run: |
41+
pip install .
42+
43+
- name: Run basic smoke test
44+
run: |
45+
python .github/scripts/basic_smoke_test.py
46+
47+
- name: Record job status
48+
if: always()
49+
run: |
50+
echo "${{ matrix.os }}=${{ job.status }}" > smoke-status.txt
51+
52+
- name: Upload status artifact
53+
if: always()
54+
uses: actions/upload-artifact@v4
55+
with:
56+
name: smoke-status-${{ matrix.os }}-py${{ matrix.python-version }}
57+
path: smoke-status.txt
58+
59+
update-smoke-badge:
60+
if: github.actor != 'github-actions[bot]'
61+
needs: build-and-test
62+
runs-on: ubuntu-latest
63+
defaults:
64+
run:
65+
shell: bash
66+
steps:
67+
- name: Check out code
68+
uses: actions/checkout@v4
69+
with:
70+
token: ${{ secrets.GITHUB_TOKEN }}
71+
72+
- name: Download status artifacts
73+
uses: actions/download-artifact@v4
74+
with:
75+
pattern: smoke-status-*
76+
path: smoke-status
77+
merge-multiple: true
78+
79+
- name: Update README smoke test badge
80+
run: |
81+
set -euo pipefail
82+
83+
linux="unknown"
84+
macos="unknown"
85+
windows="unknown"
86+
87+
# If no smoke-status artifacts were downloaded, skip badge update.
88+
if ! compgen -G "smoke-status/*/*" > /dev/null; then
89+
echo "No smoke-status artifacts found; skipping badge update."
90+
exit 0
91+
fi
92+
93+
# Each artifact directory contains a single smoke-status.txt file.
94+
for f in smoke-status/*/*; do
95+
line=$(cat "$f")
96+
os="${line%%=*}"
97+
status="${line#*=}"
98+
99+
case "$os" in
100+
ubuntu-latest)
101+
if [ "$status" = "success" ]; then
102+
if [ "$linux" = "unknown" ]; then
103+
linux="ok"
104+
fi
105+
else
106+
linux="fail"
107+
fi
108+
;;
109+
macos-latest)
110+
if [ "$status" = "success" ]; then
111+
if [ "$macos" = "unknown" ]; then
112+
macos="ok"
113+
fi
114+
else
115+
macos="fail"
116+
fi
117+
;;
118+
windows-latest)
119+
if [ "$status" = "success" ]; then
120+
if [ "$windows" = "unknown" ]; then
121+
windows="ok"
122+
fi
123+
else
124+
windows="fail"
125+
fi
126+
;;
127+
esac
128+
done
129+
130+
label="linux_${linux}-macos_${macos}-windows_${windows}"
131+
132+
if [ "$linux" = "ok" ] && [ "$macos" = "ok" ] && [ "$windows" = "ok" ]; then
133+
color="brightgreen"
134+
elif [ "$linux" = "fail" ] || [ "$macos" = "fail" ] || [ "$windows" = "fail" ]; then
135+
color="red"
136+
else
137+
color="lightgrey"
138+
fi
139+
140+
badge_url="https://img.shields.io/badge/svv_passing-${label}-${color}"
141+
link="https://github.com/${GITHUB_REPOSITORY}/actions/workflows/basic-smoke-test.yml?query=branch%3A${GITHUB_REF_NAME}"
142+
new_badge="[![SVV passing](${badge_url})](${link})"
143+
144+
BADGE_URL="${badge_url}" BADGE_LINK="${link}" BADGE_MD="${new_badge}" python - << 'PY'
145+
import os
146+
import re
147+
from pathlib import Path
148+
149+
readme_path = Path("README.md")
150+
text = readme_path.read_text(encoding="utf-8")
151+
152+
badge_url = os.environ["BADGE_URL"]
153+
link = os.environ["BADGE_LINK"]
154+
badge_md = os.environ["BADGE_MD"]
155+
156+
pattern = re.compile(r"<!-- smoke-test-badge -->.*?<!-- /smoke-test-badge -->", re.S)
157+
replacement = f"<!-- smoke-test-badge -->\n{badge_md}\n<!-- /smoke-test-badge -->"
158+
new_text, n = pattern.subn(replacement, text)
159+
if n == 0:
160+
raise SystemExit("smoke-test-badge markers not found in README.md")
161+
162+
readme_path.write_text(new_text, encoding="utf-8")
163+
PY
164+
165+
- name: Commit README badge update
166+
run: |
167+
if git diff --quiet README.md; then
168+
echo "No README changes to commit."
169+
else
170+
git config user.name "github-actions[bot]"
171+
git config user.email "github-actions[bot]@users.noreply.github.com"
172+
git add README.md
173+
git commit -m "Update smoke test status badge"
174+
git push
175+
fi

README.md

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,9 @@
77
[![codecov](https://codecov.io/github/SimVascular/svVascularize/graph/badge.svg)](https://codecov.io/github/SimVascular/svVascularize)
88
[![DOI](https://zenodo.org/badge/DOI/10.5281/zenodo.15151168.svg)]()
99
[![Docs](https://img.shields.io/badge/docs-gh--pages-brightgreen)](https://simvascular.github.io/svVascularize/)
10+
<!-- smoke-test-badge -->
11+
[![SVV passing](https://img.shields.io/badge/svv_passing-not_run-lightgrey)](https://github.com/SimVascular/svVascularize/actions/workflows/basic-smoke-test.yml?query=branch%3Amain)
12+
<!-- /smoke-test-badge -->
1013

1114
<p align="left">
1215
The svVascularize (svv) is an open-source API for automated vascular generation and multi-fidelity hemodynamic simulation
@@ -28,4 +31,5 @@ The package is published on PyPI as `svv`:
2831
pip install svv
2932
```
3033

31-
On clusters / HPC systems (for example Stanford Sherlock), use a recent Python (3.9–3.12) and `pip`, and install into a clean virtual environment or user site-packages. The runtime dependencies now require `numpy>=1.26`, which has pre-built wheels for Python 3.12 on standard x86_64 Linux, so `pip install svv` should no longer try to build NumPy (or SciPy) from source on these systems.
34+
On clusters / HPC systems (for example Stanford Sherlock), use a recent Python (3.9–3.12) and `pip`, and install into a
35+
clean virtual environment or user site-packages.

pyproject.toml

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,8 @@ requires = [
44
"wheel>=0.36; python_version < '3.12' ",
55
# Ensure PEP 517 build environments have the headers/tools needed to compile extensions
66
"Cython>=3.0.7",
7-
"numpy>=1.26"
7+
"numpy>=1.24; python_version < '3.12'",
8+
"numpy>=1.26; python_version >= '3.12'",
89
]
910

1011
build-backend = "setuptools.build_meta"

requirements.txt

Lines changed: 11 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,20 @@
1-
numpy>=1.26
2-
scipy>=1.10.1
3-
matplotlib>=3.7.5
1+
numpy>=1.24; python_version < "3.12"
2+
numpy>=1.26; python_version >= "3.12"
3+
scipy>=1.10.1; python_version < "3.12"
4+
scipy>=1.12.0; python_version >= "3.12"
5+
matplotlib>=3.7.5; python_version < "3.12"
6+
matplotlib>=3.8; python_version >= "3.12"
47
Cython~=3.0.7
58
usearch
69
scikit-image
710
tetgen
811
trimesh[all]
9-
pyvista~=0.44.2
12+
meshio
13+
open3d
14+
pyvista>=0.44.2
1015
scikit-learn
1116
tqdm
1217
pymeshfix==0.17.0
1318
numexpr
14-
pyvistaqt
15-
pyside6
19+
pyvistaqt>=0.11.0
20+
pyside6>=6.4.0

setup.py

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -402,6 +402,31 @@ def build_0d(num_cores=None):
402402
print(f"svZeroDSolver executables have been installed into: {install_tmp_prefix}")
403403

404404

405+
def install_igl_backend():
406+
"""
407+
Ensure that the `igl` Python package is available so that trimesh
408+
has a robust boolean backend. If `igl` is already importable this
409+
is a no-op; otherwise we attempt to install it via pip.
410+
411+
Any installation failure is converted into a warning so that the
412+
overall svv build can still succeed, but boolean operations may
413+
lack the libigl-backed engine in that case.
414+
"""
415+
try:
416+
import igl # type: ignore # noqa: F401
417+
return
418+
except Exception:
419+
pass
420+
421+
print("Installing 'igl' for trimesh boolean backends...")
422+
try:
423+
subprocess.check_call([sys.executable, "-m", "pip", "install", "igl"])
424+
print("Finished installing 'igl'.")
425+
except Exception as e:
426+
print(f"Warning: failed to install 'igl' ({e}). "
427+
"Trimesh boolean operations may not have a robust backend.")
428+
429+
405430
class DownloadAndBuildExt(build_ext):
406431
def run(self):
407432
# Make external tool builds opt-in to avoid brittle installs and network fetches
@@ -420,6 +445,10 @@ def run(self):
420445
except Exception as e:
421446
print(f"Warning: svZeroDSolver build failed ({e}). Continuing without building solver.")
422447

448+
# Always ensure a trimesh/libigl backend is present for boolean operations,
449+
# but do not fail the build if installation is not possible.
450+
install_igl_backend()
451+
423452
# Always proceed to build Cython extensions
424453
super().run()
425454

svv/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
__version__ = "0.0.39"
1+
__version__ = "0.0.40"
22

33

44
# If the optional companion package with compiled accelerators is installed

svv/domain/routines/boolean.py

Lines changed: 36 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -41,15 +41,43 @@ def boolean(pyvista_object_1, pyvista_object_2, operation='union', fix_mesh=True
4141
"""
4242
trimesh_object_1 = convert_to_trimesh(pyvista_object_1)
4343
trimesh_object_2 = convert_to_trimesh(pyvista_object_2)
44-
if operation == 'union':
45-
result = trimesh_object_1.union(trimesh_object_2, engine=engine)
46-
elif operation == 'intersection':
47-
result = trimesh_object_1.intersection(trimesh_object_2, engine=engine)
48-
elif operation == 'difference':
49-
result = trimesh_object_1.difference(trimesh_object_2, engine=engine)
44+
45+
def _apply(op, eng):
46+
if op == 'union':
47+
return trimesh_object_1.union(trimesh_object_2, engine=eng)
48+
elif op == 'intersection':
49+
return trimesh_object_1.intersection(trimesh_object_2, engine=eng)
50+
elif op == 'difference':
51+
return trimesh_object_1.difference(trimesh_object_2, engine=eng)
52+
else:
53+
raise ValueError("Unsupported boolean operation.")
54+
55+
result_tm = None
56+
try:
57+
try:
58+
result_tm = _apply(operation, engine)
59+
except KeyError:
60+
# If the requested engine (e.g., 'manifold') is not registered
61+
# in trimesh.boolean._engines, fall back to trimesh's default
62+
# engine selection by omitting the explicit engine argument.
63+
result_tm = _apply(operation, eng=None)
64+
except Exception:
65+
result_tm = None
66+
67+
if result_tm is not None:
68+
result = convert_to_pyvista(result_tm)
5069
else:
51-
raise ValueError("Unsupported boolean operation.")
52-
result = convert_to_pyvista(result)
70+
# Fallback to PyVista/VTK boolean operations when no trimesh
71+
# boolean backend is available on the current platform.
72+
if operation == 'union':
73+
result = pyvista_object_1.boolean_union(pyvista_object_2)
74+
elif operation == 'intersection':
75+
result = pyvista_object_1.boolean_intersection(pyvista_object_2)
76+
elif operation == 'difference':
77+
result = pyvista_object_1.boolean_difference(pyvista_object_2)
78+
else:
79+
raise ValueError("Unsupported boolean operation.")
80+
5381
if not result.is_manifold and fix_mesh:
5482
fix = pymeshfix.MeshFix(result)
5583
fix.repair(verbose=False)

0 commit comments

Comments
 (0)