devstone.py 22 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533
  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. import time
  9. class DelayedAtomic(AtomicDEVS):
  10. def __init__(self, name: str, int_delay: float, ext_delay: float, add_out_port: bool = False, prep_time=0.0, mode: str = ""):
  11. super().__init__(name)
  12. self.int_delay = int_delay
  13. self.ext_delay = ext_delay
  14. self.prep_time = prep_time
  15. self.i_in = self.addInPort("i_in")
  16. if add_out_port: # for HI and HO models
  17. self.o_out = self.addOutPort("o_out")
  18. # Dynamic Structure extras
  19. self.mode = mode
  20. self.out_ports = [] # used for dynamic structure model LI2HI
  21. def intTransition(self):
  22. if self.int_delay:
  23. pystones(self.int_delay)
  24. return "passive"
  25. def timeAdvance(self):
  26. if self.state == "active":
  27. return self.prep_time
  28. else:
  29. return INFINITY
  30. def outputFnc(self):
  31. if hasattr(self, "o_out") and self.o_out is not None:
  32. return {self.o_out: [0]}
  33. # for DSDEVS LI2HI
  34. if len(self.out_ports) == 1 and self.out_ports[0].outline != []:
  35. return {self.out_ports[0]: [0]}
  36. return {}
  37. def extTransition(self, inputs):
  38. if self.ext_delay:
  39. pystones(self.ext_delay)
  40. return "active"
  41. def modelTransition(self, state):
  42. result = False
  43. if self.mode == "LI2HI":
  44. result = self.LI2HI(state)
  45. elif self.mode == "HI2LI":
  46. result = self.HI2LI(state)
  47. return result
  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, repeat: int = 1, num: int = 1):
  301. super().__init__(name)
  302. self.period = period
  303. self.repeat = repeat
  304. self.number = num # how many models need to be generated per cycle
  305. self._counter = 0
  306. def intTransition(self):
  307. return "passive" # state is unimportant
  308. def timeAdvance(self):
  309. if self._counter >= self.repeat:
  310. return INFINITY
  311. return self.period
  312. def outputFnc(self):
  313. return {}
  314. def modelTransition(self, state):
  315. # every time TA elapses, this will be called on the atomic:
  316. # set a flag the parent coupled can read
  317. # increment counter and request modelTransition (return True)
  318. if self._counter >= self.repeat:
  319. return False
  320. remaining = self.repeat - self._counter
  321. num2create = self.number
  322. if remaining < self.number:
  323. num2create = remaining
  324. self._counter += num2create
  325. state["generate"] = True
  326. state["create"] = num2create
  327. state["created"] = 0
  328. return True
  329. class DynamicDEVStoneWrapper(DEVStoneWrapper, ABC):
  330. def __init__(self, name: str, depth: int, width: int, int_delay: float, ext_delay: float,
  331. add_atomic_out_ports: bool = False, /, prep_time=0, stats=False, root=False, mode: str = "", number: int = 1, gen_mode: str="uniform"):
  332. if number <= 0:
  333. raise ValueError("number of models to be generated must be greater than zero")
  334. self.root = root
  335. self.mode = mode
  336. self.created = 0 # keeps track of how many atomic models have been created
  337. self.current_width = 1
  338. self.number = number
  339. self.max_gen = width - 1 # dynamically generate atomic components until width is reached excludes the coupled model
  340. super().__init__(name, depth, width, int_delay, ext_delay, add_atomic_out_ports, prep_time=prep_time,
  341. stats=stats, mode=mode, dynamic=True)
  342. if self.depth == 1:
  343. self.generator = self.addSubModel(DynamicGenerator(self.name + "_gen", repeat=self.max_gen, num=self.number))
  344. def createAtomic(self):
  345. name = f"Atomic_0_{self.current_width}"
  346. if self.depth > 1:
  347. name = f"Atomic_{self.depth-1}_{self.current_width}"
  348. if self.stats:
  349. atomic = DelayedAtomicStats(name, self.int_delay, self.ext_delay,
  350. add_out_port=self.add_atomic_out_ports, prep_time=self.prep_time,
  351. mode=self.mode)
  352. else:
  353. atomic = DelayedAtomic(name, self.int_delay, self.ext_delay,
  354. add_out_port=self.add_atomic_out_ports, prep_time=self.prep_time,
  355. mode=self.mode)
  356. self.models.append(self.addSubModel(atomic))
  357. return atomic
  358. def modelTransition(self, state):
  359. if state.get("generate", True) and self.depth == 1:
  360. return True
  361. if state.get("generate", True) and self.depth > 1:
  362. number = state.get("create")
  363. for _ in range(number):
  364. atom = self.createAtomic()
  365. self.connectPorts(self.i_in, atom.i_in)
  366. if len(self.models) > 1:
  367. prev = self.models[-2] # new model was already added so the previous one isn't the last in the list anymore
  368. if isinstance(prev, AtomicDEVS) and hasattr(prev, "o_out"):
  369. self.connectPorts(prev.o_out, atom.i_in)
  370. self.current_width += 1
  371. if self.current_width > self.width:
  372. raise RuntimeError(f"The width has grown beyond what was specified!"
  373. f"\n current width: {self.current_width}"
  374. f"\n specified width: {self.width}")
  375. # enables visualisation of each level (coupled model)
  376. # if self.current_width == self.width:
  377. # self.dot(f"dLI_{self.depth}")
  378. if not self.root:
  379. state["created"] += number
  380. return True
  381. self.created += state["created"] + number
  382. return False
  383. return False
  384. class dLI(DynamicDEVStoneWrapper):
  385. """
  386. A LI model which grows in width
  387. """
  388. def __init__(self, name: str, depth: int, width: int, int_delay: float, ext_delay: float, /, prep_time=0, stats=False, root=True, number: int = 1, gen_mode: str = "uniform"):
  389. super().__init__(name, depth, width, int_delay, ext_delay, False, prep_time, stats, mode="LI-Dynamic", root=root, number=number, gen_mode=gen_mode)
  390. for idx in range(1, len(self.models)):
  391. assert isinstance(self.component_set[idx], AtomicDEVS)
  392. self.connectPorts(self.i_in, self.component_set[idx].i_in)
  393. def gen_coupled(self):
  394. return dLI("Coupled_%d" % (self.depth - 1), self.depth - 1, self.width, self.int_delay, self.ext_delay,
  395. prep_time=self.prep_time, stats=self.stats, root=False, number=self.number)
  396. class dHI(DynamicDEVStoneWrapper):
  397. """
  398. A HI model which grows in width
  399. """
  400. def __init__(self, name: str, depth: int, width: int, int_delay: float, ext_delay: float, /, prep_time=0, stats=False, root=True, number: int = 1, gen_mode: str = "uniform"):
  401. super().__init__(name, depth, width, int_delay, ext_delay, True, prep_time, stats, mode="HI-Dynamic", root=root, number=number, gen_mode=gen_mode)
  402. if len(self.models) > 1:
  403. assert isinstance(self.component_set[-1], AtomicDEVS)
  404. self.connectPorts(self.i_in, self.component_set[-1].i_in)
  405. for idx in range(1, len(self.models) - 1):
  406. assert isinstance(self.component_set[idx], AtomicDEVS)
  407. self.connectPorts(self.component_set[idx].o_out, self.component_set[idx + 1].i_in)
  408. self.connectPorts(self.i_in, self.component_set[idx].i_in)
  409. def gen_coupled(self):
  410. return dHI("Coupled_%d" % (self.depth - 1), self.depth - 1, self.width, self.int_delay, self.ext_delay,
  411. prep_time=self.prep_time, stats=self.stats, root=False, number=self.number)
  412. if __name__ == '__main__':
  413. import sys
  414. sys.setrecursionlimit(10000)
  415. root = HOmod("Root", 4, 3, 0, 0)
  416. sim = Simulator(root)
  417. sim.setVerbose(None)
  418. # sim.setTerminationTime(10.0)
  419. sim.setStateSaving("custom")
  420. sim.simulate()