Skip to content

Commit 8506607

Browse files
authored
Merge pull request #47 from zasexton/main
improved smoke test for basic cross platform cross version passing test
2 parents 1c0b11e + 4024ae3 commit 8506607

File tree

4 files changed

+77
-16
lines changed

4 files changed

+77
-16
lines changed

.github/workflows/basic-smoke-test.yml

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -74,7 +74,6 @@ jobs:
7474
with:
7575
pattern: smoke-status-*
7676
path: smoke-status
77-
merge-multiple: true
7877

7978
- name: Update README smoke test badge
8079
run: |
@@ -85,13 +84,13 @@ jobs:
8584
windows="unknown"
8685
8786
# If no smoke-status artifacts were downloaded, skip badge update.
88-
if ! compgen -G "smoke-status/*/*" > /dev/null; then
87+
if ! compgen -G "smoke-status/*/smoke-status.txt" > /dev/null; then
8988
echo "No smoke-status artifacts found; skipping badge update."
9089
exit 0
9190
fi
9291
9392
# Each artifact directory contains a single smoke-status.txt file.
94-
for f in smoke-status/*/*; do
93+
for f in smoke-status/*/smoke-status.txt; do
9594
line=$(cat "$f")
9695
os="${line%%=*}"
9796
status="${line#*=}"

