Coverage for src/ensae_teaching_cs/special/corde.py: 86%

154 statements  

« prev     ^ index     » next       coverage.py v7.1.0, created at 2023-01-27 05:44 +0100

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 

10 

11 

12class Point: 

13 """ 

14 définition d'un point : deux coordonnées et une masse 

15 """ 

16 __slots__ = "x", "y", "m" 

17 

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) 

23 

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 

31 

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) 

37 

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) 

43 

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}" 

49 

50 

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 """ 

57 

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 

84 

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() 

91 

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() 

99 

100 

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 """ 

106 

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 

133 

134 return Point(x, y, 0) 

135 

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)) 

147 

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) 

152 

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 

157 

158 return d 

159 

160 

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 """ 

167 

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 

195 

196 return Point(x, y, 0) 

197 

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)) 

209 

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) 

214 

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 

219 

220 return d 

221 

222 

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))) 

236 

237 

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. 

245 

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 

251 

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 

265 

266 La simulation ressemble à ceci dans le cas d'une corde : 

267 

268 .. raw:: html 

269 

270 <video autoplay="" controls="" loop="" height="400"> 

271 <source src="http://www.xavierdupre.fr/enseignement/complements/corde.mp4" type="video/mp4" /> 

272 </video> 

273 

274 Ou cela dans le cas d'un pendule : 

275 

276 .. raw:: html 

277 

278 <video autoplay="" controls="" loop="" height="400"> 

279 <source src="http://www.xavierdupre.fr/enseignement/complements/pendule.mp4" type="video/mp4" /> 

280 </video> 

281 

282 Pour lancer la simulation:: 

283 

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 

292 

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.") 

301 

302 pygame.init() 

303 white = 255, 255, 255 

304 screen = pygame.display.set_mode(size, flags) 

305 

306 # numéro d'itération 

307 it = 0 

308 

309 images = [] 

310 

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: 

314 

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() 

322 

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) 

327 

328 # " 

329 # unique instruction ajoutées par rapport à l'énoncé 

330 dep = c.iteration(dt) 

331 # " 

332 

333 # on met à jour l'écran 

334 pygame.display.flip() 

335 

336 if folder is not None and it % step == 0: 

337 images.append((it, screen.copy())) 

338 

339 pygame.time.wait(2) 

340 

341 # on incrémente le nombre d'itérations 

342 it += 1 

343 

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) 

350 

351 # le programme est terminé, on fait une pause pour voir le résultat final 

352 if first_click: 

353 wait_event(pygame)