@@ -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.
181183The 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
186189import matplotlib.pyplot as plt
187190import 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
194197plt.plot(t, r)
195198plt.xlabel("Time")
196199plt.ylabel("Order parameter (r)")
200+ plt.ylim(0, 1)
197201plt.grid(True)
198202plt.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
203207Create 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
248269def 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:
2682961 . advance the Vicsek dynamics by one step (` vicsek_equations ` )
2692972 . update ` xy_tail `
2702983 . 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
276302Use 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
369392ani = 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
390402We 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