Real-time interactive virtual stenting and vascular morphing with SDF-contact sculpting and regularized Kelvinlets
svMorph is a research tool for in-silico morphological editing of patient-specific vascular geometries. It enables interactive and scriptable creation of synthetic vascular pathologies — aneurysms, stenoses, and synthetic interventions — stenting, on triangulated surface meshes with associated centerlines.
The deformation engine includes a signed-distance-field (SDF) contact formulation that naturally mimics the stent–wall interface during simulated stent deployment, and regularized Kelvinlet displacement kernels (de Goes & James, 2017, Pham et al., 2024) that model synthetic aneurysm and stenosis shapes. All heavy numerics are JIT-compiled with JAX for real-time feedback on commodity hardware.
Paper If you use svMorph in published work, please cite the companion paper (reference forthcoming).
| Mode | Description |
|---|---|
| Stent deployment | SDF-contact expansion of a crimped capsule-chain stent against the vessel wall, with bounding-box culling, KD-tree influence blending, and smooth-min C¹-continuous distance fields. |
| Stent deployment with straightening | Identical to above, with concurrent projection of the stent axis toward a straight line at each step. |
| Aneurysm creation | Outward inflation at a centerline point via the scaling regularized Kelvinlet (F = s·I). |
| Stenosis creation | Inward contraction using a truncated-sphere quartic bump profile. |
All four modes are available through both the interactive GUI (PyQt6 + VTK) and headless CLI scripts suitable for batch processing and CI pipelines.
svmorph/
├── core/ # Pure computation — no VTK, no Qt
│ ├── deformation.py # Kelvinlet kernels, SDF-contact, displacement assembly
│ ├── geometry.py # Arc-length centerline resampling (branch-aware)
│ ├── mesh_data.py # Material constants, displacement application
│ ├── defaults.py # Spatial default constants (cm)
│ └── units.py # Runtime cm ↔ mm unit scaling
│
├── visualization/ # VTK rendering and I/O
│ ├── vtk_io.py # VTP read/write, mesh array extraction, centerline utilities
│ ├── renderer.py # SceneManager — VTK pipeline construction
│ └── interactor.py # MeshInteractor — interactive trackball camera + deformation dispatch
│
├── gui/ # PyQt6 application
│ └── main_window.py # MainWindow with sliders, buttons, and VTK render widget
│
├── scripts/ # Headless CLI entry points
│ ├── common.py # SimulationContext, SnapshotManager, shared CLI helpers
│ ├── deploy_stent.py
│ ├── deploy_stent_straighten.py
│ ├── create_aneurysm.py
│ └── create_stenosis.py
│
└── logging.py # Structured logging with custom TIMING level
The core/ subpackage is dependency-light (JAX, NumPy, SciPy only) and carries
no VTK or Qt dependency, making it straightforward for downstream tools — such as
3D Slicer extensions or ParaView
plugins — to import and build upon the deformation engine independently:
from svmorph.core import (
compute_sdf_contact_displacements,
compute_aneurysm_displacements,
compute_stenosis_displacements,
resample_stent_axis,
)| Dependency | Version | Notes |
|---|---|---|
| Python | 3.9+ | Tested with 3.9.19 |
| NumPy | 1.24+ | |
| SciPy | 1.10+ | KD-tree for influence-zone queries |
| VTK | 9.3 | VTP mesh I/O and rendering |
| JAX (CPU) | 0.4.30 | JIT compilation of deformation kernels |
| PyQt6 | 6.7 | GUI only — not required for headless scripts |
- Create a new conda environment and activate it:
conda create -y -n svmorph python=3.9
conda activate svmorph- Decide based on the use case:
- to install svMorph with the full interactive GUI:
pip install "svmorph[gui]"- to install only the core deformation engine and scripts without the GUI:
pip install svmorphsvMorph with the optional full GUI:
pip install -r requirements-gui.txtCore deformation engine and scripts without the GUI:
pip install -r requirements.txtMicromamba resolves native VTK binaries quickly and coexists with Homebrew and system Python.
- Create environment with conda-forge packages:
micromamba create -y -n svmorph \
python=3.9.19 \
numpy=1.24.4 \
scipy=1.10.1 \
vtk=9.3.0 \
-c conda-forge- Activate and install pip-only packages:
micromamba activate svmorph
pip install "jax[cpu]==0.4.30" "pyqt6==6.7"- Remove the duplicate Qt runtime pulled by VTK's conda deps:
micromamba remove -n svmorph qt6-main --force| Platform | Status |
|---|---|
| macOS (Apple Silicon) | Primary development platform. Use Option B above. |
| macOS (Intel) | Option A or B. Replace osx-arm64 with osx-64 if building micromamba from source. |
| Linux (x86_64) | Option A or B. Both pip wheels and conda packages are available for all dependencies. |
| Windows | Option A (pip install -r requirements.txt) in a standard Python 3.9+ environment. |
python -c "
import numpy, scipy, jax, PyQt6.QtCore
from vtkmodules.vtkCommonCore import vtkVersion
print(f'NumPy {numpy.__version__}')
print(f'SciPy {scipy.__version__}')
print(f'VTK {vtkVersion.GetVTKVersion()}')
print(f'JAX {jax.__version__}')
print(f'Qt {PyQt6.QtCore.PYQT_VERSION_STR}')
"This uses vtkmodules.vtkCommonCore.vtkVersion instead of import vtk so the check stays quick; it is the same vtkVersion class exposed as vtk.vtkVersion when you import the full vtk package.
python main.py # default: units in cm, INFO logging on
python main.py --units mm # for editing millimeter geometry, units in mm
python main.py --verbose # show per-step timing
python main.py --debug # full diagnostic outputWorkflow overview:
- Import a surface mesh and its centerline (VTP format).
- Click a centerline point to select the deformation site.
- Adjust stent dimensions, force scale, or pathology parameters via the slider panel.
- Apply deformation — one step at a time or continuously.
- Save the modified mesh to a new VTP file.
Keyboard shortcuts:
| Key | Action |
|---|---|
H |
Toggle stent visualization visibility |
D (hold) |
Continuous aneurysm deformation while held |
The control panel sits below the 3D viewport and is organized into five horizontal rows. Each row groups related controls for a specific workflow.
┌──────────────────────────────────────────────────────────────────────────┐
│ │
│ 3D VTK Viewport │
│ (trackball rotate / zoom / pan) │
│ │
├──── Row 1 ── Stent geometry ─────────────────────────────────────────────┤
│ Stent Length (cm): ◄══════════╪══════════► [1.7000] │
│ Stent Diameter (cm): ◄══════════╪══════════► [0.8000] │
├──── Row 2 ── Stent deployment ───────────────────────────────────────────┤
│ Force Scale: ◄══════════╪══════════► [1.000] │
│ [Select Point] [Straighten Stent] [Expand Stent (One Step)] [Expand …] │
├──── Row 3 ── Stenosis ──────────────────────────────────────────────────┤
│ Stenosis Min Radius (cm): [0.1] Stenosis Region Length (cm): [0.5] │
│ [Apply Stenosis (One Step)] [Apply Stenosis] │
├──── Row 4 ── Aneurysm ──────────────────────────────────────────────────┤
│ Aneurysm Max Radius (cm): [0.5] │
│ Sharpness: ◄══════════╪══════════► [1.000] │
│ [Apply Aneurysm (One Step)] [Apply Aneurysm] │
├──── Row 5 ── Utility ───────────────────────────────────────────────────┤
│ [Import Mesh] [Import Centerline] │
│ [Camera Lock] [Visualize SDF] [Place Stent] [Save Mesh] │
└──────────────────────────────────────────────────────────────────────────┘
| Control | Type | Description |
|---|---|---|
| Stent Length | slider + text | Length of the capsule-chain stent along the centerline (1.0–8.0 cm). Adjusting this recomputes and redraws the stent axis vertices in real time. |
| Stent Diameter | slider + text | Target deployed diameter of the stent (0.1–2.0 cm). This sets the radius goal for the SDF-contact expansion loop. |
Both controls are bidirectional: dragging the slider updates the text field, and typing a value snaps the slider to match.
| Control | Type | Description |
|---|---|---|
| Force Scale | slider + text | Scales the magnitude of each displacement step (−1.0 to 1.0). Higher values produce larger per-step deformations; negative values reverse the direction. |
| Select Point | button | Enters point-selection mode: the mesh becomes translucent and cyan glyphs appear at every centerline vertex. Click a vertex to select the stent's distal starting point. |
| Straighten Stent | button | Runs one combined step of SDF-contact stent expansion and axis straightening — the stent is projected toward the line connecting its endpoints. |
| Expand Stent (One Step) | button | Executes a single SDF-contact displacement iteration: the stent radius increments by one step and the vessel wall deforms outward at contact points. |
| Expand Stent | hold button | Hold to run continuous SDF-contact expansion (fires every 50 ms). Release to stop. The title bar shows live FPS, current stent radius, and diameter. |
| Control | Type | Description |
|---|---|---|
| Stenosis Min Radius | text | Target minimum lumen radius at the stenosis center. The deformation loop stops when the representative surface point reaches this radius. |
| Stenosis Region Length | text | Outer annular cutoff that controls the axial extent of the narrowing. Larger values produce longer, more gradual stenoses. |
| Apply Stenosis (One Step) | button | Executes a single inward contraction step using the truncated-sphere quartic bump profile. |
| Apply Stenosis | hold button | Hold for continuous stenosis creation (fires every 25 ms). Release to stop. |
| Control | Type | Description |
|---|---|---|
| Aneurysm Max Radius | text | Target maximum vessel radius at the aneurysm site. The loop stops when the representative surface point reaches this radius. |
| Sharpness | slider + text | Controls the Kelvinlet regularization parameter ε. Low sharpness (< 1) produces broad, diffuse bulges; high sharpness (> 1) produces focal, concentrated expansions. The mapping is exponential (slider center = 1.0). |
| Apply Aneurysm (One Step) | button | Executes a single outward scaling Kelvinlet displacement step. |
| Apply Aneurysm | hold button | Hold for continuous aneurysm inflation (fires every 50 ms). Release to stop. |
| Control | Type | Description |
|---|---|---|
| Import Mesh | button | Opens a file dialog to load a surface .vtp. The viewport reinitializes once both mesh and centerline are loaded. |
| Import Centerline | button | Opens a file dialog to load a centerline .vtp. |
| Camera Lock | toggle button | Locks the camera focal point to the currently selected centerline vertex. The button turns orange when active. Click again to release. |
| Visualize SDF | button | Evaluates the capsule-chain SDF on a 100×100×100 regular grid and renders the zero iso-surface via marching cubes. Useful for inspecting stent geometry before or during deployment. |
| Place Stent | button | Commits the current stent visualization as a persistent actor in the scene, so it remains visible when selecting a new point. |
| Save Mesh | button | Opens a "Save As" dialog to write the deformed surface mesh to a new .vtp file. |
All scripts accept --help for full argument documentation.
Deploy a stent — expands a crimped stent (initial radius 0.05 cm) to a deployed radius of 0.4 cm (diameter 0.8 cm), with a total stent length of 1.7 cm. The distal tip is placed at centerline point ID 123. Intermediate snapshots are saved every 0.1 cm of radius change.
Note:
--start-Rmust be smaller than the local vessel radius at the deployment site so the stent begins fully inside the lumen. A value of 0.05 cm works well for typical cardiovascular geometries.
python -m svmorph.scripts.deploy_stent \
--mesh surface.vtp --cline centerline.vtp \
--start 123 --target-R 0.4 --start-R 0.05 --length 1.7 \
--save-step 0.1 \
--out-mesh deployed_surface.vtp --out-cl deployed_centerline.vtpDeploy with concurrent axis straightening — same stent geometry as above, but after each expansion step the stent axis is projected toward the straight line connecting its endpoints (strength 0.075), gradually removing curvature from the deployed configuration.
python -m svmorph.scripts.deploy_stent_straighten \
--mesh surface.vtp --cline centerline.vtp \
--start 123 --target-R 0.4 --start-R 0.05 --length 1.7 \
--straightening-strength 0.075 \
--out-mesh deployed_surface.vtp --out-cl deployed_centerline.vtpCreate an aneurysm — inflates the vessel wall at centerline point ID 456 until the local maximum radius reaches 0.5 cm. Sharpness 1.0 gives a moderate focal bulge; lower values spread the deformation over a wider region. Snapshots are saved every 0.02 cm of radius growth.
python -m svmorph.scripts.create_aneurysm \
--mesh surface.vtp --cline centerline.vtp \
--center 456 --target-R 0.5 --sharpness 1.0 --force-scale -1.0 \
--save-step 0.02 --out-mesh aneurysm_surface.vtpCreate a stenosis — narrows the vessel at centerline point ID 789 until the minimum lumen radius shrinks to 0.1 cm. The stenosis region extends 0.5 cm axially from the center. Snapshots are saved every 0.02 cm of radius reduction.
python -m svmorph.scripts.create_stenosis \
--mesh surface.vtp --cline centerline.vtp \
--center 789 --target-R 0.1 --stenosis-length 0.5 --force-scale 1.0 \
--save-step 0.02 --out-mesh stenosis_surface.vtp| Flag | Description |
|---|---|
--mesh |
Input surface .vtp (required) |
--cline |
Input centerline .vtp (required) |
--out-mesh |
Output surface path |
--out-cl |
Output centerline path (stent scripts) |
--save-step |
Write intermediate snapshots at this radius interval |
--units |
cm (default) or mm |
--verbose |
TIMING-level log output |
--debug |
DEBUG-level log output |
When --save-step is provided, intermediate results are written to a
{out_mesh_stem}_intermediates/ directory as milestone VTP files.
svMorph operates on VTK XML PolyData (.vtp) files, the standard output of
SimVascular and other cardiovascular modeling
pipelines.
Surface mesh — a triangulated surface with point coordinates.
Centerline — a polyline with the following expected point data arrays (produced by SimVascular's centerline extraction or VMTK):
| Array name | Type | Description |
|---|---|---|
MaximumInscribedSphereRadius |
scalar | MIS radius at each centerline point |
CenterlineSectionArea |
scalar | Cross-sectional lumen area |
Branching centerlines are supported; the BranchIdTmp and CenterlineId arrays
are used to construct a parent-tip map for arc-length walks across bifurcations.
svMorph defaults to centimeters when --units is unspecified (equivalent to
--units cm). This matches the convention used by
SimVascular, where exported surface
meshes, centerlines and TetGen'ed mesh exteriors are typically in cm.
Some pipelines (e.g. certain VMTK or 3D Slicer workflows) produce geometry
in millimeters instead.
The --units flag (available on both the GUI and all CLI scripts) tells svMorph
which coordinate system your input files use. When you switch to --units mm,
all built-in default parameters are automatically scaled by a factor of 10 so
they remain physically correct — you do not need to manually convert them.
| Parameter | Default (cm mode) | Default (mm mode) |
|---|---|---|
| Stent diameter | 0.8 cm | 8.0 mm |
| Stent length | 1.7 cm | 17.0 mm |
| Target stent radius | 0.4 cm | 4.0 mm |
| Initial crimped radius | 0.05 cm | 0.5 mm |
| Stenosis target radius | 0.1 cm | 1.0 mm |
| Influence radius (doi in paper) | 0.65 cm | 6.5 mm |
| Contact distance (doc in paper) | 0.001 cm | 0.01 mm |
Key rules:
-
Match
--unitsto your mesh. If your VTP coordinates are in millimeters, pass--units mm. If they are in centimeters (SimVascular default), use the default--units cmor omit the flag. -
User-supplied values must be in the active unit. When you provide explicit arguments such as
--target-R 4.0or--length 17.0, those numbers are interpreted in the unit system you selected. In mm mode,--target-R 4.0means 4.0 mm; in cm mode it would mean 4.0 cm. -
GUI labels update automatically. Slider labels and text fields display the active unit name (e.g. "Stent Length (mm):") so there is no ambiguity while interacting.
-
Internally, all constants live in centimeters in
defaults.pyand are multiplied by the runtime scale factorL()fromunits.py. If you add new spatial constants, follow the same pattern.
The svmorph.core subpackage is intentionally free of VTK and Qt imports.
To build a downstream plugin:
- Import the deformation engine from
svmorph.core. - Bridge your own mesh representation to NumPy arrays matching the
simulation data dictionary layout (see
vtk_io.extract_mesh_arraysfor the reference schema). - Call displacement routines and apply the returned arrays to your mesh.
Simulation data dictionary schema:
data = {
"points": {
"surface": np.ndarray, # (N_surf, 3) surface vertex coordinates
"centerline": np.ndarray, # (N_cl, 3) centerline vertex coordinates
},
"nodes": {
"all_indices": jnp.ndarray, # selected centerline point indices
"force_center_point_id": int,
},
}- Coding style — type-annotated Python 3.9+, NumPy-style docstrings.
- Logging — use
from svmorph.logging import get_logger; logger = get_logger(__name__). Uselogger.timing(...)for performance instrumentation. - Units — store constants in centimetres in
defaults.py; multiply byL()at runtime. Never hard-code unit-dependent values outsidedefaults.py.
This project is licensed under the MIT License.
- VTK — 3D visualization and mesh processing
- JAX — composable transformations and JIT compilation
- PyQt6 — cross-platform GUI framework
- SimVascular — cardiovascular modeling pipeline
If import jax fails after a successful pip install, your interpreter may be x86_64
(Rosetta) instead of native ARM. On Apple Silicon, the JAX wheels pip installs expect a
native arm64 Python; an x86_64 interpreter will not load those wheels.
Create or recreate the environment forcing the osx-arm64 package subdir (and use Python 3.9 as elsewhere in this README) before installing:
CONDA_SUBDIR=osx-arm64 conda create -y -n svmorph python=3.9
conda activate svmorph
pip install -r requirements-gui.txt # or requirements.txt(With plain conda, CONDA_SUBDIR=osx-arm64 applies for that command; with mamba
/ micromamba, the same variable works the same way. If an old x86_64 env already
exists, prefer a new env name or remove the old one rather than mixing architectures.)
Qt 6.10 (released October 2025) introduced a regression in its macOS platform integration that causes PyQt6 applications launched from the command line to freeze immediately on startup — the window never appears and the process shows "Application Not Responding." This affects macOS Sequoia and macOS 26 Tahoe.
PyQt6 versions 6.7, 6.8, and 6.9 all work correctly up through macOS Tahoe. The requirements-gui.txt is pinned to pyqt6>=6.7,<6.10 to avoid the broken release. If you are seeing this freeze, check which PyQt6 version is installed:
python -c "import PyQt6.QtCore; print(PyQt6.QtCore.PYQT_VERSION_STR)"If the output is 6.10.x, force-downgrade to a working version:
pip install "pyqt6>=6.7,<6.10"This is expected and only happens once. The deformation engine uses JAX with JIT (Just-In-Time) compilation: the first time each @jax.jit-decorated function is called, JAX traces it and compiles it to optimized XLA machine code for your hardware. There are several such functions in the deformation module, and each compilation step can take 20–30 seconds, adding up to roughly 1–2 minutes on the very first run.
JAX automatically caches the compiled artifacts to disk (typically ~/.jax_cache). Every subsequent run loads those precompiled binaries directly, so startup is effectively instant.
This is not a bug — it is the standard JAX/XLA warm-up cost, paid once in exchange for fast GPU-accelerated numerics on all future runs. The cache persists across terminal sessions, so you only recompile if you:
- Delete the JAX cache manually
- Upgrade or change JAX/XLA/Python versions
- Switch to a different machine or hardware
Jeff Bohan Li
Cardiovascular Biomechanics Computation Lab, Stanford University
[email protected]