model.py 21 KB

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