Skip to content

Commit 7bae7cb

Browse files
author
Daniel Precioso, PhD
committed
Refactor Vicsek animation code to store order parameter history and noise mapping; update plots for order parameter and noise diagnostics.
1 parent b270d0c commit 7bae7cb

1 file changed

Lines changed: 119 additions & 107 deletions

File tree

modules/collective-motion/vicsek-animation.qmd

Lines changed: 119 additions & 107 deletions
Original file line numberDiff line numberDiff line change
@@ -81,9 +81,11 @@ def run_simulation(dt: float = 1.0) -> None:
8181

8282
# TODO: initialize the state (xy, theta)
8383

84-
# TODO: store order parameter history
84+
# store order parameter history
85+
ls_order_param = []
8586

86-
# TODO: store explored noise->order mapping
87+
# store explored noise->order mapping
88+
dict_noise = {}
8789

8890
# We stop here for now.
8991
return
@@ -181,24 +183,26 @@ This avoids creating new arrays each frame.
181183
The next plot will show the order parameter $r$ over time.
182184

183185
```{python}
184-
#| label: vicsek-anim-order-plot
186+
#| label: fig-vicsek-anim-order-plot
187+
#| fig-alt: "Example order parameter plot (r vs time)"
185188
#| echo: false
186189
import matplotlib.pyplot as plt
187190
import numpy as np
188191
189-
t = np.arange(100)
190-
r = np.random.uniform(0.0, 1.0, size=100)
191-
r = np.cumprod(r)
192-
r = np.clip(r, 0, 1)
192+
t = np.arange(1000)
193+
r = np.random.uniform(-1, 1, size=len(t))
194+
r = np.cumsum(r)
195+
r = (r - r.min()) / (r.max() - r.min()) # normalize to [0, 1]
193196
194197
plt.plot(t, r)
195198
plt.xlabel("Time")
196199
plt.ylabel("Order parameter (r)")
200+
plt.ylim(0, 1)
197201
plt.grid(True)
198202
plt.show()
199203
```
200204

201-
We will update the line data at each animation frame.
205+
We will update the line data at each animation frame, using the `ls_order_param` list to store the recent history of $r$.
202206

203207
Create an empty line in `ax_order` and set labels/limits.
204208

@@ -233,30 +237,54 @@ def init_order_plot(ax_order: Axes, order_window: int):
233237

234238
## Initialize the Noise Diagnostic Plot
235239

236-
Here we store a dictionary:
240+
In this plot we will track how the order parameter $r$ changes as we explore different noise levels $\eta$ with the slider. Visually, we expect to see a curve that starts near $r=1$ at low noise, and drops towards $r=0$ as noise increases. See @fig-vicsek-anim-noise-plot for an example (using mock data).
241+
242+
```{python}
243+
#| label: fig-vicsek-anim-noise-plot
244+
#| fig-alt: "Example noise diagnostic plot (order parameter vs noise)"
245+
#| echo: false
246+
import matplotlib.pyplot as plt
247+
noise_eta = [0.0, 0.5, 1.0, 2.0, 3.0, 4.0, 5.0]
248+
order_param = [1.0, 0.8, 0.5, 0.2, 0.1, 0.05, 0.01]
249+
plt.plot(noise_eta, order_param, color="red", marker="o", linestyle="--")
250+
plt.xlabel("Noise (eta)")
251+
plt.ylabel("Order param (r)")
252+
plt.ylim(0, 1)
253+
plt.grid(True)
254+
plt.show()
255+
```
256+
257+
To build this plot, we will maintain a dictionary `dict_noise` that maps noise levels to the recent average of the order parameter. Each time we update the animation, we will compute the recent average of $r$ and update this dictionary with the current noise level. Then we will set the line data for the noise plot using the items in this dictionary.
237258

238259
\[
239260
\eta \mapsto \text{(recent average of } r \text{)}
240261
\]
241262

242-
**Task:** initialize an empty line (red markers).
263+
Initialize an empty line (red markers).
243264

