Skip to content

Commit e488ada

Browse files
committed
Animation with motivation as color
1 parent de5a4d6 commit e488ada

File tree

3 files changed

+158
-18
lines changed

3 files changed

+158
-18
lines changed

anim.py

Lines changed: 59 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -22,10 +22,14 @@
2222
DUMMY_SPEED = -1000
2323

2424

25-
def _speed_to_color(speed: float, min_speed: float, max_speed: float) -> str:
26-
"""Map a speed value to a color using a colormap."""
27-
normalized_speed = (speed - min_speed) / (max_speed - min_speed)
28-
r, g, b = plt.cm.jet_r(normalized_speed)[:3] # type: ignore
25+
def _value_to_color(value: float, min_value: float, max_value: float) -> str:
26+
"""Map a scalar value to a color using a colormap."""
27+
if max_value <= min_value:
28+
normalized_value = 0.5
29+
else:
30+
normalized_value = (value - min_value) / (max_value - min_value)
31+
normalized_value = min(max(normalized_value, 0.0), 1.0)
32+
r, g, b = plt.cm.jet_r(normalized_value)[:3] # type: ignore
2933
return f"rgba({r*255:.0f}, {g*255:.0f}, {b*255:.0f}, 0.5)"
3034

3135

@@ -85,7 +89,10 @@ def _get_geometry_traces(area: Polygon) -> Scatter:
8589

8690

