particle_interaction_fixed.py 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395
  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 Particle(AtomicDEVS):
  12. def __init__(self, particle_id, canvas, framerate, spawn_chance, die_chance):
  13. AtomicDEVS.__init__(self, "Particle[%i]" % particle_id)
  14. self.particle_id = particle_id
  15. self.canvas = canvas
  16. self.framerate = framerate
  17. self.spawn_chance = spawn_chance
  18. self.die_chance = die_chance
  19. self.COLLISION_DETECT = self.addInPort("COLLISION_DETECT")
  20. self.POS_OUT = self.addOutPort("POS_OUT")
  21. self.SPAWNER_COMM = self.addOutPort("SPAWNER_COMM")
  22. self.COLOR_OUT = self.addOutPort("COLOR_OUT")
  23. self.r = random.uniform(5.0, 30.0)
  24. self.vel = {'x': 50 + (random.random() * 25.0), 'y': 50 + (random.random() * 25.0)}
  25. x = random.uniform(self.r, self.canvas.width - self.r)
  26. y = random.uniform(self.r, self.canvas.height - self.r)
  27. self.pos = (x, y)
  28. self.curr_state = "initializing" # stateset = ('initializing', 'initialized', 'bouncing', 'spawning', 'selected', 'deleting', 'clicked')
  29. self.color = "red"
  30. self.prev_color = "red"
  31. self.time_passed = 0.0
  32. self.collision_detected = False
  33. self.clicked = False
  34. self.remaining = 0.0
  35. def timeAdvance(self):
  36. if self.curr_state in ("deleting", "clicked"):
  37. return INFINITY
  38. return self.remaining
  39. def outputFnc(self):
  40. output = {}
  41. if self.curr_state in ("bouncing", "spawning"):
  42. output[self.POS_OUT] = (self.particle_id, self.pos, self.r, self.vel)
  43. if self.collision_detected and self.state != "spawning" and random.random() < self.spawn_chance:
  44. output[self.SPAWNER_COMM] = "spawn_particle"
  45. if self.curr_state == "selected" and self.prev_color == self.color:
  46. output[self.POS_OUT] = ("delete", self.particle_id)
  47. if self.color != self.prev_color:
  48. output[self.COLOR_OUT] = (self.particle_id, self.color)
  49. if self.curr_state == "initializing":
  50. output[self.POS_OUT] = ("created", self.particle_id, self.pos, self.r, self.vel)
  51. return output
  52. def intTransition(self):
  53. if self.curr_state == "initializing":
  54. self.curr_state = "initialized"
  55. self.remaining = 0.0
  56. elif self.curr_state == "initialized":
  57. self.curr_state = "bouncing"
  58. self.remaining = self.framerate
  59. elif self.curr_state == "selected" and self.prev_color == self.color:
  60. self.curr_state = "deleting"
  61. self.remaining = 0.0
  62. elif self.curr_state == "selected":
  63. self.prev_color = self.color
  64. self.remaining = 2.5
  65. elif self.prev_color != self.color:
  66. self.prev_color = self.color
  67. self.remaining = 0.0
  68. elif self.clicked and self.color == "orange":
  69. self.curr_state = "clicked"
  70. elif random.random() < self.die_chance: #and not self.curr_state == "bouncing":
  71. self.color = "yellow"
  72. self.curr_state = "selected"
  73. self.remaining = 0.0
  74. else:
  75. if self.time_passed < 1 and self.curr_state in ("bouncing", "spawning"):
  76. self.time_passed += self.framerate
  77. if self.time_passed >= 1:
  78. self.color = "black"
  79. self.curr_state = "bouncing"
  80. self.time_passed = 0
  81. x = self.pos[0]
  82. y = self.pos[1]
  83. if (x - self.r) <= 0 or x + self.r >= self.canvas.width:
  84. self.vel['x'] = -self.vel['x']
  85. if (y - self.r) <= 0 or y + self.r >= self.canvas.height:
  86. self.vel['y'] = -self.vel['y']
  87. self.pos = (self.pos[0] + (self.vel['x'] * self.framerate), self.pos[1] + (self.vel['y'] * self.framerate))
  88. if self.collision_detected:
  89. self.curr_state = "spawning"
  90. self.color = "blue"
  91. elif self.clicked:
  92. self.color = "orange"
  93. self.remaining = self.framerate
  94. self.collision_detected = False
  95. def extTransition(self, inputs):
  96. self.remaining -= self.elapsed
  97. if self.COLLISION_DETECT in inputs:
  98. if inputs[self.COLLISION_DETECT] == "delete_if_selected":
  99. if self.curr_state == "clicked":
  100. self.curr_state = "selected"
  101. self.remaining = 0.0
  102. elif inputs[self.COLLISION_DETECT] == "clicked":
  103. self.clicked = True
  104. elif self.curr_state == "bouncing":
  105. self.collision_detected = True
  106. deltap = inputs[self.COLLISION_DETECT][1]
  107. self.vel = {'x': self.vel['x'] + deltap[0], 'y': self.vel['y'] + deltap[1]}
  108. def modelTransition(self, passed_values):
  109. if self.curr_state == "deleting":
  110. passed_values['to_delete'] = self
  111. return True
  112. elif self.curr_state == "initialized":
  113. passed_values['connect_particle'] = (self.particle_id, self.COLLISION_DETECT)
  114. return True
  115. return False
  116. class ParticleSpawner(AtomicDEVS):
  117. def __init__(self, canvas, framerate, spawn_chance, die_chance):
  118. AtomicDEVS.__init__(self, "ParticleSpawner")
  119. self.canvas = canvas
  120. self.framerate = framerate
  121. self.spawn_chance = spawn_chance
  122. self.die_chance = die_chance
  123. self.REQUEST = self.addInPort("REQUEST")
  124. self.id_ctr = 0
  125. self.new_particle = None
  126. self.remaining = 0.0
  127. def timeAdvance(self):
  128. return self.remaining
  129. def intTransition(self):
  130. self.new_particle = Particle(self.id_ctr, self.canvas, self.framerate, self.spawn_chance, self.die_chance)
  131. self.id_ctr += 1
  132. self.remaining = 1
  133. def extTransition(self, inputs):
  134. self.remaining -= self.elapsed
  135. if self.REQUEST in inputs:
  136. self.new_particle = Particle(self.id_ctr, self.canvas, self.framerate, self.spawn_chance, self.die_chance)
  137. self.id_ctr += 1
  138. def modelTransition(self, passed_values):
  139. if self.new_particle:
  140. passed_values['new_particle'] = self.new_particle
  141. return True
  142. return False
  143. class PositionManager(AtomicDEVS):
  144. def __init__(self, framerate):
  145. AtomicDEVS.__init__(self, "PositionManager")
  146. self.PARTICLES_OUT = {}
  147. self.POS_IN = self.addInPort("POS_IN")
  148. self.INTERRUPT = self.addInPort("INTERRUPT")
  149. self.positions = {}
  150. self.collisions = set([])
  151. self.framerate = framerate
  152. self.remaining = self.framerate
  153. self.curr_state = "detecting"
  154. self.clicked = None
  155. self.delete_selected = False
  156. def timeAdvance(self):
  157. return 0 if self.curr_state == "detected" else self.remaining
  158. def outputFnc(self):
  159. output = {}
  160. if self.collisions:
  161. output = {self.PARTICLES_OUT[particle_id]: ("collision_detected", deltap) for (particle_id, deltap) in self.collisions}
  162. if self.clicked is not None:
  163. output[self.PARTICLES_OUT[self.clicked]] = "clicked"
  164. if self.delete_selected:
  165. output = {self.PARTICLES_OUT[particle_id]: "delete_if_selected" for particle_id in self.positions}
  166. return output
  167. def intTransition(self):
  168. if self.clicked is not None:
  169. self.clicked = None
  170. self.remaining = self.framerate
  171. elif self.delete_selected:
  172. self.delete_selected = False
  173. self.remaining = self.framerate
  174. elif self.curr_state == "detecting":
  175. for k1, v1 in self.positions.iteritems():
  176. for k2, v2 in self.positions.iteritems():
  177. if k1 != k2:
  178. dx = v2[0][0] - v1[0][0]
  179. dy = v2[0][1] - v1[0][1]
  180. distance = math.sqrt(dx**2 + dy**2)
  181. if (distance < (v1[1] + v2[1])) and (k1 not in self.collisions or k2 not in self.collisions):
  182. u = [dx / distance, dy / distance]
  183. vab = {'x': v2[2]['x'] - v1[2]['x'], 'y': v2[2]['y'] - v1[2]['y']}
  184. vu_mult = vab['x'] * u[0] + vab['y'] * u[1]
  185. vu = [u[0] * vu_mult, u[1] * vu_mult]
  186. deltap_mult = 1
  187. deltap = (vu[0] * deltap_mult, vu[1] * deltap_mult)
  188. self.collisions.add((k1, deltap))
  189. self.collisions.add((k2, (-deltap[0], -deltap[1])))
  190. self.curr_state = "detected"
  191. elif self.curr_state == "detected":
  192. self.collisions = set([])
  193. self.curr_state = "detecting"
  194. self.remaining = self.framerate
  195. def extTransition(self, inputs):
  196. if self.POS_IN in inputs:
  197. self.remaining -= min(self.remaining, self.elapsed)
  198. msg = inputs[self.POS_IN]
  199. if msg[0] == "created":
  200. new_particle_id = msg[1]
  201. self.PARTICLES_OUT[new_particle_id] = self.addOutPort("PARTICLES_OUT[%s]" % new_particle_id)
  202. self.positions[new_particle_id] = (msg[2], msg[3], msg[4])
  203. elif msg[0] == "delete":
  204. del self.PARTICLES_OUT[msg[1]]
  205. del self.positions[msg[1]]
  206. else:
  207. particle_id = msg[0]
  208. r = msg[2]
  209. self.positions[particle_id] = (msg[1], msg[2], msg[3])
  210. elif self.INTERRUPT in inputs:
  211. msg = inputs[self.INTERRUPT][0]
  212. if msg == "delete_selected":
  213. self.delete_selected = True
  214. else:
  215. self.clicked = int(msg)
  216. self.remaining = 0.0
  217. class Field(CoupledDEVS):
  218. def __init__(self, canvas, framerate, spawn_chance, die_chance):
  219. CoupledDEVS.__init__(self, "Field")
  220. self.canvas = canvas
  221. self.framerate = 1.0 / framerate
  222. self.INTERRUPT = self.addInPort("INTERRUPT")
  223. self.POS_OUT = self.addOutPort("POS_OUT")
  224. self.COLOR_OUT = self.addOutPort("COLOR_OUT")
  225. self.particle_spawner = self.addSubModel(ParticleSpawner(self.canvas, self.framerate, spawn_chance, die_chance))
  226. self.position_manager = self.addSubModel(PositionManager(self.framerate))
  227. self.connectPorts(self.INTERRUPT, self.position_manager.INTERRUPT)
  228. def modelTransition(self, passed_values):
  229. if 'new_particle' in passed_values:
  230. new_particle = passed_values['new_particle']
  231. self.addSubModel(new_particle)
  232. self.connectPorts(new_particle.POS_OUT, self.position_manager.POS_IN)
  233. self.connectPorts(new_particle.POS_OUT, self.POS_OUT)
  234. self.connectPorts(new_particle.COLOR_OUT, self.COLOR_OUT)
  235. self.connectPorts(new_particle.SPAWNER_COMM, self.particle_spawner.REQUEST)
  236. del passed_values['new_particle']
  237. if 'to_delete' in passed_values:
  238. self.removeSubModel(passed_values['to_delete'])
  239. del passed_values['to_delete']
  240. if 'connect_particle' in passed_values:
  241. particle_id, COLLISION_DETECT = passed_values['connect_particle']
  242. self.connectPorts(self.position_manager.PARTICLES_OUT[particle_id], COLLISION_DETECT)
  243. del passed_values['connect_particle']
  244. return False
  245. class ParticleVisualization:
  246. def __init__(self, particle_id, circle_id, middle_id, canvas, sim):
  247. self.particle_id = particle_id
  248. self.circle_id = circle_id
  249. self.middle_id = middle_id
  250. self.canvas = canvas
  251. self.sim = sim
  252. self.canvas.tag_bind(self.circle_id, "<Button-1>", self.on_click)
  253. self.canvas.tag_bind(self.middle_id, "<Button-1>", self.on_click)
  254. def on_click(self, event):
  255. self.sim.realtime_interrupt("INTERRUPT %i" % self.particle_id)
  256. # TODO: Model this in SCCD
  257. class Visualizer(tk.Toplevel):
  258. def __init__(self, resolution, framerate, sim):
  259. tk.Toplevel.__init__(self)
  260. self.framerate = int((1.0 / framerate) * 1000)
  261. self.sim = sim
  262. self.geometry('{}x{}'.format(resolution[0], resolution[1]))
  263. CANVAS_SIZE_TUPLE = (0, 0, self.winfo_screenwidth(), self.winfo_screenheight())
  264. self.canvas = tk.Canvas(self, relief=tk.RIDGE, scrollregion=CANVAS_SIZE_TUPLE)
  265. self.canvas.pack(expand = True, fill=tk.BOTH)
  266. self.text = self.canvas.create_text(5, 5, anchor='nw', text="TIME: %s" % 0.0)
  267. self.title('ParticleInteraction')
  268. self.particles = {}
  269. self.protocol("WM_DELETE_WINDOW", lambda: sys.exit(0))
  270. self.bind("<Delete>", self.delete_pressed)
  271. def delete_pressed(self, event):
  272. self.sim.realtime_interrupt("INTERRUPT delete_selected")
  273. def check_output_pos(self, msgs):
  274. for msg_contents in msgs:
  275. if msg_contents[0] == "delete":
  276. particle_id = msg_contents[1]
  277. self.canvas.delete(self.particles[particle_id].circle_id)
  278. self.canvas.delete(self.particles[particle_id].middle_id)
  279. del self.particles[particle_id]
  280. elif msg_contents[0] == "created":
  281. pass
  282. else:
  283. particle_id = msg_contents[0]
  284. x = msg_contents[1][0]
  285. y = msg_contents[1][1]
  286. r = msg_contents[2]
  287. if not particle_id in self.particles:
  288. circle_id = self.canvas.create_oval(x - r, y - r, x + r, y + r, fill="red")
  289. middle_id = self.canvas.create_oval(x - 4, y - 4, x + 4, y + 4, fill="orange")
  290. self.particles[particle_id] = ParticleVisualization(particle_id, circle_id, middle_id, self.canvas, self.sim)
  291. else:
  292. circle_id = self.particles[particle_id].circle_id
  293. middle_id = self.particles[particle_id].middle_id
  294. curr_pos = self.canvas.coords(circle_id)
  295. self.canvas.move(circle_id, x - r - curr_pos[0], y - r - curr_pos[1])
  296. curr_pos_middle = self.canvas.coords(middle_id)
  297. self.canvas.move(middle_id, x - 4 - curr_pos_middle[0], y - 4 - curr_pos_middle[1])
  298. def check_output_color(self, msgs):
  299. for msg_contents in msgs:
  300. particle_id = msg_contents[0]
  301. circle_id = self.particles[particle_id].circle_id
  302. middle_id = self.particles[particle_id].middle_id
  303. color = msg_contents[1]
  304. self.canvas.itemconfig(circle_id, fill=color)
  305. if __name__ == '__main__':
  306. root = tk.Tk()
  307. root.withdraw()
  308. try:
  309. opts, args = getopt.getopt(sys.argv[1:], "w:h:f:s:d:r:", ["width=", "height=", "framerate=", "spawn_chance=", "die_chance=", "random_seed="])
  310. except getopt.GetoptError, e:
  311. print e
  312. sys.exit(2)
  313. width, height, framerate, spawn_chance, die_chance, random_seed = [None] * 6
  314. for opt, arg in opts:
  315. if opt in ("--width", "-w"):
  316. width = int(arg)
  317. elif opt in ("--height", "-h"):
  318. height = int(arg)
  319. elif opt in ("--framerate", "-f"):
  320. framerate = int(arg)
  321. elif opt in ("--spawn_chance", "-s"):
  322. spawn_chance = float(arg)
  323. elif opt in ("--die_chance", "-d"):
  324. die_chance = float(arg)
  325. elif opt in ("--random_seed", "-r"):
  326. random_seed = int(arg)
  327. resolution = (width or 800, height or 600)
  328. framerate = framerate or 60
  329. spawn_chance = 0.2 if spawn_chance is None else spawn_chance
  330. die_chance = 0.01 if die_chance is None else die_chance
  331. if random_seed:
  332. random.seed(random_seed)
  333. model = Field(Canvas(resolution), framerate, spawn_chance, die_chance)
  334. sim = Simulator(model)
  335. sim.setRealTime(True)
  336. sim.setRealTimeInputFile(None)
  337. sim.setRealTimePorts({'INTERRUPT': model.INTERRUPT})
  338. sim.setRealTimePlatformTk(root)
  339. sim.setDSDEVS(True)
  340. sim.setClassicDEVS(True)
  341. visualizer = Visualizer(resolution, framerate, sim)
  342. sim.setListenPorts(model.POS_OUT, lambda i: visualizer.check_output_pos(i))
  343. sim.setListenPorts(model.COLOR_OUT, lambda i: visualizer.check_output_color(i))
  344. sim.simulate()
  345. root.mainloop()