devstone.py 20 KB

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