Marcel0123 commited on
Commit
dce7fdc
·
verified ·
1 Parent(s): c5da423

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +215 -123
app.py CHANGED
@@ -1,9 +1,10 @@
1
- # app.py — Crowd Behavior Lab (animated GIF, bolletjes-only)
2
- # ----------------------------------------------------------
3
- # Focus in deze stap:
4
- # - Altijd bewegende bolletjes via één geanimeerde GIF (server-side gerenderd).
5
- # - Autostart bij openen van de Space (demo.load).
6
- # - Geen pijlen, geen slider/timer zo vermijden we event-/queue-problemen.
 
7
  #
8
  # Vereisten (requirements.txt):
9
  # gradio>=4.44.0
@@ -14,11 +15,7 @@
14
 
15
  from __future__ import annotations
16
 
17
- import io
18
- import os
19
- import json
20
- import math
21
- import tempfile
22
  from dataclasses import dataclass, asdict
23
  from typing import List, Tuple, Optional, Dict
24
 
@@ -47,19 +44,19 @@ CUSTOM_CSS = """
47
 
48
 
49
  # ---------------------------
50
- # Obstakels (rechthoek-basic)
51
  # ---------------------------
52
  @dataclass
53
  class RectObstacle:
54
  x: float; y: float; w: float; h: float
55
  def contains(self, px: float, py: float) -> bool:
56
- return self.x <= px <= self.x + self.w and self.y <= py <= self.y + self.h
57
  def nearest_point(self, p: np.ndarray) -> np.ndarray:
58
  qx = np.clip(p[0], self.x, self.x + self.w)
59
  qy = np.clip(p[1], self.y, self.y + self.h)
60
  return np.array([qx, qy], dtype=np.float32)
61
  def to_json(self) -> Dict:
62
- d = asdict(self); return d
63
 
64
  @dataclass
65
  class World:
@@ -73,49 +70,53 @@ class Agent:
73
 
74
 
75
  # ---------------------------
76
- # Presets (incl. taps toelopende hals)
77
  # ---------------------------
78
  def funnel_presets():
79
  presets = {}
80
- # Taps toelopende flessenhals met segmenten
 
81
  obs = []
82
  world_w, world_h = 20.0, 12.0
83
  left_x, right_x = 2.0, 18.0
84
  steps = 8
85
  top_y0, bot_y0 = 9.5, 2.5
86
  top_y1, bot_y1 = 6.8, 5.2
 
87
  for i in range(steps):
88
- x0 = left_x + (right_x - left_x) * (i / steps)
89
- x1 = left_x + (right_x - left_x) * ((i + 1) / steps)
90
  t = i / (steps - 1)
91
  top_y = (1 - t) * top_y0 + t * top_y1
92
  bot_y = (1 - t) * bot_y0 + t * bot_y1
93
- obs.append(RectObstacle(x0, top_y, x1 - x0, world_h - top_y))
94
- obs.append(RectObstacle(x0, 0.0, x1 - x0, bot_y))
95
- presets["Flessenhals (taps)"] = obs
96
 
97
- # Dubbele funnel
98
  obs2 = []
99
  top_y0, bot_y0 = 10.0, 2.0
100
  top_mid, bot_mid = 7.0, 5.0
101
  top_end, bot_end = 9.5, 2.5
 
102
  for i in range(steps):
103
  t = i / (steps - 1)
104
- x0 = 0.5 + (9.5 - 0.5) * (i / steps)
105
- x1 = 0.5 + (9.5 - 0.5) * ((i + 1) / steps)
106
  top_y = (1 - t) * top_y0 + t * top_mid
107
  bot_y = (1 - t) * bot_y0 + t * bot_mid
108
- obs2.append(RectObstacle(x0, top_y, x1 - x0, 12.0 - top_y))
109
- obs2.append(RectObstacle(x0, 0.0, x1 - x0, bot_y))
 
110
  for i in range(steps):
111
  t = i / (steps - 1)
112
- x0 = 10.5 + (19.5 - 10.5) * (i / steps)
113
- x1 = 10.5 + (19.5 - 10.5) * ((i + 1) / steps)
114
  top_y = (1 - t) * top_mid + t * top_end
115
  bot_y = (1 - t) * bot_mid + t * bot_end
116
- obs2.append(RectObstacle(x0, top_y, x1 - x0, 12.0 - top_y))
117
- obs2.append(RectObstacle(x0, 0.0, x1 - x0, bot_y))
118
- presets["Dubbele funnel (taps)"] = obs2
119
  return presets
120
 
121
  PRESETS = {
@@ -129,8 +130,9 @@ PRESETS = {
129
  DEFAULT_PARAMS = dict(
130
  desired_speed=1.3, relax_time=0.6,
131
  people_repulsion=4.0, people_range=1.2,
132
- obstacle_repulsion=8.0, obstacle_range=1.0,
133
- noise=0.08, bounce_walls=True,
 
134
  )
135
 
136
 
@@ -166,88 +168,183 @@ def init_agents(n_agents: int, world: World, layout: str, seed: int = 42) -> Lis
166
 
167
 
168
  # ---------------------------
169
- # Social-force step
170
  # ---------------------------
171
- def social_force_step(agents: List[Agent], world: World, params: dict, dt: float = 0.12) -> None:
172
- positions = np.array([a.pos for a in agents])
173
- velocities = np.array([a.vel for a in agents])
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
174
 
 
 
 
 
 
 
 
 
175
  desired_speed = params.get("desired_speed", 1.3)
176
  relax_time = params.get("relax_time", 0.6)
177
  people_repulsion = params.get("people_repulsion", 4.0)
178
  people_range = params.get("people_range", 1.2)
179
- obstacle_repulsion = params.get("obstacle_repulsion", 8.0)
180
- obstacle_range = params.get("obstacle_range", 1.0)
181
- noise = params.get("noise", 0.08)
182
  bounce_walls = params.get("bounce_walls", True)
183
 
184
- forces = np.zeros_like(positions)
185
-
186
- # Driving force
187
- goals = np.array([a.goal for a in agents])
188
- to_goal = goals - positions
189
- dist_goal = np.linalg.norm(to_goal, axis=1) + 1e-6
190
- desired_dir = (to_goal.T / dist_goal).T
191
- desired_vel = desired_speed * desired_dir
192
- forces += (desired_vel - velocities) / max(1e-6, relax_time)
193
-
194
- # Repulsie mensen
195
- for i in range(len(agents)):
196
- diff = positions[i] - positions
197
- d = np.linalg.norm(diff, axis=1) + 1e-6
198
- mask = (d > 0) & (d < people_range * 3.0)
199
- if np.any(mask):
200
- dir_ij = (diff[mask].T / d[mask]).T
201
- mag = people_repulsion * np.exp(-d[mask] / max(1e-6, people_range))
202
- forces[i] += (dir_ij.T * mag).T.sum(axis=0)
203
-
204
- # Repulsie obstakels
205
- for i in range(len(agents)):
206
- p = positions[i]; f = np.zeros(2, dtype=np.float32)
207
- for ob in world.obstacles:
208
- q = ob.nearest_point(p)
209
- diff = p - q
210
- d = np.linalg.norm(diff) + 1e-6
211
- if d < obstacle_range * 3:
212
- dir_ = diff / d
213
- mag = obstacle_repulsion * math.exp(-d / max(1e-6, obstacle_range))
214
- f += dir_ * mag
215
- forces[i] += f
216
-
217
- # Ruis
218
- forces += noise * np.random.randn(*forces.shape)
219
-
220
- # Integratie + limiet
221
- new_vel = velocities + dt * forces
222
- speeds = np.linalg.norm(new_vel, axis=1) + 1e-6
223
- new_vel = (new_vel.T * np.minimum(1.0, (desired_speed * 1.8) / speeds)).T
224
- new_pos = positions + dt * new_vel
225
-
226
- # Collisions met obstakels
227
- for i in range(len(agents)):
228
- for ob in world.obstacles:
229
- if ob.contains(new_pos[i, 0], new_pos[i, 1]):
230
- q = ob.nearest_point(new_pos[i])
231
- dir_ = new_pos[i] - q
232
- if np.linalg.norm(dir_) < 1e-6: dir_ = np.array([0.5, 0.0], dtype=np.float32)
233
- dir_ = dir_ / (np.linalg.norm(dir_) + 1e-6)
234
- new_pos[i] = q + 0.06 * dir_
235
-
236
- # Muren
237
- if bounce_walls:
238
- for i in range(len(agents)):
239
- if new_pos[i, 0] < 0: new_pos[i, 0] = 0; new_vel[i, 0] *= -0.5
240
- if new_pos[i, 0] > world.width: new_pos[i, 0] = world.width; new_vel[i, 0] *= -0.5
241
- if new_pos[i, 1] < 0: new_pos[i, 1] = 0; new_vel[i, 1] *= -0.5
242
- if new_pos[i, 1] > world.height: new_pos[i, 1] = world.height; new_vel[i, 1] *= -0.5
243
 
244
- # Commit
245
- for i, a in enumerate(agents):
246
- a.pos = new_pos[i]; a.vel = new_vel[i]
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
247
 
248
 
249
  # ---------------------------
250
- # Visual metrics & kleur
251
  # ---------------------------
252
  def k3_distance(point: np.ndarray, all_points: np.ndarray) -> float:
253
  dists = np.linalg.norm(all_points - point, axis=1)
@@ -273,7 +370,7 @@ def stress_to_color(s: float) -> str:
273
 
274
 
275
  # ---------------------------
276
- # Rendering (één frame)
277
  # ---------------------------
278
  def render_frame(positions: np.ndarray, velocities: np.ndarray, world: World, params: dict,
279
  trail_positions: List[np.ndarray], show_trails=True, show_heatmap=False,
@@ -288,7 +385,7 @@ def render_frame(positions: np.ndarray, velocities: np.ndarray, world: World, pa
288
  ax.set_xlim(0, world.width); ax.set_ylim(0, world.height)
289
  ax.set_aspect("equal"); ax.set_facecolor("#f9fafb")
290
 
291
- # Obstacles
292
  for ob in world.obstacles:
293
  rect = plt.Rectangle((ob.x, ob.y), ob.w, ob.h, facecolor="#6b7280", alpha=0.20, edgecolor="#1f2937", linewidth=1.0)
294
  ax.add_patch(rect)
@@ -350,7 +447,7 @@ def render_frame(positions: np.ndarray, velocities: np.ndarray, world: World, pa
350
 
351
 
352
  # ---------------------------
353
- # Simulatie → lijst frames → GIF
354
  # ---------------------------
355
  def simulate_states(n_agents: int, steps: int, world: World, params: dict, layout: str):
356
  agents = init_agents(n_agents, world, layout)
@@ -359,7 +456,7 @@ def simulate_states(n_agents: int, steps: int, world: World, params: dict, layou
359
  pos = np.array([a.pos.copy() for a in agents])
360
  vel = np.array([a.vel.copy() for a in agents])
361
  states.append({"pos": pos, "vel": vel})
362
- social_force_step(agents, world, params, dt=0.12)
363
  return states
364
 
365
  def states_to_gif_path(states, world: World, params: dict,
@@ -367,8 +464,7 @@ def states_to_gif_path(states, world: World, params: dict,
367
  performance_mode=True, fps: float = 12.0) -> str:
368
  frames_np = []
369
  trail_positions: List[np.ndarray] = []
370
- for i, st in enumerate(states):
371
- # bouw korte trail
372
  trail_positions.append(st["pos"])
373
  if len(trail_positions) > 12: trail_positions.pop(0)
374
  img = render_frame(st["pos"], st["vel"], world, params, trail_positions,
@@ -378,7 +474,6 @@ def states_to_gif_path(states, world: World, params: dict,
378
 
379
  tmp = tempfile.NamedTemporaryFile(delete=False, suffix=".gif")
380
  tmp_path = tmp.name; tmp.close()
381
- # duration = seconden per frame
382
  duration = 1.0 / max(1.0, fps)
383
  imageio.mimsave(tmp_path, frames_np, format="GIF", duration=duration, loop=0)
384
  return tmp_path
@@ -411,7 +506,6 @@ def run_sim_to_gif(
411
  badge_html = f"<span class='stat-badge'>{len(world.obstacles)} obstakels</span>"
412
  return gif_path, badge_html
413
 
414
- # Autostart bij load
415
  def do_autostart(preset, obstacles_json, n_agents, steps, layout,
416
  desired_speed, relax_time, people_repulsion, people_range,
417
  obstacle_repulsion, obstacle_range, noise, bounce,
@@ -425,7 +519,7 @@ def do_autostart(preset, obstacles_json, n_agents, steps, layout,
425
  # ---------------------------
426
  # UI
427
  # ---------------------------
428
- with gr.Blocks(theme=THEME, css=CUSTOM_CSS, title="Crowd Behavior Lab — GIF") as demo:
429
  gr.Markdown("""
