Coverage for src/ensae_teaching_cs/special/voisinage_evolution.py: 97%

155 statements  

« 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 Implémente une simulation d'évolution des catégories de population 

5selon un modèle de Schelling. 

6""" 

7import random 

8import copy 

9import os 

10from pyquickhelper.loghelper import noLOG 

11from ..helpers.pygame_helper import wait_event, empty_main_loop 

12 

13 

14def round(r, g, b): 

15 """ 

16 arrondit chaque couleur 

17 """ 

18 return (int(r), int(g), int(b)) 

19 

20 

21class Ville: 

22 """ 

23 Définit une ville qui va évoluer par la suite. 

24 

25 @param colors couleurs vives : simulation sans tenir compte de riches ou pauvres, 

26 seulement regroupement 

27 @param colors_grade simulation en tenant compte des riches, du plus foncé 

28 au plus clair (riches) 

29 """ 

30 colors = {-1: (0, 0, 0), 0: (255, 0, 0), 1: (0, 255, 0), 2: (0, 0, 255), 

31 3: (255, 255, 0), 4: (255, 0, 255), 5: (0, 255, 255)} 

32 

33 colors_grade = {-1: (0, 0, 0), 0: round(131.28918999850276, 137.49288815690971, 51.799520886360227), 

34 1: round(151.28918999850276, 147.49288815690971, 71.799520886360227), 

35 2: round(191.42448385755856, 191.27629208812527, 57.413606761812389), 

36 3: round(190.99311386065693, 133.49749594932979, 41.781926646045072), 

37 4: round(167.25849848112253, 76.347509523120692, 41.289551087323403), 

38 5: round(196.76664713923063, 39.476078890841634, 31.506444053895724) 

39 } 

40 

41 def __init__(self, cote=100, group=3, taille=3, riche=False, th2=1.2, 

42 renouvellement=0.15, delay=1): 

43 """ 

44 constructeur 

45 

46 @param cote côté du carré utilisé pour la simulation 

47 @param group nombre de catégories de gens 

48 @param taille chaque individu regarde ses voisins à *+/-* taille près 

49 @param riche simulation avec riche ou non 

50 @param th2 le voisin le plus pauvre peut être contaminé, 

51 si la différence de classes est importante (`cl1 > cl2 * th2`) 

52 @param renouvellement à chaque itération, une certaine proportion des pâtés sont mis à jour, 

53 cette proportion correspond au renouvellement 

54 @param delay la simulation prend en compte la ville lors des "delay" dernières itérations 

55 

56 On tire au hasard la classe d'un pâté de maison dans un disque de rayon cote. 

57 """ 

58 if cote is None: 

59 pass 

60 else: 

61 self.mat = [[random.randint(0, group - 1) 

62 for i in range(0, cote)] for j in range(0, cote)] 

63 self.group = group 

64 self.taille = taille 

65 self.past = [] 

66 self.th2 = th2 

67 self.riche = riche 

68 self.delay = delay 

69 self.renouvellement = renouvellement 

70 c = len(self.mat) / 2 

71 R = c ** 2 / 4 

72 for i in range(0, len(self.mat)): 

73 for j in range(0, len(self.mat[0])): 

74 d = (i - c) ** 2 + (j - c) ** 2 

75 if d > R: 

76 self.mat[i][j] = -1 

77 

78 def _voisinage(self, i, j, mat): 

79 """ 

80 calcul de la répartition du voisiage 

81 

82 @param i i,j coordonnées 

83 @param j 

84 @param mat matrice 

85 @return dictionnaire { classe:nombre } 

86 """ 

87 d = {} 

88 x1 = max(0, i - self.taille) 

89 y1 = max(0, j - self.taille) 

90 x2 = min(len(self.mat), i + self.taille + 1) 

91 y2 = min(len(self.mat), j + self.taille + 1) 

92 for ii in range(x1, x2): 

93 for jj in range(y1, y2): 

94 c = mat[ii][jj] 

95 if c not in d: 

96 d[c] = 0 

97 d[c] += 1 

98 return d 

99 

100 def evolution(self): 

101 """ 

102 évolution d'une itération à l'autre 

103 

104 @return nb1,nb2 

