Skip to content

Commit 16f0b7b

Browse files
authored
Merge pull request #37 from zasexton/main
gui/tissue mesh generation
2 parents 3473dd2 + 1116ddb commit 16f0b7b

File tree

14 files changed

+2016
-121
lines changed

14 files changed

+2016
-121
lines changed

scripts/validate_gui_contrast.py

Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
#!/usr/bin/env python3
2+
"""
3+
Contrast Validation Script for svVascularize GUI
4+
5+
This script validates that all color combinations in the design tokens
6+
meet WCAG 2.1 AA accessibility standards.
7+
8+
Usage:
9+
python scripts/validate_gui_contrast.py
10+
11+
Exit codes:
12+
0: All contrast ratios pass
13+
1: One or more contrast ratios fail
14+
15+
For CI/CD integration, add this to your test suite or pre-commit hooks.
16+
"""
17+
18+
import sys
19+
from pathlib import Path
20+
21+
# Add project root to path
22+
project_root = Path(__file__).parent.parent
23+
sys.path.insert(0, str(project_root))
24+
25+
from svv.visualize.gui.theme_generator import ThemeGenerator
26+
27+
28+
def main():
29+
"""Run contrast validation and report results."""
30+
31+
# Locate token file
32+
token_file = project_root / 'svv' / 'visualize' / 'gui' / 'design_tokens_cad.json'
33+
34+
if not token_file.exists():
35+
print(f"❌ ERROR: Token file not found: {token_file}")
36+
return 1
37+
38+
print("=" * 70)
39+
print("WCAG 2.1 AA Contrast Validation for svVascularize GUI")
40+
print("=" * 70)
41+
print()
42+
43+
# Create theme generator
44+
generator = ThemeGenerator(token_file)
45+
46+
# Validate contrast
47+
results = generator.validate_contrast()
48+
49+
# Track overall pass/fail
50+
all_pass = True
51+
52+
# Report results
53+
for name, result in results.items():
54+
if result['passes']:
55+
status = "✅ PASS"
56+
else:
57+
status = "❌ FAIL"
58+
all_pass = False
59+
60+
print(f"{status} {name}")
61+
print(f" Foreground: {result['foreground']}")
62+
print(f" Background: {result['background']}")
63+
print(f" Contrast Ratio: {result['ratio']}:1 (Required: {result['required']}:1)")
64+
print(f" WCAG Level: {result['wcag_level']}")
65+
print()
66+
67+
# Summary
68+
print("=" * 70)
69+
if all_pass:
70+
print("✅ SUCCESS: All contrast ratios meet WCAG AA standards!")
71+
print("=" * 70)
72+
return 0
73+
else:
74+
print("❌ FAILURE: Some contrast ratios fail WCAG AA standards.")
75+
print("=" * 70)
76+
print()
77+
print("To fix contrast issues:")
78+
print("1. Edit svv/visualize/gui/design_tokens_cad.json")
79+
print("2. Adjust colors that are failing")
80+
print("3. Re-run this validation script")
81+
print("4. Regenerate the QSS theme: python -m svv.visualize.gui.theme_generator")
82+
print()
83+
return 1
84+
85+
86+
if __name__ == '__main__':
87+
sys.exit(main())

svv/simulation/simulation.py