8791
def _get_colormap(
88-
frame_data: pd.DataFrame, max_speed: float, color_mode: str
92+
frame_data: pd.DataFrame,
93+
min_value: float,
94+
max_value: float,
95+
color_mode: str,
8996
) -> List[Scatter]:
9097
"""Utilize scatter plots with varying colors for each agent instead of individual shapes.
9198
@@ -97,8 +104,17 @@ def _get_colormap(
97104
"color": frame_data["speed"],
98105
"colorscale": "Jet_r",
99106
"colorbar": {"title": "Speed [m/s]"},
100-
"cmin": 0,
101-
"cmax": max_speed,
107+
"cmin": min_value,
108+
"cmax": max_value,
109+
}
110+
elif color_mode == "Motivation":
111+
marker_dict = {
112+
"size": frame_data["radius"] * 2,
113+
"color": frame_data["motivation"],
114+
"colorscale": "Jet_r",
115+
"colorbar": {"title": "Motivation"},
116+
"cmin": min_value,
117+
"cmax": max_value,
102118
}
103119
else:
104120
colors = frame_data["gender"].map({2: "blue", 1: "green"})
@@ -113,7 +129,13 @@ def _get_colormap(
113129
y=frame_data["y"],
114130
mode="markers",
115131
marker=marker_dict,
116-
text=frame_data["speed"] if color_mode == "speed" else frame_data["gender"],
132+
text=(
133+
frame_data["speed"]
134+
if color_mode == "Speed"
135+
else frame_data["motivation"]
136+
if color_mode == "Motivation"
137+
else frame_data["gender"]
138+
),
117139
showlegend=False,
118140
hoverinfo="none",
119141
)
@@ -122,7 +144,10 @@ def _get_colormap(
122144

123145

124146
def _get_shapes_for_frame(
125-
frame_data: pd.DataFrame, min_speed: float, max_speed: float, color_mode: str
147+
frame_data: pd.DataFrame,
148+
min_value: float,
149+
max_value: float,
150+
color_mode: str,
126151
) -> Tuple[Shape, Scatter, Shape]:
127152
"""Construct circles as Shapes for agents, Hover and Directions."""
128153

@@ -162,7 +187,9 @@ def create_shape(row: pd.DataFrame) -> Shape:
162187
_create_orientation_line(row, color="rgba(255,255,255,0)"),
163188
)
164189
if color_mode == "Speed":
165-
color = _speed_to_color(row["speed"], min_speed, max_speed)
190+
color = _value_to_color(row["speed"], min_value, max_value)
191+
elif color_mode == "Motivation":
192+
color = _value_to_color(row["motivation"], min_value, max_value)
166193
else:
167194
gender_colors = {
168195
1: "blue", # Assuming 1 is for female
@@ -301,7 +328,14 @@ def _get_processed_frame_data(
301328
"""Process frame data and ensure it matches the maximum agent count."""
302329
frame_data = data_df[data_df["frame"] == frame_num]
303330
agent_count = len(frame_data)
304-
dummy_agent_data = {"x": 0, "y": 0, "radius": 0, "speed": DUMMY_SPEED}
331+
dummy_agent_data = {
332+
"x": 0,
333+
"y": 0,
334+
"radius": 0,
335+
"speed": DUMMY_SPEED,
336+
"motivation": 0.0,
337+
"gender": 0,
338+
}
305339
while len(frame_data) < max_agents:
306340
dummy_df = pd.DataFrame([dummy_agent_data])
307341
frame_data = pd.concat([frame_data, dummy_df], ignore_index=True)
@@ -326,8 +360,15 @@ def animate(
326360
"""Animate a trajectory."""
327361
data_df["radius"] = radius
328362
frames = data_df["frame"].unique()
329-
min_speed = data_df["speed"].min()
330-
max_speed = data_df["speed"].max()
363+
if color_mode == "Speed":
364+
min_value = data_df["speed"].min()
365+
max_value = data_df["speed"].max()
366+
elif color_mode == "Motivation":
367+
min_value = data_df["motivation"].min()
368+
max_value = data_df["motivation"].max()
369+
else:
370+
min_value = 0.0
371+
max_value = 1.0
331372
max_agents = data_df.groupby("frame").size().max()
332373
frames = []
333374
steps = []
@@ -340,14 +381,16 @@ def animate(
340381
initial_shapes,
341382
initial_hover_trace,
342383
initial_arrows,
343-
) = _get_shapes_for_frame(initial_frame_data, min_speed, max_speed, color_mode)
344-
color_map_trace = _get_colormap(initial_frame_data, max_speed, color_mode)
384+
) = _get_shapes_for_frame(initial_frame_data, min_value, max_value, color_mode)
385+
color_map_trace = _get_colormap(
386+
initial_frame_data, min_value, max_value, color_mode
387+
)
345388
for frame_num in selected_frames:
346389
frame_data, agent_count = _get_processed_frame_data(
347390
data_df, frame_num, max_agents
348391
)
349392
shapes, hover_traces, arrows = _get_shapes_for_frame(
350-
frame_data, min_speed, max_speed, color_mode
393+
frame_data, min_value, max_value, color_mode
351394
)
352395
# title = f"<b>{title_note + ' | ' if title_note else ''}N: {agent_count}</b>"
353396
title = f"<b>{title_note + ' | ' if title_note else ''}Number of Agents: {initial_agent_count}</b>"

app.py

Lines changed: 59 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,17 +5,23 @@
55
Date: August 11, 2023
66
"""
77

8+
from pathlib import Path
9+
810
import jupedsim as jps
9-
import streamlit as st
11+
import pandas as pd
1012
import pedpy
13+
import streamlit as st
14+
from anim import animate
15+
from jupedsim.internal.notebook_utils import read_sqlite_file
1116
from src import docs
1217
import glob
1318
from src.logger_config import init_logger
1419
from src.simutilities import (
20+
call_simulation,
1521
extract_motivation_parameters,
1622
plot_motivation_model,
1723
)
18-
from src.ui import init_sidebar, simulation_tab
24+
from src.ui import init_sidebar, simulation_tab, ui_simulation_controls
1925

