Skip to content

Commit 7675f4c

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

File tree

3 files changed

+230
-104
lines changed

3 files changed

+230
-104
lines changed

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

Lines changed: 126 additions & 78 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
+++
@@ -44,23 +44,18 @@ on suppose:
4444

4545
## imports
4646

47-
importer les librairies qui vont bien pour cet exercice; on utilisera le mode `ipympl` de `matplotlib`
47+
on pourra utiliser le mode `ipympl` de `matplotlib`
4848

4949
```{code-cell} ipython3
50-
# à vous
51-
```
52-
53-
```{code-cell} ipython3
54-
# prune-cell
5550
import numpy as np
5651
import matplotlib.pyplot as plt
5752
5853
%matplotlib ipympl
5954
```
6055

61-
## initialisation
56+
## initialisation aléatoire
6257

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
58+
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
6459

6560
```{code-cell} ipython3
6661
# les bornes pour le tirage au sort initial
@@ -75,9 +70,11 @@ speed_max = 1.
7570
```{code-cell} ipython3
7671
# votre code
7772
78-
def init_problem(n):
73+
def init_problem(N):
7974
"""
8075
retourne un tuple masses, positions, speeds
76+
de formes resp. (N,) (2, N) (2, N)
77+
tiré au sort dans les bornes définies ci-dessus
8178
"""
8279
return None, None, None
8380
```
@@ -86,20 +83,19 @@ def init_problem(n):
8683
# prune-cell
8784
8885
def init_problem(N):
89-
"""
90-
retourne un tuple masses, positions, speeds
91-
"""
9286
masses = np.random.uniform(0, mass_max, size=N)
9387
X = np.random.uniform(x_min, x_max, N)
9488
Y = np.random.uniform(y_min, y_max, N)
9589
angles = np.random.uniform(0, 2*np.pi, N)
9690
modules = np.random.uniform(0, speed_max, N)
9791
Sx = modules * np.cos(angles)
9892
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-
)
93+
return (
94+
masses,
95+
# reshape(2, -1) is equivalent to (2, N) here
96+
np.concatenate((X, Y)).reshape((2, -1)),
97+
np.concatenate((Sx, Sy)).reshape((2, -1))
98+
)
10399
```
104100

105101
```{code-cell} ipython3
@@ -117,6 +113,30 @@ except:
117113
print("KO")
118114
```
119115

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

122142
à 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 +152,7 @@ pour cela on se propose d'écrire la fonction suivante
132152
```{code-cell} ipython3
133153
# votre code
134154
135-
def forces(positions, masses, G=1.0):
155+
def forces(masses, positions, G=1.0):
136156
"""
137157
returns an array of shape (2, N)
138158
that contains the force felt by each mass from all the others
@@ -145,7 +165,7 @@ def forces(positions, masses, G=1.0):
145165
```{code-cell} ipython3
146166
# prune-cell
147167
148-
def forces(positions, masses, G=1.0):
168+
def forces(masses, positions, G=1.0):
149169
150170
# creating an extra dimension so we can do the n-square difference
151171
delta = positions.T[None, :, :] - positions.T[:, None, :] # (N, N, 2)
@@ -165,6 +185,19 @@ def forces(positions, masses, G=1.0):
165185
return forces.T # shape (2, N)
166186
```
167187

188+
```{code-cell} ipython3
189+
# pour tester, voici les valeurs attendues avec la config prédéfinie
190+
191+
masses, positions, speeds = init3()
192+
193+
f = forces(masses, positions)
194+
195+
# should be true
196+
np.all(np.isclose(f, np.array([
197+
[ 0. , -0.12257258, 0.12257258],
198+
[ 0. , -0.02451452, 0.02451452]])))
199+
```
200+
168201
## le simulateur
169202

170203
à 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 +210,7 @@ cela pourrait se passer dans une fonction qui ressemblerait à ceci
177210
def simulate(masses, positions, speeds, dt=0.1, nb_steps=100):
178211
"""
179212
should return the positions across time
180-
so an array of shape (nb_steps, N, 2)
213+
so an array of shape (nb_steps, 2, N)
181214
optional dt is the time step
182215
"""
183216
pass
@@ -189,34 +222,66 @@ def simulate(masses, positions, speeds, dt=0.1, nb_steps=100):
189222
def simulate(masses, positions, speeds, dt=0.1, nb_steps=100):
190223
# initialize result
191224
N = masses.shape[0]
192-
result = np.empty((nb_steps, N, 2))
225+
result = np.empty((nb_steps, 2, N))
193226
# store initial position
194-
result[0] = positions.T
227+
result[0] = positions
195228
196229
# because we have stored the initial state
197230
# we need to start at 1 here, we dont have space for more
198231
for step_counter in range(1, nb_steps):
199-
# compute forces
200-
accelerations = forces(positions, masses) / masses
232+
# compute forces and accelerations
233+
accelerations = forces(masses, positions) / masses
234+
201235
# apply on speeds
202-
speeds = speeds * (1 + dt*accelerations)
236+
speeds += dt*accelerations
237+
203238
# apply on positions
204-
positions = positions * (1 + dt*speeds)
239+
positions += dt*speeds
240+
205241
# store current position
206-
result[step_counter] = positions.T
242+
result[step_counter] = positions
207243
return result
208244
```
209245

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

