Pixedar's picture
Deploy MindVisualizer live runtime
6242ad2
Raw
History Blame Contribute Delete
28.8 kB
#!/usr/bin/env python3
"""HF-safe live MindVisualizer wrapper.
This is a web transport for the Python/VTK runtime. It imports the original
project modules for field loading, particle helpers, mesh overlay, probes, and
GPT analysis instead of reimplementing the algorithms in TypeScript.
"""
from __future__ import annotations
import argparse
import asyncio
import os
import sys
import threading
from pathlib import Path
import numpy as np
import vtk
from scipy.spatial import cKDTree
from vtkmodules.util.numpy_support import numpy_to_vtk
from vtkmodules.vtkInteractionStyle import vtkInteractorStyleTrackballCamera
PROJECT_ROOT = Path(__file__).resolve().parent
DATA_DIR = PROJECT_ROOT / "data"
MDN_DIR = DATA_DIR / "mdn"
MESH_DIR = DATA_DIR / "meshes"
ALIGNMENT_FILE = DATA_DIR / "brain_alignment.json"
DEFAULT_META = MDN_DIR / "mdn_particles_rdcim_teacher_edge_hq_grid125_meta.json"
DEFAULT_OOS = MDN_DIR / "mdn_particles_rdcim_teacher_edge_hq_training_points.npy"
if str(PROJECT_ROOT) not in sys.path:
sys.path.insert(0, str(PROJECT_ROOT))
from src.colormaps import bicolor_white_center, rainbow_rgb01, turbo_rgb01
from src.field_loader import TriLinearSampler, load_field, load_points_any
from src.main import (
VtkTextOverlay,
add_window_legend,
build_cloud,
compute_fraction_mask,
densify_oos_surface,
lattice_positions,
wrap_inside,
)
from src.mesh_overlay import FlowMeshOverlay
from src.probe import ProbeSystem
from src.region_analyzer import analyze_probe_path, analyze_with_gpt
def ensure_ssl() -> None:
try:
import certifi
cert_file = certifi.where()
for name in ("SSL_CERT_FILE", "REQUESTS_CA_BUNDLE"):
current = os.environ.get(name, "")
if not current or not os.path.isfile(current):
os.environ[name] = cert_file
except Exception:
pass
class LiveMindVisualizer:
def __init__(self, args: argparse.Namespace):
self.args = args
self.rng = np.random.default_rng(2025)
self.paused = False
self.field_mode = "mean"
self.colour_mode = "SPEED"
self.cmap_mode = "TURBO"
self.filter_mode = "OFF"
self.filter_top = True
self.filter_frac = 1.0
self.clip_on = False
self.clip_frac = 1.0
self.mdn_alpha = 1.0
self.oos_visible = False
self.probe_busy = False
self.gpt_pending: str | None = None
self._load_data()
self._setup_vtk()
def _load_data(self) -> None:
self.field = load_field(self.args.meta)
self.G = self.field["G"]
self.amin = self.field["amin"]
self.amax = self.field["amax"]
self.V_mean = self.field["mean"]
self.MUS = self.field["mus"]
self.PI = self.field["pi"]
self.ENT = self.field["ENT"]
self.K = len(self.MUS)
self.sampler_mean = TriLinearSampler(self.V_mean, self.amin, self.amax)
self.diag = float(np.linalg.norm(self.amax - self.amin))
self.target_step = self.args.max_step_frac * max(self.diag, 1e-6)
self.vmax_mean = self._interior_percentile(self.V_mean)
self.samplers_comp = [TriLinearSampler(mu, self.amin, self.amax) for mu in self.MUS]
self.sampler_gated = None
self.V_gated = None
if self.PI is not None and self.PI.ndim == 4 and self.PI.shape[-1] == self.K and self.K > 0:
idx = np.argmax(self.PI, axis=-1)
self.V_gated = np.zeros_like(self.MUS[0], dtype=np.float32)
for k_i in range(self.K):
self.V_gated += self.MUS[k_i] * (idx == k_i)[..., None].astype(np.float32)
self.sampler_gated = TriLinearSampler(self.V_gated, self.amin, self.amax)
self.field_modes = ["mean"]
if self.sampler_gated is not None:
self.field_modes.append("gated")
self.field_modes += [f"comp{i + 1}" for i in range(self.K)]
self.sampler_by_name = {"mean": self.sampler_mean}
if self.sampler_gated is not None:
self.sampler_by_name["gated"] = self.sampler_gated
for i, sampler in enumerate(self.samplers_comp):
self.sampler_by_name[f"comp{i + 1}"] = sampler
self.vmax_by_name = {"mean": self.vmax_mean}
if self.V_gated is not None:
self.vmax_by_name["gated"] = self._interior_percentile(self.V_gated)
for i, mu in enumerate(self.MUS):
self.vmax_by_name[f"comp{i + 1}"] = self._interior_percentile(mu)
self.ent_min = self.ent_max = 0.0
if self.ENT is not None:
self.ent_min = float(np.min(self.ENT))
self.ent_max = float(np.max(self.ENT))
self.oos_pts = None
if self.args.oos is not None and Path(self.args.oos).exists():
self.oos_pts = load_points_any(Path(self.args.oos)).astype(np.float32)
self._seed_particles()
self._prepare_oos_distance()
def _interior_percentile(self, vec: np.ndarray, q: float = 100.0) -> float:
mag = np.linalg.norm(vec.reshape(-1, 3), axis=1)
nz = mag[mag > 0]
return float(np.percentile(nz, q)) if nz.size else 1.0
def _seed_particles(self) -> None:
overlap_radius = float(self.args.overlap_frac) * max(self.diag, 1e-9)
overlap_sigma = float(self.args.overlap_jitter) * max(self.diag, 1e-9)
self.seed_mode = "grid"
self.seed_points = None
self.n_axis = None
train = self.field["TRAIN"]
if train is not None and self.oos_pts is not None and len(train) > 0 and len(self.oos_pts) > 0:
try:
tree = cKDTree(train.astype(np.float32))
dists, _ = tree.query(self.oos_pts.astype(np.float32), k=1)
candidates = self.oos_pts[dists <= overlap_radius]
if candidates.size > 0:
self.seed_mode = "overlap"
if self.args.overlap_limit and len(candidates) > self.args.overlap_limit:
idx = self.rng.choice(len(candidates), size=int(self.args.overlap_limit), replace=False)
candidates = candidates[idx]
self.seed_points = candidates.astype(np.float32)
except Exception as exc:
print("[seed] overlap failed:", exc)
if self.seed_mode == "overlap" and self.seed_points is not None:
if self.args.oos_seed_limit > 0 and len(self.seed_points) > self.args.oos_seed_limit:
idx = self.rng.choice(len(self.seed_points), size=self.args.oos_seed_limit, replace=False)
self.seed_points = self.seed_points[idx]
if self.args.oos_fill_count > 0 and self.oos_pts is not None:
extras = densify_oos_surface(
base_pts=self.seed_points,
full_oos_pts=self.oos_pts,
amin=self.amin,
amax=self.amax,
extra_count=self.args.oos_fill_count,
)
if extras is not None and len(extras) > 0:
self.seed_points = np.concatenate([self.seed_points, extras], axis=0).astype(np.float32)
if self.seed_mode == "overlap" and self.seed_points is not None:
if overlap_sigma > 0:
jitter = self.rng.standard_normal(self.seed_points.shape).astype(np.float32) * overlap_sigma
self.P0 = np.clip(self.seed_points + jitter, self.amin, self.amax)
else:
self.P0 = np.clip(self.seed_points, self.amin, self.amax)
else:
self.n_axis = max(2, self.G // max(1, self.args.stride))
self.P0 = lattice_positions(
self.n_axis,
self.amin,
self.amax,
margin=0.02,
jitter=self.args.respawn_jitter,
seed=2025,
)
self.P = self.P0.copy()
self.Np = len(self.P)
self.base_ttl_lo = max(8, 60 // 2)
self.base_ttl_hi = 60
self.ttl_scale = 1.0
self.ttl = self._sample_ttl(self.Np)
self.ages = self.rng.integers(0, np.maximum(1, self.ttl), size=self.Np, dtype=np.int32)
self.ent_prev = self.sampler_mean.sample_scalar(self.ENT, self.P).astype(np.float32) if self.ENT is not None else None
self.ent_d_ema = np.zeros(self.Np, np.float32) if self.ENT is not None else None
def _sample_ttl(self, n: int) -> np.ndarray:
base = self.rng.integers(self.base_ttl_lo, self.base_ttl_hi + 1, size=int(n))
return np.maximum(1, np.round(base * self.ttl_scale)).astype(np.int32)
def _prepare_oos_distance(self) -> None:
self.OOS_DIST = None
self.sampler_oosdist = None
self.far_oos_params = None
if self.args.no_far_from_oos or self.oos_pts is None or len(self.oos_pts) == 0:
return
Ng = int(max(8, self.args.oos_dist_grid))
tree_oos = cKDTree(self.oos_pts.astype(np.float32))
gridP = lattice_positions(Ng, self.amin, self.amax, margin=0.0, jitter=0.0, seed=0)
dists, _ = tree_oos.query(gridP, k=1)
self.OOS_DIST = dists.reshape(Ng, Ng, Ng).astype(np.float32)
self.sampler_oosdist = TriLinearSampler(np.zeros((Ng, Ng, Ng, 3), np.float32), self.amin, self.amax)
self.far_oos_params = {
"d0": float(self.args.oos_dist_thresh_frac) * self.diag,
"gamma": float(self.args.oos_dist_gamma),
"boost": float(self.args.oos_death_boost),
}
def _setup_vtk(self) -> None:
init_rgba = np.tile(np.array([[255, 255, 255, 48]], np.uint8), (self.Np, 1))
self.points, self.color_arr, self.poly, self.p_actor = build_cloud(self.P, point_size=2.0, rgba=init_rgba)
self.ren = vtk.vtkRenderer()
self.ren.SetBackground(0, 0, 0)
self.ren.AddActor(self.p_actor)
self.win = vtk.vtkRenderWindow()
self.win.SetOffScreenRendering(1)
self.win.AddRenderer(self.ren)
self.win.SetSize(self.args.window_size[0], self.args.window_size[1])
self.win.SetWindowName("mindVisualizer Live")
self.iren = vtk.vtkRenderWindowInteractor()
self.iren.SetRenderWindow(self.win)
self.iren.SetInteractorStyle(vtkInteractorStyleTrackballCamera())
self.iren.Initialize()
self.o_actor = None
if self.oos_pts is not None and len(self.oos_pts) > 0:
rgb_oos = np.tile(np.array([[255, 255, 255]], np.uint8), (len(self.oos_pts), 1))
oos_rgba = np.concatenate([rgb_oos, np.full((len(self.oos_pts), 1), 72, np.uint8)], axis=1)
_, _, _, self.o_actor = build_cloud(self.oos_pts, point_size=1.5, rgba=oos_rgba, opacity=72 / 255.0)
self.flow_mesh = None
try:
self.flow_mesh = FlowMeshOverlay(ren=self.ren, win=self.win, mesh_dir=MESH_DIR, alignment_file=ALIGNMENT_FILE)
grid_cache = DATA_DIR / "label_grid_cache.npz"
if not self.flow_mesh.load_label_grid(grid_cache):
self.flow_mesh.build_label_grid()
self.flow_mesh.save_label_grid(grid_cache)
if self.flow_mesh.get_all_region_keys():
self.flow_mesh.cycle(+1)
except Exception as exc:
print("[flow mesh] disabled:", exc)
self.text_overlay = VtkTextOverlay(self.ren)
self.probe_sys = ProbeSystem(self.ren, self.win, self.amin, self.amax)
if self.flow_mesh is not None:
self.probe_sys.set_mesh_overlay(self.flow_mesh)
self.probe_sys.set_multi_count(self.args.multi_probe)
if self.args.branching and self.PI is not None:
self.probe_sys.set_branching(True, pi_field=self.PI, mus_samplers=self.samplers_comp)
add_window_legend(
self.ren,
[
"mindVisualizer live Python/VTK",
"space pause | f field | v colour | m cmap | q/w mesh | hide",
"[ ] dt | +/- speed | 1/2 lifetime | 7/8 alpha | y/u/z/x filter",
"o OOS | j clip | a/d frac | probe | analyze | clear",
],
font_px=12,
)
self.ren.ResetCamera()
for _ in range(12):
V = self.sampler_mean.sample_vec(self.P)
self.P = wrap_inside(
self.P + V * (self.args.dt * self.args.speed_scale) * (self.target_step / max(self.vmax_mean, 1e-9)),
self.amin,
self.amax,
margin_frac=self.args.margin,
)
if self.ENT is not None:
self.ent_prev = self.sampler_mean.sample_scalar(self.ENT, self.P).astype(np.float32)
self.ent_d_ema[:] = 0.0
self.win.Render()
def current_sampler(self) -> TriLinearSampler:
return self.sampler_by_name.get(self.field_mode, self.sampler_mean)
def vmax_for_mode(self) -> float:
return self.vmax_by_name.get(self.field_mode, self.vmax_mean)
def map_rgb_from_t(self, t01: np.ndarray) -> np.ndarray:
return rainbow_rgb01(t01) if self.cmap_mode == "RAINBOW" else turbo_rgb01(t01)
def apply_colors(self, speed_vals: np.ndarray, P_world: np.ndarray, dent_vals=None, vis_mask=None) -> None:
eps = 1e-12
if self.colour_mode == "ENTROPY" and self.ENT is not None:
scal = self.sampler_mean.sample_scalar(self.ENT, P_world)
t = (scal - self.ent_min) / max(self.ent_max - self.ent_min, eps)
base_rgb = self.map_rgb_from_t(np.clip(t, 0, 1))
elif self.colour_mode == "DELTA_ENTROPY" and dent_vals is not None:
a = np.abs(dent_vals)
scale = float(np.percentile(a, 97)) if a.size else 1.0
if not np.isfinite(scale) or scale <= eps:
scale = 1.0
base_rgb = self.map_rgb_from_t(np.clip(a / scale, 0, 1))
elif self.colour_mode == "DIR_DELTA_ENTROPY" and dent_vals is not None:
base_rgb = bicolor_white_center(dent_vals)
else:
smin = float(np.min(speed_vals)) if speed_vals.size else 0.0
smax = float(np.max(speed_vals)) if speed_vals.size else 1.0
t = (speed_vals - smin) / max(smax - smin, eps)
base_rgb = self.map_rgb_from_t(np.clip(t, 0, 1))
alpha = int(np.clip(255.0 * self.mdn_alpha, 5, 255))
a = np.full(len(base_rgb), alpha, np.uint8)
if vis_mask is not None:
a = a.copy()
a[~vis_mask] = 0
rgba = np.concatenate([base_rgb, a[:, None]], axis=1).astype(np.uint8)
self.color_arr.DeepCopy(numpy_to_vtk(rgba, deep=True))
self.color_arr.Modified()
self.poly.Modified()
def step(self) -> None:
if self.gpt_pending is not None:
result = self.gpt_pending
self.gpt_pending = None
self.probe_busy = False
self.text_overlay.show_gpt(result)
self.text_overlay.add_log("GPT analysis complete")
if self.paused:
self.win.Render()
return
sampler = self.current_sampler()
Vraw = sampler.sample_vec(self.P)
vmx = float(self.vmax_for_mode())
if self.clip_on:
thr = float(max(1e-9, self.clip_frac * vmx))
speed = np.linalg.norm(Vraw, axis=1)
k = np.minimum(1.0, thr / (speed + 1e-9)).astype(np.float32)
Vstep = Vraw * k[:, None]
eff_vmax = min(vmx, thr)
else:
Vstep = Vraw
eff_vmax = vmx
step_size = self.args.dt * self.args.speed_scale * (self.target_step / max(eff_vmax, 1e-9))
self.P = wrap_inside(self.P + Vstep * step_size, self.amin, self.amax, margin_frac=self.args.margin)
dent = None
if self.ENT is not None:
ent_now = self.sampler_mean.sample_scalar(self.ENT, self.P).astype(np.float32)
d = ent_now - self.ent_prev
self.ent_d_ema = (1.0 - 0.2) * self.ent_d_ema + 0.2 * d
self.ent_prev = ent_now
dent = self.ent_d_ema
age_inc = np.ones(len(self.P), np.int32)
if self.sampler_oosdist is not None and self.OOS_DIST is not None and self.far_oos_params is not None:
d_oos = self.sampler_oosdist.sample_scalar(self.OOS_DIST, self.P).astype(np.float32)
d0 = max(1e-9, self.far_oos_params["d0"])
t = np.clip((d_oos - d0) / d0, 0.0, 1.0)
gate = np.power(t, float(max(0.1, self.far_oos_params["gamma"]))).astype(np.float32)
age_inc += np.floor(gate * float(max(0.0, self.far_oos_params["boost"])) + 1e-9).astype(np.int32)
self.ages += age_inc
dead = self.ages >= self.ttl
if np.any(dead):
if self.seed_mode == "overlap" and self.seed_points is not None and len(self.seed_points) > 0:
sel = self.rng.integers(0, len(self.seed_points), size=dead.sum())
base = self.seed_points[sel]
if self.args.overlap_jitter > 0:
sigma = float(self.args.overlap_jitter) * max(self.diag, 1e-9)
jitter = self.rng.standard_normal((dead.sum(), 3)).astype(np.float32) * sigma
self.P[dead] = np.clip(base + jitter, self.amin, self.amax)
else:
self.P[dead] = np.clip(base, self.amin, self.amax)
else:
jitter_world = (self.amax - self.amin) * (self.args.respawn_jitter / max((self.n_axis or 2) - 1, 1))
jitter = (self.rng.random((dead.sum(), 3)).astype(np.float32) - 0.5) * 2.0 * jitter_world
self.P[dead] = np.clip(self.P0[dead] + jitter, self.amin, self.amax)
self.ages[dead] = 0
self.ttl[dead] = self._sample_ttl(dead.sum())
if self.ENT is not None:
self.ent_prev[dead] = self.sampler_mean.sample_scalar(self.ENT, self.P[dead]).astype(np.float32)
self.ent_d_ema[dead] = 0.0
if self.probe_sys.active:
self.probe_sys.step(sampler, step_size)
speed = np.linalg.norm(Vstep, axis=1)
if self.filter_mode == "OFF":
vis_mask = np.ones(len(self.P), dtype=bool)
else:
vis_mask = compute_fraction_mask(speed, self.filter_frac, top=self.filter_top)
self.points.SetData(numpy_to_vtk(self.P, deep=True))
self.points.Modified()
self.apply_colors(speed, self.P, dent_vals=dent, vis_mask=vis_mask)
self.win.Render()
def cycle_field(self) -> str:
idx = self.field_modes.index(self.field_mode)
self.field_mode = self.field_modes[(idx + 1) % len(self.field_modes)]
return self.field_mode
def cycle_colour(self) -> str:
modes = ["SPEED"]
if self.ENT is not None:
modes += ["ENTROPY", "DELTA_ENTROPY", "DIR_DELTA_ENTROPY"]
idx = modes.index(self.colour_mode) if self.colour_mode in modes else 0
self.colour_mode = modes[(idx + 1) % len(modes)]
return self.colour_mode
def place_probe_center(self) -> None:
pos = ((self.amin + self.amax) * 0.5).astype(np.float32)
self.probe_sys.place(pos)
self.text_overlay.add_log("Probe placed at brain center")
def analyze_probe(self, use_rag: bool) -> None:
if not self.probe_sys.active or self.flow_mesh is None or self.probe_busy:
self.text_overlay.add_log("No probe ready to analyze")
return
self.probe_sys.freeze()
snapshots = []
for probe in self.probe_sys.get_all_probes():
if not probe.path or len(probe.path) < 3:
continue
snapshots.append(
{
"path": probe.get_path_array().copy(),
"field_mags": probe.get_field_mags_array().copy(),
"ghost": probe.ghost,
"label": probe.label,
}
)
if not snapshots:
self.text_overlay.add_log("No probe data")
return
self.probe_busy = True
self.text_overlay.add_log("Analyzing probe path...")
threading.Thread(target=self._analyze_async, args=(snapshots, use_rag), daemon=True).start()
def _analyze_async(self, snapshots, use_rag: bool) -> None:
try:
transitions = []
for snap in snapshots:
transitions.extend(
analyze_probe_path(
snap["path"],
self.flow_mesh,
sample_every=5,
field_mags=snap["field_mags"] if len(snap["field_mags"]) == len(snap["path"]) else None,
entropy_sampler=self.sampler_mean if self.ENT is not None else None,
entropy_field=self.ENT,
)
)
if not transitions:
self.gpt_pending = "No brain regions detected along probe path."
return
self.gpt_pending = analyze_with_gpt(transitions, use_rag=use_rag, model=self.args.model, debug=False)
except Exception as exc:
self.gpt_pending = f"[GPT ERROR] {exc}"
@property
def status(self) -> str:
mesh_count = len(self.flow_mesh.get_all_region_keys()) if self.flow_mesh else 0
return (
f"{self.field_mode} | {self.colour_mode} | "
f"{self.Np:,} particles | {mesh_count} meshes | "
f"{'paused' if self.paused else 'running'}"
)
def build_arg_parser() -> argparse.ArgumentParser:
parser = argparse.ArgumentParser(description="MindVisualizer Trame live server")
parser.add_argument("--host", default="0.0.0.0")
parser.add_argument("--port", type=int, default=int(os.environ.get("PORT", "7860")))
parser.add_argument("--meta", type=Path, default=DEFAULT_META)
parser.add_argument("--oos", type=Path, default=DEFAULT_OOS)
parser.add_argument("--fps", type=int, default=int(os.environ.get("MINDVIS_FPS", "18")))
parser.add_argument("--stride", type=int, default=3)
parser.add_argument("--dt", type=float, default=1.0)
parser.add_argument("--max-step-frac", type=float, default=0.01)
parser.add_argument("--speed-scale", type=float, default=1.0)
parser.add_argument("--margin", type=float, default=0.0)
parser.add_argument("--respawn-jitter", type=float, default=0.015)
parser.add_argument("--oos-seed-limit", type=int, default=50000)
parser.add_argument("--oos-fill-count", type=int, default=10000)
parser.add_argument("--overlap-frac", type=float, default=0.01)
parser.add_argument("--overlap-jitter", type=float, default=0.002)
parser.add_argument("--overlap-limit", type=int, default=200000)
parser.add_argument("--no-far-from-oos", action="store_true")
parser.add_argument("--oos-dist-grid", type=int, default=96)
parser.add_argument("--oos-dist-thresh-frac", type=float, default=0.009)
parser.add_argument("--oos-dist-gamma", type=float, default=3.0)
parser.add_argument("--oos-death-boost", type=float, default=8.0)
parser.add_argument("--multi-probe", type=int, default=1)
parser.add_argument("--branching", action="store_true")
parser.add_argument("--window-size", type=int, nargs=2, default=[1320, 820])
parser.add_argument("--model", default=os.environ.get("OPENAI_MODEL", "gpt-5.4-mini"))
parser.add_argument("--rag", action="store_true")
return parser
def main() -> None:
ensure_ssl()
args = build_arg_parser().parse_args()
from trame.app import get_server
from trame.ui.vuetify3 import SinglePageLayout
from trame.widgets import html, vtk as vtk_widgets, vuetify3
app = LiveMindVisualizer(args)
server = get_server(client_type="vue3")
state, ctrl = server.state, server.controller
state.trame__title = "MindVisualizer Live"
state.status = app.status
def refresh_status() -> None:
state.status = app.status
state.flush()
def update_view() -> None:
if hasattr(ctrl, "view_update"):
ctrl.view_update()
refresh_status()
@ctrl.add("toggle_pause")
def toggle_pause(**_):
app.paused = not app.paused
update_view()
@ctrl.add("cycle_field")
def cycle_field(**_):
app.cycle_field()
update_view()
@ctrl.add("cycle_colour")
def cycle_colour(**_):
app.cycle_colour()
update_view()
@ctrl.add("cycle_cmap")
def cycle_cmap(**_):
app.cmap_mode = "RAINBOW" if app.cmap_mode == "TURBO" else "TURBO"
update_view()
@ctrl.add("mesh_prev")
def mesh_prev(**_):
if app.flow_mesh:
app.flow_mesh.cycle(-1)
update_view()
@ctrl.add("mesh_next")
def mesh_next(**_):
if app.flow_mesh:
app.flow_mesh.cycle(+1)
update_view()
@ctrl.add("mesh_hide")
def mesh_hide(**_):
if app.flow_mesh:
app.flow_mesh.hide()
update_view()
@ctrl.add("mesh_alpha_down")
def mesh_alpha_down(**_):
if app.flow_mesh:
app.flow_mesh.set_opacity(1.0 / 1.25)
update_view()
@ctrl.add("mesh_alpha_up")
def mesh_alpha_up(**_):
if app.flow_mesh:
app.flow_mesh.set_opacity(1.25)
update_view()
@ctrl.add("toggle_oos")
def toggle_oos(**_):
if app.o_actor is not None:
if app.oos_visible:
app.ren.RemoveActor(app.o_actor)
else:
app.ren.AddActor(app.o_actor)
app.oos_visible = not app.oos_visible
update_view()
@ctrl.add("place_probe")
def place_probe(**_):
app.place_probe_center()
update_view()
@ctrl.add("analyze_probe")
def analyze_probe(**_):
app.analyze_probe(use_rag=args.rag)
update_view()
@ctrl.add("clear_probe")
def clear_probe(**_):
app.probe_sys.clear()
app.text_overlay.clear_log()
app.text_overlay.hide_gpt()
update_view()
@ctrl.add("reset_camera")
def reset_camera(**_):
if hasattr(ctrl, "view_reset_camera"):
ctrl.view_reset_camera()
update_view()
async def animation_loop():
delay = 1.0 / max(1, int(args.fps))
while True:
app.step()
if hasattr(ctrl, "view_update"):
ctrl.view_update()
state.status = app.status
state.flush()
await asyncio.sleep(delay)
ctrl.on_server_ready.add(lambda **_: asyncio.create_task(animation_loop()))
with SinglePageLayout(server) as layout:
layout.title.set_text("MindVisualizer Live")
with layout.toolbar:
vuetify3.VBtn("Pause", click=ctrl.toggle_pause, density="compact", variant="tonal")
vuetify3.VBtn("Field", click=ctrl.cycle_field, density="compact", variant="tonal")
vuetify3.VBtn("Colour", click=ctrl.cycle_colour, density="compact", variant="tonal")
vuetify3.VBtn("Cmap", click=ctrl.cycle_cmap, density="compact", variant="tonal")
vuetify3.VDivider(vertical=True, classes="mx-2")
vuetify3.VBtn("Mesh -", click=ctrl.mesh_prev, density="compact", variant="tonal")
vuetify3.VBtn("Mesh +", click=ctrl.mesh_next, density="compact", variant="tonal")
vuetify3.VBtn("Hide", click=ctrl.mesh_hide, density="compact", variant="tonal")
vuetify3.VBtn("Alpha -", click=ctrl.mesh_alpha_down, density="compact", variant="tonal")
vuetify3.VBtn("Alpha +", click=ctrl.mesh_alpha_up, density="compact", variant="tonal")
vuetify3.VDivider(vertical=True, classes="mx-2")
vuetify3.VBtn("OOS", click=ctrl.toggle_oos, density="compact", variant="tonal")
vuetify3.VBtn("Probe", click=ctrl.place_probe, density="compact", variant="tonal")
vuetify3.VBtn("Analyze", click=ctrl.analyze_probe, density="compact", variant="tonal")
vuetify3.VBtn("Clear", click=ctrl.clear_probe, density="compact", variant="tonal")
vuetify3.VSpacer()
html.Div("{{ status }}", classes="text-caption text-medium-emphasis")
with layout.content:
with vuetify3.VContainer(fluid=True, classes="pa-0 fill-height", style="background: #000;"):
view = vtk_widgets.VtkRemoteView(
app.win,
ref="view",
interactive_ratio=0.65,
interactive_quality=55,
)
ctrl.view_update = view.update
ctrl.view_reset_camera = view.reset_camera
server.start(host=args.host, port=args.port)
if __name__ == "__main__":
main()