Skip to content

Commit 3f1c3fc

Browse files
committed
sanity pass on n-body with tests
1 parent 7c449c6 commit 3f1c3fc

File tree

3 files changed

+304
-100
lines changed

3 files changed

+304
-100
lines changed

notebooks/numpy-tps/n-body/.teacher/README-n-body-corrige-nb.md

Lines changed: 161 additions & 74 deletions
Original file line numberDiff line numberDiff line change
@@ -24,11 +24,11 @@ date: 2025-09-20
2424

2525
pour faire cette activité sur votre ordi localement, {download}`commencez par télécharger le zip<ARTEFACTS-n-body.zip>`
2626

27-
dans ce TP on vous invite à écrire un simulateur de la trajectoire de n corps qui interagissent entre eux au travers de leur poids, pour produire des sorties de ce genre
27+
dans ce TP on vous invite à écrire un simulateur de la trajectoire de n corps qui interagissent entre eux au travers de leurs masses, pour produire des sorties de ce genre
2828

2929
```{image} media/init3.png
3030
:align: center
31-
:width: 400px
31+
:width: 600px
3232
```
3333

3434
+++
@@ -40,27 +40,27 @@ on suppose:
4040
- chacun est décrit par une masse constante
4141
- et au début du monde chacun possède une position et une vitesse
4242

43+
```{admonition} la 3D
44+
en option on vous proposera, une fois votre code fonctionnel en 2D, de passer à la 3D
45+
ça peut valoir le coup d'anticiper ça dès le premier jet, si vous vous sentez de le faire comme ça
46+
```
47+
4348
+++
4449

4550
## imports
4651

47-
importer les librairies qui vont bien pour cet exercice; on utilisera le mode `ipympl` de `matplotlib`
48-
49-
```{code-cell} ipython3
50-
# à vous
51-
```
52+
on pourra utiliser le mode `ipympl` de `matplotlib`
5253

5354
```{code-cell} ipython3
54-
# prune-cell
5555
import numpy as np
5656
import matplotlib.pyplot as plt
5757
5858
%matplotlib ipympl
5959
```
6060

61-
## initialisation
61+
## initialisation aléatoire
6262

63-
en fixant arbitrairement des limites dans l'espace des positions, des vitesses et des masses, on tire au hasard une configuration de départ pour la simulation
63+
en fixant arbitrairement des limites dans l'espace des positions, des vitesses et des masses, la fonction `init_problem()` tire au hasard une configuration de départ pour la simulation
6464

6565
```{code-cell} ipython3
6666
# les bornes pour le tirage au sort initial
@@ -75,9 +75,11 @@ speed_max = 1.
7575
```{code-cell} ipython3
7676
# votre code
7777
78-
def init_problem(n):
78+
def init_problem(N):
7979
"""
8080
retourne un tuple masses, positions, speeds
81+
de formes resp. (N,) (2, N) (2, N)
82+
tiré au sort dans les bornes définies ci-dessus
8183
"""
8284
return None, None, None
8385
```
@@ -86,20 +88,19 @@ def init_problem(n):
8688
# prune-cell
8789
8890
def init_problem(N):
89-
"""
90-
retourne un tuple masses, positions, speeds
91-
"""
9291
masses = np.random.uniform(0, mass_max, size=N)
9392
X = np.random.uniform(x_min, x_max, N)
9493
Y = np.random.uniform(y_min, y_max, N)
9594
angles = np.random.uniform(0, 2*np.pi, N)
9695
modules = np.random.uniform(0, speed_max, N)
9796
Sx = modules * np.cos(angles)
9897
Sy = modules * np.sin(angles)
99-
return (masses,
100-
np.concatenate((X, Y)).reshape((2, -1)),
101-
np.concatenate((Sx, Sy)).reshape((2, -1))
102-
)
98+
return (
99+
masses,
100+
# reshape(2, -1) is equivalent to (2, N) here
101+
np.concatenate((X, Y)).reshape((2, -1)),
102+
np.concatenate((Sx, Sy)).reshape((2, -1))
103+
)
103104
```
104105