105 """ 

106 

107 keep = copy.deepcopy(self.mat) 

108 self.past.append(keep) 

109 if len(self.past) > self.delay: 

110 del self.past[:len(self.past) - self.delay] 

111 

112 # def fff(x, c): 

113 # if c not in x: 

114 # return 0 

115 # elif x[c] >= sum(x.values()) * self.th: 

116 # return 1 

117 # else: 

118 # return 0 

119 

120 # on renouvelle une certaine proportion de pâtés (renouvellement) 

121 # tiré au hasard 

122 nb1, nb2 = 0, 0 

123 for n in range(0, int(len(self.mat) ** 2 * self.renouvellement)): 

124 

125 # on tire deux voisins au hasard 

126 i = random.randint(0, len(self.mat) - 1) 

127 j = random.randint(0, len(self.mat) - 1) 

128 k = i + random.randint(-1, 1) 

129 l_ = j + random.randint(-1, 1) 

130 if k == i and l_ == j: 

131 continue 

132 x1 = max(0, k) 

133 y1 = max(0, l_) 

134 x2 = min(len(self.mat) - 1, k) 

135 y2 = min(len(self.mat) - 1, l_) 

136 if x1 != x2 or y1 != y2: 

137 continue 

138 

139 # calcul des deux voisinages 

140 v1 = self._voisinage(i, j, self.mat) 

141 v2 = self._voisinage(k, l_, self.mat) 

142 c = self.mat[i][j] 

143 d = self.mat[k][l_] 

144 

145 # c,d : leurs catégorie 

146 

147 if c >= 0 and d >= 0: 

148 # s'ils sont tous les deux habités 

149 if v1.get(c, 0) < v2.get(c, 0) and v1.get(d, 0) > v2.get(d, 0): 

150 # premier cas: si l'un voisin a plus de voisins qui ressemblent à l'autre 

151 # et réciproquement, ils échangent 

152 self.mat[k][l_] = c 

153 self.mat[i][j] = d 

154 nb1 += 1 

155 elif v1.get(c, 0) > v2.get(d, 0) * self.th2 and (not self.riche or c > d): 

156 # deuxième cas : cas riche, le voisin le plus pauvre peut-être contaminé 

157 # si la différence est importante 

158 self.mat[k][l_] = c 

159 nb2 += 1 

160 elif c == -1: 

161 # celui qui n'est pas habité prend la couleur de l'autre 

162 self.mat[i][j] = d 

163 elif d == -1: 

164 # celui qui n'est pas habité prend la couleur de l'autre 

165 self.mat[k][l_] = c 

166 

167 return nb1, nb2 

168 

169 def count(self): 

170 """ 

171 @return la population 

172 """ 

173 d = {} 

174 for line in self.mat: 

175 for c in line: 

176 if c not in d: 

177 d[c] = 1 

178 else: 

179 d[c] += 1 

180 return d 

181 

182 

183class VilleImage(Ville): 

184 """ 

185 Définit une ville à partir d'une image (donc non aléatoire). 

186 """ 

187 

188 def __init__(self, image, 

189 cote=100, 

190 group=3, 

191 taille=3, 

192 riche=False, 

193 th2=1.2, 

194 renouvellement=0.15, 

195 delay=1): 

196 """ 

197 constructeur 

198 

199 @param image nom d'une image pour définir l'initialisation 

200 @param cote cote du carré utilisé pour la simulation 

201 @param group nombre de catégories de gens 

202 @param taille chaque individu regarde ses voisins à +- taille près 

203 @param riche simulation avec riche ou non 

204 @param th2 le voisin le plus pauvre peut-être contaminé, 

205 si la différence de classes est importante (cl1 > cl2 * th2) 

206 @param renouvellement à chaque itération, une certaine proportion des pâtés sont mis à jour, 

207 cette proportion correspond à renouvellement 

208 @param delay la simulation prend en compte la ville lors des "delay" dernières itérations 

209 

210 On tire au hasard la classe d'un pâté de maison dans un disque de rayon cote. 

