GENISIS / simulation_viewer.py
ChiragHariprasad
Deploy latest GENISIS
bcdb2af
Raw
History Blame Contribute Delete
37.9 kB
"""
GENESIS Urban Layout Simulation Viewer
Loads OBJ models and runs Traffic, Rain, and Wind simulations using PyVista
"""
import numpy as np
import pyvista as pv
import json
import argparse
from pathlib import Path
from dataclasses import dataclass, field
from typing import List, Tuple, Dict, Optional, Any
from enum import Enum
import time
# ============================================================================
# SIMULATION MODE ENUM
# ============================================================================
class SimulationMode(Enum):
NONE = 0
TRAFFIC = 1
RAIN = 2
WIND = 3
# ============================================================================
# TRAFFIC SIMULATION
# ============================================================================
@dataclass
class TrafficAgent:
"""Single traffic agent (vehicle/pedestrian)"""
position: np.ndarray
velocity: np.ndarray
road_index: int
progress: float # 0.0 to 1.0 along road
speed: float
agent_type: str # 'car', 'bike', 'pedestrian'
class TrafficSimulation:
"""Traffic simulation with agents moving along roads"""
def __init__(self, roads_data: List[Dict], num_agents: int = 50):
self.roads = roads_data
self.num_agents = num_agents
self.agents: List[TrafficAgent] = []
self.agent_colors = {
'car': [1.0, 0.2, 0.2], # Red
'bike': [0.2, 1.0, 0.2], # Green
'pedestrian': [0.2, 0.2, 1.0] # Blue
}
self.paused = False
self._initialize_agents()
def _initialize_agents(self):
"""Create initial agents on roads"""
self.agents = []
if not self.roads:
return
agent_types = ['car', 'bike', 'pedestrian']
type_speeds = {'car': 15.0, 'bike': 8.0, 'pedestrian': 2.0}
type_weights = [0.5, 0.3, 0.2]
for i in range(self.num_agents):
# Random road selection weighted by length
road_lengths = [r.get('length', 10) for r in self.roads]
total_length = sum(road_lengths)
if total_length == 0:
continue
probs = [l / total_length for l in road_lengths]
road_idx = np.random.choice(len(self.roads), p=probs)
road = self.roads[road_idx]
# Random position along road
progress = np.random.random()
start = np.array(road['start_point'] + [0.5]) # Slight elevation
end = np.array(road['end_point'] + [0.5])
position = start + progress * (end - start)
# Direction along road (randomly forward or backward)
direction = 1 if np.random.random() > 0.5 else -1
road_vec = end - start
road_length = np.linalg.norm(road_vec)
if road_length > 0:
velocity = direction * road_vec / road_length
else:
velocity = np.array([1.0, 0.0, 0.0])
# Agent type
agent_type = np.random.choice(agent_types, p=type_weights)
speed = type_speeds[agent_type] * (0.8 + 0.4 * np.random.random())
agent = TrafficAgent(
position=position,
velocity=velocity * speed,
road_index=road_idx,
progress=progress,
speed=speed,
agent_type=agent_type
)
self.agents.append(agent)
def update(self, dt: float = 0.016):
"""Update agent positions"""
if self.paused or not self.roads:
return
for agent in self.agents:
road = self.roads[agent.road_index]
start = np.array(road['start_point'] + [0.5])
end = np.array(road['end_point'] + [0.5])
road_vec = end - start
road_length = np.linalg.norm(road_vec)
if road_length == 0:
continue
# Update progress along road
direction = np.sign(np.dot(agent.velocity, road_vec))
agent.progress += direction * agent.speed * dt / road_length
# Handle road endpoints - switch to connected road or reverse
if agent.progress > 1.0 or agent.progress < 0.0:
# Find connected road or reverse
current_end = end if agent.progress > 1.0 else start
connected = self._find_connected_road(agent.road_index, current_end)
if connected is not None:
agent.road_index = connected
new_road = self.roads[connected]
new_start = np.array(new_road['start_point'] + [0.5])
new_end = np.array(new_road['end_point'] + [0.5])
# Determine entry point
dist_to_start = np.linalg.norm(current_end[:2] - new_start[:2])
dist_to_end = np.linalg.norm(current_end[:2] - new_end[:2])
if dist_to_start < dist_to_end:
agent.progress = 0.0
new_vec = new_end - new_start
else:
agent.progress = 1.0
new_vec = new_start - new_end
new_length = np.linalg.norm(new_vec)
if new_length > 0:
agent.velocity = new_vec / new_length * agent.speed
else:
# Reverse direction
agent.velocity = -agent.velocity
agent.progress = np.clip(agent.progress, 0.0, 1.0)
# Update actual position
agent.position = start + agent.progress * road_vec
agent.position[2] = 0.5 # Keep at road level
def _find_connected_road(self, current_road_idx: int,
endpoint: np.ndarray,
tolerance: float = 5.0) -> Optional[int]:
"""Find a road connected to the given endpoint"""
for i, road in enumerate(self.roads):
if i == current_road_idx:
continue
start = np.array(road['start_point'])
end = np.array(road['end_point'])
if np.linalg.norm(endpoint[:2] - start) < tolerance:
return i
if np.linalg.norm(endpoint[:2] - end) < tolerance:
return i
return None
def get_points_and_colors(self) -> Tuple[np.ndarray, np.ndarray]:
"""Get agent positions and colors for rendering"""
if not self.agents:
return np.array([[0, 0, 0]]), np.array([[1, 1, 1]])
positions = np.array([a.position for a in self.agents])
colors = np.array([self.agent_colors[a.agent_type] for a in self.agents])
return positions, colors
def reset(self):
"""Reset simulation"""
self._initialize_agents()
# ============================================================================
# RAIN SIMULATION
# ============================================================================
class RainSimulation:
"""Rain particle simulation"""
def __init__(self, bounds: Tuple[float, float, float, float],
max_height: float = 100.0,
num_drops: int = 2000,
rain_speed: float = 50.0):
self.bounds = bounds # minx, miny, maxx, maxy
self.max_height = max_height
self.num_drops = num_drops
self.rain_speed = rain_speed
self.drops: np.ndarray = None
self.velocities: np.ndarray = None
self.paused = False
self._initialize_drops()
def _initialize_drops(self):
"""Initialize raindrop positions"""
minx, miny, maxx, maxy = self.bounds
# Random positions within bounds
x = np.random.uniform(minx, maxx, self.num_drops)
y = np.random.uniform(miny, maxy, self.num_drops)
z = np.random.uniform(0, self.max_height, self.num_drops)
self.drops = np.column_stack([x, y, z])
# Velocities - mostly downward with slight variation
vx = np.random.uniform(-2, 2, self.num_drops)
vy = np.random.uniform(-2, 2, self.num_drops)
vz = np.full(self.num_drops, -self.rain_speed)
self.velocities = np.column_stack([vx, vy, vz])
def update(self, dt: float = 0.016):
"""Update raindrop positions"""
if self.paused:
return
# Move drops
self.drops += self.velocities * dt
# Reset drops that hit ground
ground_mask = self.drops[:, 2] < 0
if np.any(ground_mask):
minx, miny, maxx, maxy = self.bounds
num_reset = np.sum(ground_mask)
self.drops[ground_mask, 0] = np.random.uniform(minx, maxx, num_reset)
self.drops[ground_mask, 1] = np.random.uniform(miny, maxy, num_reset)
self.drops[ground_mask, 2] = self.max_height
def get_points_and_colors(self) -> Tuple[np.ndarray, np.ndarray]:
"""Get drop positions and colors"""
# Color based on height - lighter at top, darker near ground
heights = self.drops[:, 2] / self.max_height
colors = np.column_stack([
0.3 + 0.4 * heights, # R
0.5 + 0.3 * heights, # G
0.8 + 0.2 * heights # B
])
return self.drops.copy(), colors
def get_rain_lines(self) -> pv.PolyData:
"""Get rain as short lines for better visualization"""
line_length = 2.0
# Create line endpoints
starts = self.drops.copy()
ends = self.drops + np.array([0, 0, line_length])
# Build lines
points = []
lines = []
for i in range(len(self.drops)):
idx = len(points)
points.append(starts[i])
points.append(ends[i])
lines.append([2, idx, idx + 1])
if not points:
return pv.PolyData()
mesh = pv.PolyData(np.array(points))
mesh.lines = np.array(lines)
return mesh
def reset(self):
"""Reset simulation"""
self._initialize_drops()
# ============================================================================
# WIND SIMULATION
# ============================================================================
class WindSimulation:
"""Wind particle flow simulation"""
def __init__(self, bounds: Tuple[float, float, float, float],
max_height: float = 50.0,
num_particles: int = 1500,
wind_speed: float = 20.0,
wind_direction: float = 45.0): # degrees
self.bounds = bounds
self.max_height = max_height
self.num_particles = num_particles
self.base_wind_speed = wind_speed
self.wind_direction = np.radians(wind_direction)
self.particles: np.ndarray = None
self.velocities: np.ndarray = None
self.lifetimes: np.ndarray = None
self.max_lifetime = 5.0
self.paused = False
self.buildings: List[Dict] = []
self._initialize_particles()
def set_buildings(self, plots_data: List[Dict]):
"""Set building data for wind interaction"""
self.buildings = plots_data
def set_wind_direction(self, degrees: float):
"""Set wind direction in degrees"""
self.wind_direction = np.radians(degrees)
self._update_base_velocity()
def _update_base_velocity(self):
"""Update base velocity based on wind direction"""
vx = self.base_wind_speed * np.cos(self.wind_direction)
vy = self.base_wind_speed * np.sin(self.wind_direction)
# Add variation to existing particles
if self.velocities is not None:
noise_x = np.random.uniform(-3, 3, self.num_particles)
noise_y = np.random.uniform(-3, 3, self.num_particles)
noise_z = np.random.uniform(-1, 1, self.num_particles)
self.velocities[:, 0] = vx + noise_x
self.velocities[:, 1] = vy + noise_y
self.velocities[:, 2] = noise_z
def _initialize_particles(self):
"""Initialize wind particles"""
minx, miny, maxx, maxy = self.bounds
# Spawn particles from upwind edge
wind_dx = np.cos(self.wind_direction)
wind_dy = np.sin(self.wind_direction)
# Determine spawn edge based on wind direction
if abs(wind_dx) > abs(wind_dy):
if wind_dx > 0: # Wind from left
x = np.full(self.num_particles, minx - 10)
else: # Wind from right
x = np.full(self.num_particles, maxx + 10)
y = np.random.uniform(miny, maxy, self.num_particles)
else:
x = np.random.uniform(minx, maxx, self.num_particles)
if wind_dy > 0: # Wind from bottom
y = np.full(self.num_particles, miny - 10)
else: # Wind from top
y = np.full(self.num_particles, maxy + 10)
z = np.random.uniform(1, self.max_height, self.num_particles)
self.particles = np.column_stack([x, y, z])
# Initialize velocities
vx = self.base_wind_speed * wind_dx + np.random.uniform(-3, 3, self.num_particles)
vy = self.base_wind_speed * wind_dy + np.random.uniform(-3, 3, self.num_particles)
vz = np.random.uniform(-1, 1, self.num_particles)
self.velocities = np.column_stack([vx, vy, vz])
# Lifetimes for particle recycling
self.lifetimes = np.random.uniform(0, self.max_lifetime, self.num_particles)
def update(self, dt: float = 0.016):
"""Update particle positions with building interaction"""
if self.paused:
return
# Update lifetimes
self.lifetimes -= dt
# Move particles
self.particles += self.velocities * dt
# Add turbulence
turbulence = np.random.uniform(-0.5, 0.5, self.particles.shape) * dt * 10
self.velocities += turbulence
# Clamp vertical velocity
self.velocities[:, 2] = np.clip(self.velocities[:, 2], -5, 5)
# Simple building interaction - deflect around buildings
for building in self.buildings:
bounds = building.get('bounds', [0, 0, 0, 0])
height = building.get('max_building_height', 10)
bminx, bminy, bmaxx, bmaxy = bounds
# Check particles near building
in_x = (self.particles[:, 0] > bminx) & (self.particles[:, 0] < bmaxx)
in_y = (self.particles[:, 1] > bminy) & (self.particles[:, 1] < bmaxy)
below_height = self.particles[:, 2] < height
affected = in_x & in_y & below_height
if np.any(affected):
# Push particles up and around
self.velocities[affected, 2] += 10 * dt
# Deflect horizontally
center_x = (bminx + bmaxx) / 2
center_y = (bminy + bmaxy) / 2
dx = self.particles[affected, 0] - center_x
dy = self.particles[affected, 1] - center_y
self.velocities[affected, 0] += np.sign(dx) * 5 * dt
self.velocities[affected, 1] += np.sign(dy) * 5 * dt
# Reset expired or out-of-bounds particles
minx, miny, maxx, maxy = self.bounds
margin = 50
out_of_bounds = (
(self.particles[:, 0] < minx - margin) |
(self.particles[:, 0] > maxx + margin) |
(self.particles[:, 1] < miny - margin) |
(self.particles[:, 1] > maxy + margin) |
(self.particles[:, 2] < 0) |
(self.particles[:, 2] > self.max_height * 2) |
(self.lifetimes < 0)
)
if np.any(out_of_bounds):
self._respawn_particles(out_of_bounds)
def _respawn_particles(self, mask: np.ndarray):
"""Respawn particles at upwind edge"""
num_respawn = np.sum(mask)
if num_respawn == 0:
return
minx, miny, maxx, maxy = self.bounds
wind_dx = np.cos(self.wind_direction)
wind_dy = np.sin(self.wind_direction)
# Spawn at upwind edge
if abs(wind_dx) > abs(wind_dy):
if wind_dx > 0:
self.particles[mask, 0] = minx - 10
else:
self.particles[mask, 0] = maxx + 10
self.particles[mask, 1] = np.random.uniform(miny, maxy, num_respawn)
else:
self.particles[mask, 0] = np.random.uniform(minx, maxx, num_respawn)
if wind_dy > 0:
self.particles[mask, 1] = miny - 10
else:
self.particles[mask, 1] = maxy + 10
self.particles[mask, 2] = np.random.uniform(1, self.max_height, num_respawn)
# Reset velocities
self.velocities[mask, 0] = self.base_wind_speed * wind_dx + np.random.uniform(-3, 3, num_respawn)
self.velocities[mask, 1] = self.base_wind_speed * wind_dy + np.random.uniform(-3, 3, num_respawn)
self.velocities[mask, 2] = np.random.uniform(-1, 1, num_respawn)
# Reset lifetimes
self.lifetimes[mask] = self.max_lifetime
def get_points_and_colors(self) -> Tuple[np.ndarray, np.ndarray]:
"""Get particle positions and colors"""
# Color based on velocity magnitude
speeds = np.linalg.norm(self.velocities, axis=1)
max_speed = self.base_wind_speed * 1.5
normalized_speed = np.clip(speeds / max_speed, 0, 1)
# Color gradient: blue (slow) -> cyan -> white (fast)
colors = np.column_stack([
0.5 + 0.5 * normalized_speed, # R
0.8 + 0.2 * normalized_speed, # G
1.0 * np.ones_like(normalized_speed) # B
])
return self.particles.copy(), colors
def get_streamlines(self) -> pv.PolyData:
"""Get wind as short streamlines"""
line_length = 3.0
# Normalize velocities for direction
speeds = np.linalg.norm(self.velocities, axis=1, keepdims=True)
speeds = np.maximum(speeds, 0.001)
directions = self.velocities / speeds
# Create line endpoints
starts = self.particles.copy()
ends = self.particles + directions * line_length
points = []
lines = []
for i in range(len(self.particles)):
idx = len(points)
points.append(starts[i])
points.append(ends[i])
lines.append([2, idx, idx + 1])
if not points:
return pv.PolyData()
mesh = pv.PolyData(np.array(points))
mesh.lines = np.array(lines)
return mesh
def reset(self):
"""Reset simulation"""
self._initialize_particles()
# ============================================================================
# MAIN SIMULATION VIEWER
# ============================================================================
class UrbanSimulationViewer:
"""Main simulation viewer combining all simulations"""
def __init__(self, obj_path: str, json_path: str = None):
self.obj_path = Path(obj_path)
self.json_path = Path(json_path) if json_path else None
# Load data
self.mesh = None
self.layout_data = None
self.bounds = None
# Simulations
self.traffic_sim = None
self.rain_sim = None
self.wind_sim = None
# Visualization
self.plotter = None
self.current_mode = SimulationMode.NONE
self.simulation_actors = {}
self.is_paused = False
self.last_update_time = time.time()
# Load resources
self._load_resources()
self._initialize_simulations()
def _load_resources(self):
"""Load OBJ mesh and JSON data"""
print("=" * 60)
print("LOADING RESOURCES")
print("=" * 60)
# Load OBJ
if self.obj_path.exists():
print(f"Loading OBJ: {self.obj_path}")
self.mesh = pv.read(str(self.obj_path))
self.bounds = self.mesh.bounds
print(f" Vertices: {self.mesh.n_points}")
print(f" Faces: {self.mesh.n_cells}")
print(f" Bounds: {self.bounds}")
else:
raise FileNotFoundError(f"OBJ file not found: {self.obj_path}")
# Load JSON if provided
if self.json_path and self.json_path.exists():
print(f"Loading JSON: {self.json_path}")
with open(self.json_path, 'r') as f:
self.layout_data = json.load(f)
print(f" Roads: {len(self.layout_data.get('roads', []))}")
print(f" Plots: {len(self.layout_data.get('plots', []))}")
else:
print("No JSON data - using mesh bounds for simulation")
# Create minimal data from mesh bounds
minx, maxx, miny, maxy, minz, maxz = self.bounds
self.layout_data = {
'roads': [],
'plots': [],
'site_info': {
'bounds': [minx, miny, maxx, maxy]
}
}
def _initialize_simulations(self):
"""Initialize all simulation systems"""
print("\n" + "=" * 60)
print("INITIALIZING SIMULATIONS")
print("=" * 60)
# Get bounds
if self.layout_data and 'site_info' in self.layout_data:
bounds = self.layout_data['site_info']['bounds']
sim_bounds = (bounds[0], bounds[1], bounds[2], bounds[3])
else:
minx, maxx, miny, maxy, _, _ = self.bounds
sim_bounds = (minx, miny, maxx, maxy)
# Calculate max building height
max_height = 50.0
if self.layout_data and 'plots' in self.layout_data:
heights = [p.get('max_building_height', 10) for p in self.layout_data['plots']]
if heights:
max_height = max(heights) + 20
# Traffic simulation
roads = self.layout_data.get('roads', []) if self.layout_data else []
if roads:
self.traffic_sim = TrafficSimulation(roads, num_agents=100)
print(f"Traffic: {len(self.traffic_sim.agents)} agents on {len(roads)} roads")
else:
# Generate fake roads from bounds for demo
fake_roads = self._generate_fake_roads(sim_bounds)
self.traffic_sim = TrafficSimulation(fake_roads, num_agents=50)
print(f"Traffic: {len(self.traffic_sim.agents)} agents (generated roads)")
# Rain simulation
self.rain_sim = RainSimulation(
bounds=sim_bounds,
max_height=max_height + 50,
num_drops=3000,
rain_speed=60.0
)
print(f"Rain: {self.rain_sim.num_drops} drops")
# Wind simulation
self.wind_sim = WindSimulation(
bounds=sim_bounds,
max_height=max_height,
num_particles=2000,
wind_speed=25.0,
wind_direction=45.0
)
# Set buildings for wind interaction
if self.layout_data and 'plots' in self.layout_data:
self.wind_sim.set_buildings(self.layout_data['plots'])
print(f"Wind: {self.wind_sim.num_particles} particles")
def _generate_fake_roads(self, bounds: Tuple) -> List[Dict]:
"""Generate fake roads if JSON not provided"""
minx, miny, maxx, maxy = bounds
roads = []
# Grid of roads
num_h = 5
num_v = 5
for i in range(num_h):
y = miny + (i + 1) * (maxy - miny) / (num_h + 1)
roads.append({
'start_point': [minx, y],
'end_point': [maxx, y],
'width': 12.0,
'length': maxx - minx
})
for i in range(num_v):
x = minx + (i + 1) * (maxx - minx) / (num_v + 1)
roads.append({
'start_point': [x, miny],
'end_point': [x, maxy],
'width': 12.0,
'length': maxy - miny
})
return roads
def _setup_plotter(self):
"""Setup PyVista plotter with controls"""
self.plotter = pv.Plotter(title="GENESIS Urban Simulation Viewer")
self.plotter.set_background('black')
# Add mesh
self.plotter.add_mesh(
self.mesh,
color='lightgray',
opacity=0.9,
show_edges=False,
name='layout_mesh'
)
# Set top-down camera
self._set_top_down_view()
# Add text overlay
self._add_ui_text()
# Setup key bindings
self.plotter.add_key_event('space', self._toggle_pause)
self.plotter.add_key_event('r', self._reset_simulations)
self.plotter.add_key_event('0', lambda: self._set_mode(SimulationMode.NONE))
self.plotter.add_key_event('1', lambda: self._set_mode(SimulationMode.TRAFFIC))
self.plotter.add_key_event('2', lambda: self._set_mode(SimulationMode.RAIN))
self.plotter.add_key_event('3', lambda: self._set_mode(SimulationMode.WIND))
self.plotter.add_key_event('Up', lambda: self._adjust_wind_direction(15))
self.plotter.add_key_event('Down', lambda: self._adjust_wind_direction(-15))
self.plotter.add_key_event('t', self._toggle_top_view)
self.plotter.add_key_event('p', self._toggle_perspective)
def _set_top_down_view(self):
"""Set camera to top-down view"""
if self.bounds:
minx, maxx, miny, maxy, minz, maxz = self.bounds
cx = (minx + maxx) / 2
cy = (miny + maxy) / 2
# Calculate camera height based on scene size
width = maxx - minx
height = maxy - miny
cam_height = max(width, height) * 1.5
self.plotter.camera_position = [
(cx, cy, cam_height), # Camera position
(cx, cy, 0), # Focal point
(0, 1, 0) # Up vector
]
def _toggle_top_view(self):
"""Toggle between top-down and isometric view"""
if self.bounds:
minx, maxx, miny, maxy, minz, maxz = self.bounds
cx = (minx + maxx) / 2
cy = (miny + maxy) / 2
width = maxx - minx
height = maxy - miny
cam_dist = max(width, height) * 1.2
self.plotter.camera_position = [
(cx, cy, cam_dist),
(cx, cy, 0),
(0, 1, 0)
]
def _toggle_perspective(self):
"""Toggle to perspective/isometric view"""
if self.bounds:
minx, maxx, miny, maxy, minz, maxz = self.bounds
cx = (minx + maxx) / 2
cy = (miny + maxy) / 2
width = maxx - minx
height = maxy - miny
cam_dist = max(width, height) * 0.8
self.plotter.camera_position = [
(cx - cam_dist, cy - cam_dist, cam_dist * 0.7),
(cx, cy, 0),
(0, 0, 1)
]
def _add_ui_text(self):
"""Add UI text overlay"""
instructions = (
"Controls:\n"
"SPACE - Pause/Resume\n"
"R - Reset\n"
"0 - No overlay\n"
"1 - Traffic\n"
"2 - Rain\n"
"3 - Wind\n"
"T - Top view\n"
"P - Perspective\n"
"↑/↓ - Wind direction"
)
self.plotter.add_text(
instructions,
position='upper_left',
font_size=10,
color='white',
name='instructions'
)
self.status_text = self.plotter.add_text(
"Mode: None | Status: Running",
position='upper_right',
font_size=12,
color='cyan',
name='status'
)
def _update_status_text(self):
"""Update status text"""
mode_names = {
SimulationMode.NONE: "None",
SimulationMode.TRAFFIC: "Traffic",
SimulationMode.RAIN: "Rain",
SimulationMode.WIND: "Wind"
}
status = "Paused" if self.is_paused else "Running"
mode = mode_names.get(self.current_mode, "Unknown")
extra = ""
if self.current_mode == SimulationMode.WIND:
direction = np.degrees(self.wind_sim.wind_direction)
extra = f" | Direction: {direction:.0f}°"
text = f"Mode: {mode} | Status: {status}{extra}"
self.plotter.add_text(
text,
position='upper_right',
font_size=12,
color='cyan',
name='status'
)
def _toggle_pause(self):
"""Toggle pause state"""
self.is_paused = not self.is_paused
if self.traffic_sim:
self.traffic_sim.paused = self.is_paused
if self.rain_sim:
self.rain_sim.paused = self.is_paused
if self.wind_sim:
self.wind_sim.paused = self.is_paused
self._update_status_text()
print(f"{'Paused' if self.is_paused else 'Resumed'}")
def _reset_simulations(self):
"""Reset all simulations"""
if self.traffic_sim:
self.traffic_sim.reset()
if self.rain_sim:
self.rain_sim.reset()
if self.wind_sim:
self.wind_sim.reset()
print("Simulations reset")
def _set_mode(self, mode: SimulationMode):
"""Set simulation display mode"""
self.current_mode = mode
self._clear_simulation_actors()
self._update_status_text()
mode_names = {
SimulationMode.NONE: "None",
SimulationMode.TRAFFIC: "Traffic",
SimulationMode.RAIN: "Rain",
SimulationMode.WIND: "Wind"
}
print(f"Mode: {mode_names.get(mode, 'Unknown')}")
def _adjust_wind_direction(self, delta: float):
"""Adjust wind direction"""
if self.wind_sim:
new_dir = np.degrees(self.wind_sim.wind_direction) + delta
self.wind_sim.set_wind_direction(new_dir)
self._update_status_text()
print(f"Wind direction: {new_dir:.0f}°")
def _clear_simulation_actors(self):
"""Clear simulation visualization actors"""
for name in list(self.simulation_actors.keys()):
try:
self.plotter.remove_actor(name)
except:
pass
self.simulation_actors = {}
def _update_callback(self):
"""Main update callback for animation"""
current_time = time.time()
dt = min(current_time - self.last_update_time, 0.1) # Cap dt
self.last_update_time = current_time
# Update simulations
if self.current_mode == SimulationMode.TRAFFIC and self.traffic_sim:
self.traffic_sim.update(dt)
self._render_traffic()
elif self.current_mode == SimulationMode.RAIN and self.rain_sim:
self.rain_sim.update(dt)
self._render_rain()
elif self.current_mode == SimulationMode.WIND and self.wind_sim:
self.wind_sim.update(dt)
self._render_wind()
def _render_traffic(self):
"""Render traffic agents"""
if not self.traffic_sim:
return
points, colors = self.traffic_sim.get_points_and_colors()
if len(points) > 0:
point_cloud = pv.PolyData(points)
point_cloud['colors'] = (colors * 255).astype(np.uint8)
# Remove old actor
if 'traffic_points' in self.simulation_actors:
try:
self.plotter.remove_actor('traffic_points')
except:
pass
actor = self.plotter.add_mesh(
point_cloud,
scalars='colors',
rgb=True,
point_size=15,
render_points_as_spheres=True,
name='traffic_points'
)
self.simulation_actors['traffic_points'] = actor
def _render_rain(self):
"""Render rain particles"""
if not self.rain_sim:
return
# Use lines for rain
rain_mesh = self.rain_sim.get_rain_lines()
if rain_mesh.n_points > 0:
if 'rain_lines' in self.simulation_actors:
try:
self.plotter.remove_actor('rain_lines')
except:
pass
actor = self.plotter.add_mesh(
rain_mesh,
color='lightblue',
line_width=1,
opacity=0.6,
name='rain_lines'
)
self.simulation_actors['rain_lines'] = actor
def _render_wind(self):
"""Render wind particles"""
if not self.wind_sim:
return
# Use streamlines for wind
wind_mesh = self.wind_sim.get_streamlines()
if wind_mesh.n_points > 0:
if 'wind_lines' in self.simulation_actors:
try:
self.plotter.remove_actor('wind_lines')
except:
pass
actor = self.plotter.add_mesh(
wind_mesh,
color='white',
line_width=2,
opacity=0.7,
name='wind_lines'
)
self.simulation_actors['wind_lines'] = actor
def run(self):
"""Run the simulation viewer"""
print("\n" + "=" * 60)
print("STARTING SIMULATION VIEWER")
print("=" * 60)
print("\nControls:")
print(" SPACE - Pause/Resume")
print(" R - Reset simulations")
print(" 0 - No overlay")
print(" 1 - Traffic simulation")
print(" 2 - Rain simulation")
print(" 3 - Wind simulation")
print(" T - Top-down view")
print(" P - Perspective view")
print(" ↑/↓ - Change wind direction")
print("\nClose window to exit.")
self._setup_plotter()
# Start with traffic mode
self._set_mode(SimulationMode.TRAFFIC)
# Timer-driven animation (works on older PyVista)
self.plotter.add_timer_event(max_steps=10**9, duration=33, callback=self._update_callback)
# Show plotter
self.plotter.show()
# ============================================================================
# CLI INTERFACE
# ============================================================================
def main():
parser = argparse.ArgumentParser(
description='GENESIS Urban Layout Simulation Viewer',
formatter_class=argparse.RawDescriptionHelpFormatter,
epilog="""
Examples:
python simulation_viewer.py layout.obj
python simulation_viewer.py layout.obj --json layout.json
python simulation_viewer.py layout.obj -j layout.json --wind-dir 90
"""
)
parser.add_argument('obj_file', type=str, help='Path to OBJ file')
parser.add_argument('--json', '-j', type=str, help='Path to JSON layout file')
parser.add_argument('--wind-dir', type=float, default=45.0,
help='Initial wind direction in degrees (default: 45)')
parser.add_argument('--num-traffic', type=int, default=100,
help='Number of traffic agents (default: 100)')
parser.add_argument('--num-rain', type=int, default=3000,
help='Number of rain drops (default: 3000)')
parser.add_argument('--num-wind', type=int, default=2000,
help='Number of wind particles (default: 2000)')
args = parser.parse_args()
# Validate files
obj_path = Path(args.obj_file)
if not obj_path.exists():
print(f"Error: OBJ file not found: {obj_path}")
return 1
json_path = args.json
if json_path and not Path(json_path).exists():
print(f"Warning: JSON file not found: {json_path}")
print("Proceeding without layout data...")
json_path = None
# Create and run viewer
try:
viewer = UrbanSimulationViewer(str(obj_path), json_path)
# Apply custom settings
if viewer.wind_sim:
viewer.wind_sim.set_wind_direction(args.wind_dir)
viewer.run()
except Exception as e:
print(f"Error: {e}")
import traceback
traceback.print_exc()
return 1
return 0
if __name__ == "__main__":
exit(main())