244265
```{python}
245266
#| label: vicsek-anim-step5-noise-plot
246267
#| echo: true
247268
248269
def init_noise_plot(ax_noise: Axes):
249270
# TODO: create the line artist
250-
# (line_noise,) = ax_noise.plot([], [], color="red", marker="o", linestyle="--")
251271
252272
# TODO: set limits and labels (match your slider range)
253-
# ax_noise.set_xlim(0, 5)
254-
# ax_noise.set_ylim(0, 1)
255-
# ax_noise.set_xlabel("Noise (eta)")
256-
# ax_noise.set_ylabel("Order param (r)")
257273
258-
# return line_noise
259-
...
274+
# return
275+
```
276+
277+
::: {.callout-tip collapse="true"}
278+
## Solved
279+
280+
```python
281+
def init_noise_plot(ax_noise: Axes):
282+
(line_noise,) = ax_noise.plot([], [], color="red", marker="o", linestyle="--")
283+
ax_noise.set_xlim(0, 5)
284+
ax_noise.set_ylim(0, 1)
285+
ax_noise.set_xlabel("Noise (eta)")
286+
ax_noise.set_ylabel("Order param (r)")
287+
return line_noise
260288
```
261289

262290
---
@@ -268,10 +296,8 @@ This is the core loop:
268296
1. advance the Vicsek dynamics by one step (`vicsek_equations`)
269297
2. update `xy_tail`
270298
3. update the particle artists
271-
4. update order-parameter history + plot
272-
5. update the noise diagnostic
273-
274-
**Task:** implement `update_animation(frame)`.
299+
4. update order-parameter history `ls_order_param` and its plot
300+
5. update the noise diagnostic `dict_noise` and its plot
275301

276302
Use this checklist:
277303

@@ -283,87 +309,84 @@ Use this checklist:
283309
- [ ] compute a *recent average* (e.g. last third of the window)
284310
- [ ] update `dict_noise[noise_eta]` and redraw the noise curve
285311

286-
```{python}
287-
#| label: vicsek-anim-step6-update
288-
#| echo: true
312+
You can follow the structure of the `update_animation` function below, which includes comments for each step.
289313

290-
def make_update_fn(
291-
*,
292-
dt: float,
293-
TAIL_LEN: int,
294-
ORDER_WINDOW: int,
295-
ax_plane: Axes,
296-
plt_particles,
297-
plt_current,
298-
line_order_param,
299-
line_noise,
300-
):
301-
# You will capture state with closures + `nonlocal`.
302-
# This is the simplest pattern in Matplotlib animations.
303-
304-
# TODO: initialize these in run_simulation and pass them in
305-
xy = None
306-
theta = None
307-
xy_tail = None
308-
noise_eta = None
309-
radius_interaction = None
310-
v0 = None
311-
box_size = None
312-
dict_noise = None
313-
ls_order_param = None
314+
```python
315+
def update_animation(frame: int):
316+
nonlocal xy, xy_tail, theta, noise_eta, v0, radius_interaction, box_size, dict_noise, ls_order_param
314317

315-
def update_animation(frame: int):
316-
nonlocal xy, theta, xy_tail, noise_eta, radius_interaction, v0, box_size, dict_noise, ls_order_param
317-
318-
# 1) Vicsek step
319-
# xy, theta = vicsek_equations(
320-
# xy, theta,
321-
# v0=v0, dt=dt,
322-
# radius_interaction=radius_interaction,
323-
# box_size=box_size,
324-
# noise=noise_eta,
325-
# )
326-
327-
# 2) Tail update
328-
# xy_tail = np.roll(xy_tail, shift=-1, axis=2)
329-
# xy_tail[:, :, -1] = xy
330-
331-
# 3) Particle artists
332-
# plt_particles.set_data(xy_tail[0].flatten(), xy_tail[1].flatten())
333-
# plt_current.set_data(xy[0], xy[1])
334-
335-
# 4) Order parameter window + plot
336-
# ls_order_param.append(vicsek_order_parameter(theta))
337-
# ls_order_param = ls_order_param[-ORDER_WINDOW:]
338-
# x_vals = np.arange(len(ls_order_param))
339-
# line_order_param.set_data(x_vals, ls_order_param)
340-
341-
# 5) Noise diagnostic (recent average)
342-
# order_param = np.mean(ls_order_param[-ORDER_WINDOW // 3 :])
343-
# dict_noise[noise_eta] = order_param
344-
# dict_noise = dict(sorted(dict_noise.items()))
345-
# if dict_noise:
346-
# line_noise.set_data(*zip(*dict_noise.items()))
347-
# else:
348-
# line_noise.set_data([], [])
318+
# TODO: advance the Vicsek equations
349319