Lines changed: 11 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -85,13 +85,13 @@ def build_meshes(self, fluid=True, tissue=False, hausd=0.0001, hsize=None, minra
8585
if tissue:
8686
extension_scale = 4.0
8787
for i in range(5):
88-
new_root = self.synthetic_object.data[0, 0:3] - extension_scale * self.synthetic_object.data[0, 21]*self.synthetic_object.data.get('w_basis', 0)
88+
new_root = self.synthetic_object.data[0, 0:3] + extension_scale * self.synthetic_object.data[0, 21]*self.synthetic_object.data.get('w_basis', 0)
8989
if self.synthetic_object.domain(new_root.reshape(1, 3)) > 0:
9090
break
9191
else:
9292
extension_scale += 1.0
9393
root_extension = self.synthetic_object.data[0, 21] * extension_scale
94-
self.synthetic_object.data[0, 0:3] -= root_extension * self.synthetic_object.data.get('w_basis', 0)
94+
self.synthetic_object.data[0, 0:3] += root_extension * self.synthetic_object.data.get('w_basis', 0)
9595
fluid_surface_mesh = self.synthetic_object.export_solid(watertight=True)
9696
tet_fluid = tetgen.TetGen(fluid_surface_mesh)
9797
try:
@@ -107,7 +107,7 @@ def build_meshes(self, fluid=True, tissue=False, hausd=0.0001, hsize=None, minra
107107
if isinstance(fluid_volume_mesh, type(None)):
108108
print("Failed to generate fluid volume mesh.")
109109
else:
110-
hsize = fluid_surface_mesh.hsize
110+
hsize = fluid_surface_mesh.cell_data["hsize"][0]
111111
fluid_surface_mesh = fluid_volume_mesh.extract_surface()
112112
# faces, wall_surfaces, cap_surfaces, lumen_surfaces, _
113113
# fluid_surface_faces = extract_faces(fluid_surface_mesh, fluid_volume_mesh)
@@ -163,7 +163,8 @@ def build_meshes(self, fluid=True, tissue=False, hausd=0.0001, hsize=None, minra
163163
fluid_wall = remesh_volume(fluid_wall, hausd=hausd, nosurf=True, verbosity=4)
164164
self.fluid_domain_wall_layers.append(fluid_wall)
165165
fluid_surface_mesh = fluid_volume_mesh.extract_surface()
166-
fluid_surface_mesh.hsize = hsize
166+
fluid_surface_mesh.cell_data["hsize"] = hsize
167+
fluid_surface_mesh.cell_data["hsize"][0] = hsize
167168
self.fluid_domain_surface_meshes.append(fluid_surface_mesh)
168169
self.fluid_domain_volume_meshes.append(fluid_volume_mesh)
169170
if tissue:
@@ -180,7 +181,7 @@ def build_meshes(self, fluid=True, tissue=False, hausd=0.0001, hsize=None, minra
180181
fluid_surface_boolean_mesh = deepcopy(self.fluid_domain_surface_meshes[-1])
181182
else:
182183
fluid_surface_boolean_mesh = deepcopy(self.fluid_domain_wall_layers[-1])
183-
hsize = fluid_surface_boolean_mesh.hsize
184+
hsize = fluid_surface_boolean_mesh.cell_data["hsize"][0]
184185
tissue_domain = remesh_surface(self.synthetic_object.domain.boundary, hausd=hausd) # Check if this should be remeshed
185186
area = tissue_domain.area
186187
tissue_domain = boolean(tissue_domain, fluid_surface_boolean_mesh, operation='difference')
@@ -192,9 +193,9 @@ def build_meshes(self, fluid=True, tissue=False, hausd=0.0001, hsize=None, minra
192193
hmin = ((4.0*low_tri_area)/3.0**0.5) ** (0.5)
193194
upper_tri_area = area / lower_num_triangles
194195
hmax = ((4.0*upper_tri_area)/3.0**0.5) ** (0.5)
195-
tissue_domain = remesh_surface(tissue_domain, hausd=hausd)
196-
else:
197-
tissue_domain = remesh_surface(tissue_domain, hausd=hausd)
196+
#tissue_domain = remesh_surface(tissue_domain, hausd=hausd)
197+
#else:
198+
# #tissue_domain = remesh_surface(tissue_domain, hausd=hausd)
198199
tet_tissue = tetgen.TetGen(tissue_domain)
199200
if not fluid:
200201
self.synthetic_object.data[0, 0:3] += root_extension * self.synthetic_object.data.get('w_basis', 0)
@@ -212,7 +213,7 @@ def build_meshes(self, fluid=True, tissue=False, hausd=0.0001, hsize=None, minra
212213
if isinstance(tissue_volume_mesh, type(None)):
213214
print("Failed to generate tissue volume mesh.")
214215
else:
215-
if remesh_volume:
216+
if remesh_vol:
216217
tissue_volume_mesh = remesh_volume(tissue_volume_mesh, hausd=hausd, nosurf=True)
217218
tissue_domain = tissue_volume_mesh.extract_surface()
218219
self.tissue_domain_surface_meshes.append(tissue_domain)
@@ -371,7 +372,7 @@ def build_meshes(self, fluid=True, tissue=False, hausd=0.0001, hsize=None, minra
371372
if tissue:
372373
tissue_domain = deepcopy(self.synthetic_object.domain.boundary)
373374
tissue_domain = tissue_domain.compute_normals(auto_orient_normals=True)
374-
fluid_hsize = min([mesh.hsize for mesh in self.fluid_domain_surface_meshes])
375+
fluid_hsize = min([mesh.cell_data["hsize"][0] for mesh in self.fluid_domain_surface_meshes])
375376
radii = []
376377
for net in range(len(self.synthetic_object.networks)):
377378
for tr in range(len(self.synthetic_object.networks[net])):

svv/simulation/utils/extract_faces.py

Lines changed: 72 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -396,14 +396,18 @@ def compute_circularity(loop_polydata):
396396
if not isinstance(mesh, type(None)):
397397
global_nodes = mesh.points
398398
global_node_tree = cKDTree(global_nodes)
399-
global_elements = mesh.cell_connectivity.reshape(-1, 4)
400-
global_elements = numpy.sort(global_elements, axis=1)
401-
tet_faces = []
402-
for i in tqdm.trange(global_elements.shape[0], desc="Building tetrahedral faces", leave=False):
403-
for j in range(4):
404-
idx = set(list(range(4))) - set([j])
405-
tet_faces.append(global_elements[i, list(idx)])
406-
tet_face_tree = cKDTree(tet_faces)
399+
# Build a robust face->cell index using canonical keys (sorted tuples)
400+
face_to_cell = {}
401+
n_cells = mesh.n_cells
402+
for i in tqdm.trange(n_cells, desc="Indexing cell faces", leave=False):
403+
cell = mesh.GetCell(i)
404+
nfaces = cell.GetNumberOfFaces()
405+
for j in range(nfaces):
406+
face = cell.GetFace(j)
407+
npts = face.GetNumberOfPoints()
408+
# Canonicalize face nodes to a sorted tuple so orientation/order doesn't matter
409+
key = tuple(sorted(face.GetPointId(k) for k in range(npts)))
410+
face_to_cell[key] = i
407411
#for i, cap in enumerate(iscap):
408412
# if not cap == 1:
409413
# walls.append(faces[i])
@@ -416,18 +420,33 @@ def compute_circularity(loop_polydata):
416420
wall_cells = surface.extract_cells(walls[i])
417421
wall_surface = wall_cells.extract_surface()
418422
if not isinstance(mesh, type(None)):
419-
wall_surface.point_data["GlobalNodeID"] = numpy.zeros(wall_surface.n_points, dtype=int)
420-
wall_surface.cell_data['GlobalElementID'] = numpy.zeros(wall_surface.n_cells, dtype=int)
421-
wall_surface.cell_data['ModelFaceID'] = numpy.ones(wall_surface.n_cells, dtype=int)
423+
wall_surface.point_data["GlobalNodeID"] = numpy.zeros(wall_surface.n_points, dtype=numpy.int32)
424+
wall_surface.cell_data['GlobalElementID'] = numpy.zeros(wall_surface.n_cells, dtype=numpy.int32)
425+
wall_surface.cell_data['ModelFaceID'] = numpy.ones(wall_surface.n_cells, dtype=numpy.int32)
422426
_, indices = global_node_tree.query(wall_surface.points)
423427
wall_surface.point_data["GlobalNodeID"] = indices.astype(int)
424428
# Assign Global Element IDs
425429
wall_faces = wall_surface.point_data["GlobalNodeID"][wall_surface.faces]
426-
wall_faces = wall_faces.reshape(-1, 4)[:, 1:]
427-
wall_faces = numpy.sort(wall_faces, axis=1)
428-
_, indices = tet_face_tree.query(wall_faces)
429-
wall_surface.cell_data["GlobalElementID"] = indices // 4
430-
wall_surface.cell_data["GlobalElementID"] = wall_surface.cell_data["GlobalElementID"].astype(numpy.int32)
430+
wall_faces = wall_faces.reshape(-1, 4)[:, 1:].tolist()
431+
elem_ids = []
432+
for face in wall_faces:
433+
# Use the same canonical key used to build the map
434+
elem_ids.append(face_to_cell[tuple(sorted(face))])
435+
wall_surface.cell_data["GlobalElementID"] = numpy.array(elem_ids, dtype=numpy.int32)
436+
#wall_faces = numpy.sort(wall_faces, axis=1)
437+
#dists, indices = tet_face_tree.query(wall_faces)
438+
#if not numpy.all(numpy.isclose(dists, 0.0)):
439+
# # Identify a small sample of mismatches for debugging
440+
# bad_idx = numpy.where(~numpy.isclose(dists, 0.0))[0]
441+
# sample = bad_idx[:5]
442+
# examples = wall_faces[sample]
443+
# raise ValueError(
444+
# f"Failed to map all wall surface faces to volume mesh faces: {bad_idx.size} mismatches. "
445+
# f"Example face node triples (GlobalNodeID) that failed exact match: {examples.tolist()}"
446+
# )
447+
#wall_surface.cell_data["GlobalElementID"] = indices // 4
448+
#wall_surface.cell_data["GlobalElementID"] = wall_surface.cell_data["GlobalElementID"].astype(numpy.int32)
449+
431450
boundaries = wall_surface.extract_feature_edges(boundary_edges=True, manifold_edges=False,
432451
feature_edges=False, non_manifold_edges=False)
433452
boundaries = boundaries.split_bodies()
@@ -444,19 +463,31 @@ def compute_circularity(loop_polydata):
444463
cap_cells = surface.extract_cells(face_cap)
445464
cap_surface = cap_cells.extract_surface()
446465
if not isinstance(mesh, type(None)):
447-
cap_surface.point_data["GlobalNodeID"] = numpy.zeros(cap_surface.n_points, dtype=int)
448-
cap_surface.cell_data["GlobalElementID"] = numpy.zeros(cap_surface.n_cells, dtype=int)
449-
cap_surface.cell_data["ModelFaceID"] = numpy.ones(cap_surface.n_cells, dtype=int) * (i + 2)
466+
cap_surface.point_data["GlobalNodeID"] = numpy.zeros(cap_surface.n_points, dtype=numpy.int32)
467+
cap_surface.cell_data["GlobalElementID"] = numpy.zeros(cap_surface.n_cells, dtype=numpy.int32)
468+
cap_surface.cell_data["ModelFaceID"] = numpy.ones(cap_surface.n_cells, dtype=numpy.int32)
450469
# Assign Global Node IDs
451470
_, indices = global_node_tree.query(cap_surface.points)
452471
cap_surface.point_data["GlobalNodeID"] = indices.astype(int)
453472
# Assign Global Element IDs
454473
cap_faces = cap_surface.point_data["GlobalNodeID"][cap_surface.faces]
455-
cap_faces = cap_faces.reshape(-1, 4)[:, 1:]
456-
cap_faces = numpy.sort(cap_faces, axis=1)
457-
_, indices = tet_face_tree.query(cap_faces)
458-
cap_surface.cell_data["GlobalElementID"] = indices // 4
459-
cap_surface.cell_data["GlobalElementID"] = cap_surface.cell_data["GlobalElementID"].astype(numpy.int32)
474+
cap_faces = cap_faces.reshape(-1, 4)[:, 1:].tolist()
475+
elem_ids = []
476+
for face in cap_faces:
477+
elem_ids.append(face_to_cell[tuple(sorted(face))])
478+
cap_surface.cell_data["GlobalElementID"] = numpy.array(elem_ids, dtype=numpy.int32)
479+
#cap_faces = numpy.sort(cap_faces, axis=1)
480+
#dists, indices = tet_face_tree.query(cap_faces)
481+
#if not numpy.all(numpy.isclose(dists, 0.0)):
482+
# bad_idx = numpy.where(~numpy.isclose(dists, 0.0))[0]
483+
# sample = bad_idx[:5]
484+
# examples = cap_faces[sample]
485+
# raise ValueError(
486+
# f"Failed to map all cap surface faces to volume mesh faces: {bad_idx.size} mismatches. "
487+
# f"Example face node triples (GlobalNodeID) that failed exact match: {examples.tolist()}"
488+
# )
489+
#cap_surface.cell_data["GlobalElementID"] = indices // 4
490+
#cap_surface.cell_data["GlobalElementID"] = cap_surface.cell_data["GlobalElementID"].astype(numpy.int32)
460491
boundaries = cap_surface.extract_feature_edges(boundary_edges=True, manifold_edges=False,
461492
feature_edges=False, non_manifold_edges=False)
462493
boundaries = boundaries.split_bodies()
@@ -482,11 +513,23 @@ def compute_circularity(loop_polydata):
482513
lumen_surface.point_data["GlobalNodeID"] = indices.astype(int)
483514
# Assign Global Element IDs
484515
lumen_faces = lumen_surface.point_data["GlobalNodeID"][lumen_surface.faces]
485-
lumen_faces = lumen_faces.reshape(-1, 4)[:, 1:]
486-
lumen_faces = numpy.sort(lumen_faces, axis=1)
487-
_, indices = tet_face_tree.query(lumen_faces)
488-
lumen_surface.cell_data["GlobalElementID"] = indices // 4
489-
lumen_surface.cell_data["GlobalElementID"] = lumen_surface.cell_data["GlobalElementID"].astype(numpy.int32)
516+
lumen_faces = lumen_faces.reshape(-1, 4)[:, 1:].tolist()
517+
elem_ids = []
518+
for face in lumen_faces:
519+
elem_ids.append(face_to_cell[tuple(sorted(face))])
520+
lumen_surface.cell_data["GlobalElementID"] = numpy.array(elem_ids, dtype=numpy.int32)
521+
#lumen_faces = numpy.sort(lumen_faces, axis=1)
522+
#dists, indices = tet_face_tree.query(lumen_faces)
523+
#if not numpy.all(numpy.isclose(dists, 0.0)):
524+
# bad_idx = numpy.where(~numpy.isclose(dists, 0.0))[0]
525+
# sample = bad_idx[:5]
526+
# examples = lumen_faces[sample]
527+
# raise ValueError(
528+
# f"Failed to map all lumen surface faces to volume mesh faces: {bad_idx.size} mismatches. "
529+
# f"Example face node triples (GlobalNodeID) that failed exact match: {examples.tolist()}"
530+
# )
531+
#lumen_surface.cell_data["GlobalElementID"] = indices // 4
532+
#lumen_surface.cell_data["GlobalElementID"] = lumen_surface.cell_data["GlobalElementID"].astype(numpy.int32)
490533
boundaries = lumen_surface.extract_feature_edges(boundary_edges=True, manifold_edges=False,
491534
feature_edges=False, non_manifold_edges=False)
492535
boundaries = boundaries.split_bodies()

svv/tree/export/export_solid.py

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -380,7 +380,8 @@ def union_tubes(tubes, lines, cap_resolution=40):
380380
hsize = min(hsize, (min(lines[i]['radius'])*2*numpy.pi)/cap_resolution)
381381
model = remesh_surface(model, hsiz=hsize)
382382
model = model.compute_normals(auto_orient_normals=True)
383-
model.hsize = hsize
383+
model.cell_data['hsize'] = 0
384+
model.cell_data['hsize'][0] = hsize
384385
return model
385386

386387

@@ -413,9 +414,11 @@ def build_watertight_solid(tree, cap_resolution=40):
413414
non_manifold_model = non_manifold_model.extract_surface()
414415
fix = pymeshfix.MeshFix(non_manifold_model)
415416
fix.repair(verbose=True)
416-
hsize = model.hsize
417+
hsize = model.cell_data["hsize"][0] #hsize
417418
model = fix.mesh.compute_normals(auto_orient_normals=True)
418-
model.hsize = hsize
419+
#model.hsize = hsize
420+
model.cell_data['hsize'] = 0
421+
model.cell_data['hsize'][0] = hsize
419422
return model
420423

421424

svv/visualize/gui/__init__.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,13 @@ def launch_gui(domain=None, block=True, style='cad'):
8282
if style == 'cad':
8383
gui = VascularizeCADGUI(domain=domain)
8484
else:
85+
import warnings
86+
warnings.warn(
87+
"The 'modern' GUI style is deprecated and will be removed in a future version. "
88+
"Please use 'cad' style (the default) instead.",
89+
DeprecationWarning,
90+
stacklevel=2
91+
)
8592
gui = VascularizeGUI(domain=domain)
8693

8794
gui.show()

0 commit comments

Comments
 (0)