430
  <div id='title' class='card' style='margin-bottom:12px'>
431
  <h1>👥 Crowd Behavior Lab</h1>
@@ -434,31 +528,31 @@ with gr.Blocks(theme=THEME, css=CUSTOM_CSS, title="Crowd Behavior Lab — GIF")
434
  <span class="legend-dot legend-orange" style="margin-left:12px;"></span>Stress
435
  <span class="legend-dot legend-red" style="margin-left:12px;"></span>Paniek
436
  </div>
437
- <p style="margin-top:6px">Deze versie rendert een geanimeerde GIF voor gegarandeerde beweging.</p>
438
  </div>
439
  """)
440
 
441
  with gr.Row(equal_height=False):
442
  with gr.Column(scale=1):
443
  gr.Markdown("### Scène & parameters", elem_classes=["card"])
444
- preset = gr.Dropdown(list(PRESETS.keys()), value="Flessenhals (taps)", label="Obstakelpreset")
445
  obstacles_json = gr.Textbox(label="Extra obstakels (JSON)", value="[]", lines=4)
446
 
447
  layout = gr.Radio(["Links→Rechts","Rechts→Links","Twee-richtingen","Willekeurig"],
448
  value="Twee-richtingen", label="Stroomrichting")
449
 
450
  with gr.Row():
451
- n_agents = gr.Slider(5, 200, value=80, step=1, label="Aantal agenten")
452
- steps = gr.Slider(30, 500, value=200, step=10, label="Simulatiestappen")
453
 
454
  with gr.Accordion("Krachten & gedrag", open=False):
455
  desired_speed = gr.Slider(0.5, 2.5, value=1.3, step=0.05, label="Gewenste snelheid (m/s)")
456
  relax_time = gr.Slider(0.2, 2.0, value=0.6, step=0.05, label="Relaxatietijd")
457
  people_repulsion = gr.Slider(0.0, 10.0, value=4.0, step=0.1, label="Repulsie tussen mensen")
458
  people_range = gr.Slider(0.2, 3.0, value=1.2, step=0.05, label="Interactieradius (mensen)")
459
- obstacle_repulsion = gr.Slider(0.0, 16.0, value=8.0, step=0.1, label="Repulsie obstakels")
460
- obstacle_range = gr.Slider(0.2, 3.0, value=1.0, step=0.05, label="Interactieradius (obstakels)")
461
- noise = gr.Slider(0.0, 0.6, value=0.08, step=0.01, label="Gedragsruis")
462
  bounce = gr.Checkbox(value=True, label="Veerkrachtige muren (bounce)")
463
 
464
  gr.Markdown("### Visualisatie", elem_classes=["card"])
@@ -474,10 +568,9 @@ with gr.Blocks(theme=THEME, css=CUSTOM_CSS, title="Crowd Behavior Lab — GIF")
474
 
475
  with gr.Column(scale=2):
476
  gr.Markdown("### Geanimeerde simulatie", elem_classes=["card"])
477
- # Belangrijk: GIF tonen als filepath
478
- canvas = gr.Image(label="Simulatie (GIF)", format="png", interactive=False) # format negeren; pad eindigt op .gif
479
 
480
- # Autostart bij openen
481
  demo.load(
482
  fn=do_autostart,
483
  inputs=[preset, obstacles_json, n_agents, steps, layout,
@@ -498,5 +591,4 @@ with gr.Blocks(theme=THEME, css=CUSTOM_CSS, title="Crowd Behavior Lab — GIF")
498
  )
499
 
500
  if __name__ == "__main__":
501
- # Geen timer/queue meer nodig voor animatie — GIF speelt in de browser.
502
  demo.launch()
 
1
+ # app.py — Crowd Behavior Lab (GIF, solide afbakening met swept collisions)
2
+ # ------------------------------------------------------------------------
3
+ # Wat is nieuw:
4
+ # - Swept collision (segment-rect) per agent per stap geen tunneling
5
+ # - Iteratieve push-out + normale-reflectie van snelheid
6
+ # - Funnel-segmenten met overlap_eps zodat er geen kieren zijn
7
+ # - Nog steeds: geanimeerde GIF (betrouwbare beweging), geen pijlen
8
  #
9
  # Vereisten (requirements.txt):
10
  # gradio>=4.44.0
 
15
 
16
  from __future__ import annotations
17
 
18
+ import io, os, json, math, tempfile
 
 
 
 
19
  from dataclasses import dataclass, asdict
20
  from typing import List, Tuple, Optional, Dict
21
 
 
44
 
45
 
46
  # ---------------------------
47
+ # Obstakels (rechthoek)
48
  # ---------------------------
49
  @dataclass
50
  class RectObstacle:
51
  x: float; y: float; w: float; h: float
52
  def contains(self, px: float, py: float) -> bool:
53
+ return (self.x <= px <= self.x + self.w) and (self.y <= py <= self.y + self.h)
54
  def nearest_point(self, p: np.ndarray) -> np.ndarray:
55
  qx = np.clip(p[0], self.x, self.x + self.w)
56
  qy = np.clip(p[1], self.y, self.y + self.h)
57
  return np.array([qx, qy], dtype=np.float32)
58
  def to_json(self) -> Dict:
59
+ return asdict(self)
60
 
61
  @dataclass
62
  class World:
 
70
 
71
 
72
  # ---------------------------
73
+ # Presets (incl. taps toelopende hals met overlap)
74
  # ---------------------------
75
  def funnel_presets():
76
  presets = {}
77
+
78
+ # Taps toelopende flessenhals met segment-overlap om kieren te dichten
79
  obs = []
80
  world_w, world_h = 20.0, 12.0
81
  left_x, right_x = 2.0, 18.0
82
  steps = 8
83
  top_y0, bot_y0 = 9.5, 2.5
84
  top_y1, bot_y1 = 6.8, 5.2
85
+ overlap_eps = 0.07 # kleine overlap tussen segmenten
86
  for i in range(steps):
87
+ x0 = left_x + (right_x - left_x) * (i / steps) - (overlap_eps if i > 0 else 0.0)
88
+ x1 = left_x + (right_x - left_x) * ((i + 1) / steps) + (overlap_eps if i < steps - 1 else 0.0)
89
  t = i / (steps - 1)
90
  top_y = (1 - t) * top_y0 + t * top_y1
91
  bot_y = (1 - t) * bot_y0 + t * bot_y1
92
+ obs.append(RectObstacle(max(0.0,x0), top_y, min(world_w,x1)-max(0.0,x0), world_h - top_y)) # bovenwand
93
+ obs.append(RectObstacle(max(0.0,x0), 0.0, min(world_w,x1)-max(0.0,x0), bot_y)) # onderwand
94
+ presets["Flessenhals (taps, solide)"] = obs
95
 
96
+ # Dubbele funnel met overlap
97
  obs2 = []
98
  top_y0, bot_y0 = 10.0, 2.0
99
  top_mid, bot_mid = 7.0, 5.0
100
  top_end, bot_end = 9.5, 2.5
101
+ # links -> midden
102
  for i in range(steps):
103
  t = i / (steps - 1)
104
+ x0 = 0.5 + (9.5 - 0.5) * (i / steps) - (overlap_eps if i > 0 else 0.0)
105
+ x1 = 0.5 + (9.5 - 0.5) * ((i + 1) / steps) + (overlap_eps if i < steps - 1 else 0.0)
106
  top_y = (1 - t) * top_y0 + t * top_mid
107
  bot_y = (1 - t) * bot_y0 + t * bot_mid
108
+ obs2.append(RectObstacle(max(0.0,x0), top_y, min(world_w,x1)-max(0.0,x0), 12.0 - top_y))
109
+ obs2.append(RectObstacle(max(0.0,x0), 0.0, min(world_w,x1)-max(0.0,x0), bot_y))
110
+ # midden -> rechts
111
  for i in range(steps):
112
  t = i / (steps - 1)
113
+ x0 = 10.5 + (19.5 - 10.5) * (i / steps) - (overlap_eps if i > 0 else 0.0)
114
+ x1 = 10.5 + (19.5 - 10.5) * ((i + 1) / steps) + (overlap_eps if i < steps - 1 else 0.0)
115
  top_y = (1 - t) * top_mid + t * top_end
116
  bot_y = (1 - t) * bot_mid + t * bot_end
117
+ obs2.append(RectObstacle(max(0.0,x0), top_y, min(world_w,x1)-max(0.0,x0), 12.0 - top_y))
118
+ obs2.append(RectObstacle(max(0.0,x0), 0.0, min(world_w,x1)-max(0.0,x0), bot_y))
119
+ presets["Dubbele funnel (solide)"] = obs2
120
  return presets
121
 
122
  PRESETS = {
 
130
  DEFAULT_PARAMS = dict(
131
  desired_speed=1.3, relax_time=0.6,
132
  people_repulsion=4.0, people_range=1.2,
133
+ obstacle_repulsion=10.0, # iets sterker langs wanden
134
+ obstacle_range=1.2, # iets verder van tevoren afremmen
135
+ noise=0.07, bounce_walls=True,
136
  )
137
 
138
 
 
168
 
169
 
170
  # ---------------------------
171
+ # Swept collision utils (segment vs axis-aligned rect)
172
  # ---------------------------
173
+ def segment_rect_intersection(p0: np.ndarray, p1: np.ndarray, ob: RectObstacle):
174
+ """
175
+ Liang-Barsky/slab: parametrize P(t)=p0+t*(p1-p0), t∈[0,1].
176
+ Return (hit:bool, t_enter:float, normal:np.ndarray) for first contact; normal is outward rect normal.
177
+ """
178
+ dirv = p1 - p0
179
+ tmin, tmax = 0.0, 1.0
180
+ normal = np.array([0.0, 0.0], dtype=np.float32)
181
+
182
+ def slab(p, d, slab_min, slab_max, axis):
183
+ nonlocal tmin, tmax, normal
184
+ if abs(d) < 1e-9:
185
+ # parallel; must be within slab to proceed
186
+ if p < slab_min or p > slab_max:
187
+ return False
188
+ return True
189
+ t1 = (slab_min - p) / d
190
+ t2 = (slab_max - p) / d
191
+ n1 = np.array([0,0], dtype=np.float32); n2 = np.array([0,0], dtype=np.float32)
192
+ if axis == 0:
193
+ n1 = np.array([-1, 0], dtype=np.float32) # left wall outward
194
+ n2 = np.array([ 1, 0], dtype=np.float32) # right wall
195
+ else:
196
+ n1 = np.array([0,-1], dtype=np.float32) # bottom
197
+ n2 = np.array([0, 1], dtype=np.float32) # top
198
+ if t1 > t2:
199
+ t1, t2 = t2, t1
200
+ n1, n2 = n2, n1
201
+ if t1 > tmin:
202
+ tmin = t1
203
+ normal = n1
204
+ if t2 < tmax:
205
+ tmax = t2
206
+ if tmin > tmax:
207
+ return False
208
+ return True
209
+
210
+ if not slab(p0[0], dirv[0], ob.x, ob.x + ob.w, 0): return (False, None, None)
211
+ if not slab(p0[1], dirv[1], ob.y, ob.y + ob.h, 1): return (False, None, None)
212
+ if tmin < 0.0 or tmin > 1.0:
213
+ return (False, None, None)
214
+ return (True, float(tmin), normal)
215
+
216
 
217
+ # ---------------------------
218
+ # Social-force step (met swept collisions + push-out)
219
+ # ---------------------------
220
+ def social_force_step(agents: List[Agent], world: World, params: dict, dt: float = 0.10, substeps: int = 2) -> None:
221
+ """
222
+ Kleine dt + substeps om nauwkeuriger rond randen te bewegen.
223
+ Swept-collision per substep; na beweging nog iteratief push-out.
224
+ """
225
  desired_speed = params.get("desired_speed", 1.3)
226
  relax_time = params.get("relax_time", 0.6)
227
  people_repulsion = params.get("people_repulsion", 4.0)
228
  people_range = params.get("people_range", 1.2)
229
+ obstacle_repulsion = params.get("obstacle_repulsion", 10.0)
230
+ obstacle_range = params.get("obstacle_range", 1.2)
231
+ noise = params.get("noise", 0.07)
232
  bounce_walls = params.get("bounce_walls", True)
233
 
234
+ h = dt / max(1, substeps)
235
+
236
+ for _ in range(substeps):
237
+ positions = np.array([a.pos for a in agents])
238
+ velocities = np.array([a.vel for a in agents])
239
+ forces = np.zeros_like(positions)
240
+
241
+ # Driving force
242
+ goals = np.array([a.goal for a in agents])
243
+ to_goal = goals - positions
244
+ dist_goal = np.linalg.norm(to_goal, axis=1) + 1e-6
245
+ desired_dir = (to_goal.T / dist_goal).T
246
+ desired_vel = desired_speed * desired_dir
247
+ forces += (desired_vel - velocities) / max(1e-6, relax_time)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
248
 
249
+ # Repulsie mensen
250
+ for i in range(len(agents)):
251
+ diff = positions[i] - positions
252
+ d = np.linalg.norm(diff, axis=1) + 1e-6
253
+ mask = (d > 0) & (d < people_range * 3.0)
254
+ if np.any(mask):
255
+ dir_ij = (diff[mask].T / d[mask]).T
256
+ mag = people_repulsion * np.exp(-d[mask] / max(1e-6, people_range))
257
+ forces[i] += (dir_ij.T * mag).T.sum(axis=0)
258
+
259
+ # Repulsie obstakels (continu)
260
+ for i in range(len(agents)):
261
+ p = positions[i]
262
+ f = np.zeros(2, dtype=np.float32)
263
+ for ob in world.obstacles:
264
+ q = ob.nearest_point(p)
265
+ diff = p - q
266
+ d = np.linalg.norm(diff) + 1e-6
267
+ if d < obstacle_range * 3:
268
+ dir_ = diff / d
269
+ mag = obstacle_repulsion * math.exp(-d / max(1e-6, obstacle_range))
270
+ f += dir_ * mag
271
+ forces[i] += f
272
+
273
+ # Ruis
274
+ forces += noise * np.random.randn(*forces.shape)
275
+
276
+ # Integratie
277
+ new_vel = velocities + h * forces
278
+ speeds = np.linalg.norm(new_vel, axis=1) + 1e-6
279
+ max_speed = desired_speed * 1.8
280
+ new_vel = (new_vel.T * np.minimum(1.0, max_speed / speeds)).T
281
+ trial_pos = positions + h * new_vel # gewenste nieuwe pos voor dit substep
282
+
283
+ # Swept collision per agent
284
+ for i, a in enumerate(agents):
285
+ p0 = a.pos.copy()
286
+ p1 = trial_pos[i].copy()
287
+ v = new_vel[i].copy()
288
+ hit_any = False
289
+ earliest_t = 1.0
290
+ hit_normal = np.array([0.0, 0.0], dtype=np.float32)
291
+ ob_hit = None
292
+
293
+ for ob in world.obstacles:
294
+ hit, t_enter, normal = segment_rect_intersection(p0, p1, ob)
295
+ if hit and t_enter < earliest_t:
296
+ earliest_t = t_enter
297
+ hit_any = True
298
+ hit_normal = normal
299
+ ob_hit = ob
300
+
301
+ if hit_any:
302
+ # Ga tot net vóór impact en reflecteer snelheid over wandnormaal
303
+ eps = 1e-3
304
+ p_hit = p0 + max(0.0, earliest_t - eps) * (p1 - p0)
305
+ # Reflecteer: v' = v - 2*(v·n)*n
306
+ vn = np.dot(v, hit_normal)
307
+ v_ref = v - 2.0 * vn * hit_normal
308
+ # Klein beetje tangentiele demping voor stabiliteit
309
+ v_ref *= 0.6
310
+ a.pos = p_hit
311
+ a.vel = v_ref
312
+ else:
313
+ a.pos = p1
314
+ a.vel = v
315
+
316
+ # World-bounds
317
+ if bounce_walls:
318
+ if a.pos[0] < 0: a.pos[0] = 0; a.vel[0] *= -0.5
319
+ if a.pos[0] > world.width: a.pos[0] = world.width; a.vel[0] *= -0.5
320
+ if a.pos[1] < 0: a.pos[1] = 0; a.vel[1] *= -0.5
321
+ if a.pos[1] > world.height: a.pos[1] = world.height; a.vel[1] *= -0.5
322
+
323
+ # Iteratieve push-out (mocht je eindigen ín een obstakel door numeriek effect)
324
+ for _it in range(4):
325
+ inside = False
326
+ for ob in world.obstacles:
327
+ if ob.contains(a.pos[0], a.pos[1]):
328
+ inside = True
329
+ q = ob.nearest_point(a.pos)
330
+ d = a.pos - q
331
+ nrm = np.linalg.norm(d)
332
+ if nrm < 1e-9:
333
+ d = np.array([0.5, 0], dtype=np.float32)
334
+ nrm = 0.5
335
+ n = d / nrm
336
+ a.pos = q + 0.05 * n
337
+ # demp component naar binnen toe
338
+ vn = np.dot(a.vel, n)
339
+ if vn < 0:
340
+ a.vel = a.vel - vn * n
341
+ break
342
+ if not inside:
343
+ break
344
 
345
 
346
  # ---------------------------
347
+ # Visual metrics & kleuren
348
  # ---------------------------
349
  def k3_distance(point: np.ndarray, all_points: np.ndarray) -> float:
350
  dists = np.linalg.norm(all_points - point, axis=1)
 
370
 
371
 
372
  # ---------------------------
373
+ # Rendering van één frame
374
  # ---------------------------
375
  def render_frame(positions: np.ndarray, velocities: np.ndarray, world: World, params: dict,
376
  trail_positions: List[np.ndarray], show_trails=True, show_heatmap=False,
 
385
  ax.set_xlim(0, world.width); ax.set_ylim(0, world.height)
386
  ax.set_aspect("equal"); ax.set_facecolor("#f9fafb")
387
 
388
+ # Obstakels
389
  for ob in world.obstacles:
390
  rect = plt.Rectangle((ob.x, ob.y), ob.w, ob.h, facecolor="#6b7280", alpha=0.20, edgecolor="#1f2937", linewidth=1.0)
391
  ax.add_patch(rect)
 
447
 
448
 
449
  # ---------------------------
450
+ # Simulatie → frames → GIF
451
  # ---------------------------
452
  def simulate_states(n_agents: int, steps: int, world: World, params: dict, layout: str):
453
  agents = init_agents(n_agents, world, layout)
 
456
  pos = np.array([a.pos.copy() for a in agents])
457
  vel = np.array([a.vel.copy() for a in agents])
458
  states.append({"pos": pos, "vel": vel})
459
+ social_force_step(agents, world, params, dt=0.10, substeps=2)
460
  return states
461
 
462
  def states_to_gif_path(states, world: World, params: dict,
 
464
  performance_mode=True, fps: float = 12.0) -> str:
465
  frames_np = []
466
  trail_positions: List[np.ndarray] = []
467
+ for st in states:
 
468
  trail_positions.append(st["pos"])
469
  if len(trail_positions) > 12: trail_positions.pop(0)
470
  img = render_frame(st["pos"], st["vel"], world, params, trail_positions,
 
474
 
475
  tmp = tempfile.NamedTemporaryFile(delete=False, suffix=".gif")
476
  tmp_path = tmp.name; tmp.close()
 
477
  duration = 1.0 / max(1.0, fps)
478
  imageio.mimsave(tmp_path, frames_np, format="GIF", duration=duration, loop=0)
479
  return tmp_path
 
506
  badge_html = f"<span class='stat-badge'>{len(world.obstacles)} obstakels</span>"
507
  return gif_path, badge_html
508
 
 
509
  def do_autostart(preset, obstacles_json, n_agents, steps, layout,
510
  desired_speed, relax_time, people_repulsion, people_range,
511
  obstacle_repulsion, obstacle_range, noise, bounce,
 
519
  # ---------------------------
520
  # UI
521
  # ---------------------------
522
+ with gr.Blocks(theme=THEME, css=CUSTOM_CSS, title="Crowd Behavior Lab — GIF (solide)") as demo:
523
  gr.Markdown("""