211 """ 

212 Ville.__init__(self, cote, group, taille, riche, 

213 th2, renouvellement, delay) 

214 self._initialisation(image) 

215 

216 def _initialisation(self, im): 

217 for i in range(0, len(self.mat)): 

218 for j in range(0, len(self.mat[0])): 

219 p = im.get_at((i, j)) 

220 

221 mins = 1e6 

222 best = None 

223 for k, v in Ville.colors_grade.items(): 

224 s = 0 

225 for z in [0, 1, 2]: 

226 s += (v[z] - p[z]) ** 2 

227 s = s ** 0.5 

228 if s < mins: 

229 mins = s 

230 best = k 

231 self.mat[i][j] = best 

232 

233 

234def display(self, screen, x, pygame): 

235 """ 

236 affichage 

237 @param screen écran 

238 @param x dimension d'un pâté de maison 

239 """ 

240 screen.fill((0, 0, 0)) 

241 if self.riche: 

242 colors = Ville.colors_grade 

243 else: 

244 colors = Ville.colors 

245 for i in range(0, len(self.mat)): 

246 for j in range(0, len(self.mat[i])): 

247 c = colors[self.mat[i][j]] 

248 pygame.draw.rect(screen, c, pygame.Rect(i * x, j * x, x, x)) 

249 

250 

251def pygame_simulation(pygame, first_click=False, folder=None, 

252 x=6, nb=100, group=6, max_iter=150, th2=1.75, 

253 image=None, flags=0, fLOG=noLOG): 

254 """ 

255 Simulation graphique. 

256 Illuste la résolution du puzzle 

257 

258 @param pygame module pygame 

259 @param first_click attend la pression d'un clic de souris avant de commencer 

260 @param folder répertoire où stocker les images de la simulation 

261 @param size taille de l'écran 

262 @param delay delay between two tries 

263 @param x pour l'affichage, taille d'un pâté de maison à l'écran 

264 @param group ... 

265 @param nb taille du carré de la simulation en nombre de pâtés de maisons 

266 @param th2 ... 

267 @param max_iter nombre d'itérations 

268 @param image définition de la ville 

269 @param flags see `pygame.display.set_mode <https://www.pygame.org/docs/ref/display.html#pygame.display.set_mode>`_ 

270 @param fLOG logging function 

271 @return @see cl Ville 

272 

273 La simulation ressemble à ceci : 

274 

275 .. raw:: html 

276 

277 <video autoplay="" controls="" loop="" height="500"> 

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

279 </video> 

280 

281 Pour lancer la simulation:: 

282 

283 from ensae_teaching_cs.special.voisinage_evolution import pygame_simulation 

284 import pygame 

285 pygame_simulation(pygame) 

286 

287 Voir :ref:`l-simulation_voisinage`. 

288 """ 

289 

290 if image is None: 

291 this = os.path.dirname(__file__) 

292 image = os.path.join(this, "paris_today.png") 

293 

294 image = pygame.image.load(image) 

295 image = pygame.transform.scale(image, (100, 100)) 

296 

297 pygame.init() 

298 size = nb * x, nb * x 

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

300 

301 ville = VilleImage(image, nb, group, th2=th2, riche=True) 

302 

303 if first_click and pygame is not None: 

304 wait_event(pygame) 

305 

306 if pygame is not None: 

307 display(ville, screen, x, pygame) 

308 pygame.display.flip() 

309 images = [] 

310 if folder is not None: 

311 images.append(screen.copy()) 

312 

313 fLOG(ville.count()) 

314 for i in range(0, max_iter): 

315 nb = ville.evolution() 

316 fLOG("iteration ", i, " ch ", nb) 

317 if pygame is not None: 

318 if folder is not None: 

319 images.append(screen.copy()) 

320 display(ville, screen, x, pygame) 

321 pygame.display.flip() 

322 empty_main_loop(pygame) 

323 

324 fLOG(ville.count()) 

325 if first_click and pygame is not None: 

326 wait_event(pygame) 

327 

328 if folder is not None and pygame is not None: 

329 images.append(screen.copy()) 

330 

331 if folder is not None: 

332 fLOG("saving images") 

333 for it, screen in enumerate(images): 

334 if it % 10 == 0: 

335 fLOG("saving image:", it) 

336 image = os.path.join(folder, "image_%04d.png" % it) 

337 pygame.image.save(screen, image) 

338 

339 return ville