105106
```{code-cell} ipython3
@@ -117,6 +118,30 @@ except:
117118
print("KO")
118119
```
119120

121+
## initialisation reproductible
122+
123+
par commodité on vous donne la fonction suivante qui crée 3 objets:
124+
125+
- le premier - pensez au soleil - de masse 3, an centre de la figure, de vitesse nulle
126+
- et deux objets de masse 1, disposés symétriquement autour du soleil
127+
- position initiale (5, 1) et vitesse initiale (-1, 0)
128+
- symétrique en (-5, -1) et vitesse initiale (1, 0)
129+
130+
```{code-cell} ipython3
131+
# for your convenience
132+
133+
def init3():
134+
# first element is sun-like: heavy, at the center, and no speed
135+
masses = np.array([3, 1, 1], dtype=float)
136+
positions = np.array([
137+
[0, 5, -5],
138+
[0, 1, -1]], dtype=float)
139+
speeds = np.array([
140+
[0, -1, 1],
141+
[0, 0, 0]], dtype=float)
142+
return masses, positions, speeds
143+
```
144+
120145
## les forces
121146

122147
à présent, on va écrire un fonction qui va calculer les influences de toutes les particules entre elles, suivant la loi de Newton
@@ -132,7 +157,7 @@ pour cela on se propose d'écrire la fonction suivante
132157
```{code-cell} ipython3
133158
# votre code
134159
135-
def forces(positions, masses, G=1.0):
160+
def forces(masses, positions, G=1.0):
136161
"""
137162
returns an array of shape (2, N)
138163
that contains the force felt by each mass from all the others
@@ -145,7 +170,7 @@ def forces(positions, masses, G=1.0):
145170
```{code-cell} ipython3
146171
# prune-cell
147172
148-
def forces(positions, masses, G=1.0):
173+
def forces(masses, positions, G=1.0):
149174
150175
# creating an extra dimension so we can do the n-square difference
151176
delta = positions.T[None, :, :] - positions.T[:, None, :] # (N, N, 2)
@@ -165,6 +190,19 @@ def forces(positions, masses, G=1.0):
165190
return forces.T # shape (2, N)
166191
```
167192

193+
```{code-cell} ipython3
194+
# pour tester, voici les valeurs attendues avec la config prédéfinie
195+
196+
masses, positions, speeds = init3()
197+
198+
f = forces(masses, positions)
199+
200+
# should be true
201+
np.all(np.isclose(f, np.array([
202+
[ 0. , -0.12257258, 0.12257258],
203+
[ 0. , -0.02451452, 0.02451452]])))
204+
```
205+
168206
## le simulateur
169207

170208
à présent il nous reste à utiliser cette brique de base pour "faire avancer" le modèle depuis son état initial et sur un nombre fixe d'itérations
@@ -177,7 +215,7 @@ cela pourrait se passer dans une fonction qui ressemblerait à ceci
177215
def simulate(masses, positions, speeds, dt=0.1, nb_steps=100):
178216
"""
179217
should return the positions across time
180-
so an array of shape (nb_steps, N, 2)
218+
so an array of shape (nb_steps, 2, N)
181219
optional dt is the time step
182220
"""
183221
pass
@@ -189,34 +227,71 @@ def simulate(masses, positions, speeds, dt=0.1, nb_steps=100):
189227
def simulate(masses, positions, speeds, dt=0.1, nb_steps=100):
190228
# initialize result
191229
N = masses.shape[0]
192-
result = np.empty((nb_steps, N, 2))
230+
result = np.empty((nb_steps, 2, N))
193231
# store initial position
194-
result[0] = positions.T
232+
result[0] = positions
195233
196234
# because we have stored the initial state
197235
# we need to start at 1 here, we dont have space for more
198236
for step_counter in range(1, nb_steps):
199-
# compute forces
200-
accelerations = forces(positions, masses) / masses
237+
# compute forces and accelerations
238+
accelerations = forces(masses, positions) / masses
239+
201240
# apply on speeds
202-
speeds = speeds * (1 + dt*accelerations)
241+
speeds += dt*accelerations
242+
203243
# apply on positions
204-
positions = positions * (1 + dt*speeds)
244+
positions += dt*speeds
245+
205246
# store current position
206-
result[step_counter] = positions.T
247+
result[step_counter] = positions
207248
return result
208249
```
209250