README.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,10 +5,10 @@
55
![Platform](https://img.shields.io/badge/platform-macOS%20|%20linux%20|%20windows-blue)
66
![Latest Release](https://img.shields.io/github/v/release/SimVascular/svVascularize?label=latest)
77
[![codecov](https://codecov.io/github/SimVascular/svVascularize/graph/badge.svg)](https://codecov.io/github/SimVascular/svVascularize)
8-
[![DOI](https://zenodo.org/badge/DOI/10.5281/zenodo.15151168.svg)]()
8+
[![DOI](https://zenodo.org/badge/DOI/10.5281/zenodo.15151168.svg)](https://doi.org/10.5281/zenodo.15151168)
99
[![Docs](https://img.shields.io/badge/docs-gh--pages-brightgreen)](https://simvascular.github.io/svVascularize/)
1010
<!-- 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)
11+
[![SVV passing](https://img.shields.io/badge/svv_passing-linux_ok-macos_ok-windows_ok-brightgreen)](https://github.com/zasexton/svVascularize/actions/workflows/basic-smoke-test.yml?query=branch%3Amain)
1212
<!-- /smoke-test-badge -->
1313

1414
<p align="left">

svv/simulation/simulation.py

Lines changed: 9 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,8 @@
2525

2626
# Defer 1D/0D ROM imports to their respective methods to avoid importing
2727
# vtk-heavy modules during Simulation class import.
28-
28+
from svv.simulation.fluid.rom.zero_d.zerod_tree import export_0d_simulation as export_tree_0d_simulation
29+
from svv.simulation.fluid.rom.zero_d.zerod_forest import export_0d_simulation as export_forest_0d_simulation
2930

3031
class Simulation(object):
3132
def __init__(self, synthetic_object, name=None, directory=None):
@@ -768,14 +769,16 @@ def construct_1d_fluid_simulation(self, *args, viscosity=None, density=None, tim
768769
def write_1d_fluid_simulation(self, *args):
769770
pass
770771

771-
def construct_0d_fluid_equation(self, *args):
772-
pass
773-
774772
def construct_0d_fluid_simulation(self, *args):
775773
pass
776774

777-
def write_0d_fluid_simulation(self, *args):
778-
pass
775+
def write_0d_fluid_simulation(self, *args, **kwargs):
776+
if isinstance(self.synthetic_object, svv.tree.tree.Tree):
777+
export_tree_0d_simulation(self.synthetic_object, kwargs)
778+
elif isinstance(self.synthetic_object, svv.forest.forest.Forest) and not isinstance(self.synthetic_object.connections, type(None)):
779+
for idx in range(len(self.synthetic_object.networks)):
780+
# args are the network_id and the inlet selection
781+
export_forest_0d_simulation(self.synthetic_object, args, kwargs)
779782

780783
def construct_3d_tissue_perfusion_equation(self, *args):
781784
pass

svv/simulation/utils/extract_faces.py

Lines changed: 64 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -95,6 +95,16 @@ def extract_faces(surface, mesh, crease_angle: float = 60, verbose: bool = False
9595
for i in new_idx:
9696
new_faces.append(faces[i])
9797
faces = new_faces
98+
# Precompute boundary-loop KD-trees for all faces (used for robust matching)
99+
all_boundary_trees = []
100+
for i in range(len(faces)):
101+
f = surface.extract_cells(faces[i]).extract_surface()
102+
loops = f.extract_feature_edges(boundary_edges=True,
103+
manifold_edges=False,
104+
feature_edges=False,
105+
non_manifold_edges=False)
106+
splits = loops.split_bodies()
107+
all_boundary_trees.append([cKDTree(splits[j].points) for j in range(splits.n_blocks)])
98108
iscap = []
99109
wall_faces = []
100110
cap_faces = []
@@ -348,13 +358,62 @@ def compute_circularity(loop_polydata):
348358
wall_boundary_trees.append(tmp_wall_boundary_trees)
349359
iscap.append(0)
350360
wall_faces.append(faces[i])
351-
# Do not reclassify single-loop planar faces: these are caps by definition.
352-
# A cap’s boundary loop will naturally coincide with a wall boundary; this is expected
353-
# and should not trigger reclassification.
361+
362+
# Post-classification validation: a CAP must have a single boundary loop that
363+
# matches exactly one LUMEN boundary and no other face boundaries.
364+
# If a cap loop is shared among multiple faces or not shared with any lumen,
365+
# demote it to a wall.
366+
def boundaries_match(tree_a, tree_b, tol=1e-9):
367+
dists, _ = tree_a.query(tree_b.data)
368+
return numpy.all(numpy.isclose(dists, 0.0, atol=tol))
369+
370+
for i in range(len(faces)):
371+
if iscap[i] != 1:
372+
continue
373+
# Require exactly one boundary loop on the cap face
374+
cap_loops = all_boundary_trees[i]
375+
if len(cap_loops) != 1:
376+
iscap[i] = 0
377+
continue
378+
cap_loop_tree = cap_loops[0]
379+
lumen_matches = 0
380+
other_matches = 0
381+
for j in range(len(faces)):
382+
if j == i:
383+
continue
384+
for other_loop in all_boundary_trees[j]:
385+
if boundaries_match(cap_loop_tree, other_loop):
386+
if iscap[j] == 2:
387+
lumen_matches += 1
388+
else:
389+
other_matches += 1
390+
# Early exit if already invalid
391+
if lumen_matches > 1 or other_matches > 0:
392+
break
393+
# Enforce: shared only with a single lumen loop
394+
if not (lumen_matches == 1 and other_matches == 0):
395+
iscap[i] = 0 # demote to wall
396+
# Rebuild type-specific face lists and boundary trees after cap validation
397+
wall_faces = []
398+
cap_faces = []
399+
lumen_faces = []
400+
wall_boundary_trees = []
401+
cap_boundary_trees = []
402+
lumen_boundary_trees = []
403+
for i in range(len(faces)):
404+
if iscap[i] == 0:
405+
wall_faces.append(faces[i])
406+
wall_boundary_trees.append(all_boundary_trees[i])
407+
elif iscap[i] == 1:
408+
cap_faces.append(faces[i])
409+
cap_boundary_trees.append(all_boundary_trees[i])
410+
elif iscap[i] == 2:
411+
lumen_faces.append(faces[i])
412+
lumen_boundary_trees.append(all_boundary_trees[i])
354413

355414
# IMPORTANT: Caps and lumens should NEVER be merged with walls or each other
356-
# Only walls can be combined if they share boundaries
357-
# Combine cap and lumen boundary trees to prevent walls from merging with them
415+
# Only walls can be combined if they share boundaries. Prevent merges across
416+
# cap and lumen boundaries by providing them as non-wall constraints.
358417
all_non_wall_boundary_trees = cap_boundary_trees + lumen_boundary_trees
359418

360419
if combine_walls and len(wall_faces) > 0:

0 commit comments

Comments
 (0)