Pixedar's picture
Deploy full MindVisualizer Python runtime
03e863f
Raw
History Blame Contribute Delete
45 kB
#!/usr/bin/env python3
"""mindVisualizer — Brain flow particle visualizer with probe analysis.
Filtered and modular version of simulate_mdn_flow.py, focused on:
- MDN particle flow with OOS seeding and far-from-OOS death acceleration
- Flow mesh overlay (all available brain region meshes, key: q/w)
- Probe system: single, multi-probe, and branching modes
- GPT-powered region transition analysis (RAG or direct)
- Brain region state database with perturbation propagation
Run with no arguments for defaults:
python -m src.main
Key bindings:
space pause/resume
f cycle field (mean / gated / comp1..6)
v cycle color mode (SPEED / ENTROPY / DELTA_ENTROPY / DIR_DELTA_ENTROPY)
m cycle colormap (TURBO / RAINBOW)
q / w cycle flow mesh (prev / next)
\\ (backslash) hide flow mesh
9 / 0 mesh opacity -/+
[ / ] dt -/+
+ / - speed scale +/-
1 / 2 particle lifetime -/+
7 / 8 particle opacity -/+
y toggle speed filter
u filter direction (top/bottom)
z / x filter fraction -/+
o toggle OOS overlay
g place probe (click in scene first)
G (shift+g) analyze probe path with GPT (async)
b toggle branching mode
n cycle multi-probe count (1/4/8)
s initialize brain states (prompts in console)
S (shift+s) propagate state through probe path
c clear probe
Escape quit
"""
import argparse
import os
import sys
import threading
from pathlib import Path
import numpy as np
import vtk
from vtkmodules.util.numpy_support import numpy_to_vtk
from vtkmodules.vtkCommonCore import vtkUnsignedCharArray
from vtkmodules.vtkInteractionStyle import vtkInteractorStyleTrackballCamera
from vtkmodules.vtkFiltersModeling import vtkOutlineFilter
from scipy.spatial import cKDTree
from .field_loader import load_field, TriLinearSampler, load_points_any
from .colormaps import turbo_rgb01, rainbow_rgb01, bicolor_white_center
from .mesh_overlay import FlowMeshOverlay
from .probe import ProbeSystem
from .region_analyzer import (analyze_probe_path, analyze_with_gpt,
format_transitions_text)
from .brain_state import BrainStateDB
# ---------- paths (relative to project root) ----------
PROJECT_ROOT = Path(__file__).resolve().parent.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"
DEFAULT_EXTRA_PARCELLATION = DATA_DIR / "extra_parcellation" / "combined_atlas.nii.gz"
DEFAULT_EXTRA_LABELS = DATA_DIR / "extra_parcellation" / "combined_atlas_labels.json"
# ---------- utility functions ----------
def lattice_positions(n_per_axis, amin, amax, margin=0.02, jitter=0.012, seed=42):
rng = np.random.default_rng(seed)
fx = np.linspace(margin, 1.0 - margin, n_per_axis, dtype=np.float32)
X, Y, Z = np.meshgrid(fx, fx, fx, indexing="ij")
P = np.stack([X, Y, Z], axis=-1).reshape(-1, 3)
if jitter > 0:
P += (rng.random(P.shape, dtype=np.float32) - 0.5) * 2.0 * (jitter / max(n_per_axis - 1, 1))
P = np.clip(P, 0, 1)
span = (amax - amin).astype(np.float32)
return (amin + P * span).astype(np.float32)
def wrap_inside(P, amin, amax, margin_frac=0.0):
span = amax - amin
offset = amin + margin_frac * span
inner = span * (1.0 - 2.0 * margin_frac)
D = P - offset
D = (D % inner + inner) % inner
return offset + D
def build_cloud(P_init, point_size=2.0, rgba=None, opacity=1.0):
N = len(P_init)
vtk_points = vtk.vtkPoints()
vtk_points.SetData(numpy_to_vtk(P_init, deep=True))
pd = vtk.vtkPolyData()
pd.SetPoints(vtk_points)
verts = vtk.vtkCellArray()
verts.Allocate(N)
verts.InsertNextCell(N)
for i in range(N):
verts.InsertCellPoint(i)
pd.SetVerts(verts)
colors = vtkUnsignedCharArray()
if rgba is not None:
C = rgba.astype(np.uint8, copy=False)
else:
C = np.tile(np.array([[255, 255, 255, 48]], np.uint8), (N, 1))
colors.SetName("RGBA")
colors.SetNumberOfComponents(4)
colors.SetNumberOfTuples(N)
colors.DeepCopy(numpy_to_vtk(C, deep=True))
pd.GetPointData().SetScalars(colors)
mapper = vtk.vtkPolyDataMapper()
mapper.SetInputData(pd)
mapper.SetColorModeToDirectScalars()
mapper.ScalarVisibilityOn()
mapper.SetScalarModeToUsePointData()
actor = vtk.vtkActor()
actor.SetMapper(mapper)
actor.GetProperty().SetPointSize(point_size)
actor.GetProperty().SetOpacity(float(opacity))
actor.GetProperty().LightingOff()
actor.GetProperty().SetInterpolationToFlat()
return vtk_points, colors, pd, actor
def compute_fraction_mask(vals, frac, top):
N = len(vals)
if N == 0:
return np.zeros(0, dtype=bool)
frac = float(np.clip(frac, 0.0, 1.0))
k = int(np.ceil(N * frac))
if k <= 0:
return np.zeros(N, dtype=bool)
if k >= N:
return np.ones(N, dtype=bool)
mask = np.zeros(N, dtype=bool)
if top:
idx = np.argpartition(vals, N - k)[N - k:]
else:
idx = np.argpartition(vals, k)[:k]
mask[idx] = True
return mask
def densify_oos_surface(base_pts, full_oos_pts, amin, amax, extra_count,
k=16, jitter_frac=0.4, power=2.0, seed=2025):
if extra_count <= 0 or len(base_pts) == 0 or len(full_oos_pts) == 0:
return np.zeros((0, 3), np.float32)
cloud = np.asarray(full_oos_pts, dtype=np.float32)
m_add = min(extra_count, 500_000)
eps = 1e-9
k_eff = int(max(3, min(k, len(cloud))))
tree = cKDTree(cloud)
dists, _ = tree.query(base_pts, k=k_eff)
if dists.ndim == 1:
dists = dists[:, None]
r_k = dists[:, -1].astype(np.float32) + eps
med = float(np.median(r_k)) + eps
w = np.power(r_k / med, float(power)).astype(np.float64)
sw = float(np.sum(w))
if not np.isfinite(sw) or sw <= 0.0:
return np.zeros((0, 3), np.float32)
w /= sw
rng = np.random.default_rng(seed)
sel = rng.choice(len(base_pts), size=m_add, replace=True, p=w)
extras = np.zeros((m_add, 3), np.float32)
for j, i in enumerate(sel):
s_loc = float(r_k[i]) * float(jitter_frac)
extras[j] = base_pts[i] + rng.standard_normal(3).astype(np.float32) * s_loc
return np.clip(extras, amin, amax).astype(np.float32)
def add_window_legend(ren, lines, font_px=13):
txt = "\n".join(lines)
ta = vtk.vtkTextActor()
ta.SetInput(txt)
tp = ta.GetTextProperty()
tp.SetColor(1, 1, 1)
tp.SetFontSize(int(font_px))
tp.SetOpacity(0.92)
tp.SetFontFamilyToCourier()
ta.SetPosition(10, 10)
ren.AddActor(ta)
return ta
class VtkTextOverlay:
"""Scrolling text log + GPT explanation panel in the VTK window."""
def __init__(self, ren: vtk.vtkRenderer, max_lines: int = 8):
self._ren = ren
self._max_lines = max_lines
self._log_lines: list[str] = []
self._log_actor = vtk.vtkTextActor()
self._log_actor.SetInput("")
tp = self._log_actor.GetTextProperty()
tp.SetColor(1.0, 0.9, 0.3)
tp.SetFontSize(13)
tp.SetOpacity(0.95)
tp.SetFontFamilyToCourier()
tp.SetJustificationToRight()
tp.SetVerticalJustificationToTop()
self._log_actor.GetPositionCoordinate().SetCoordinateSystemToNormalizedDisplay()
self._log_actor.GetPositionCoordinate().SetValue(0.99, 0.97)
ren.AddActor(self._log_actor)
self._gpt_actor = vtk.vtkTextActor()
self._gpt_actor.SetInput("")
tp2 = self._gpt_actor.GetTextProperty()
tp2.SetColor(0.8, 1.0, 0.8)
tp2.SetFontSize(14)
tp2.SetOpacity(0.95)
tp2.SetFontFamilyToCourier()
tp2.SetJustificationToRight()
tp2.SetVerticalJustificationToTop()
self._gpt_actor.GetPositionCoordinate().SetCoordinateSystemToNormalizedDisplay()
self._gpt_actor.GetPositionCoordinate().SetValue(0.99, 0.70)
self._gpt_actor.VisibilityOff()
ren.AddActor(self._gpt_actor)
def add_log(self, text: str):
self._log_lines.append(text)
if len(self._log_lines) > self._max_lines:
self._log_lines = self._log_lines[-self._max_lines:]
self._log_actor.SetInput("\n".join(self._log_lines))
def clear_log(self):
self._log_lines.clear()
self._log_actor.SetInput("")
def show_gpt(self, text: str):
wrapped = []
for line in text.split("\n"):
while len(line) > 60:
brk = line.rfind(" ", 0, 60)
if brk <= 0:
brk = 60
wrapped.append(line[:brk])
line = line[brk:].lstrip()
wrapped.append(line)
if len(wrapped) > 20:
wrapped = wrapped[:20] + ["..."]
self._gpt_actor.SetInput("\n".join(wrapped))
self._gpt_actor.VisibilityOn()
def hide_gpt(self):
self._gpt_actor.SetInput("")
self._gpt_actor.VisibilityOff()
# ---------- main ----------
def main():
# Fix SSL cert path (Git on Windows sets bad path)
try:
import certifi
cert_file = certifi.where()
cur = os.environ.get("SSL_CERT_FILE", "")
if not cur or not os.path.isfile(cur):
os.environ["SSL_CERT_FILE"] = cert_file
cur2 = os.environ.get("REQUESTS_CA_BUNDLE", "")
if not cur2 or not os.path.isfile(cur2):
os.environ["REQUESTS_CA_BUNDLE"] = cert_file
except ImportError:
pass
try:
from dotenv import load_dotenv
load_dotenv(PROJECT_ROOT / ".env")
except ImportError:
pass
ap = argparse.ArgumentParser(description="mindVisualizer - Brain flow particle visualizer")
ap.add_argument("--meta", type=Path, default=DEFAULT_META)
ap.add_argument("--oos", type=Path, default=DEFAULT_OOS)
ap.add_argument("--fps", type=int, default=60)
ap.add_argument("--stride", type=int, default=3)
ap.add_argument("--dt", type=float, default=1.0)
ap.add_argument("--max-step-frac", type=float, default=0.01)
ap.add_argument("--speed-scale", type=float, default=1.0)
ap.add_argument("--margin", type=float, default=0.0)
ap.add_argument("--respawn-jitter", type=float, default=0.015)
ap.add_argument("--oos-seed-limit", type=int, default=50000)
ap.add_argument("--oos-fill-count", type=int, default=10000)
ap.add_argument("--overlap-frac", type=float, default=0.01)
ap.add_argument("--overlap-jitter", type=float, default=0.002)
ap.add_argument("--overlap-limit", type=int, default=200000)
ap.add_argument("--no-far-from-oos", action="store_true")
ap.add_argument("--oos-dist-grid", type=int, default=96)
ap.add_argument("--oos-dist-thresh-frac", type=float, default=0.009)
ap.add_argument("--oos-dist-gamma", type=float, default=3.0)
ap.add_argument("--oos-death-boost", type=float, default=8.0)
ap.add_argument("--no-flow-mesh", action="store_true")
ap.add_argument("--no-rag", action="store_true",
help="Skip RAG, use only GPT's own knowledge")
ap.add_argument("--multi-probe", type=int, default=1,
help="Number of probes to spawn (1=single, 4=neighborhood)")
ap.add_argument("--branching", action="store_true",
help="Enable MDN component branching")
ap.add_argument("--window-size", type=int, nargs=2, default=[1200, 800])
ap.add_argument("--hq", action="store_true",
help="Use high-quality GPT model (gpt-5.4) instead of gpt-5.4-mini")
ap.add_argument("--debug", action="store_true",
help="Print full LLM prompts to console before each call")
ap.add_argument("--extra-parcellation", type=Path, default=None,
help="Path to NIfTI parcellation file for finer subregion labels "
"(e.g., Brainnetome BN_Atlas_246_1mm.nii.gz)")
ap.add_argument("--extra-parcellation-labels", type=Path, default=None,
help="JSON label map for extra parcellation (label_id -> name)")
args = ap.parse_args()
model = "gpt-5.4" if args.hq else "gpt-5.4-mini"
print(f"[config] LLM model: {model}")
far_from_oos = not args.no_far_from_oos
flow_mesh_enabled = not args.no_flow_mesh
use_rag = not args.no_rag
debug_mode = args.debug
# ---------- load field ----------
print(f"[field] loading {args.meta} ...")
fld = load_field(args.meta)
G = fld["G"]
amin, amax = fld["amin"], fld["amax"]
V_mean = fld["mean"]
MUS = fld["mus"]
PI = fld["pi"]
ENT = fld["ENT"]
K = len(MUS)
sampler_mean = TriLinearSampler(V_mean, amin, amax)
diag = float(np.linalg.norm(amax - amin))
target_step = args.max_step_frac * max(diag, 1e-6)
def interior_percentile(V, q=100.0):
mag = np.linalg.norm(V.reshape(-1, 3), axis=1)
nz = mag[mag > 0]
return float(np.percentile(nz, q)) if nz.size else 1.0
vmax_mean = interior_percentile(V_mean, q=100.0)
sampler_gated = None
V_gated = None
if PI is not None and PI.ndim == 4 and PI.shape[-1] == K and K > 0:
idx = np.argmax(PI, axis=-1)
V_gated = np.zeros_like(MUS[0], dtype=np.float32)
for k_i in range(K):
mask = (idx == k_i)[..., None].astype(np.float32)
V_gated += MUS[k_i] * mask
sampler_gated = TriLinearSampler(V_gated, amin, amax)
samplers_comp = [TriLinearSampler(mu, amin, amax) for mu in MUS]
field_modes = ["mean"]
if sampler_gated is not None:
field_modes.append("gated")
field_modes += [f"comp{i + 1}" for i in range(K)]
field_mode = [field_modes[0]]
sampler_by_name = {"mean": sampler_mean}
if sampler_gated is not None:
sampler_by_name["gated"] = sampler_gated
for i, s in enumerate(samplers_comp):
sampler_by_name[f"comp{i + 1}"] = s
vmax_by_name = {"mean": vmax_mean}
if V_gated is not None:
vmax_by_name["gated"] = interior_percentile(V_gated, q=100.0)
for i, mu in enumerate(MUS):
vmax_by_name[f"comp{i + 1}"] = interior_percentile(mu, q=100.0)
def current_sampler():
return sampler_by_name.get(field_mode[0], sampler_mean)
def vmax_for_mode():
return vmax_by_name.get(field_mode[0], vmax_mean)
ent_min = ent_max = 0.0
if ENT is not None:
ent_min, ent_max = float(np.min(ENT)), float(np.max(ENT))
# ---------- load OOS ----------
oos_pts = None
if args.oos is not None:
try:
raw = load_points_any(Path(args.oos))
if raw.size > 0:
oos_pts = raw.astype(np.float32)
print(f"[oos] loaded {len(oos_pts)} points")
except Exception as e:
print("[oos] failed to load:", e)
# ---------- seeding ----------
rng = np.random.default_rng(2025)
overlap_radius = float(args.overlap_frac) * max(diag, 1e-9)
overlap_sigma = float(args.overlap_jitter) * max(diag, 1e-9)
seed_mode = "grid"
seed_points = None
n_axis = None
if fld["TRAIN"] is not None and oos_pts is not None and len(fld["TRAIN"]) > 0 and len(oos_pts) > 0:
try:
tree = cKDTree(fld["TRAIN"].astype(np.float32))
dists, _ = tree.query(oos_pts.astype(np.float32), k=1)
cand = oos_pts[dists <= overlap_radius]
if cand.size > 0:
seed_mode = "overlap"
if args.overlap_limit and len(cand) > args.overlap_limit:
idx = rng.choice(len(cand), size=int(args.overlap_limit), replace=False)
cand = cand[idx]
seed_points = cand.astype(np.float32)
print(f"[seed] overlap: {len(seed_points)} points")
except Exception as e:
print("[seed] overlap failed:", e)
if seed_mode == "overlap" and seed_points is not None:
if args.oos_seed_limit > 0 and len(seed_points) > args.oos_seed_limit:
prev = len(seed_points)
idx_seed = rng.choice(prev, size=args.oos_seed_limit, replace=False)
seed_points = seed_points[idx_seed]
print(f"[seed] capped to {len(seed_points)} (from {prev})")
if args.oos_fill_count > 0 and oos_pts is not None and seed_mode == "overlap" and seed_points is not None:
extras = densify_oos_surface(base_pts=seed_points, full_oos_pts=oos_pts,
amin=amin, amax=amax, extra_count=args.oos_fill_count)
if extras is not None and len(extras) > 0:
seed_points = np.concatenate([seed_points, extras], axis=0).astype(np.float32)
print(f"[seed] oos-fill: added {len(extras)} -> total {len(seed_points)}")
if seed_mode == "overlap" and seed_points is not None:
if overlap_sigma > 0:
J = rng.standard_normal(seed_points.shape).astype(np.float32) * overlap_sigma
P0 = np.clip(seed_points + J, amin, amax)
else:
P0 = np.clip(seed_points, amin, amax)
else:
n_axis = max(2, G // max(1, args.stride))
P0 = lattice_positions(n_axis, amin, amax, margin=0.02, jitter=args.respawn_jitter, seed=2025)
P = P0.copy()
Np = len(P)
_base_ttl_lo, _base_ttl_hi = max(8, 60 // 2), 60
ttl_state = {"scale": 1.0}
def _sample_ttl(n):
base = rng.integers(_base_ttl_lo, _base_ttl_hi + 1, size=int(n))
return np.maximum(1, np.round(base * ttl_state["scale"])).astype(np.int32)
ttl = _sample_ttl(Np)
ages = rng.integers(0, np.maximum(1, ttl), size=Np, dtype=np.int32)
ent_prev = sampler_mean.sample_scalar(ENT, P).astype(np.float32) if ENT is not None else None
ent_d_ema = np.zeros(Np, np.float32) if ENT is not None else None
ent_alpha = 0.2
# ---------- VTK setup ----------
init_rgba = np.tile(np.array([[255, 255, 255, 48]], np.uint8), (Np, 1))
points, color_arr, poly, p_actor = build_cloud(P, point_size=2.0, rgba=init_rgba)
ren = vtk.vtkRenderer()
ren.SetBackground(0, 0, 0)
ren.AddActor(p_actor)
ren.ResetCamera()
o_actor = None
if oos_pts is not None and len(oos_pts) > 0:
rgb_oos = np.tile(np.array([[255, 255, 255]], np.uint8), (len(oos_pts), 1))
oos_rgba = np.concatenate([rgb_oos, np.full((len(oos_pts), 1), 72, np.uint8)], axis=1)
_, _, _, o_actor = build_cloud(oos_pts, point_size=1.5, rgba=oos_rgba, opacity=72 / 255.0)
oos_visible = [False]
# OOS distance grid
OOS_DIST = None
sampler_oosdist = None
far_oos_params = None
if far_from_oos and oos_pts is not None and len(oos_pts) > 0:
Ng = int(max(8, args.oos_dist_grid))
print(f"[oos-dist] precomputing {Ng}^3 ...")
tree_oos = cKDTree(oos_pts.astype(np.float32))
gridP = lattice_positions(Ng, amin, amax, margin=0.0, jitter=0.0, seed=0)
d, _ = tree_oos.query(gridP, k=1)
OOS_DIST = d.reshape(Ng, Ng, Ng).astype(np.float32)
sampler_oosdist = TriLinearSampler(np.zeros((Ng, Ng, Ng, 3), np.float32), amin, amax)
far_oos_params = {
"d0": float(args.oos_dist_thresh_frac) * diag,
"gamma": float(args.oos_dist_gamma),
"boost": float(args.oos_death_boost),
}
print(f"[oos-dist] ready: d0={far_oos_params['d0']:.4g}")
win = vtk.vtkRenderWindow()
win.AddRenderer(ren)
win.SetSize(*args.window_size)
win.SetWindowName("mindVisualizer")
# Flow mesh overlay
flow_mesh = None
if flow_mesh_enabled:
try:
flow_mesh = FlowMeshOverlay(ren=ren, win=win,
mesh_dir=MESH_DIR, alignment_file=ALIGNMENT_FILE)
n_regions = len(flow_mesh.get_all_region_keys())
print(f"[flow mesh] ready ({n_regions} regions)")
# Load or build voxel label grid for fast point-in-region queries
grid_cache = DATA_DIR / "label_grid_cache.npz"
if not flow_mesh.load_label_grid(grid_cache):
print("[flow mesh] No cached label grid found — building now "
"(this takes ~30-60s, one-time only)...")
flow_mesh.build_label_grid()
flow_mesh.save_label_grid(grid_cache)
print("[flow mesh] Label grid cached for future runs.")
except Exception as e:
print("[flow mesh] disabled:", e)
flow_mesh = None
# Optional extra parcellation (auto-detect combined atlas if no explicit path)
if flow_mesh:
_ep_nifti = args.extra_parcellation
_ep_labels = args.extra_parcellation_labels
if _ep_nifti is None and DEFAULT_EXTRA_PARCELLATION.exists():
_ep_nifti = DEFAULT_EXTRA_PARCELLATION
if _ep_labels is None and DEFAULT_EXTRA_LABELS.exists():
_ep_labels = DEFAULT_EXTRA_LABELS
if _ep_nifti is not None:
try:
from .extra_parcellation import ExtraParcellation
extra = ExtraParcellation(_ep_nifti, _ep_labels)
if extra.load():
flow_mesh.set_extra_parcellation(extra)
except Exception as e:
print(f"[extra-parcellation] disabled: {e}")
# Text overlay
text_overlay = VtkTextOverlay(ren)
# Brain state database
brain_state_db = BrainStateDB(model=model, debug=args.debug)
# Probe system
probe_sys = ProbeSystem(ren, win, amin, amax)
if flow_mesh is not None:
probe_sys.set_mesh_overlay(flow_mesh)
probe_sys.set_multi_count(args.multi_probe)
if args.branching and PI is not None:
probe_sys.set_branching(True, pi_field=PI, mus_samplers=samplers_comp)
# Boundary constraint
if sampler_oosdist is not None and OOS_DIST is not None and far_oos_params is not None:
_boundary_d0 = far_oos_params["d0"] * 2.0
def _probe_boundary_check(pos):
d = float(sampler_oosdist.sample_scalar(OOS_DIST, pos[None, :])[0])
return d < _boundary_d0
probe_sys.set_boundary_check(_probe_boundary_check)
# Region change callback
def _on_region_change(entered, left, is_ghost=False, label=""):
tag = " [branch]" if is_ghost else ""
for name in left:
text_overlay.add_log(f"LEFT{tag}: {name}")
for name in entered:
text_overlay.add_log(f"ENTERED{tag}: {name}")
probe_sys.set_on_region_change(_on_region_change)
picker = vtk.vtkCellPicker()
picker.SetTolerance(0.01)
# ---------- state ----------
available_colour_modes = (
["SPEED"]
+ (["ENTROPY"] if ENT is not None else [])
+ (["DELTA_ENTROPY"] if ENT is not None else [])
+ (["DIR_DELTA_ENTROPY"] if ENT is not None else [])
)
colour_idx = [0]
cmap_mode = ["TURBO"]
state = {"paused": False, "dt": float(args.dt), "scale": float(args.speed_scale)}
filter_state = {"mode": "OFF", "dir_top": True, "frac": 1.0}
clip_state = {"on": False, "frac": 1.0}
mdn_alpha = [1.0]
last_rgba = [init_rgba.copy()]
probe_mode = [False]
# Async GPT result holder
gpt_pending = {"result": None, "running": False}
def map_rgb_from_t(t01):
return rainbow_rgb01(t01) if cmap_mode[0] == "RAINBOW" else turbo_rgb01(t01)
def apply_colors(speed_vals, P_world, dent_vals=None, vis_mask=None):
mode = available_colour_modes[colour_idx[0]]
eps = 1e-12
if mode == "ENTROPY" and ENT is not None:
scal = sampler_mean.sample_scalar(ENT, P_world)
t = (scal - ent_min) / max(ent_max - ent_min, eps)
base_rgb = map_rgb_from_t(np.clip(t, 0, 1))
elif 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 = map_rgb_from_t(np.clip(a / scale, 0, 1))
elif 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 = map_rgb_from_t(np.clip(t, 0, 1))
a_val = int(np.clip(255.0 * mdn_alpha[0], 5, 255))
a = np.full(len(base_rgb), a_val, 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)
color_arr.DeepCopy(numpy_to_vtk(rgba, deep=True))
color_arr.Modified()
poly.Modified()
last_rgba[0] = rgba
# Warm-up
for _ in range(12):
V = sampler_mean.sample_vec(P)
P = wrap_inside(P + V * (args.dt * args.speed_scale) * (target_step / max(vmax_mean, 1e-9)),
amin, amax, margin_frac=args.margin)
if ENT is not None:
ent_prev = sampler_mean.sample_scalar(ENT, P).astype(np.float32)
ent_d_ema[:] = 0.0
# ---------- timer callback ----------
def on_timer(_o, _e):
nonlocal P, ages, ttl, ent_prev, ent_d_ema
# Check for async GPT result
if gpt_pending["result"] is not None:
explanation = gpt_pending["result"]
gpt_pending["result"] = None
gpt_pending["running"] = False
print("\n--- GPT INTERPRETATION ---")
print(explanation)
print("--- END ---\n")
text_overlay.show_gpt(explanation)
text_overlay.add_log("GPT analysis complete")
if state["paused"]:
return
samp = current_sampler()
Vraw = samp.sample_vec(P)
vmx = float(vmax_for_mode())
if clip_state["on"]:
thr = float(max(1e-9, clip_state["frac"] * vmx))
s = np.linalg.norm(Vraw, axis=1)
k = np.minimum(1.0, thr / (s + 1e-9)).astype(np.float32)
Vstep = Vraw * k[:, None]
eff_vmax = min(vmx, thr)
else:
Vstep = Vraw
eff_vmax = vmx
step = state["dt"] * state["scale"] * (target_step / max(eff_vmax, 1e-9))
P = wrap_inside(P + Vstep * step, amin, amax, margin_frac=args.margin)
dent = None
if ENT is not None:
ent_now = sampler_mean.sample_scalar(ENT, P).astype(np.float32)
d = ent_now - ent_prev
ent_d_ema = (1.0 - ent_alpha) * ent_d_ema + ent_alpha * d
ent_prev = ent_now
dent = ent_d_ema
age_inc = np.ones(len(P), np.int32)
if sampler_oosdist is not None and OOS_DIST is not None and far_oos_params is not None:
d_oos = sampler_oosdist.sample_scalar(OOS_DIST, P).astype(np.float32)
d0 = max(1e-9, far_oos_params["d0"])
t = np.clip((d_oos - d0) / d0, 0.0, 1.0)
gate = np.power(t, float(max(0.1, far_oos_params["gamma"]))).astype(np.float32)
extra = np.floor(gate * float(max(0.0, far_oos_params["boost"])) + 1e-9).astype(np.int32)
age_inc += extra
ages += age_inc
dead = ages >= ttl
if np.any(dead):
if seed_mode == "overlap" and seed_points is not None and len(seed_points) > 0:
sel = rng.integers(0, len(seed_points), size=dead.sum())
base = seed_points[sel]
if overlap_sigma > 0:
J = rng.standard_normal((dead.sum(), 3)).astype(np.float32) * overlap_sigma
P[dead] = np.clip(base + J, amin, amax)
else:
P[dead] = np.clip(base, amin, amax)
else:
jitter_world = (amax - amin) * (args.respawn_jitter / max((n_axis or 2) - 1, 1))
J = (rng.random((dead.sum(), 3)).astype(np.float32) - 0.5) * 2.0 * jitter_world
P[dead] = np.clip(P0[dead] + J, amin, amax)
ages[dead] = 0
ttl[dead] = _sample_ttl(dead.sum())
if ENT is not None:
ent_prev[dead] = sampler_mean.sample_scalar(ENT, P[dead]).astype(np.float32)
ent_d_ema[dead] = 0.0
# Probe step
if probe_sys.active:
probe_sys.step(samp, step)
# Render
speed = np.linalg.norm(Vstep, axis=1)
if filter_state["mode"] == "OFF":
vis_mask = np.ones(len(P), dtype=bool)
else:
vis_mask = compute_fraction_mask(speed, filter_state["frac"],
top=filter_state["dir_top"])
points.SetData(numpy_to_vtk(P, deep=True))
points.Modified()
apply_colors(speed, P, dent_vals=dent, vis_mask=vis_mask)
win.Render()
# ---------- click handler ----------
def on_left_click(obj, ev):
if not probe_mode[0]:
return
x, y = obj.GetEventPosition()
if picker.Pick(x, y, 0, ren) <= 0:
return
px, py, pz = picker.GetPickPosition()
if not np.isfinite([px, py, pz]).all():
return
pos = np.array([px, py, pz], np.float32)
probe_sys.place(pos)
probe_mode[0] = False
n = len(probe_sys.probes)
print(f"[probe] {n} probe(s) placed. Shift+G to analyze, C to clear.")
# ---------- async GPT ----------
def _run_gpt_full_async(probe_snapshots, use_rag_flag):
"""Run FULL analysis + GPT in background thread (no VTK calls)."""
try:
all_transitions = []
branch_transitions = []
for snap in probe_snapshots:
path_arr = snap["path"]
field_mags = snap["field_mags"]
transitions = analyze_probe_path(
path_arr, flow_mesh, sample_every=5,
field_mags=field_mags if len(field_mags) == len(path_arr) else None,
entropy_sampler=sampler_mean if ENT is not None else None,
entropy_field=ENT,
)
if snap["ghost"]:
branch_transitions.extend(transitions)
else:
all_transitions.extend(transitions)
if not all_transitions:
gpt_pending["result"] = "No brain regions detected along probe path."
return
# Log transitions (console only, no VTK)
print("\n--- PROBE TRAJECTORY ANALYSIS ---")
for i, t in enumerate(all_transitions, 1):
print(f" {i}. {t['region_name']} ({t['relative_position']}), "
f"steps {t['entry_idx']}-{t['exit_idx']}")
if branch_transitions:
print(f" + {len(branch_transitions)} branch transitions")
rag_label = "RAG" if use_rag_flag else "direct"
print(f"\n[GPT] Sending to GPT ({rag_label})...")
explanation = analyze_with_gpt(all_transitions, use_rag=use_rag_flag,
model=model, debug=debug_mode)
gpt_pending["result"] = explanation
except Exception as e:
gpt_pending["result"] = f"[GPT ERROR] {e}"
# ---------- key handler ----------
def on_keypress(obj, ev):
nonlocal ttl, ages
key = obj.GetKeySym()
key_lower = key.lower() if key else ""
shift = bool(obj.GetShiftKey())
if key_lower == "escape":
obj.TerminateApp()
elif key_lower == "space":
state["paused"] = not state["paused"]
print(f"[{'PAUSED' if state['paused'] else 'RUNNING'}]")
elif key_lower == "f":
i = field_modes.index(field_mode[0])
field_mode[0] = field_modes[(i + 1) % len(field_modes)]
print(f"[field] {field_mode[0]}")
elif key_lower == "v":
if available_colour_modes:
colour_idx[0] = (colour_idx[0] + 1) % len(available_colour_modes)
print(f"[colour] {available_colour_modes[colour_idx[0]]}")
elif key_lower == "m":
cmap_mode[0] = "RAINBOW" if cmap_mode[0] == "TURBO" else "TURBO"
print(f"[cmap] {cmap_mode[0]}")
# Flow mesh
elif key_lower == "q" and flow_mesh:
flow_mesh.cycle(-1)
elif key_lower == "w" and flow_mesh:
flow_mesh.cycle(+1)
elif key_lower == "backslash" and flow_mesh:
flow_mesh.hide()
elif key_lower == "9" and flow_mesh:
flow_mesh.set_opacity(1.0 / 1.25)
elif key_lower == "0" and flow_mesh:
flow_mesh.set_opacity(1.25)
# Sim controls
elif key_lower == "bracketleft":
state["dt"] /= 1.25
print(f"[dt] {state['dt']:.4f}")
elif key_lower == "bracketright":
state["dt"] *= 1.25
print(f"[dt] {state['dt']:.4f}")
elif key_lower in ("plus", "equal"):
state["scale"] *= 1.25
print(f"[speed-scale] {state['scale']:.3f}")
elif key_lower in ("minus", "underscore"):
state["scale"] /= 1.25
print(f"[speed-scale] {state['scale']:.3f}")
elif key_lower == "1":
ttl_state["scale"] = float(np.clip(ttl_state["scale"] / 1.25, 0.1, 100.0))
ttl[:] = np.maximum(1, np.round(ttl * (1.0 / 1.25))).astype(np.int32)
print(f"[lifetime] scale = {ttl_state['scale']:.3f}")
elif key_lower == "2":
ttl_state["scale"] = float(np.clip(ttl_state["scale"] * 1.25, 0.1, 100.0))
ttl[:] = np.maximum(1, np.round(ttl * 1.25)).astype(np.int32)
print(f"[lifetime] scale = {ttl_state['scale']:.3f}")
elif key_lower == "7":
mdn_alpha[0] = float(np.clip(mdn_alpha[0] / 1.25, 0.02, 1.0))
print(f"[alpha] {mdn_alpha[0]:.2f}")
elif key_lower == "8":
mdn_alpha[0] = float(np.clip(mdn_alpha[0] * 1.25, 0.02, 1.0))
print(f"[alpha] {mdn_alpha[0]:.2f}")
elif key_lower == "y":
filter_state["mode"] = "OFF" if filter_state["mode"] == "SPEED" else "SPEED"
print(f"[filter] mode={filter_state['mode']}")
elif key_lower == "u":
filter_state["dir_top"] = not filter_state["dir_top"]
print(f"[filter] dir={'TOP' if filter_state['dir_top'] else 'BOTTOM'}")
elif key_lower == "z":
filter_state["frac"] = float(np.clip(filter_state["frac"] / 1.25, 0.0, 1.0))
print(f"[filter] frac={filter_state['frac'] * 100:.1f}%")
elif key_lower == "x":
filter_state["frac"] = float(np.clip(filter_state["frac"] * 1.25, 0.0, 1.0))
print(f"[filter] frac={filter_state['frac'] * 100:.1f}%")
elif key_lower == "j":
clip_state["on"] = not clip_state["on"]
print(f"[speed-clip] {'ON' if clip_state['on'] else 'OFF'}")
elif key_lower == "a":
clip_state["frac"] = float(np.clip(clip_state["frac"] / 1.25, 0.02, 10.0))
print(f"[speed-clip] frac={clip_state['frac']:.3f}")
elif key_lower == "d":
clip_state["frac"] = float(np.clip(clip_state["frac"] * 1.25, 0.02, 10.0))
print(f"[speed-clip] frac={clip_state['frac']:.3f}")
elif key_lower == "o":
if o_actor is not None:
if oos_visible[0]:
ren.RemoveActor(o_actor)
oos_visible[0] = False
else:
ren.AddActor(o_actor)
oos_visible[0] = True
print(f"[oos overlay] visible = {oos_visible[0]}")
win.Render()
# ---------- probe controls ----------
elif key_lower == "g" and not shift:
probe_mode[0] = True
print("[probe] Click in the scene to place probe...")
elif key_lower == "g" and shift:
if not probe_sys.active:
print("[probe] No probe active. Press g then click.")
text_overlay.add_log("No probe to analyze")
elif flow_mesh is None:
print("[probe] Flow mesh required.")
elif gpt_pending["running"]:
print("[probe] GPT analysis already running...")
else:
# Stop probe movement — we're analyzing the path so far
probe_sys.freeze()
print("[probe] Probe stopped. Analyzing trajectory...")
text_overlay.add_log("Please wait — analyzing probe path...")
win.Render()
# Collect probe data snapshots (lightweight, no VTK calls)
probe_snapshots = []
for p in probe_sys.get_all_probes():
if not p.path or len(p.path) < 3:
continue
probe_snapshots.append({
"path": p.get_path_array().copy(),
"field_mags": p.get_field_mags_array().copy(),
"ghost": p.ghost,
"label": p.label,
})
if not probe_snapshots:
print("[probe] No probe data.")
text_overlay.add_log("No probe data")
else:
gpt_pending["running"] = True
t = threading.Thread(
target=_run_gpt_full_async,
args=(probe_snapshots, use_rag),
daemon=True
)
t.start()
elif key_lower == "c":
probe_sys.clear()
probe_mode[0] = False
text_overlay.clear_log()
text_overlay.hide_gpt()
# ---------- branching / multi-probe ----------
elif key_lower == "b":
new_state = not probe_sys._branching_enabled
if PI is not None:
probe_sys.set_branching(new_state, pi_field=PI, mus_samplers=samplers_comp)
text_overlay.add_log(f"Branching {'ON' if new_state else 'OFF'}")
else:
print("[probe] No PI field - branching not available")
elif key_lower == "n":
counts = [1, 4, 8]
cur = probe_sys._multi_count
idx = counts.index(cur) if cur in counts else 0
new_count = counts[(idx + 1) % len(counts)]
probe_sys.set_multi_count(new_count)
text_overlay.add_log(f"Multi-probe: {new_count}")
# ---------- brain state ----------
elif key_lower == "s" and not shift:
# Initialize brain states
if flow_mesh is None:
print("[brain-state] Flow mesh required.")
return
region_names = []
for k in flow_mesh.get_all_region_keys():
name = flow_mesh.get_region_name(k)
if hasattr(flow_mesh, '_hemispheres') and k in flow_mesh._hemispheres:
region_names.append(f"{name} (left)")
region_names.append(f"{name} (right)")
else:
region_names.append(name)
print("[brain-state] Enter global brain state (or press Enter for default):")
print(" Example: 'someone thinking about loved ones'")
def _init_states_async():
try:
# Read from stdin (blocking)
import sys
global_state = input(" > ").strip()
result = brain_state_db.initialize_from_global(
global_state, region_names,
callback=lambda msg: print(f" {msg}")
)
print(f"[brain-state] {result}")
text_overlay.add_log("Brain states initialized")
except Exception as e:
print(f"[brain-state] Error: {e}")
t = threading.Thread(target=_init_states_async, daemon=True)
t.start()
elif key_lower == "s" and shift:
# Propagate state through probe path
if not probe_sys.active or flow_mesh is None:
print("[brain-state] Need active probe + flow mesh.")
return
if not brain_state_db.has_states():
print("[brain-state] Initialize states first (press 's').")
return
# Snapshot probe data (lightweight)
primary = probe_sys.get_primary_probe()
if primary is None or len(primary.path) < 3:
print("[brain-state] No probe path.")
return
prop_snapshot = {
"path": primary.get_path_array().copy(),
"field_mags": primary.get_field_mags_array().copy(),
}
def _propagate_async():
try:
path_arr = prop_snapshot["path"]
field_mags = prop_snapshot["field_mags"]
transitions = analyze_probe_path(
path_arr, flow_mesh, sample_every=5,
field_mags=field_mags if len(field_mags) == len(path_arr) else None,
)
if not transitions:
print("[brain-state] No regions in path.")
return
def _hemi_name(t):
name = t["region_name"]
h = t.get("hemisphere")
return f"{name} ({h})" if h else name
source = _hemi_name(transitions[0])
affected = [_hemi_name(t) for t in transitions[1:]]
strengths = {_hemi_name(t): t.get("avg_flow_strength", 0.0) or 0.0
for t in transitions}
print(f"[brain-state] Propagating from {source}...")
updates = brain_state_db.propagate_through_regions(
source, affected, flow_strengths=strengths,
callback=lambda m: print(f" {m}")
)
if updates:
summary = brain_state_db.summarize_changes(updates, source)
story = brain_state_db.generate_flow_story(updates, source)
print(f"\n--- PROPAGATION SUMMARY ---")
print(summary)
print(f"\n--- INFORMATION FLOW STORY ---")
print(story)
print("--- END ---\n")
gpt_pending["result"] = f"{summary}\n\n{story}"
else:
print("[brain-state] No state changes.")
except Exception as e:
print(f"[brain-state] Error: {e}")
t = threading.Thread(target=_propagate_async, daemon=True)
t.start()
# ---------- legend ----------
add_window_legend(ren, [
"mindVisualizer",
"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 | g probe | G(shift) analyze(async)",
"b branching | n multi(1/4/8) | s states | S propagate",
"c clear | Esc quit",
], font_px=12)
# ---------- interactor ----------
iren = vtk.vtkRenderWindowInteractor()
iren.SetRenderWindow(win)
iren.SetInteractorStyle(vtkInteractorStyleTrackballCamera())
iren.Initialize()
iren.AddObserver("TimerEvent", on_timer)
iren.AddObserver("KeyPressEvent", on_keypress)
iren.AddObserver("LeftButtonPressEvent", on_left_click)
iren.CreateRepeatingTimer(max(1, int(1000 / max(1, args.fps))))
win.Render()
rag_status = "RAG" if use_rag else "direct (no RAG)"
print("\n=== mindVisualizer ready ===")
print(f" Particles: {Np} | Seed: {seed_mode} | Field: {field_mode[0]}")
print(f" Colours: {', '.join(available_colour_modes)}")
print(f" Meshes: {len(flow_mesh.get_all_region_keys()) if flow_mesh else 0} regions")
print(f" GPT: {rag_status} | Multi-probe: {probe_sys._multi_count} | "
f"Branching: {'ON' if probe_sys._branching_enabled else 'OFF'}")
print(f" Brain states: {'loaded' if brain_state_db.has_states() else 'not initialized (press s)'}")
print(f" Press 'g' then click to place probe. Shift+G to analyze.")
print()
iren.Start()
if __name__ == "__main__":
try:
main()
except Exception as e:
print("[ERROR]", e)
import traceback
traceback.print_exc()
sys.exit(1)