devstone.py 22 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524
  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 dot(self, name) -> None:
  127. result = ("\n\ndigraph {\n\tlayout=dot;"
  128. "\n\tnodesep=0.25;"
  129. "\n\tranksep=0.2;"
  130. "\n\trankdir=LR;"
  131. "\n\tsplines=ortho;\n")
  132. result += f"\tCoupled_{self.depth}[shape=box, label=\"C_{self.depth}\"];\n"
  133. coupled = self.component_set[0]
  134. result += f"\t{coupled.name}[shape=box, label=\"{'C_' + '_'.join(coupled.name.split('_')[1:3])}\"];\n"
  135. for model in self.models:
  136. result += f"\t{model.name}[shape=ellipse, label=\"{'A_' + '_'.join(model.name.split('_')[1:3])}\"];\n"
  137. for connections in self.i_in.outline:
  138. result += f"\tCoupled_{self.depth}:i_in -> {str(connections).split('.')[-2]}:{connections.name};\n"
  139. for model in self.models:
  140. if not hasattr(model, "o_out"):
  141. continue
  142. for connections in model.o_out.outline:
  143. result += f"\t{model.name}:o_out -> {str(connections).split('.')[2]}:{connections.name};\n"
  144. result += "}\n"
  145. src = Source(result)
  146. src.view(filename=f'{name}_{self.depth}')
  147. @abstractmethod
  148. def gen_coupled(self):
  149. """ :return a coupled method with i_in and o_out ports"""
  150. pass
  151. class LI(DEVStoneWrapper):
  152. def __init__(self, name: str, depth: int, width: int, int_delay: float, ext_delay: float, prep_time=0, stats=False):
  153. super().__init__(name, depth, width, int_delay, ext_delay, add_atomic_out_ports=False, prep_time=prep_time,
  154. stats=stats)
  155. for idx in range(1, len(self.component_set)):
  156. assert isinstance(self.component_set[idx], AtomicDEVS)
  157. self.connectPorts(self.i_in, self.component_set[idx].i_in)
  158. def gen_coupled(self):
  159. return LI("Coupled_%d" % (self.depth - 1), self.depth - 1, self.width, self.int_delay, self.ext_delay,
  160. prep_time=self.prep_time, stats=self.stats)
  161. class HI(DEVStoneWrapper):
  162. def __init__(self, name: str, depth: int, width: int, int_delay: float, ext_delay: float, prep_time=0, stats=False):
  163. super().__init__(name, depth, width, int_delay, ext_delay, add_atomic_out_ports=True, prep_time=prep_time,
  164. stats=stats)
  165. if len(self.component_set) > 1:
  166. assert isinstance(self.component_set[-1], AtomicDEVS)
  167. self.connectPorts(self.i_in, self.component_set[-1].i_in)
  168. for idx in range(1, len(self.component_set) - 1):
  169. assert isinstance(self.component_set[idx], AtomicDEVS)
  170. self.connectPorts(self.component_set[idx].o_out, self.component_set[idx + 1].i_in)
  171. self.connectPorts(self.i_in, self.component_set[idx].i_in)
  172. def gen_coupled(self):
  173. return HI("Coupled_%d" % (self.depth - 1), self.depth - 1, self.width, self.int_delay, self.ext_delay,
  174. prep_time=self.prep_time, stats=self.stats)
  175. class HO(DEVStoneWrapper):
  176. def __init__(self, name: str, depth: int, width: int, int_delay: float, ext_delay: float, prep_time=0, stats=False):
  177. super().__init__(name, depth, width, int_delay, ext_delay, add_atomic_out_ports=True, prep_time=prep_time,
  178. stats=stats)
  179. self.i_in2 = self.addInPort("i_in2")
  180. self.o_out2 = self.addOutPort("o_out2")
  181. assert len(self.component_set) > 0
  182. if isinstance(self.component_set[0], CoupledDEVS):
  183. self.connectPorts(self.i_in, self.component_set[0].i_in2)
  184. if len(self.component_set) > 1:
  185. assert isinstance(self.component_set[-1], AtomicDEVS)
  186. self.connectPorts(self.i_in2, self.component_set[-1].i_in)
  187. self.connectPorts(self.component_set[-1].o_out, self.o_out2)
  188. for idx in range(1, len(self.component_set) - 1):
  189. assert isinstance(self.component_set[idx], AtomicDEVS)
  190. self.connectPorts(self.component_set[idx].o_out, self.component_set[idx + 1].i_in)
  191. self.connectPorts(self.i_in2, self.component_set[idx].i_in)
  192. self.connectPorts(self.component_set[idx].o_out, self.o_out2)
  193. def gen_coupled(self):
  194. return HO("Coupled_%d" % (self.depth - 1), self.depth - 1, self.width, self.int_delay, self.ext_delay,
  195. prep_time=self.prep_time, stats=self.stats)
  196. class HOmod(CoupledDEVS):
  197. def __init__(self, name: str, depth: int, width: int, int_delay: float, ext_delay: float, prep_time=0, stats=False):
  198. super().__init__(name)
  199. self.depth = depth
  200. self.width = width
  201. self.int_delay = int_delay
  202. self.ext_delay = ext_delay
  203. self.prep_time = prep_time
  204. self.stats = stats
  205. self.i_in = self.addInPort("i_in")
  206. self.i_in2 = self.addInPort("i_in2")
  207. self.o_out = self.addOutPort("o_out")
  208. if depth < 1:
  209. raise ValueError("Invalid depth")
  210. if width < 1:
  211. raise ValueError("Invalid width")
  212. if int_delay < 0:
  213. raise ValueError("Invalid int_delay")
  214. if ext_delay < 0:
  215. raise ValueError("Invalid ext_delay")
  216. if depth == 1:
  217. if self.stats:
  218. atomic = DelayedAtomicStats("Atomic_0_0", int_delay, ext_delay, add_out_port=True, prep_time=prep_time)
  219. else:
  220. atomic = DelayedAtomic("Atomic_0_0", int_delay, ext_delay, add_out_port=True, prep_time=prep_time)
  221. self.addSubModel(atomic)
  222. self.connectPorts(self.i_in, atomic.i_in)
  223. self.connectPorts(atomic.o_out, self.o_out)
  224. else:
  225. coupled = HOmod("Coupled_%d" % (self.depth - 1), self.depth - 1, self.width, self.int_delay, self.ext_delay, prep_time=prep_time, stats=stats)
  226. self.addSubModel(coupled)
  227. self.connectPorts(self.i_in, coupled.i_in)
  228. self.connectPorts(coupled.o_out, self.o_out)
  229. if width >= 2:
  230. atomics = defaultdict(list)
  231. # Generate atomic components
  232. for i in range(width):
  233. min_row_idx = 0 if i < 2 else i - 1
  234. for j in range(min_row_idx, width - 1):
  235. if self.stats:
  236. atomic = DelayedAtomicStats("Atomic_%d_%d_%d" % (depth - 1, i, j), int_delay, ext_delay,
  237. add_out_port=True, prep_time=prep_time)
  238. else:
  239. atomic = DelayedAtomic("Atomic_%d_%d_%d" % (depth - 1, i, j), int_delay, ext_delay,
  240. add_out_port=True, prep_time=prep_time)
  241. self.addSubModel(atomic)
  242. atomics[i].append(atomic)
  243. # Connect EIC
  244. for atomic in atomics[0]:
  245. self.connectPorts(self.i_in2, atomic.i_in)
  246. for i in range(1, width):
  247. atomic_set = atomics[i]
  248. self.connectPorts(self.i_in2, atomic_set[0].i_in)
  249. # Connect IC
  250. for atomic in atomics[0]: # First row to coupled component
  251. self.connectPorts(atomic.o_out, coupled.i_in2)
  252. for i in range(len(atomics[1])): # Second to first rows
  253. for j in range(len(atomics[0])):
  254. self.connectPorts(atomics[1][i].o_out, atomics[0][j].i_in)
  255. for i in range(2, width): # Rest of rows
  256. for j in range(len(atomics[i])):
  257. self.connectPorts(atomics[i][j].o_out, atomics[i - 1][j + 1].i_in)
  258. class LI2HI(DEVStoneWrapper):
  259. """
  260. Dynamic DEVStone variant that starts as an LI
  261. and (gradually??) transforms into an HI model
  262. """
  263. def __init__(self, name: str, depth: int, width: int, int_delay: float, ext_delay: float, prep_time=0, stats=False):
  264. super().__init__(name, depth, width, int_delay, ext_delay, add_atomic_out_ports=False, prep_time=prep_time,
  265. stats=stats, mode="LI2HI")
  266. for idx in range(1, len(self.component_set)):
  267. assert isinstance(self.component_set[idx], AtomicDEVS)
  268. self.connectPorts(self.i_in, self.component_set[idx].i_in)
  269. def gen_coupled(self):
  270. return LI2HI("Coupled_%d" % (self.depth - 1), self.depth - 1, self.width, self.int_delay, self.ext_delay,
  271. prep_time=self.prep_time, stats=self.stats)
  272. def modelTransition(self, state):
  273. if state.get("LI2HI", True):
  274. for i in range(len(self.component_set) - 1): # -1 because the last model can't be connected
  275. # basically checks if it is an atomic model and if it got the extra output port
  276. if hasattr(self.component_set[i], "out_ports") and len(self.component_set[i].out_ports) == 1:
  277. # print(f"Connect {self.component_set[i].name} to {self.component_set[i+1].name}")
  278. self.connectPorts(self.component_set[i].out_ports[0], self.component_set[i + 1].i_in)
  279. return False
  280. return False
  281. class HI2LI(DEVStoneWrapper):
  282. """
  283. Dynamic DEVStone variant that starts as an HI
  284. and (gradually??) transforms into an LI model
  285. """
  286. def __init__(self, name: str, depth: int, width: int, int_delay: float, ext_delay: float, prep_time=0, stats=False):
  287. super().__init__(name, depth, width, int_delay, ext_delay, add_atomic_out_ports=True, prep_time=prep_time,
  288. stats=stats, mode="HI2LI")
  289. if len(self.component_set) > 1:
  290. assert isinstance(self.component_set[-1], AtomicDEVS)
  291. self.connectPorts(self.i_in, self.component_set[-1].i_in)
  292. for idx in range(1, len(self.component_set) - 1):
  293. assert isinstance(self.component_set[idx], AtomicDEVS)
  294. self.connectPorts(self.component_set[idx].o_out, self.component_set[idx + 1].i_in)
  295. self.connectPorts(self.i_in, self.component_set[idx].i_in)
  296. def gen_coupled(self):
  297. return HI2LI("Coupled_%d" % (self.depth - 1), self.depth - 1, self.width, self.int_delay, self.ext_delay,
  298. prep_time=self.prep_time, stats=self.stats)
  299. class DynamicGenerator(AtomicDEVS):
  300. def __init__(self, name: str, period: float = 1.0, repeat: int = 1):
  301. super().__init__(name)
  302. self.period = period
  303. self.repeat = repeat
  304. self._counter = 0
  305. def intTransition(self):
  306. return "passive" # state is unimportant
  307. def timeAdvance(self):
  308. if self._counter >= self.repeat:
  309. return INFINITY
  310. return self.period
  311. def outputFnc(self):
  312. return {}
  313. def modelTransition(self, state):
  314. # every time TA elapses, this will be called on the atomic:
  315. # set a flag the parent coupled can read
  316. # increment counter and request modelTransition (return True)
  317. if self._counter >= self.repeat:
  318. return False
  319. self._counter += 1
  320. state["generate"] = True
  321. return True
  322. class dLI(DEVStoneWrapper):
  323. """
  324. A LI model which grows in width
  325. """
  326. def __init__(self, name: str, depth: int, width: int, int_delay: float, ext_delay: float, generator_mode: str = "uniform", root=True, prep_time=0, stats=False):
  327. super().__init__(name, depth, width, int_delay, ext_delay, add_atomic_out_ports=False, prep_time=prep_time,
  328. stats=stats, mode="LI-Dynamic", dynamic=True)
  329. self.root = root
  330. for idx in range(1, len(self.component_set)):
  331. assert isinstance(self.component_set[idx], AtomicDEVS)
  332. self.connectPorts(self.i_in, self.component_set[idx].i_in)
  333. self.current_width = 1
  334. self.max_gen = width - 1 # dynamically generate atomic components until width is reached excludes the coupled model
  335. if self.depth == 1:
  336. self.generator = self.addSubModel(DynamicGenerator(self.name + "_gen", repeat=self.max_gen))
  337. def gen_coupled(self):
  338. return dLI("Coupled_%d" % (self.depth - 1), self.depth - 1, self.width, self.int_delay, self.ext_delay,
  339. prep_time=self.prep_time, stats=self.stats, root=False)
  340. def modelTransition(self, state):
  341. if state.get("generate", True) and self.depth == 1:
  342. return True
  343. if state.get("generate", True) and self.depth > 1:
  344. name = "Atomic_%d_%d" % (self.depth - 1, self.current_width) if self.depth > 1 else "Atomic_0_%d" % self.current_width
  345. if self.stats:
  346. new_atom = DelayedAtomicStats(name, self.int_delay, self.ext_delay,
  347. add_out_port=self.add_atomic_out_ports, prep_time=self.prep_time, mode="LI-Dynamic")
  348. else:
  349. new_atom = DelayedAtomic(name, self.int_delay, self.ext_delay,
  350. add_out_port=self.add_atomic_out_ports, prep_time=self.prep_time, mode="LI-Dynamic")
  351. self.models.append(self.addSubModel(new_atom))
  352. self.connectPorts(self.i_in, new_atom.i_in)
  353. self.current_width += 1
  354. # if self.current_width == self.width:
  355. # self.dot(f"dLI_{self.depth}")
  356. if not self.root:
  357. return True
  358. return False
  359. return False
  360. class dHI(DEVStoneWrapper):
  361. """
  362. A HI model which grows in width
  363. """
  364. def __init__(self, name: str, depth: int, width: int, int_delay: float, ext_delay: float, generator_mode: str = "uniform", root=True, prep_time=0, stats=False):
  365. super().__init__(name, depth, width, int_delay, ext_delay, add_atomic_out_ports=True, prep_time=prep_time,
  366. stats=stats, mode="HI-Dynamic", dynamic=True)
  367. self.root = root
  368. if len(self.component_set) > 1:
  369. assert isinstance(self.component_set[-1], AtomicDEVS)
  370. self.connectPorts(self.i_in, self.component_set[-1].i_in)
  371. for idx in range(1, len(self.component_set) - 1):
  372. assert isinstance(self.component_set[idx], AtomicDEVS)
  373. self.connectPorts(self.component_set[idx].o_out, self.component_set[idx + 1].i_in)
  374. self.connectPorts(self.i_in, self.component_set[idx].i_in)
  375. self.current_width = 1
  376. self.max_gen = width - 1 # dynamically generate atomic components until width is reached excludes the coupled model
  377. if self.depth == 1:
  378. self.generator = self.addSubModel(DynamicGenerator(self.name + "_gen", repeat=self.max_gen))
  379. def gen_coupled(self):
  380. return dHI("Coupled_%d" % (self.depth - 1), self.depth - 1, self.width, self.int_delay, self.ext_delay,
  381. prep_time=self.prep_time, stats=self.stats, root=False)
  382. def modelTransition(self, state):
  383. if state.get("generate", True) and self.depth == 1:
  384. return True
  385. if state.get("generate", True) and self.depth > 1:
  386. name = "Atomic_%d_%d" % (self.depth - 1, self.current_width) if self.depth > 1 else "Atomic_0_%d" % self.current_width
  387. if self.stats:
  388. new_atom = DelayedAtomicStats(name, self.int_delay, self.ext_delay,
  389. add_out_port=self.add_atomic_out_ports, prep_time=self.prep_time, mode="LI-Dynamic")
  390. else:
  391. new_atom = DelayedAtomic(name, self.int_delay, self.ext_delay,
  392. add_out_port=self.add_atomic_out_ports, prep_time=self.prep_time, mode="LI-Dynamic")
  393. self.models.append(self.addSubModel(new_atom))
  394. self.connectPorts(self.i_in, new_atom.i_in)
  395. if len(self.models) > 1:
  396. prev = self.models[-2]
  397. # print("last: ", prev.name)
  398. if isinstance(prev, AtomicDEVS) and hasattr(prev, "o_out"):
  399. self.connectPorts(prev.o_out, new_atom.i_in)
  400. self.current_width += 1
  401. if not self.root:
  402. return True
  403. return False
  404. return False
  405. if __name__ == '__main__':
  406. import sys
  407. sys.setrecursionlimit(10000)
  408. root = HOmod("Root", 4, 3, 0, 0)
  409. sim = Simulator(root)
  410. sim.setVerbose(None)
  411. # sim.setTerminationTime(10.0)
  412. sim.setStateSaving("custom")
  413. sim.simulate()