350-
return (plt_particles, plt_current, line_order_param, line_noise)
320+
# TODO: update the tail tensor
321+
# TODO: update the particle artists with set_data
322+
323+
# TODO: update the order parameter history and plot
324+
325+
# TODO: compute a recent average and update dict_noise
326+
# TODO: update the noise diagnostic plot with set_data
351327

352-
return update_animation
328+
return (plt_particles, plt_current, line_order_param, line_noise)
353329
```
354330

331+
**Tip:** blitting requires you to return artists
332+
If you set `blit=True`, your update function **must** return an iterable of the artists that changed, as shown above. If you forget to return them, the animation will run but the plot will not update (it will look frozen).
333+
334+
Move the function `update_animation` inside `run_simulation` so it can access the state variables with `nonlocal`.
335+
355336
::: {.callout-tip collapse="true"}
356-
### Tip: blitting requires you to return artists
357-
If you set `blit=True`, your update function **must** return an iterable of the artists that changed.
337+
## Solved
338+
339+
```python
340+
def update_animation(frame: int):
341+
nonlocal \
342+
xy, \
343+
xy_tail, \
344+
theta, \
345+
noise_eta, \
346+
v0, \
347+
radius_interaction, \
348+
box_size, \
349+
dict_noise, \
350+
ls_order_param
351+
xy, theta = vicsek_equations(
352+
xy,
353+
theta,
354+
v0=v0,
355+
dt=dt,
356+
radius_interaction=radius_interaction,
357+
box_size=box_size,
358+
noise=noise_eta,
359+
)
360+
361+
# Update tails
362+
xy_tail = np.roll(xy_tail, shift=-1, axis=2)
363+
xy_tail[:, :, -1] = xy
364+
plt_particles.set_data(xy_tail[0].flatten(), xy_tail[1].flatten())
365+
plt_current.set_data(xy[0], xy[1])
366+
367+
# Update order parameter
368+
ls_order_param.append(vicsek_order_parameter(theta))
369+
ls_order_param = ls_order_param[-ORDER_WINDOW:]
370+
x_vals = np.arange(len(ls_order_param))
371+
line_order_param.set_data(x_vals, ls_order_param)
372+
373+
# Average the last ORDER_WINDOW//3 values to get the order parameter (similar to Couzin)
374+
order_param = np.mean(ls_order_param[-ORDER_WINDOW // 3 :])
375+
dict_noise[noise_eta] = order_param
376+
dict_noise = dict(sorted(dict_noise.items()))
377+
if dict_noise:
378+
line_noise.set_data(*zip(*dict_noise.items()))
379+
else:
380+
line_noise.set_data([], [])
381+
return (plt_particles, plt_current, line_order_param, line_noise)
382+
```
358383
:::
359384

360385
---
361386

362-
## Step 7 — Create the animation object
363-
364-
Matplotlib uses `FuncAnimation`.
387+
## Create the Animation Object
365388

366-
**Task:** create:
389+
Matplotlib uses `FuncAnimation`. Inside `run_simulation`, after defining `update_animation`, create the animation object:
367390

368391
```python
369392
ani = animation.FuncAnimation(fig, update_animation, interval=0, blit=True)
@@ -372,28 +395,17 @@ ani = animation.FuncAnimation(fig, update_animation, interval=0, blit=True)
372395
- `interval=0` makes it as fast as your computer can handle.
373396
- You can set a larger interval (e.g. 20 ms) to reduce CPU usage.
374397

375-
```{python}
376-
#| label: vicsek-anim-step7-ani
377-
#| echo: true
378-
379-
def make_animation(fig, update_animation):
380-
# TODO: create FuncAnimation
381-
# ani = animation.FuncAnimation(fig, update_animation, interval=0, blit=True)
382-
# return ani
383-
...
384-
```
385-
386398
---
387399

388-
## Step 8 — Add sliders
400+
## Add Sliders
389401

390402
We want sliders for:
391403

392-
- Number of boids (special: requires reinitializing particles)
393-
- Interaction radius
394-
- Noise
395-
- Speed
396-
- Box size
404+
- Number of boids $N$ (special: requires reinitializing particles).
405+
- Interaction radius $r$.
406+
- Noise $\eta$.
407+
- Speed $v_0$.
408+
- Box size $L$.
397409

398410
### Step 8A — Create slider axes
399411

0 commit comments

Comments
 (0)