DEVS.py 20 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537
  1. # -*- coding: Latin-1 -*-
  2. """
  3. Classes and tools for DEVS model specification
  4. """
  5. from devsexception import DEVSException
  6. class BaseDEVS(object):
  7. """
  8. Abstract base class for AtomicDEVS and CoupledDEVS classes.
  9. This class provides basic DEVS attributes and query/set methods.
  10. """
  11. def __init__(self, name):
  12. """
  13. Constructor
  14. :param name: the name of the DEVS model
  15. """
  16. # Prevent any attempt to instantiate this abstract class
  17. if self.__class__ == BaseDEVS:
  18. raise DEVSException ("Cannot instantiate abstract class '%s' ... "
  19. % (self.__class__.__name__))
  20. # The parent of the current model
  21. self.parent = None
  22. # The local name of the model
  23. self.name = name
  24. self.IPorts = []
  25. self.OPorts = []
  26. self.ports = []
  27. # Initialise the times
  28. self.timeLast = (0.0, 0)
  29. self.timeNext = (0.0, 1)
  30. self.location = None
  31. # Variables used for optimisations
  32. self.myInput = {}
  33. self.myOutput = {}
  34. # The state queue, used for time warp
  35. self.oldStates = []
  36. # List of all memoized states, only useful in distributed simulation
  37. # with memoization enabled
  38. self.memo = []
  39. def modelTransition(self, state):
  40. """
  41. DEFAULT function for Dynamic Structure DEVS, always returning False (thus indicating that no structural change is needed)
  42. :param state: a dict that can be used to save some kind of state, this object is maintained by the kernel and will be passed each time
  43. :returns: bool -- whether or not a structural change is necessary
  44. """
  45. return False
  46. def addPort(self, name, isInput):
  47. """
  48. Utility function to create a new port and add it everywhere where it is necessary
  49. :param name: the name of the port
  50. :param isInput: whether or not this is an input port
  51. """
  52. name = name if name is not None else "port%s" % len(self.ports)
  53. port = Port(isInput=isInput, name=name)
  54. if isInput:
  55. self.IPorts.append(port)
  56. else:
  57. self.OPorts.append(port)
  58. port.port_id = len(self.ports)
  59. self.ports.append(port)
  60. port.hostDEVS = self
  61. return port
  62. def addInPort(self, name=None):
  63. """
  64. Add an input port to the DEVS model.
  65. addInPort is the only proper way to add input ports to a DEVS model.
  66. As for the CoupledDEVS.addSubModel method, calls
  67. to addInPort and addOutPort can appear in any DEVS'
  68. descriptive class constructor, or the methods can be used with an
  69. instantiated object.
  70. The methods add a reference to the new port in the DEVS' IPorts
  71. attributes and set the port's hostDEVS attribute. The modeler
  72. should typically save the returned reference somewhere.
  73. :param name: the name of the port. A unique ID will be generated in case None is passed
  74. :returns: port -- the generated port
  75. """
  76. return self.addPort(name, True)
  77. def addOutPort(self, name=None):
  78. """Add an output port to the DEVS model.
  79. addOutPort is the only proper way to
  80. add output ports to DEVS. As for the CoupledDEVS.addSubModel method, calls
  81. to addInPort and addOutPort can appear in any DEVS'
  82. descriptive class constructor, or the methods can be used with an
  83. instantiated object.
  84. The methods add a reference to the new port in the DEVS'
  85. OPorts attributes and set the port's hostDEVS attribute. The modeler
  86. should typically save the returned reference somewhere.
  87. :param name: the name of the port. A unique ID will be generated in case None is passed
  88. :returns: port -- the generated port
  89. """
  90. return self.addPort(name, False)
  91. def getModelName(self):
  92. """
  93. Get the local model name
  94. :returns: string -- the name of the model
  95. """
  96. return str(self.name)
  97. def getModelFullName(self):
  98. """
  99. Get the full model name, including the path from the root
  100. :returns: string -- the fully qualified name of the model
  101. """
  102. return self.fullName
  103. class AtomicDEVS(BaseDEVS):
  104. """
  105. Abstract base class for all atomic-DEVS descriptive classes.
  106. """
  107. def __init__(self, name=None):
  108. """
  109. Constructor for an AtomicDEVS model
  110. :param name: name of the model, can be None to have an automatically generated name
  111. """
  112. # Prevent any attempt to instantiate this abstract class
  113. if self.__class__ == AtomicDEVS:
  114. raise DEVSException("Cannot instantiate abstract class '%s' ... "
  115. % (self.__class__.__name__))
  116. # The minimal constructor shall first call the superclass
  117. # (i.e., BaseDEVS) constructor.
  118. BaseDEVS.__init__(self, name)
  119. self.elapsed = 0.0
  120. self.state = None
  121. def extTransition(self, inputs):
  122. """
  123. DEFAULT External Transition Function.
  124. Accesses state and elapsed attributes, as well as inputs
  125. through the passed dictionary. Returns the new state.
  126. .. note:: Should only write to the *state* attribute.
  127. :param inputs: dictionary containing all ports and their corresponding outputs
  128. :returns: state -- the new state of the model
  129. """
  130. return self.state
  131. def intTransition(self):
  132. """
  133. DEFAULT Internal Transition Function.
  134. .. note:: Should only write to the *state* attribute.
  135. :returns: state -- the new state of the model
  136. .. versionchanged:: 2.1 The *elapsed* attribute is no longer guaranteed to be correct as this isn't required by the DEVS formalism.
  137. """
  138. return self.state
  139. def confTransition(self, inputs):
  140. """
  141. DEFAULT Confluent Transition Function.
  142. Accesses state and elapsed attributes, as well as inputs
  143. through the passed dictionary. Returns the new state.
  144. .. note:: Should only write to the *state* attribute.
  145. :param inputs: dictionary containing all ports and their corresponding outputs
  146. :returns: state -- the new state of the model
  147. """
  148. self.state = self.intTransition()
  149. self.state = self.extTransition(inputs)
  150. return self.state
  151. def outputFnc(self):
  152. """
  153. DEFAULT Output Function.
  154. Accesses only state attribute. Returns the output on the different ports as a dictionary.
  155. .. note:: Should **not** write to any attribute.
  156. :returns: dictionary containing output ports as keys and lists of output on that port as value
  157. .. versionchanged:: 2.1 The *elapsed* attribute is no longer guaranteed to be correct as this isn't required by the DEVS formalism.
  158. """
  159. return {}
  160. def timeAdvance(self):
  161. """
  162. DEFAULT Time Advance Function.
  163. .. note:: Should ideally be deterministic, though this is not mandatory for simulation.
  164. :returns: the time advance of the model
  165. .. versionchanged:: 2.1 The *elapsed* attribute is no longer guaranteed to be correct as this isn't required by the DEVS formalism.
  166. """
  167. # By default, return infinity
  168. return float('inf')
  169. def finalize(self, name, model_counter, model_ids, locations, selectHierarchy):
  170. """
  171. Finalize the model hierarchy by doing all pre-simulation configuration
  172. .. note:: Parameters *model_ids* and *locations* are updated by reference.
  173. :param name: the name of the hierarchy above
  174. :param model_counter: the model ID counter
  175. :param model_ids: a list with all model_ids and their model
  176. :param locations: dictionary of locations and where every model runs
  177. :param selectHierarchy: hierarchy to perform selections in Classic DEVS
  178. :returns: int -- the new model ID counter
  179. """
  180. # Give a name
  181. self.fullName = name + str(self.getModelName())
  182. # Give a unique ID to the model itself
  183. self.model_id = model_counter
  184. self.selectHierarchy = selectHierarchy + [self]
  185. # Add the element to its designated place in the model_ids list
  186. model_ids.append(self)
  187. # Do a quick check, since this is vital to correct operation
  188. if model_ids[self.model_id] != self:
  189. raise DEVSException("Something went wrong while initializing models: IDs don't match")
  190. locations[self.location].append(self.model_id)
  191. # Return the unique ID counter, incremented so it stays unique
  192. return model_counter + 1
  193. class CoupledDEVS(BaseDEVS):
  194. """
  195. Abstract base class for all coupled-DEVS descriptive classes.
  196. """
  197. def __init__(self, name=None):
  198. """
  199. Constructor.
  200. :param name: the name of the coupled model, can be None for an automatically generated name
  201. """
  202. # Prevent any attempt to instantiate this abstract class
  203. if self.__class__ == CoupledDEVS:
  204. raise DEVSException("Cannot instantiate abstract class '%s' ... "
  205. % (self.__class__.__name__))
  206. # The minimal constructor shall first call the superclass
  207. # (i.e., BaseDEVS) constructor.
  208. BaseDEVS.__init__(self, name)
  209. # All components of this coupled model (the submodels)
  210. self.componentSet = []
  211. def finalize(self, name, model_counter, model_ids, locations, selectHierarchy):
  212. """
  213. Finalize the model hierarchy by doing all pre-simulation configuration
  214. .. note:: Parameters *model_ids* and *locations* are updated by reference.
  215. :param name: the name of the hierarchy above
  216. :param model_counter: the model ID counter
  217. :param model_ids: a list with all model_ids and their model
  218. :param locations: dictionary of locations and where every model runs
  219. :param selectHierarchy: hierarchy to perform selections in Classic DEVS
  220. :returns: int -- the new model ID counter
  221. """
  222. # Set name, even though it will never be requested
  223. self.fullName = name + str(self.getModelName())
  224. for i in self.componentSet:
  225. model_counter = i.finalize(self.fullName + ".", model_counter,
  226. model_ids, locations, selectHierarchy + [self])
  227. return model_counter
  228. def addSubModel(self, model, location = None):
  229. """
  230. Adds a specified model to the current coupled model as its child. This
  231. is the function that must be used to make distributed simulation
  232. possible.
  233. :param model: the model to be added as a child
  234. :param location: the location at which the child must run
  235. :returns: model -- the model that was created as a child
  236. .. versionchanged:: 2.1.3
  237. model can no longer be a string, this was previously a lot more efficient in partial distribution, though this functionality was removed together with the partial distribution functionality.
  238. """
  239. model.parent = self
  240. if location is not None:
  241. location = int(location)
  242. model.location = location if location is not None else self.location
  243. if model.location is not None and isinstance(model, CoupledDEVS):
  244. # Set the location of all children
  245. for i in model.componentSet:
  246. i.setLocation(model.location)
  247. self.componentSet.append(model)
  248. if hasattr(self, "fullName"):
  249. # Full Name is only created when starting the simulation, so we are currently in a running simulation
  250. # Dynamic Structure change
  251. self.server.getSelfProxy().dsScheduleModel(model)
  252. return model
  253. def connectPorts(self, p1, p2, z = None):
  254. """
  255. Connects two ports together. The coupling is to begin at p1 and
  256. to end at p2.
  257. :param p1: the port at the start of the new connection
  258. :param p2: the port at the end of the new connection
  259. :param z: the translation function for the events
  260. either input-to-input, output-to-input or output-to-output.
  261. """
  262. # For a coupling to be valid, two requirements must be met:
  263. # 1- at least one of the DEVS the ports belong to is a child of the
  264. # coupled-DEVS (i.e., self), while the other is either the
  265. # coupled-DEVS itself or another of its children. The DEVS'
  266. # 'parenthood relationship' uniquely determine the type of coupling;
  267. # 2- the types of the ports are consistent with the 'parenthood' of the
  268. # associated DEVS. This validates the coupling determined above.
  269. # Internal Coupling:
  270. if ((p1.hostDEVS.parent == self and p2.hostDEVS.parent == self) and
  271. (p1.type() == 'OUTPORT' and p2.type() == 'INPORT')):
  272. if p1.hostDEVS is p2.hostDEVS:
  273. raise DEVSException(("In coupled model '%s', connecting ports" +
  274. " '%s' and '%s' belong to the same model" +
  275. " '%s'. " +
  276. " Direct feedback coupling not allowed") % (
  277. self.getModelFullName(),
  278. p1.getPortFullName(),
  279. p2.getPortFullName(),
  280. p1.hostDEVS.getModelFullName()))
  281. else:
  282. p1.outLine.append(p2)
  283. p2.inLine.append(p1)
  284. # External input couplings:
  285. elif ((p1.hostDEVS == self and p2.hostDEVS.parent == self) and
  286. (p1.type() == p2.type() == 'INPORT')):
  287. p1.outLine.append(p2)
  288. p2.inLine.append(p1)
  289. # Eternal output couplings:
  290. elif ((p1.hostDEVS.parent == self and p2.hostDEVS == self) and
  291. (p1.type() == p2.type() == 'OUTPORT')):
  292. p1.outLine.append(p2)
  293. p2.inLine.append(p1)
  294. # Other cases (illegal coupling):
  295. else:
  296. raise DEVSException(("Illegal coupling in coupled model '%s' " +
  297. "between ports '%s' and '%s'") % (
  298. self.getModelName(), p1.getPortName(),
  299. p2.getPortName()))
  300. p1.zFunctions[p2] = z
  301. if hasattr(self, "server"):
  302. self.server.getSelfProxy().dsUndoDirectConnect()
  303. class RootDEVS(BaseDEVS):
  304. """
  305. The artificial RootDEVS model is the only 'coupled' model in the simulation after direct connection is performed.
  306. """
  307. def __init__(self, components, models, schedulerType):
  308. """
  309. Basic constructor.
  310. :param components: the atomic DEVS models that are the cildren, only those that are ran locally should be mentioned
  311. :param models: all models that have to be passed to the scheduler, thus all models, even non-local ones
  312. :param schedulerType: type of scheduler to use (string representation)
  313. """
  314. BaseDEVS.__init__(self, "ROOT model")
  315. self.componentSet = components
  316. self.timeNext = (float('inf'), 1)
  317. self.local_model_ids = set()
  318. for i in self.componentSet:
  319. self.local_model_ids.add(i.model_id)
  320. self.models = models
  321. self.schedulerType = schedulerType
  322. self.directConnected = True
  323. def directConnect(self):
  324. """
  325. Perform direct connection on the models again
  326. """
  327. directConnect(self.models, True)
  328. def setTimeNext(self):
  329. """
  330. Reset the timeNext
  331. """
  332. try:
  333. self.timeNext = self.scheduler.readFirst()
  334. except IndexError:
  335. # No element found in the scheduler, so put it to INFINITY
  336. self.timeNext = (float('inf'), 1)
  337. class Port(object):
  338. """
  339. Class for DEVS model ports (both input and output). This class provides basic port attributes and query methods.
  340. """
  341. def __init__(self, isInput, name=None):
  342. """
  343. Constructor. Creates an input port if isInput evaluates to True, and
  344. an output port otherwise.
  345. :param isInput: whether or not this is an input port
  346. :param name: the name of the port. If None is provided, a unique ID is generated
  347. """
  348. self.inLine = []
  349. self.outLine = []
  350. self.hostDEVS = None
  351. self.msgcount = 0
  352. # The name of the port
  353. self.name = name
  354. self.isInput = isInput
  355. self.zFunctions = {}
  356. def getPortName(self):
  357. """
  358. Returns the name of the port
  359. :returns: local name of the port
  360. """
  361. return self.name
  362. def getPortFullName(self):
  363. """
  364. Returns the complete name of the port
  365. :returns: fully qualified name of the port
  366. """
  367. return "%s.%s" % (self.hostDEVS.getModelFullName(), self.getPortName())
  368. def type(self):
  369. """
  370. Returns the 'type' of the object
  371. :returns: either 'INPORT' or 'OUTPORT'
  372. """
  373. if self.isInput:
  374. return 'INPORT'
  375. else:
  376. return 'OUTPORT'
  377. def appendZ(first_z, new_z):
  378. if first_z is None:
  379. return new_z
  380. elif new_z is None:
  381. return first_z
  382. else:
  383. return lambda x: new_z(first_z(x))
  384. def directConnect(componentSet, local):
  385. """
  386. Perform direct connection on this CoupledDEVS model
  387. :param componentSet: the iterable to direct connect
  388. :param local: whether or not simulation is local; if it is, dynamic structure code will be prepared
  389. :returns: the direct connected componentSet
  390. """
  391. newlist = []
  392. for i in componentSet:
  393. if isinstance(i, CoupledDEVS):
  394. componentSet.extend(i.componentSet)
  395. else:
  396. # Found an atomic model
  397. newlist.append(i)
  398. componentSet = newlist
  399. # All and only all atomic models are now direct children of this model
  400. for i in componentSet:
  401. # Remap the output ports
  402. for outport in i.OPorts:
  403. # The new contents of the line
  404. outport.routingOutLine = []
  405. worklist = [(p, outport.zFunctions.get(p, None)) for p in outport.outLine]
  406. for outline, z in worklist:
  407. # If it is a coupled model, we must expand this model
  408. if isinstance(outline.hostDEVS, CoupledDEVS):
  409. for inline in outline.outLine:
  410. # Add it to the current iterating list, so we can just continue
  411. worklist.append((inline, appendZ(z, outline.zFunctions[inline])))
  412. # If it is a Coupled model, we should just continue
  413. # expanding it and not add it to the finished line
  414. if not isinstance(inline.hostDEVS, CoupledDEVS):
  415. outport.routingOutLine.append((inline, appendZ(z, outline.zFunctions[inline])))
  416. else:
  417. for ol, z in outport.routingOutLine:
  418. if ol == outline:
  419. break
  420. else:
  421. # Add to the new line if it isn't already there
  422. # Note that it isn't really mandatory to check for this,
  423. # it is a lot cleaner to do so.
  424. # This will greatly increase the complexity of the connector though
  425. outport.routingOutLine.append((outline, z))
  426. # Remap the input ports: identical to the output ports, only in the reverse direction
  427. for inport in i.IPorts:
  428. inport.routingInLine = []
  429. for inline in inport.inLine:
  430. if isinstance(inline.hostDEVS, CoupledDEVS):
  431. for outline in inline.inLine:
  432. inport.inLine.append(outline)
  433. if not isinstance(outline.hostDEVS, CoupledDEVS):
  434. inport.routingInLine.append(outline)
  435. elif inline not in inport.routingInLine:
  436. inport.routingInLine.append(inline)
  437. return componentSet