212-
ne reste plus qu'à dessiner
273+
ne reste plus qu'à dessiner; quelques indices potentiels:
274+
275+
- chaque corps a une couleur; l'appelant peut vous passer un jeu de couleurs, sinon en tirer un au hasard
276+
- pour l'épaisseur de chaque point, on peut imaginer utiliser la masse de l'objet
277+
ou peut-être aussi, à tester, la vitesse de l'objet (plus c'est lent et plus on l'affiche en gros ?)
213278

214279
```{code-cell} ipython3
215280
# votre code
216281
217-
def draw(simulation, colors=None):
282+
def draw(simulation, masses, colors=None):
218283
"""
219-
takes as input the result of simulate() above,
284+
takes as input the result of simulate() above,
220285
and draws the nb_steps positions of each of the N bodies
221286
ideally it should return a matplotlib Axes object
222287
@@ -229,9 +294,9 @@ def draw(simulation, colors=None):
229294
```{code-cell} ipython3
230295
# prune-cell
231296
232-
def draw(simulation, colors=None):
297+
def draw(simulation, masses, colors=None):
233298
234-
N = simulation.shape[1]
299+
nb_steps, _, N = simulation.shape
235300
# use colors if provided, else randomize
236301
colors = (colors if colors is not None
237302
# not too dark
@@ -240,78 +305,61 @@ def draw(simulation, colors=None):
240305
# create a figure and its axes
241306
fig, ax = plt.subplots()
242307
243-
# stay safe
244-
ax.set_aspect("equal")
245-
ax.axis("off")
308+
# not reaslly needed but may come in handy though
309+
# ax.axis("off")
310+
# ax.set_aspect('equal')
246311
247312
248-
nb_steps = simulation.shape[0]
249313
ax.set_title(f"we have {N} bodies over {nb_steps} steps")
250314
for step in range(nb_steps):
251-
ax.scatter(simulation[step, :, 0], simulation[step, :, 1], color=colors, lw=1)
315+
ax.scatter(simulation[step, 0, :], simulation[step, 1, :], c=colors, s=masses)
252316
return ax
253317
```
254318

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

259321
```{code-cell} ipython3
260-
import numpy as np
322+
# for convenience
261323
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
324+
colors3 = np.array([
325+
[32, 32, 32],
326+
(228, 90, 146),
327+
(111, 0, 255),
328+
]) / 255
269329
```
270330

331+
## putting it all together
332+
333+
pour commencer et tester, on se met dans l'état initial reproductible
334+
271335
```{code-cell} ipython3
272336
# décommentez ceci pour tester votre code
273337
274338
# 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
339+
# draw(simulate(masses, positions, speeds), masses, colors)
282340
```
283341

284342
```{code-cell} ipython3
285343
# prune-cell
286344
287-
masses, positions, speeds = init3()
288-
draw(simulate(masses, positions, speeds));
289-
```
290-
291-
```{code-cell} ipython3
292-
# prune-begin
293-
```
294-
295-
```{code-cell} ipython3
296-
N = 3
297-
colors = np.random.uniform(0.3, 1., size=(N, 3))
298-
```
345+
steps = 100
299346
300-
```{code-cell} ipython3
301-
fig, ax = plt.subplots()
302-
303-
for time in range(s.shape[0]):
304-
ax.scatter(s[time,:,0], s[time,:,1],color=colors, lw=1)
305-
```
347+
masses, positions, speeds = init3()
348+
s = simulate(masses, positions, speeds, nb_steps=steps)
349+
draw(s, masses, colors3);
306350
307-
```{code-cell} ipython3
308-
s.shape
351+
plt.savefig("init3.png")
309352
```
310353

311-
```{code-cell} ipython3
312-
s.max()
354+
et avec ces données vous devriez obtenir plus ou moins une sortie de ce genre
355+
```{image} media/init3.png
313356
```
314357

315358
```{code-cell} ipython3
359+
# après vous avez le droit de vous enhardir avec des scénarii plus compliqués
360+
# par exemple
316361
362+
# m5, p5, s5 = init_problem(5)
363+
# sim5 = simulate(m5, p5, s5, nb_steps=100)
364+
# draw(sim5, m5);
317365
```

0 commit comments

Comments
 (0)