Coverage for src/ensae_teaching_cs/special/corde.py: 86%
154 statements
« prev ^ index » next coverage.py v7.1.0, created at 2023-04-28 06:23 +0200
« prev ^ index » next coverage.py v7.1.0, created at 2023-04-28 06:23 +0200
1# -*- coding: utf-8 -*-
2"""
3@file
4@brief Simulates a string which is falling but tied by its extremities. See :ref:`l-corde`.
5"""
6import os
7import math
8from pyquickhelper.loghelper import fLOG
9from ..helpers.pygame_helper import wait_event, empty_main_loop
12class Point:
13 """
14 définition d'un point : deux coordonnées et une masse
15 """
16 __slots__ = "x", "y", "m"
18 def __init__(self, x, y, m):
19 """
20 définit un point de la corde, de coordonnées (x,y) et de masse m
21 """
22 self.x, self.y, self.m = float(x), float(y), float(m)
24 def deplace_point(self, dep, dt):
25 """
26 déplace un point, le vecteur de déplacement est dp * dt
27 où dep est aussi un point
28 """
29 self.x += float(dep.x) * dt
30 self.y += float(dep.y) * dt
32 def difference(self, p):
33 """
34 retourne le vecteur qui relie deux points, retourne un point
35 """
36 return Point(p.x - self.x, p.y - self.y, 0)
38 def norme(self):
39 """
40 retourne la norme du vecteur (x,y)
41 """
42 return math.sqrt(self.x * self.x + self.y * self.y)
44 def __str__(self):
45 """
46 afficher le point
47 """
48 return f"(x,y) = ({self.x:4.2f},{self.y:4.2f}) masse {self.m:f}"
51class ObjetMasseReliees:
52 """
53 Définit un objet commun à une corde ou un pendule
54 physiquement représenté comme un ensemble de masses
55 reliées des élastiques.
56 """
58 def __init__(self, nb, p1, p2, m, k, g, f, lo):
59 """
60 @param nb nombre de points
61 @param p1 coordoonnées du premier point (fixe)
62 @param p2 coordoonnées du dernier point (fixe)
63 @param m masse de la corde,
64 répartie entre tous les points
65 @param k raideur de l'élastique
66 @param g intensité de l'apesanteur,
67 valeur positive
68 @param f vitesse de freinage
69 @param lo longueur totale de la corde
70 """
71 x1, y1 = p1[0], p1[1]
72 x2, y2 = p2[0], p2[1]
73 self.list = []
74 self.vitesse = []
75 for i in range(0, nb):
76 x = x1 + float(i) * (x2 - x1) / float(nb - 1)
77 y = y1 + float(i) * (y2 - y1) / float(nb - 1)
78 self.list.append(Point(x, y, float(m) / nb))
79 self.vitesse.append(Point(0, 0, 0))
80 self.k = k * nb
81 self.g = g
82 self.lo = float(lo) / (nb - 1)
83 self.f = f
85 def force_point(self, i):
86 """
87 Calcule les forces qui s'exerce en un point,
88 retourne un point *x, y*.
89 """
90 raise NotImplementedError()
92 def iteration(self, dt):
93 """
94 Calcule les déplacements de chaque point et les met à jour,
95 on ne déplace pas les points situés aux extrémités,
96 retourne la somme des vitesses et des accélérations au carré.
97 """
98 raise NotImplementedError()
101class Corde(ObjetMasseReliees):
102 """
103 Définition d'une corde, une liste de masses reliées
104 par des élastiques et attachées au deux extrémités.
105 """
107 def force_point(self, i):
108 """
109 calcule les forces qui s'exerce en un point, retourne un point x,y
110 """
111 x, y = 0, 0
112 # poids
113 y -= self.g * self.list[i].m
114 # voisin de gauche
115 dxdy = self.list[i].difference(self.list[i - 1])
116 d = dxdy.norme()
117 if d > self.lo:
118 dxdy.x = (d - self.lo) / d * dxdy.x
119 dxdy.y = (d - self.lo) / d * dxdy.y
120 x += self.k * dxdy.x
121 y += self.k * dxdy.y
122 # voisin de droite
123 dxdy = self.list[i].difference(self.list[i + 1])
124 d = dxdy.norme()
125 if d > self.lo:
126 dxdy.x = (d - self.lo) / d * dxdy.x
127 dxdy.y = (d - self.lo) / d * dxdy.y
128 x += self.k * dxdy.x
129 y += self.k * dxdy.y
130 # freinage
131 x += - self.f * self.vitesse[i].x
132 y += - self.f * self.vitesse[i].y
134 return Point(x, y, 0)
136 def iteration(self, dt):
137 """
138 Calcule les déplacements de chaque point et les met à jour,
139 on ne déplace pas les points situés aux extrémités,
140 retourne la somme des vitesses et des accélérations au carré.
141 """
142 force = [Point(0, 0, 0)]
143 for i in range(1, len(self.list) - 1):
144 xy = self.force_point(i)
145 force.append(xy)
146 force.append(Point(0, 0, 0))
148 # déplacement
149 for i in range(1, len(self.list) - 1):
150 self.vitesse[i].deplace_point(force[i], dt)
151 self.list[i].deplace_point(self.vitesse[i], dt)
153 d = 0
154 for f in force:
155 d += self.vitesse[0].x ** 2 + f.x ** 2
156 d += self.vitesse[1].y ** 2 + f.y ** 2
158 return d
161class Pendule(ObjetMasseReliees):
162 """
163 Définition d'un pendule, une liste de masses reliées
164 par des élastiques et attachées à une extrémités.
165 Contribution de *Pascal Grandeau*.
166 """
168 def force_point(self, i):
169 """
170 calcule les forces qui s'exerce en un point, retourne un point x,y
171 """
172 x, y = 0, 0
173 # poids
174 y -= self.g * self.list[i].m
175 # voisin de gauche
176 dxdy = self.list[i].difference(self.list[i - 1])
177 d = dxdy.norme()
178 if d > self.lo:
179 dxdy.x = (d - self.lo) / d * dxdy.x
180 dxdy.y = (d - self.lo) / d * dxdy.y
181 x += self.k * dxdy.x
182 y += self.k * dxdy.y
183 # voisin de droite
184 if i < len(self.list) - 1:
185 dxdy = self.list[i].difference(self.list[i + 1])
186 d = dxdy.norme()
187 if d > self.lo:
188 dxdy.x = (d - self.lo) / d * dxdy.x
189 dxdy.y = (d - self.lo) / d * dxdy.y
190 x += self.k * dxdy.x
191 y += self.k * dxdy.y
192 # freinage
193 x += - self.f * self.vitesse[i].x
194 y += - self.f * self.vitesse[i].y
196 return Point(x, y, 0)
198 def iteration(self, dt):
199 """
200 Calcule les déplacements de chaque point et les met à jour,
201 on ne déplace pas les points situés aux extrémités,
202 retourne la somme des vitesses et des accélérations au carré
203 """
204 force = [Point(0, 0, 0)]
205 for i in range(1, len(self.list)):
206 xy = self.force_point(i)
207 force.append(xy)
208 force.append(Point(0, 0, 0))
210 # déplacement
211 for i in range(1, len(self.list)):
212 self.vitesse[i].deplace_point(force[i], dt)
213 self.list[i].deplace_point(self.vitesse[i], dt)
215 d = 0
216 for _ in force:
217 d += self.vitesse[0].x ** 2 + force[i].x ** 2
218 d += self.vitesse[1].y ** 2 + force[i].y ** 2
220 return d
223def display_masses(corde, screen, pygame):
224 """
225 affichage de la corde à l'aide du module pyagame
226 """
227 y = screen.get_size()[1]
228 color = (0, 0, 0)
229 for p in corde.list:
230 pygame.draw.circle(
231 screen, color, (int(p.x), int(y - p.y)), int(p.m + 1))
232 for i in range(0, len(corde.list) - 1):
233 pygame.draw.line(screen, color,
234 (int(corde.list[i].x), int(y - corde.list[i].y)),
235 (int(corde.list[i + 1].x), int(y - corde.list[i + 1].y)))
238def pygame_simulation(pygame, first_click=False, folder=None,
239 iter=1000, size=(800, 500), nb=10,
240 m=40, k=0.1, g=0.1, f=0.02, dt=0.1, step=10,
241 flags=0, model='corde', fLOG=fLOG):
242 """
243 Simulation graphique.
244 Simule la chute d'une corde suspendue à ces deux extrémités.
246 @param pygame module pygame (avoids importing in this file)
247 @param first_click starts the simulation after a first click
248 @param folder to save the simulation, an image per simulation
249 @param iter number of iterations to run
250 @param fLOG logging function
252 @param nb nombre de points
253 @param m masse de la corde,
254 répartie entre tous les points
255 @param k raideur de l'élastique
256 @param g intensité de l'apesanteur,
257 valeur positive
258 @param f vitesse de freinage
259 @param dt petit temps
260 @param step marche
261 @param flags see `pygame.display.set_mode
262 <https://www.pygame.org/docs/ref/display.html#pygame.display.set_mode>`_
263 @param model ``'corde'`` ou ``'pendule'``
264 @param fLOG logging function
266 La simulation ressemble à ceci dans le cas d'une corde :
268 .. raw:: html
270 <video autoplay="" controls="" loop="" height="400">
271 <source src="http://www.xavierdupre.fr/enseignement/complements/corde.mp4" type="video/mp4" />
272 </video>
274 Ou cela dans le cas d'un pendule :
276 .. raw:: html
278 <video autoplay="" controls="" loop="" height="400">
279 <source src="http://www.xavierdupre.fr/enseignement/complements/pendule.mp4" type="video/mp4" />
280 </video>
282 Pour lancer la simulation::
284 from ensae_teaching_cs.special.corde import pygame_simulation
285 import pygame
286 pygame_simulation(pygame, model='corde')
287 """
288 # création de la corde
289 nb = 10
290 dx = size[0] // 8
291 dy = size[1] // 8
293 if model == 'corde':
294 c = Corde(nb, (dx, size[1] - dy), (size[0] - dx, size[1] - dy),
295 m=m, k=k, g=g, f=f, lo=size[0])
296 elif model == 'pendule':
297 c = Pendule(nb, (size[0] // 2, size[1] - dy), (size[0] - dx, size[1] - dy),
298 m=m, k=k, g=g, f=f, lo=size[0] // 2)
299 else:
300 raise ValueError(f"Model '{model}' is not recognized.")
302 pygame.init()
303 white = 255, 255, 255
304 screen = pygame.display.set_mode(size, flags)
306 # numéro d'itération
307 it = 0
309 images = []
311 # continue tant que dep n'est pas proche de 0
312 dep = len(c.list) * (size[0] * size[0] + size[1] * size[1])
313 while dep > 1e-4 and it < iter:
315 if it % step == 0:
316 if it % (step * 10) == 0:
317 fLOG(f"it={it}/{iter} dep={dep} #{len(images)}")
318 empty_main_loop(pygame)
319 screen.fill(white)
320 display_masses(c, screen, pygame)
321 pygame.display.flip()
323 # on fait une pause dès la première itérations pour voir la corde
324 # dans sa position initiale
325 if it == 0 and first_click:
326 wait_event(pygame)
328 # "
329 # unique instruction ajoutées par rapport à l'énoncé
330 dep = c.iteration(dt)
331 # "
333 # on met à jour l'écran
334 pygame.display.flip()
336 if folder is not None and it % step == 0:
337 images.append((it, screen.copy()))
339 pygame.time.wait(2)
341 # on incrémente le nombre d'itérations
342 it += 1
344 if folder is not None:
345 fLOG("saving images")
346 for it, screen in images:
347 fLOG("saving image:", it)
348 image = os.path.join(folder, "image_%04d.png" % it)
349 pygame.image.save(screen, image)
351 # le programme est terminé, on fait une pause pour voir le résultat final
352 if first_click:
353 wait_event(pygame)