524
  <div id='title' class='card' style='margin-bottom:12px'>
525
  <h1>👥 Crowd Behavior Lab</h1>
 
528
  <span class="legend-dot legend-orange" style="margin-left:12px;"></span>Stress
529
  <span class="legend-dot legend-red" style="margin-left:12px;"></span>Paniek
530
  </div>
531
+ <p style="margin-top:6px">Solide afbakening met swept collisions. Agents blijven binnen de lijnen.</p>
532
  </div>
533
  """)
534
 
535
  with gr.Row(equal_height=False):
536
  with gr.Column(scale=1):
537
  gr.Markdown("### Scène & parameters", elem_classes=["card"])
538
+ preset = gr.Dropdown(list(PRESETS.keys()), value="Flessenhals (taps, solide)", label="Obstakelpreset")
539
  obstacles_json = gr.Textbox(label="Extra obstakels (JSON)", value="[]", lines=4)
540
 
541
  layout = gr.Radio(["Links→Rechts","Rechts→Links","Twee-richtingen","Willekeurig"],
542
  value="Twee-richtingen", label="Stroomrichting")
543
 
544
  with gr.Row():
545
+ n_agents = gr.Slider(5, 200, value=90, step=1, label="Aantal agenten")
546
+ steps = gr.Slider(30, 500, value=220, step=10, label="Simulatiestappen")
547
 
548
  with gr.Accordion("Krachten & gedrag", open=False):
549
  desired_speed = gr.Slider(0.5, 2.5, value=1.3, step=0.05, label="Gewenste snelheid (m/s)")
550
  relax_time = gr.Slider(0.2, 2.0, value=0.6, step=0.05, label="Relaxatietijd")
551
  people_repulsion = gr.Slider(0.0, 10.0, value=4.0, step=0.1, label="Repulsie tussen mensen")
552
  people_range = gr.Slider(0.2, 3.0, value=1.2, step=0.05, label="Interactieradius (mensen)")
553
+ obstacle_repulsion = gr.Slider(0.0, 16.0, value=10.0, step=0.1, label="Repulsie obstakels")
554
+ obstacle_range = gr.Slider(0.2, 3.0, value=1.2, step=0.05, label="Interactieradius (obstakels)")
555
+ noise = gr.Slider(0.0, 0.6, value=0.07, step=0.01, label="Gedragsruis")
556
  bounce = gr.Checkbox(value=True, label="Veerkrachtige muren (bounce)")
557
 
558
  gr.Markdown("### Visualisatie", elem_classes=["card"])
 
568
 
569
  with gr.Column(scale=2):
570
  gr.Markdown("### Geanimeerde simulatie", elem_classes=["card"])
571
+ canvas = gr.Image(label="Simulatie (GIF)", format="png", interactive=False)
 
572
 
573
+ # Autostart
574
  demo.load(
575
  fn=do_autostart,
576
  inputs=[preset, obstacles_json, n_agents, steps, layout,
 
591
  )
592
 
593
  if __name__ == "__main__":
 
594
  demo.launch()