251+
```{code-cell} ipython3
252+
# pour tester
253+
254+
SMALL_STEPS = 4
255+
256+
s = simulate(masses, positions, speeds, nb_steps=SMALL_STEPS)
257+
258+
try:
259+
if s.shape == (SMALL_STEPS, 2, 3):
260+
print("shape OK")
261+
except Exception as exc:
262+
print(f"OOPS {type(exc)} {exc}")
263+
```
264+
265+
```{code-cell} ipython3
266+
# pour tester: should be true
267+
268+
positions1 = s[1]
269+
270+
np.all(np.isclose(positions1, np.array([
271+
[ 0. , 4.89877427, -4.89877427],
272+
[ 0. , 0.99975485, -0.99975485]
273+
])))
274+
```
275+
210276
## dessiner
211277

212-
ne reste plus qu'à dessiner
278+
ne reste plus qu'à dessiner; quelques indices potentiels:
279+
280+
- 1. chaque corps a une couleur; l'appelant peut vous passer un jeu de couleurs, sinon en tirer un au hasard
281+
- 2.a pour l'épaisseur de chaque point, on peut imaginer utiliser la masse de l'objet
282+
2.b ou peut-être aussi, à tester, la vitesse de l'objet (plus c'est lent et plus on l'affiche en gros ?)
283+
284+
```{admonition} masses et vitesses ?
285+
j'ai choisi de repasser à `draw()` le tableau des masses à cause de 2.a;
286+
si j'avais voulu implémenter 2.b il faudrait tripoter un peu plus nos interfaces - car en l'état on n'a pas accès aux vitesses pendant la simulation - mais n'hésitez pas à le faire si nécessaire..
287+
```
213288

