particle_interaction.py 20 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484
  1. from pypdevs.DEVS import AtomicDEVS, CoupledDEVS
  2. from pypdevs.simulator import Simulator
  3. from pypdevs.infinity import INFINITY
  4. import random, math, sys, getopt
  5. import Tkinter as tk
  6. from Queue import Empty
  7. class Canvas:
  8. def __init__(self, resolution):
  9. self.width = resolution[0]
  10. self.height = resolution[1]
  11. class ParticleState:
  12. def __init__(self, canvas, particle_id):
  13. self.r = random.uniform(5.0, 30.0)
  14. x = random.uniform(self.r, canvas.width - self.r)
  15. y = random.uniform(self.r, canvas.height - self.r)
  16. self.pos = (x, y)
  17. self.vel = {'x': random.choice([1,-1]) * (50 + (random.random() * 25.0)), 'y': random.choice([1,-1]) * (50 + (random.random() * 25.0))}
  18. self.phase = "initializing" # phases: ('initializing', 'initialized', 'bouncing', 'spawning', 'selected', 'deleting', 'clicked')
  19. self.color = "red"
  20. self.prev_color = "red"
  21. self.collision_detected = False
  22. self.clicked = False
  23. self.frames_passed = 0
  24. self.frames_remaining = 0
  25. self.remaining = 0
  26. self.particle_id = particle_id
  27. class Particle(AtomicDEVS):
  28. def __init__(self, particle_id, canvas, framerate, spawn_chance, die_chance):
  29. AtomicDEVS.__init__(self, "Particle[%i]" % particle_id)
  30. self.particle_id = particle_id
  31. self.canvas = canvas
  32. self.framerate = framerate
  33. self.spawn_chance = 0.0
  34. self.die_chance = 0.0
  35. self.COLLISION_DETECT = self.addInPort("COLLISION_DETECT")
  36. self.POS_OUT = self.addOutPort("POS_OUT")
  37. self.SPAWNER_COMM = self.addOutPort("SPAWNER_COMM")
  38. self.COLOR_OUT = self.addOutPort("COLOR_OUT")
  39. self.state = ParticleState(canvas, particle_id)
  40. def timeAdvance(self):
  41. return self.state.remaining
  42. def outputFnc(self):
  43. output = {}
  44. if self.state.phase in ("bouncing", "spawning", "selected") or self.state.clicked:
  45. output[self.POS_OUT] = (self.particle_id, self.state.pos, self.state.r, self.state.vel)
  46. if self.state.collision_detected and self.state.phase != "spawning" and random.random() < self.spawn_chance:
  47. output[self.SPAWNER_COMM] = "spawn_particle"
  48. if self.state.phase == "selected" and self.state.prev_color == self.state.color:
  49. output[self.POS_OUT] = ("delete", self.particle_id)
  50. if self.state.color != self.state.prev_color:
  51. output[self.COLOR_OUT] = (self.particle_id, self.state.color)
  52. if self.state.phase == "initializing":
  53. output[self.POS_OUT] = ("created", self.particle_id, self.state.pos, self.state.r, self.state.vel)
  54. return output
  55. def intTransition(self):
  56. if self.state.phase == "initializing":
  57. self.state.phase = "initialized"
  58. self.state.remaining = 0
  59. elif self.state.phase == "initialized":
  60. self.state.phase = "bouncing"
  61. self.state.remaining = 1
  62. elif self.state.phase == "selected":
  63. if self.state.prev_color == self.state.color:
  64. self.state.phase = "deleting"
  65. self.state.remaining = INFINITY
  66. else:
  67. self.state.prev_color = self.state.color
  68. self.state.remaining = int(2.5 * self.framerate)
  69. elif self.state.prev_color != self.state.color:
  70. self.state.prev_color = self.state.color
  71. self.state.remaining = 0
  72. elif self.state.phase == "clicked":
  73. self.state.remaining = INFINITY
  74. elif random.random() < self.die_chance:
  75. self.state.vel = {'x': 0, 'y': 0}
  76. self.state.color = "yellow"
  77. self.state.phase = "selected"
  78. self.state.remaining = 0
  79. else:
  80. if self.state.frames_passed < self.framerate and self.state.phase in ("bouncing", "spawning") and self.state.color != "black":
  81. self.state.frames_passed += 1
  82. if self.state.frames_passed >= self.framerate and self.state.phase == "bouncing":
  83. self.state.color = "black"
  84. self.state.phase = "bouncing"
  85. self.state.frames_passed = 0
  86. if self.state.frames_passed >= 1 * self.framerate and self.state.phase == "spawning":
  87. self.state.color = "black"
  88. self.state.phase = "bouncing"
  89. self.state.frames_passed = 0
  90. x = self.state.pos[0]
  91. y = self.state.pos[1]
  92. if (x - self.state.r) <= 0 or x + self.state.r >= self.canvas.width:
  93. self.state.vel['x'] = -self.state.vel['x']
  94. if (y - self.state.r) <= 0 or y + self.state.r >= self.canvas.height:
  95. self.state.vel['y'] = -self.state.vel['y']
  96. self.state.pos = (self.state.pos[0] + (self.state.vel['x'] / self.framerate), self.state.pos[1] + (self.state.vel['y'] / self.framerate))
  97. if self.state.collision_detected:
  98. self.state.phase = "spawning"
  99. self.state.color = "blue"
  100. elif self.state.clicked:
  101. self.state.vel = {'x': 0, 'y': 0}
  102. self.state.phase = "clicked"
  103. self.state.color = "orange"
  104. self.state.clicked = False
  105. self.state.remaining = 1
  106. self.state.collision_detected = False
  107. return self.state
  108. def extTransition(self, inputs):
  109. if self.COLLISION_DETECT in inputs:
  110. if inputs[self.COLLISION_DETECT] == "delete_if_selected":
  111. if self.state.phase == "clicked":
  112. self.state.phase = "selected"
  113. self.state.remaining = 0
  114. elif inputs[self.COLLISION_DETECT] == "clicked":
  115. if self.state.phase == "bouncing":
  116. self.state.clicked = True
  117. self.state.remaining = 0
  118. elif self.state.phase in ("bouncing", "clicked", "deleting"):
  119. self.state.phase = "bouncing"
  120. self.state.collision_detected = True
  121. new_speed = inputs[self.COLLISION_DETECT][1]
  122. self.state.vel = {'x': new_speed[0], 'y': new_speed[1]}
  123. self.state.remaining = 0
  124. else:
  125. self.state.remaining -= self.elapsed
  126. return self.state
  127. def modelTransition(self, passed_values):
  128. if self.state.phase == "deleting":
  129. passed_values['to_delete'] = self
  130. return True
  131. elif self.state.phase == "initialized":
  132. passed_values['connect_particle'] = (self.particle_id, self.COLLISION_DETECT)
  133. return True
  134. return False
  135. class ParticleSpawnerState:
  136. def __init__(self):
  137. self.id_ctr = 0
  138. self.new_particle = False
  139. self.remaining = 0
  140. class ParticleSpawner(AtomicDEVS):
  141. def __init__(self, canvas, framerate, spawn_chance, die_chance):
  142. AtomicDEVS.__init__(self, "ParticleSpawner")
  143. self.canvas = canvas
  144. self.framerate = framerate
  145. self.spawn_chance = spawn_chance
  146. self.die_chance = die_chance
  147. self.REQUEST = self.addInPort("REQUEST")
  148. self.state = ParticleSpawnerState()
  149. def timeAdvance(self):
  150. return self.state.remaining
  151. def intTransition(self):
  152. self.state.new_particle = True
  153. self.state.id_ctr += 1
  154. self.state.remaining = self.framerate
  155. return self.state
  156. def extTransition(self, inputs):
  157. self.state.remaining -= self.elapsed
  158. if self.REQUEST in inputs:
  159. self.state.new_particle = True
  160. self.state.id_ctr += 1
  161. return self.state
  162. def modelTransition(self, passed_values):
  163. if self.state.new_particle:
  164. passed_values['new_particle'] = Particle(self.state.id_ctr, self.canvas, self.framerate, self.spawn_chance, self.die_chance)
  165. return True
  166. return False
  167. class PositionManagerState:
  168. def __init__(self):
  169. self.positions = {}
  170. self.collisions = set([])
  171. self.phase = "detecting"
  172. self.clicked = None
  173. self.delete_selected = False
  174. self.frames = 0
  175. self.remaining = 0
  176. self.PARTICLES_OUT = {}
  177. def __str__(self):
  178. return str((self.phase, self.frames, self.remaining))
  179. class PositionManager(AtomicDEVS):
  180. def __init__(self, framerate):
  181. AtomicDEVS.__init__(self, "PositionManager")
  182. self.framerate = framerate
  183. self.TIME_OUT = self.addOutPort("TIME_OUT")
  184. self.POS_IN = self.addInPort("POS_IN")
  185. self.INTERRUPT = self.addInPort("INTERRUPT")
  186. self.state = PositionManagerState()
  187. def timeAdvance(self):
  188. return self.state.remaining
  189. def outputFnc(self):
  190. output = {}
  191. if self.state.collisions:
  192. output = {self.state.PARTICLES_OUT[particle_id]: ("collision_detected", deltap) for (particle_id, deltap) in self.state.collisions}
  193. if self.state.clicked is not None:
  194. if self.state.clicked in self.state.PARTICLES_OUT:
  195. output[self.state.PARTICLES_OUT[self.state.clicked]] = "clicked"
  196. if self.state.delete_selected:
  197. output = {self.state.PARTICLES_OUT[particle_id]: "delete_if_selected" for particle_id in self.state.positions}
  198. output[self.TIME_OUT] = self.state.frames
  199. return output
  200. def intTransition(self):
  201. self.state.frames += self.timeAdvance()
  202. if self.state.clicked is not None:
  203. self.state.clicked = None
  204. self.state.remaining = 1
  205. elif self.state.delete_selected:
  206. self.state.delete_selected = False
  207. self.state.remaining = 1
  208. elif self.state.phase == "detecting":
  209. for k1, v1 in self.state.positions.iteritems():
  210. for k2, v2 in self.state.positions.iteritems():
  211. if k1 != k2:
  212. dx = v2[0][0] - v1[0][0]
  213. dy = v2[0][1] - v1[0][1]
  214. distance = math.sqrt(dx**2 + dy**2)
  215. if (distance < (v1[1] + v2[1])) and (k1 not in self.state.collisions or k2 not in self.state.collisions):
  216. '''
  217. u = [dx / distance, dy / distance]
  218. vab = {'x': v2[2]['x'] - v1[2]['x'], 'y': v2[2]['y'] - v1[2]['y']}
  219. vu_mult = vab['x'] * u[0] + vab['y'] * u[1]
  220. vu = [u[0] * vu_mult, u[1] * vu_mult]
  221. deltap_mult = 1
  222. deltap = (vu[0] * deltap_mult, vu[1] * deltap_mult)
  223. self.state.collisions.add((k1, deltap))
  224. self.state.collisions.add((k2, (-deltap[0], -deltap[1])))
  225. '''
  226. new_speed_1 = (v2[2]['x'], v2[2]['y'])
  227. new_speed_2 = (v1[2]['x'], v1[2]['y'])
  228. self.state.collisions.add((k1, new_speed_1))
  229. self.state.collisions.add((k2, new_speed_2))
  230. self.state.phase = "detected"
  231. self.state.remaining = 0
  232. elif self.state.phase == "detected":
  233. self.state.collisions = set([])
  234. self.state.phase = "detecting"
  235. self.state.remaining = 1
  236. return self.state
  237. def extTransition(self, inputs):
  238. self.state.remaining -= self.elapsed
  239. self.state.frames += self.elapsed
  240. if self.POS_IN in inputs:
  241. msg = inputs[self.POS_IN]
  242. if msg[0] == "created":
  243. new_particle_id = msg[1]
  244. self.state.PARTICLES_OUT[new_particle_id] = self.addOutPort("PARTICLES_OUT[%s]" % new_particle_id)
  245. self.state.positions[new_particle_id] = (msg[2], msg[3], msg[4])
  246. elif msg[0] == "delete":
  247. del self.state.PARTICLES_OUT[msg[1]]
  248. del self.state.positions[msg[1]]
  249. else:
  250. particle_id = msg[0]
  251. r = msg[2]
  252. self.state.positions[particle_id] = (msg[1], msg[2], msg[3])
  253. # TODO: Fix this, needs to be rounded to integer.
  254. elif self.INTERRUPT in inputs:
  255. msg = inputs[self.INTERRUPT][0]
  256. if msg == "delete_selected":
  257. self.state.delete_selected = True
  258. else:
  259. self.state.clicked = int(msg)
  260. return self.state
  261. class Field(CoupledDEVS):
  262. def __init__(self, canvas, framerate, spawn_chance, die_chance):
  263. CoupledDEVS.__init__(self, "Field")
  264. self.INTERRUPT = self.addInPort("INTERRUPT")
  265. self.POS_OUT = self.addOutPort("POS_OUT")
  266. self.COLOR_OUT = self.addOutPort("COLOR_OUT")
  267. self.TIME_OUT = self.addOutPort("TIME_OUT")
  268. self.particle_spawner = self.addSubModel(ParticleSpawner(canvas, framerate, spawn_chance, die_chance))
  269. self.position_manager = self.addSubModel(PositionManager(framerate))
  270. self.connectPorts(self.INTERRUPT, self.position_manager.INTERRUPT)
  271. self.connectPorts(self.position_manager.TIME_OUT, self.TIME_OUT)
  272. def modelTransition(self, passed_values):
  273. if 'new_particle' in passed_values:
  274. new_particle = passed_values['new_particle']
  275. self.addSubModel(new_particle)
  276. self.connectPorts(new_particle.POS_OUT, self.position_manager.POS_IN)
  277. self.connectPorts(new_particle.POS_OUT, self.POS_OUT)
  278. self.connectPorts(new_particle.COLOR_OUT, self.COLOR_OUT)
  279. self.connectPorts(new_particle.SPAWNER_COMM, self.particle_spawner.REQUEST)
  280. del passed_values['new_particle']
  281. if 'to_delete' in passed_values:
  282. self.removeSubModel(passed_values['to_delete'])
  283. del passed_values['to_delete']
  284. if 'connect_particle' in passed_values:
  285. particle_id, COLLISION_DETECT = passed_values['connect_particle']
  286. self.connectPorts(self.position_manager.state.PARTICLES_OUT[particle_id], COLLISION_DETECT)
  287. del passed_values['connect_particle']
  288. return False
  289. def select(self, imm_children):
  290. pos_mgr, particle_spawner = [None] * 2
  291. for c in imm_children:
  292. if c.getModelName() == "PositionManager":
  293. pos_mgr = c
  294. elif c.getModelName() == "ParticleSpawner":
  295. particle_spawner = c
  296. if particle_spawner:
  297. return particle_spawner
  298. elif pos_mgr:
  299. return imm_children[0] #pos_mgr
  300. else:
  301. return imm_children[0]
  302. '''
  303. class ParticleVisualization:
  304. def __init__(self, particle_id, circle_id, middle_id, canvas, sim):
  305. self.particle_id = particle_id
  306. self.circle_id = circle_id
  307. self.middle_id = middle_id
  308. self.canvas = canvas
  309. self.sim = sim
  310. self.canvas.tag_bind(self.circle_id, "<Button-1>", self.on_click)
  311. self.canvas.tag_bind(self.middle_id, "<Button-1>", self.on_click)
  312. def on_click(self, event):
  313. self.sim.realtime_interrupt("INTERRUPT %i" % self.particle_id)
  314. # TODO: Model this in SCCD
  315. class Visualizer(tk.Toplevel):
  316. def __init__(self, root, resolution, framerate, sim):
  317. tk.Toplevel.__init__(self)
  318. self.framerate = int((1.0 / framerate) * 1000)
  319. self.sim = sim
  320. self.root = root
  321. self.geometry('{}x{}'.format(resolution[0], resolution[1]))
  322. CANVAS_SIZE_TUPLE = (0, 0, self.winfo_screenwidth(), self.winfo_screenheight())
  323. self.canvas = tk.Canvas(self, relief=tk.RIDGE, scrollregion=CANVAS_SIZE_TUPLE)
  324. self.canvas.pack(expand = True, fill=tk.BOTH)
  325. self.text = self.canvas.create_text(5, 5, anchor='nw', text="TIME: %s" % 0.0)
  326. self.particles = {}
  327. self.protocol("WM_DELETE_WINDOW", lambda: self.root.destroy())
  328. self.bind("<Delete>", self.delete_pressed)
  329. self.after(self.framerate, self.check_output)
  330. def delete_pressed(self, event):
  331. self.sim.realtime_interrupt("INTERRUPT delete_selected")
  332. def check_output(self):
  333. while True:
  334. try:
  335. msg = self.sim.model.out_msg_queue.get(False)
  336. self.canvas.itemconfig(self.text, text=msg[0][0])
  337. port = msg[1]
  338. if port == self.sim.model.POS_OUT:
  339. msg_contents = msg[2][0]
  340. if msg_contents[0] == "delete":
  341. particle_id = msg_contents[1]
  342. self.canvas.delete(self.particles[particle_id].circle_id)
  343. self.canvas.delete(self.particles[particle_id].middle_id)
  344. del self.particles[particle_id]
  345. elif msg_contents[0] == "created":
  346. pass
  347. else:
  348. particle_id = msg_contents[0]
  349. x = msg_contents[1][0]
  350. y = msg_contents[1][1]
  351. r = msg_contents[2]
  352. if not particle_id in self.particles:
  353. circle_id = self.canvas.create_oval(x - r, y - r, x + r, y + r, fill="red")
  354. middle_id = self.canvas.create_oval(x - 4, y - 4, x + 4, y + 4, fill="orange")
  355. self.particles[particle_id] = ParticleVisualization(particle_id, circle_id, middle_id, self.canvas, self.sim)
  356. else:
  357. circle_id = self.particles[particle_id].circle_id
  358. middle_id = self.particles[particle_id].middle_id
  359. curr_pos = self.canvas.coords(circle_id)
  360. self.canvas.move(circle_id, x - r - curr_pos[0], y - r - curr_pos[1])
  361. curr_pos_middle = self.canvas.coords(middle_id)
  362. self.canvas.move(middle_id, x - 4 - curr_pos_middle[0], y - 4 - curr_pos_middle[1])
  363. elif port == self.sim.model.COLOR_OUT:
  364. msg_contents = msg[2][0]
  365. particle_id = msg_contents[0]
  366. circle_id = self.particles[particle_id].circle_id
  367. middle_id = self.particles[particle_id].middle_id
  368. color = msg_contents[1]
  369. self.canvas.itemconfig(circle_id, fill=color)
  370. except Empty:
  371. break
  372. self.after(self.framerate, self.check_output)
  373. if __name__ == '__main__':
  374. root = tk.Tk()
  375. root.withdraw()
  376. try:
  377. opts, args = getopt.getopt(sys.argv[1:], "w:h:f:s:d:r:m", ["width=", "height=", "framerate=", "spawn_chance=", "die_chance=", "random_seed=", "main_loop"])
  378. except getopt.GetoptError, e:
  379. print e
  380. sys.exit(2)
  381. width, height, framerate, spawn_chance, die_chance, random_seed, ml = [None] * 7
  382. for opt, arg in opts:
  383. if opt in ("--width", "-w"):
  384. width = int(arg)
  385. elif opt in ("--height", "-h"):
  386. height = int(arg)
  387. elif opt in ("--framerate", "-f"):
  388. framerate = int(arg)
  389. elif opt in ("--spawn_chance", "-s"):
  390. spawn_chance = float(arg)
  391. elif opt in ("--die_chance", "-d"):
  392. die_chance = float(arg)
  393. elif opt in ("--random_seed", "-r"):
  394. random_seed = int(arg)
  395. elif opt in ("--main_loop", "-m"):
  396. ml = True
  397. resolution = (width or 800, height or 600)
  398. framerate = framerate or 60
  399. spawn_chance = 0.2 if spawn_chance is None else spawn_chance
  400. die_chance = 0.01 if die_chance is None else die_chance
  401. if random_seed:
  402. random.seed(random_seed)
  403. model = Field(Canvas(resolution), framerate, spawn_chance, die_chance)
  404. sim = Simulator(model)
  405. sim.setRealTime(True)
  406. sim.setRealTimeInputFile(None)
  407. sim.setRealTimePorts({'INTERRUPT': model.INTERRUPT})
  408. sim.setRealTimePlatformTk(root)
  409. sim.setDSDEVS(True)
  410. sim.setClassicDEVS(True)
  411. sim.setTerminationTime(60)
  412. visualizer = Visualizer(root, resolution, framerate, sim)
  413. visualizer.title('ParticleInteraction %s' % ml)
  414. import cProfile
  415. pr = cProfile.Profile()
  416. pr.enable()
  417. sim.simulate()
  418. if ml:
  419. root.mainloop()
  420. else:
  421. while True:
  422. try:
  423. root.update_idletasks()
  424. root.update()
  425. except:
  426. break
  427. pr.disable()
  428. pr.print_stats(sort='tottime')
  429. '''