2026
if __name__ == "__main__":
2127
init_logger()
@@ -34,7 +40,58 @@
3440
case "Documentation":
3541
docs.main()
3642
case "Simulation":
43+
c1, c2, _c3 = st.columns(3)
3744
data = simulation_tab()
45+
config_file, output_file, fps = ui_simulation_controls(data)
46+
47+
if c1.button("Run Simulation"):
48+
call_simulation(config_file, output_file, data)
49+
50+
if c2.button("Visualization"):
51+
output_path = Path(output_file)
52+
if output_path.exists():
53+
trajectory_data, walkable_area = read_sqlite_file(output_file)
54+
motivation_path = output_path.with_name(
55+
output_path.stem + "_motivation.csv"
56+
)
57+
if not motivation_path.exists():
58+
st.warning(f"Motivation file not found: {motivation_path}")
59+
else:
60+
trajectory_df = trajectory_data.data.copy()
61+
motivation_df = pd.read_csv(motivation_path)
62+
data_with_motivation = trajectory_df.merge(
63+
motivation_df[["frame", "id", "motivation"]],
64+
on=["frame", "id"],
65+
how="left",
66+
)
67+
data_with_motivation["motivation"] = data_with_motivation[
68+
"motivation"
69+
].fillna(1.0)
70+
data_with_motivation["gender"] = 1
71+
data_with_motivation["speed"] = 0.0
72+
width = data["motivation_parameters"]["width"]
73+
vertices = data["motivation_parameters"]["motivation_doors"][
74+
0
75+
]["vertices"]
76+
x0 = 0.5 * (vertices[0][0] + vertices[1][0]) - width
77+
y0 = 0.5 * (vertices[0][1] + vertices[1][1]) - width
78+
x1 = 0.5 * (vertices[0][0] + vertices[1][0]) + width
79+
y1 = 0.5 * (vertices[1][1] + vertices[1][1]) + width
80+
81+
animation = animate(
82+
data_with_motivation,
83+
walkable_area,
84+
every_nth_frame=int(fps),
85+
color_mode="Motivation",
86+
radius=0.1,
87+
x0=x0,
88+
y0=y0,
89+
x1=x1,
90+
y1=y1,
91+
)
92+
st.plotly_chart(animation)
93+
else:
94+
st.warning(f"Trajectory file not found: {output_file}")
3895

3996
params = extract_motivation_parameters(data)
4097
mapping = params.get("mapping_block", {})

src/simutilities.py

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,20 @@
11
"""Utilities for simulation."""
22

3+
import logging
34
from pathlib import Path
45
from typing import Any, Dict, List, Tuple
56

67
import streamlit as st
78
from src import motivation_model as mm
89
from src import motivation_mapping as mmap
910
from src.inifile_parser import (
11+
parse_fps,
1012
parse_motivation_doors,
1113
parse_motivation_strategy,
1214
parse_normal_time_gap,
1315
parse_normal_v_0,
1416
parse_number_agents,
17+
parse_simulation_time,
1518
parse_time_step,
1619
parse_velocity_init_parameters,
1720
)
@@ -182,6 +185,43 @@ def create_motivation_strategy(params: Dict[str, Any]) -> mm.MotivationStrategy:
182185
raise ValueError(f"Unknown strategy: {strategy}")
183186

184187

188+
def call_simulation(config_file: str, output_file: str, data: Dict[str, Any]) -> None:
189+
"""Run the simulation based on the provided configuration and data."""
190+
from simulation import init_and_run_simulation
191+
192+
msg = st.empty()
193+
output_path = Path(output_file)
194+
if output_path.exists():
195+
output_path.unlink()
196+
197+
number_agents = parse_number_agents(data)
198+
fps = parse_fps(data)
199+
time_step = parse_time_step(data)
200+
simulation_time = parse_simulation_time(data)
201+
open_door_time = float(data["simulation_parameters"].get("open_door_time", 0.0))
202+
strategy = parse_motivation_strategy(data)
203+
204+
logging.info(f"Call simulation {number_agents = }")
205+
msg.code(f"Running simulation with {number_agents}. Strategy: <{strategy}>...")
206+
207+
with st.spinner("Simulating..."):
208+
try:
209+
evac_time, _ = init_and_run_simulation(
210+
fps,
211+
time_step,
212+
simulation_time,
213+
open_door_time,
214+
data,
215+
output_path,
216+
msg,
217+
)
218+
except ValueError as exc:
219+
st.error(f"Logistic configuration error: {exc}")
220+
return
221+
222+
msg.code(f"Finished simulation. Evac time {evac_time:.2f} s")
223+
224+
185225
def plot_motivation_model(params: Dict[str, Any]) -> None:
186226
"""Plot the motivation model based on the given parameters.
187227

0 commit comments

Comments
 (0)