devstone.py 22 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530
  1. from abc import ABC, abstractmethod
  2. from collections import defaultdict
  3. from pystone import pystones
  4. from pypdevs.DEVS import AtomicDEVS, CoupledDEVS
  5. from pypdevs.infinity import INFINITY
  6. from pypdevs.simulator import Simulator
  7. from graphviz import Source
  8. class DelayedAtomic(AtomicDEVS):
  9. def __init__(self, name: str, int_delay: float, ext_delay: float, add_out_port: bool = False, prep_time=0.0, mode:str = ""):
  10. super().__init__(name)
  11. self.int_delay = int_delay
  12. self.ext_delay = ext_delay
  13. self.prep_time = prep_time
  14. self.i_in = self.addInPort("i_in")
  15. if add_out_port: # for HI and HO models
  16. self.o_out = self.addOutPort("o_out")
  17. # Dynamic Structure extras
  18. self.mode = mode
  19. self.out_ports = [] # used for dynamic structure model LI2HI
  20. def intTransition(self):
  21. if self.int_delay:
  22. pystones(self.int_delay)
  23. return "passive"
  24. def timeAdvance(self):
  25. if self.state == "active":
  26. return self.prep_time
  27. else:
  28. return INFINITY
  29. def outputFnc(self):
  30. if hasattr(self, "o_out") and self.o_out is not None:
  31. return {self.o_out: [0]}
  32. # for DSDEVS LI2HI
  33. if len(self.out_ports) == 1 and self.out_ports[0].outline != []:
  34. # print(f"{self.name}: should be last call!!")
  35. # print("Send to ", self.out_ports[0].outline)
  36. return {self.out_ports[0]: [0]}
  37. return {}
  38. def extTransition(self, inputs):
  39. if self.ext_delay:
  40. pystones(self.ext_delay)
  41. return "active"
  42. def modelTransition(self, state):
  43. if self.mode == "LI2HI":
  44. return self.LI2HI(state)
  45. elif self.mode == "HI2LI":
  46. return self.HI2LI(state)
  47. return False
  48. def LI2HI(self, state):
  49. if len(self.out_ports) == 0 and not hasattr(self, "o_out"):
  50. # every automic dev gets the extra out port
  51. # even though the last atomic could miss it see fig
  52. # except the atomic model in the coupled model at max depth
  53. # which has a output port by default
  54. state["LI2HI"] = True
  55. state["name"] = self.name
  56. self.out_ports.append(self.addOutPort("o_HI")) # standard LI model doesn't have by default
  57. # print(f"{self.name}: should be first call!!")
  58. return True # send to the parent to connect ports
  59. return False
  60. def HI2LI(self, state):
  61. if hasattr(self, "o_out") and self.o_out is not None:
  62. state["HI2LI"] = True
  63. state["name"] = self.name
  64. self.removePort(self.o_out)
  65. self.o_out = None
  66. return False
  67. class DelayedAtomicStats(DelayedAtomic):
  68. def __init__(self, name: str, int_delay: float, ext_delay: float, add_out_port: bool = False, prep_time=0, mode:str = ""):
  69. super().__init__(name, int_delay, ext_delay, add_out_port, prep_time, mode)
  70. self.int_count = 0
  71. self.ext_count = 0
  72. def intTransition(self):
  73. self.int_count += 1
  74. return super().intTransition()
  75. def extTransition(self, inputs):
  76. self.ext_count += 1
  77. return super().extTransition(inputs)
  78. class DEVStoneWrapper(CoupledDEVS, ABC):
  79. def __init__(self, name: str, depth: int, width: int, int_delay: float, ext_delay: float,
  80. add_atomic_out_ports: bool = False, prep_time=0, stats=False, mode: str = "", dynamic: bool = False):
  81. super().__init__(name)
  82. self.depth = depth
  83. self.width = width
  84. self.int_delay = int_delay
  85. self.ext_delay = ext_delay
  86. self.prep_time = prep_time
  87. self.stats = stats
  88. self.add_atomic_out_ports = add_atomic_out_ports
  89. self.i_in = self.addInPort("i_in")
  90. self.o_out = self.addOutPort("o_out")
  91. self.models = [] # stores all models part of the coupled model
  92. # added to deal with DS DEVS which don't keep it all in the component_set
  93. if depth < 1:
  94. raise ValueError("Invalid depth")
  95. if width < 1:
  96. raise ValueError("Invalid width")
  97. if int_delay < 0:
  98. raise ValueError("Invalid int_delay")
  99. if ext_delay < 0:
  100. raise ValueError("Invalid ext_delay")
  101. if depth == 1:
  102. if self.stats:
  103. atomic = DelayedAtomicStats("Atomic_0_0", int_delay, ext_delay, add_out_port=True, prep_time=1.0, mode=mode)
  104. else:
  105. atomic = DelayedAtomic("Atomic_0_0", int_delay, ext_delay, add_out_port=True, prep_time=1.0, mode=mode)
  106. self.models.append(self.addSubModel(atomic))
  107. self.connectPorts(self.i_in, atomic.i_in)
  108. self.connectPorts(atomic.o_out, self.o_out)
  109. else:
  110. coupled = self.gen_coupled()
  111. self.addSubModel(coupled)
  112. self.connectPorts(self.i_in, coupled.i_in)
  113. self.connectPorts(coupled.o_out, self.o_out)
  114. if dynamic:
  115. return # width will be dynamically generated
  116. # + (idx * 1e-6) to address too many transitions at the same time
  117. # => problem for Classic DEVS only it seems
  118. for idx in range(width - 1):
  119. if self.stats:
  120. atomic = DelayedAtomicStats("Atomic_%d_%d" % (depth - 1, idx), int_delay, ext_delay,
  121. add_out_port=add_atomic_out_ports, prep_time=1.0 + (idx * 1e-6), mode=mode)
  122. else:
  123. atomic = DelayedAtomic("Atomic_%d_%d" % (depth - 1, idx), int_delay, ext_delay,
  124. add_out_port=add_atomic_out_ports, prep_time=1.0 + (idx * 1e-6), mode=mode)
  125. self.models.append(self.addSubModel(atomic))
  126. def extend(self, string: str, width: int):
  127. length = len(string)
  128. return string + " "*(width - length)
  129. def __str__(self):
  130. result = [self.extend(self.name, 16) + "\n", "-" * 20 + "\n"]
  131. first = self.component_set[0]
  132. coupled = ""
  133. if isinstance(first, CoupledDEVS):
  134. result.append("| " + self.extend(first.name, 16) + " |\n")
  135. coupled = "\n\n" + first.__str__()
  136. for model in self.models:
  137. result.append("| " + self.extend(model.name, 16) + " |\n")
  138. result.append("-"*20 + "\n")
  139. result.append(coupled)
  140. return "".join(result)
  141. def dot(self, name) -> None:
  142. result = ("\n\ndigraph {\n\tlayout=dot;"
  143. "\n\tnodesep=0.25;"
  144. "\n\tranksep=0.2;"
  145. "\n\trankdir=LR;"
  146. "\n\tsplines=ortho;\n")
  147. result += f"\tCoupled_{self.depth}[shape=box, label=\"C_{self.depth}\"];\n"
  148. coupled = self.component_set[0]
  149. result += f"\t{coupled.name}[shape=box, label=\"{'C_' + '_'.join(coupled.name.split('_')[1:3])}\"];\n"
  150. for model in self.models:
  151. result += f"\t{model.name}[shape=ellipse, label=\"{'A_' + '_'.join(model.name.split('_')[1:3])}\"];\n"
  152. for connections in self.i_in.outline:
  153. result += f"\tCoupled_{self.depth}:i_in -> {str(connections).split('.')[2]}:{connections.name};\n"
  154. for model in self.models:
  155. if not hasattr(model, "o_out"):
  156. continue
  157. for connections in model.o_out.outline:
  158. result += f"\t{model.name}:o_out -> {str(connections).split('.')[2]}:{connections.name};\n"
  159. result += "}\n"
  160. src = Source(result)
  161. src.view(filename=f'{name}_{self.depth}')
  162. @abstractmethod
  163. def gen_coupled(self):
  164. """ :return a coupled method with i_in and o_out ports"""
  165. pass
  166. class LI(DEVStoneWrapper):
  167. def __init__(self, name: str, depth: int, width: int, int_delay: float, ext_delay: float, prep_time=0, stats=False):
  168. super().__init__(name, depth, width, int_delay, ext_delay, add_atomic_out_ports=False, prep_time=prep_time,
  169. stats=stats)
  170. for idx in range(1, len(self.component_set)):
  171. assert isinstance(self.component_set[idx], AtomicDEVS)
  172. self.connectPorts(self.i_in, self.component_set[idx].i_in)
  173. def gen_coupled(self):
  174. return LI("Coupled_%d" % (self.depth - 1), self.depth - 1, self.width, self.int_delay, self.ext_delay,
  175. prep_time=self.prep_time, stats=self.stats)
  176. class HI(DEVStoneWrapper):
  177. def __init__(self, name: str, depth: int, width: int, int_delay: float, ext_delay: float, prep_time=0, stats=False):
  178. super().__init__(name, depth, width, int_delay, ext_delay, add_atomic_out_ports=True, prep_time=prep_time,
  179. stats=stats)
  180. if len(self.component_set) > 1:
  181. assert isinstance(self.component_set[-1], AtomicDEVS)
  182. self.connectPorts(self.i_in, self.component_set[-1].i_in)
  183. for idx in range(1, len(self.component_set) - 1):
  184. assert isinstance(self.component_set[idx], AtomicDEVS)
  185. self.connectPorts(self.component_set[idx].o_out, self.component_set[idx + 1].i_in)
  186. self.connectPorts(self.i_in, self.component_set[idx].i_in)
  187. def gen_coupled(self):
  188. return HI("Coupled_%d" % (self.depth - 1), self.depth - 1, self.width, self.int_delay, self.ext_delay,
  189. prep_time=self.prep_time, stats=self.stats)
  190. class HO(DEVStoneWrapper):
  191. def __init__(self, name: str, depth: int, width: int, int_delay: float, ext_delay: float, prep_time=0, stats=False):
  192. super().__init__(name, depth, width, int_delay, ext_delay, add_atomic_out_ports=True, prep_time=prep_time,
  193. stats=stats)
  194. self.i_in2 = self.addInPort("i_in2")
  195. self.o_out2 = self.addOutPort("o_out2")
  196. assert len(self.component_set) > 0
  197. if isinstance(self.component_set[0], CoupledDEVS):
  198. self.connectPorts(self.i_in, self.component_set[0].i_in2)
  199. if len(self.component_set) > 1:
  200. assert isinstance(self.component_set[-1], AtomicDEVS)
  201. self.connectPorts(self.i_in2, self.component_set[-1].i_in)
  202. self.connectPorts(self.component_set[-1].o_out, self.o_out2)
  203. for idx in range(1, len(self.component_set) - 1):
  204. assert isinstance(self.component_set[idx], AtomicDEVS)
  205. self.connectPorts(self.component_set[idx].o_out, self.component_set[idx + 1].i_in)
  206. self.connectPorts(self.i_in2, self.component_set[idx].i_in)
  207. self.connectPorts(self.component_set[idx].o_out, self.o_out2)
  208. def gen_coupled(self):
  209. return HO("Coupled_%d" % (self.depth - 1), self.depth - 1, self.width, self.int_delay, self.ext_delay,
  210. prep_time=self.prep_time, stats=self.stats)
  211. class HOmod(CoupledDEVS):
  212. def __init__(self, name: str, depth: int, width: int, int_delay: float, ext_delay: float, prep_time=0, stats=False):
  213. super().__init__(name)
  214. self.depth = depth
  215. self.width = width
  216. self.int_delay = int_delay
  217. self.ext_delay = ext_delay
  218. self.prep_time = prep_time
  219. self.stats = stats
  220. self.i_in = self.addInPort("i_in")
  221. self.i_in2 = self.addInPort("i_in2")
  222. self.o_out = self.addOutPort("o_out")
  223. if depth < 1:
  224. raise ValueError("Invalid depth")
  225. if width < 1:
  226. raise ValueError("Invalid width")
  227. if int_delay < 0:
  228. raise ValueError("Invalid int_delay")
  229. if ext_delay < 0:
  230. raise ValueError("Invalid ext_delay")
  231. if depth == 1:
  232. if self.stats:
  233. atomic = DelayedAtomicStats("Atomic_0_0", int_delay, ext_delay, add_out_port=True, prep_time=prep_time)
  234. else:
  235. atomic = DelayedAtomic("Atomic_0_0", int_delay, ext_delay, add_out_port=True, prep_time=prep_time)
  236. self.addSubModel(atomic)
  237. self.connectPorts(self.i_in, atomic.i_in)
  238. self.connectPorts(atomic.o_out, self.o_out)
  239. else:
  240. coupled = HOmod("Coupled_%d" % (self.depth - 1), self.depth - 1, self.width, self.int_delay, self.ext_delay, prep_time=prep_time, stats=stats)
  241. self.addSubModel(coupled)
  242. self.connectPorts(self.i_in, coupled.i_in)
  243. self.connectPorts(coupled.o_out, self.o_out)
  244. if width >= 2:
  245. atomics = defaultdict(list)
  246. # Generate atomic components
  247. for i in range(width):
  248. min_row_idx = 0 if i < 2 else i - 1
  249. for j in range(min_row_idx, width - 1):
  250. if self.stats:
  251. atomic = DelayedAtomicStats("Atomic_%d_%d_%d" % (depth - 1, i, j), int_delay, ext_delay,
  252. add_out_port=True, prep_time=prep_time)
  253. else:
  254. atomic = DelayedAtomic("Atomic_%d_%d_%d" % (depth - 1, i, j), int_delay, ext_delay,
  255. add_out_port=True, prep_time=prep_time)
  256. self.addSubModel(atomic)
  257. atomics[i].append(atomic)
  258. # Connect EIC
  259. for atomic in atomics[0]:
  260. self.connectPorts(self.i_in2, atomic.i_in)
  261. for i in range(1, width):
  262. atomic_set = atomics[i]
  263. self.connectPorts(self.i_in2, atomic_set[0].i_in)
  264. # Connect IC
  265. for atomic in atomics[0]: # First row to coupled component
  266. self.connectPorts(atomic.o_out, coupled.i_in2)
  267. for i in range(len(atomics[1])): # Second to first rows
  268. for j in range(len(atomics[0])):
  269. self.connectPorts(atomics[1][i].o_out, atomics[0][j].i_in)
  270. for i in range(2, width): # Rest of rows
  271. for j in range(len(atomics[i])):
  272. self.connectPorts(atomics[i][j].o_out, atomics[i - 1][j + 1].i_in)
  273. class LI2HI(DEVStoneWrapper):
  274. """
  275. Dynamic DEVStone variant that starts as an LI
  276. and (gradually??) transforms into an HI model
  277. """
  278. def __init__(self, name: str, depth: int, width: int, int_delay: float, ext_delay: float, prep_time=0, stats=False):
  279. super().__init__(name, depth, width, int_delay, ext_delay, add_atomic_out_ports=False, prep_time=prep_time,
  280. stats=stats, mode="LI2HI")
  281. for idx in range(1, len(self.component_set)):
  282. assert isinstance(self.component_set[idx], AtomicDEVS)
  283. self.connectPorts(self.i_in, self.component_set[idx].i_in)
  284. def gen_coupled(self):
  285. return LI2HI("Coupled_%d" % (self.depth - 1), self.depth - 1, self.width, self.int_delay, self.ext_delay,
  286. prep_time=self.prep_time, stats=self.stats)
  287. def modelTransition(self, state):
  288. if state.get("LI2HI", True):
  289. for i in range(len(self.component_set) - 1): # -1 because the last model can't be connected
  290. # basically checks if it is an atomic model and if it got the extra output port
  291. if hasattr(self.component_set[i], "out_ports") and len(self.component_set[i].out_ports) == 1:
  292. # print(f"Connect {self.component_set[i].name} to {self.component_set[i+1].name}")
  293. self.connectPorts(self.component_set[i].out_ports[0], self.component_set[i + 1].i_in)
  294. return False
  295. return False
  296. class HI2LI(DEVStoneWrapper):
  297. """
  298. Dynamic DEVStone variant that starts as an HI
  299. and (gradually??) transforms into an LI model
  300. """
  301. def __init__(self, name: str, depth: int, width: int, int_delay: float, ext_delay: float, prep_time=0, stats=False):
  302. super().__init__(name, depth, width, int_delay, ext_delay, add_atomic_out_ports=True, prep_time=prep_time,
  303. stats=stats, mode="HI2LI")
  304. if len(self.component_set) > 1:
  305. assert isinstance(self.component_set[-1], AtomicDEVS)
  306. self.connectPorts(self.i_in, self.component_set[-1].i_in)
  307. for idx in range(1, len(self.component_set) - 1):
  308. assert isinstance(self.component_set[idx], AtomicDEVS)
  309. self.connectPorts(self.component_set[idx].o_out, self.component_set[idx + 1].i_in)
  310. self.connectPorts(self.i_in, self.component_set[idx].i_in)
  311. def gen_coupled(self):
  312. return HI2LI("Coupled_%d" % (self.depth - 1), self.depth - 1, self.width, self.int_delay, self.ext_delay,
  313. prep_time=self.prep_time, stats=self.stats)
  314. class DynamicGenerator(AtomicDEVS):
  315. def __init__(self, name: str, period: float = 1.0, repeat: int = 1):
  316. super().__init__(name)
  317. self.period = period
  318. self.repeat = repeat
  319. self._counter = 0
  320. self._active = True
  321. def intTransition(self):
  322. return "passive" # state is unimportant
  323. def timeAdvance(self):
  324. # stop when counter >= repeat
  325. if self._counter >= self.repeat:
  326. return INFINITY
  327. return self.period
  328. def outputFnc(self):
  329. return {}
  330. def modelTransition(self, state):
  331. # every time TA elapses, this will be called on the atomic:
  332. # set a flag the parent coupled can read
  333. # increment counter and request modelTransition (return True)
  334. if self._counter >= self.repeat:
  335. return False
  336. self._counter += 1
  337. state["generate"] = True
  338. return True
  339. class dLI(DEVStoneWrapper):
  340. """
  341. A LI model which grows in width
  342. """
  343. def __init__(self, name: str, depth: int, width: int, int_delay: float, ext_delay: float, prep_time=0, stats=False):
  344. super().__init__(name, depth, width, int_delay, ext_delay, add_atomic_out_ports=False, prep_time=prep_time,
  345. stats=stats, mode="LI-Dynamic", dynamic=True)
  346. for idx in range(1, len(self.component_set)):
  347. assert isinstance(self.component_set[idx], AtomicDEVS)
  348. self.connectPorts(self.i_in, self.component_set[idx].i_in)
  349. self.current_width = 1
  350. self.max_gen = width - 1 # dynamically generate atomic components until width is reached excludes the coupled model
  351. self.generator = self.addSubModel(DynamicGenerator(self.name + "_gen", repeat=self.max_gen))
  352. def gen_coupled(self):
  353. return dLI("Coupled_%d" % (self.depth - 1), self.depth - 1, self.width, self.int_delay, self.ext_delay,
  354. prep_time=self.prep_time, stats=self.stats)
  355. def modelTransition(self, state):
  356. if state.get("generate", True) and self.depth > 1:
  357. name = "Atomic_%d_%d" % (self.depth - 1, self.current_width) if self.depth > 1 else "Atomic_0_%d" % self.current_width
  358. if self.stats:
  359. new_atom = DelayedAtomicStats(name, self.int_delay, self.ext_delay,
  360. add_out_port=self.add_atomic_out_ports, prep_time=self.prep_time, mode="LI-Dynamic")
  361. else:
  362. new_atom = DelayedAtomic(name, self.int_delay, self.ext_delay,
  363. add_out_port=self.add_atomic_out_ports, prep_time=self.prep_time, mode="LI-Dynamic")
  364. self.models.append(self.addSubModel(new_atom))
  365. self.connectPorts(self.i_in, new_atom.i_in)
  366. self.current_width += 1
  367. # print(f"{self.name}: added {new_atom.name} (width is now {self.width})")
  368. return False
  369. return False
  370. class dHI(DEVStoneWrapper):
  371. """
  372. A HI model which grows in width
  373. """
  374. def __init__(self, name: str, depth: int, width: int, int_delay: float, ext_delay: float, prep_time=0, stats=False):
  375. super().__init__(name, depth, width, int_delay, ext_delay, add_atomic_out_ports=True, prep_time=prep_time,
  376. stats=stats, mode="HI-Dynamic", dynamic=True)
  377. if len(self.component_set) > 1:
  378. assert isinstance(self.component_set[-1], AtomicDEVS)
  379. self.connectPorts(self.i_in, self.component_set[-1].i_in)
  380. for idx in range(1, len(self.component_set) - 1):
  381. assert isinstance(self.component_set[idx], AtomicDEVS)
  382. self.connectPorts(self.component_set[idx].o_out, self.component_set[idx + 1].i_in)
  383. self.connectPorts(self.i_in, self.component_set[idx].i_in)
  384. self.current_width = 1
  385. self.max_gen = width - 1 # dynamically generate atomic components until width is reached excludes the coupled model
  386. self.generator = self.addSubModel(DynamicGenerator(self.name + "_gen", repeat=self.max_gen))
  387. def gen_coupled(self):
  388. return dHI("Coupled_%d" % (self.depth - 1), self.depth - 1, self.width, self.int_delay, self.ext_delay,
  389. prep_time=self.prep_time, stats=self.stats)
  390. def modelTransition(self, state):
  391. if state.get("generate", True) and self.depth > 1:
  392. name = "Atomic_%d_%d" % (self.depth - 1, self.current_width) if self.depth > 1 else "Atomic_0_%d" % self.current_width
  393. if self.stats:
  394. new_atom = DelayedAtomicStats(name, self.int_delay, self.ext_delay,
  395. add_out_port=self.add_atomic_out_ports, prep_time=self.prep_time, mode="LI-Dynamic")
  396. else:
  397. new_atom = DelayedAtomic(name, self.int_delay, self.ext_delay,
  398. add_out_port=self.add_atomic_out_ports, prep_time=self.prep_time, mode="LI-Dynamic")
  399. self.models.append(self.addSubModel(new_atom))
  400. self.connectPorts(self.i_in, new_atom.i_in)
  401. if len(self.models) > 1:
  402. prev = self.models[-2]
  403. print("last: ", prev.name)
  404. if isinstance(prev, AtomicDEVS) and hasattr(prev, "o_out"):
  405. self.connectPorts(prev.o_out, new_atom.i_in)
  406. self.current_width += 1
  407. return False
  408. return False
  409. if __name__ == '__main__':
  410. import sys
  411. sys.setrecursionlimit(10000)
  412. root = HOmod("Root", 4, 3, 0, 0)
  413. sim = Simulator(root)
  414. sim.setVerbose(None)
  415. # sim.setTerminationTime(10.0)
  416. sim.setStateSaving("custom")
  417. sim.simulate()