214289
```{code-cell} ipython3
215290
# votre code
216291
217-
def draw(simulation, colors=None):
292+
def draw(simulation, masses, colors=None):
218293
"""
219-
takes as input the result of simulate() above,
294+
takes as input the result of simulate() above,
220295
and draws the nb_steps positions of each of the N bodies
221296
ideally it should return a matplotlib Axes object
222297
@@ -229,9 +304,9 @@ def draw(simulation, colors=None):
229304
```{code-cell} ipython3
230305
# prune-cell
231306
232-
def draw(simulation, colors=None):
307+
def draw(simulation, masses, colors=None):
233308
234-
N = simulation.shape[1]
309+
nb_steps, _, N = simulation.shape
235310
# use colors if provided, else randomize
236311
colors = (colors if colors is not None
237312
# not too dark
@@ -240,78 +315,90 @@ def draw(simulation, colors=None):
240315
# create a figure and its axes
241316
fig, ax = plt.subplots()
242317
243-
# stay safe
244-
ax.set_aspect("equal")
245-
ax.axis("off")
318+
# not really needed but may come in handy though
319+
# ax.axis("off")
320+
# ax.set_aspect('equal')
246321
247322
248-
nb_steps = simulation.shape[0]
249323
ax.set_title(f"we have {N} bodies over {nb_steps} steps")
250324
for step in range(nb_steps):
251-
ax.scatter(simulation[step, :, 0], simulation[step, :, 1], color=colors, lw=1)
325+
ax.scatter(simulation[step, 0, :], simulation[step, 1, :], c=colors, s=masses)
252326
return ax
253327
```
254328

255-
## putting it all together
256-
257-
on se met dans un état initial reproductible - surtout pour que vous puissiez facilement votre code
329+
## un jeu de couleurs
258330

259331
```{code-cell} ipython3
260-
import numpy as np
332+
# for convenience
261333
262-
def init3():
263-
# N = 3
264-
# the first element is heavy, at the center, and has no speed
265-
masses = np.array([3, 1, 1], dtype=float)
266-
positions = np.array([[0, 5, -5],[0, 1, -1]], dtype=float)
267-
speeds = np.array([[0, -1, 1],[0, 0, 0]], dtype=float)
268-
return masses, positions, speeds
334+
colors3 = np.array([
335+
[32, 32, 32],
336+
(228, 90, 146),
337+
(111, 0, 255),
338+
]) / 255
269339
```
270340

341+
## on assemble le tout
342+
343+
pour commencer et tester, on se met dans l'état initial reproductible
344+
271345
```{code-cell} ipython3
272346
# décommentez ceci pour tester votre code
273347
274348
# masses, positions, speeds = init3()
275-
# s = simulate(masses, positions, speeds)
276-
# print(f"{s.shape=}")
277-
# draw(s);
278-
```
279-
280-
et avec ces données vous devriez obtenir plus ou moins une sortie de ce genre
281-
```{image} media/init3.png
349+
# draw(simulate(masses, positions, speeds), masses, colors)
282350
```
283351

284352
```{code-cell} ipython3
285353
# prune-cell
286354
355+
steps = 100
356+
287357
masses, positions, speeds = init3()
288-
draw(simulate(masses, positions, speeds));
289-
```
358+
s = simulate(masses, positions, speeds, nb_steps=steps)
359+
draw(s, masses, colors3);
290360
291-
```{code-cell} ipython3
292-
# prune-begin
361+
plt.savefig("init3.png")
293362
```
294363

295-
```{code-cell} ipython3
296-
N = 3
297-
colors = np.random.uniform(0.3, 1., size=(N, 3))
364+
et avec ces données vous devriez obtenir plus ou moins une sortie de ce genre
365+
```{image} media/init3.png
298366
```
299367

300368
```{code-cell} ipython3
301-
fig, ax = plt.subplots()
369+
# après vous avez le droit de vous enhardir avec des scénarii plus compliqués
370+
# par exemple
302371
303-
for time in range(s.shape[0]):
304-
ax.scatter(s[time,:,0], s[time,:,1],color=colors, lw=1)
372+
# m5, p5, s5 = init_problem(5)
373+
# sim5 = simulate(m5, p5, s5, nb_steps=200)
374+
# draw(sim5, m5);
305375
```
306376

307-
```{code-cell} ipython3
308-
s.shape
309-
```
377+
***
378+
***
379+
***
310380

311-
```{code-cell} ipython3
312-
s.max()
313-
```
381+
+++
314382

315-
```{code-cell} ipython3
383+
## partie optionnelle
316384

317-
```
385+
+++
386+
387+
### option 1: la 3D
388+
389+
modifiez votre code pour passer à une simulation en 3D
390+
391+
+++
392+
393+
### option 2: un rendu plus interactif
394+
395+
le rendu sous forme de multiple scatter plots donne une idée du résultat mais c'est très améliorable
396+
voyez un peu si vous arrivez à produire un outil un peu plus convivial pour explorer les résultats de manière interactive; avec genre
397+
398+
- une animation qui affiche les points au fur et à mesure du temps
399+
- qu'on peut controler un peu comme une vidéo avec pause / backward / forward
400+
- l'option de laisser la trace du passé
401+
- et si vous avez un code 3d, la possibilité de changer le point de vue de la caméra sur le monde
402+
- etc etc...
403+
404+
pas obligé de rester dans Jupyter Lab hein, il y a plein de technos rigolotes qui savent se décliner sur le web, vous avez l'embarras du choix...

0 commit comments

Comments
 (0)