DEVS.py 35 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955
  1. # Copyright 2014 Modelling, Simulation and Design Lab (MSDL) at
  2. # McGill University and the University of Antwerp (http://msdl.cs.mcgill.ca/)
  3. #
  4. # Licensed under the Apache License, Version 2.0 (the "License");
  5. # you may not use this file except in compliance with the License.
  6. # You may obtain a copy of the License at
  7. #
  8. # http://www.apache.org/licenses/LICENSE-2.0
  9. #
  10. # Unless required by applicable law or agreed to in writing, software
  11. # distributed under the License is distributed on an "AS IS" BASIS,
  12. # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  13. # See the License for the specific language governing permissions and
  14. # limitations under the License.
  15. """
  16. Classes and tools for DEVS model specification
  17. """
  18. #from pypdevs.logger import debug, warn, info, error
  19. #from pypdevs.util import *
  20. import time
  21. class BaseDEVS(object):
  22. """
  23. Abstract base class for AtomicDEVS and CoupledDEVS classes.
  24. This class provides basic DEVS attributes and query/set methods.
  25. """
  26. def __init__(self, name):
  27. """
  28. Constructor
  29. :param name: the name of the DEVS model
  30. """
  31. # Prevent any attempt to instantiate this abstract class
  32. if self.__class__ == BaseDEVS:
  33. raise DEVSException ("Cannot instantiate abstract class '%s' ... "
  34. % (self.__class__.__name__))
  35. # The parent of the current model
  36. self.parent = None
  37. # The local name of the model
  38. self.name = name
  39. self.IPorts = []
  40. self.OPorts = []
  41. self.ports = {}
  42. # Initialise the times
  43. self.time_last = (0.0, 0)
  44. self.time_next = (0.0, 1)
  45. self.location = None
  46. # Variables used for optimisations
  47. self.my_input = {}
  48. self.my_output = {}
  49. # The state queue, used for time warp
  50. self.old_states = []
  51. # List of all memoized states, only useful in distributed simulation
  52. # with memoization enabled
  53. self.memo = []
  54. def simSettings(self, simulator):
  55. """
  56. Modifies the simulation settings from within the model.
  57. This function is called _before_ direct connection and distribution is performed, so the user can still access the complete hierarchy.
  58. .. note:: This function is *only* called on the root model of the simulation, thus the model passed to the constructor of the Simulator object.
  59. :param simulator: the simulator object on which settings can be configured
  60. """
  61. pass
  62. def modelTransition(self, state):
  63. """
  64. DEFAULT function for Dynamic Structure DEVS, always returning False (thus indicating that no structural change is needed)
  65. :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
  66. :returns: bool -- whether or not a structural change is necessary
  67. """
  68. return False
  69. def getVCDVariables(self):
  70. """
  71. Fetch all the variables, suitable for VCD variable generation
  72. :returns: list -- all variables needed for VCD tracing
  73. """
  74. var_list = []
  75. for I in self.ports.itervalues():
  76. var_list.append([self.getModelFullName(), I.getPortName()])
  77. return var_list
  78. def removePort(self, port):
  79. """
  80. Remove a port (either input or output) from the model, disconnecting all of its connections.
  81. :param port: the port to remove
  82. """
  83. if not hasattr(self, "full_name"):
  84. raise DEVSException("removePort should only be called during a simulation")
  85. if port.is_input:
  86. self.IPorts.remove(port)
  87. else:
  88. self.OPorts.remove(port)
  89. del self.ports[port.port_id]
  90. # Also remove all connections to this port
  91. self.server.getSelfProxy().dsRemovePort(port)
  92. def addPort(self, name, is_input):
  93. """
  94. Utility function to create a new port and add it everywhere where it is necessary
  95. :param name: the name of the port
  96. :param is_input: whether or not this is an input port
  97. """
  98. name = name if name is not None else "port%s" % len(self.ports)
  99. port = Port(is_input=is_input, name=name)
  100. if is_input:
  101. self.IPorts.append(port)
  102. else:
  103. self.OPorts.append(port)
  104. port.port_id = port.getPortName()
  105. self.ports[port.port_id] = port
  106. port.host_DEVS = self
  107. if hasattr(self, "full_name"):
  108. self.server.getSelfProxy().dsAddPort(port)
  109. return port
  110. def addInPort(self, name=None):
  111. """
  112. Add an input port to the DEVS model.
  113. addInPort is the only proper way to add input ports to a DEVS model.
  114. As for the CoupledDEVS.addSubModel method, calls
  115. to addInPort and addOutPort can appear in any DEVS'
  116. descriptive class constructor, or the methods can be used with an
  117. instantiated object.
  118. The methods add a reference to the new port in the DEVS' IPorts
  119. attributes and set the port's hostDEVS attribute. The modeler
  120. should typically save the returned reference somewhere.
  121. :param name: the name of the port. A unique ID will be generated in case None is passed
  122. :returns: port -- the generated port
  123. """
  124. return self.addPort(name, True)
  125. def addOutPort(self, name=None):
  126. """Add an output port to the DEVS model.
  127. addOutPort is the only proper way to
  128. add output ports to DEVS. As for the CoupledDEVS.addSubModel method, calls
  129. to addInPort and addOutPort can appear in any DEVS'
  130. descriptive class constructor, or the methods can be used with an
  131. instantiated object.
  132. The methods add a reference to the new port in the DEVS'
  133. OPorts attributes and set the port's hostDEVS attribute. The modeler
  134. should typically save the returned reference somewhere.
  135. :param name: the name of the port. A unique ID will be generated in case None is passed
  136. :returns: port -- the generated port
  137. """
  138. return self.addPort(name, False)
  139. def getModelName(self):
  140. """
  141. Get the local model name
  142. :returns: string -- the name of the model
  143. """
  144. if self.name is None:
  145. return str(self.model_id)
  146. else:
  147. return str(self.name)
  148. def getModelFullName(self):
  149. """
  150. Get the full model name, including the path from the root
  151. :returns: string -- the fully qualified name of the model
  152. """
  153. return self.full_name
  154. class AtomicDEVS(BaseDEVS):
  155. """
  156. Abstract base class for all atomic-DEVS descriptive classes.
  157. """
  158. def __init__(self, name=None):
  159. """
  160. Constructor for an AtomicDEVS model
  161. :param name: name of the model, can be None to have an automatically generated name
  162. """
  163. # Prevent any attempt to instantiate this abstract class
  164. if self.__class__ == AtomicDEVS:
  165. raise DEVSException("Cannot instantiate abstract class '%s' ... "
  166. % (self.__class__.__name__))
  167. # The minimal constructor shall first call the superclass
  168. # (i.e., BaseDEVS) constructor.
  169. BaseDEVS.__init__(self, name)
  170. self.elapsed = 0.0
  171. self.state = None
  172. self.relocatable = True
  173. self.last_read_time = (0, 0)
  174. def setLocation(self, location, force=False):
  175. """
  176. Sets the location of the atomic DEVS model if it was not already set
  177. :param location: the location to set
  178. :param force: whether or not to force this location, even if another is already defined
  179. """
  180. if self.location is None or force:
  181. self.location = location
  182. def fetchActivity(self, time, activities):
  183. """
  184. Fetch the activity of the model up to a certain time
  185. :param time: the time up to which the activity should be calculated
  186. :param activities: dictionary containing all activities for the models
  187. """
  188. accumulator = 0.0
  189. for state in self.old_states:
  190. if state.time_last[0] < time:
  191. accumulator += state.activity
  192. activities[self.model_id] = accumulator
  193. def setGVT(self, gvt, activities, last_state_only):
  194. """
  195. Set the GVT of the model, cleaning up the states vector as required
  196. for the time warp algorithm
  197. :param gvt: the new value of the GVT
  198. :param activities: dictionary containing all activities for the models
  199. :param last_state_only: whether or not to only use a single state for activity
  200. """
  201. copy = None
  202. activity = 0
  203. for i in range(len(self.old_states)):
  204. state = self.old_states[i]
  205. if state.time_last[0] >= gvt:
  206. # Possible that all elements should be kept,
  207. # in which case it will return -1 and only keep the last element
  208. # So the copy element should be AT LEAST 0
  209. copy = max(0, i-1)
  210. break
  211. elif not last_state_only:
  212. activity += state.activity
  213. if self.old_states == []:
  214. # We have no memory, so we are normally in sequential simulation
  215. self.old_states = []
  216. elif copy is None:
  217. self.old_states = [self.old_states[-1]]
  218. else:
  219. self.old_states = self.old_states[copy:]
  220. if last_state_only:
  221. activity = self.old_states[0].activity
  222. activities[self.model_id] = activity
  223. def revert(self, time, memorize):
  224. """
  225. Revert the model to the specified time. All necessary cleanup for this
  226. model will be done (fossil collection).
  227. :param time: the time up to which should be reverted
  228. :param memorize: whether or not the saved states should still be kept for memoization
  229. """
  230. new_state = len(self.old_states) - 1
  231. for state in reversed(self.old_states[1:]):
  232. if state.time_last < time:
  233. break
  234. new_state -= 1
  235. state = self.old_states[new_state]
  236. self.time_last = state.time_last
  237. self.time_next = state.time_next
  238. self.state = state.loadState()
  239. if memorize:
  240. # Reverse it too
  241. self.memo = self.old_states[:-len(self.old_states) + new_state - 1:-1]
  242. self.old_states = self.old_states[:new_state + 1]
  243. # Check if one of the reverted states was ever read for the termination condition
  244. if self.last_read_time > time:
  245. # It seems it was, so notify the main revertion algorithm of this
  246. self.last_read_time = (0, 0)
  247. return True
  248. else:
  249. return False
  250. # NOTE clearing the myInput happens in the parent
  251. def getState(self, request_time, first_call=True):
  252. """
  253. For the distributed termination condition: fetch the state of the model at a certain time
  254. :param request_time: the time (including age!) for which the state should be fetched
  255. :param first_call: whether or not this is the first call of a possible recursive call
  256. :returns: state -- the state at that time
  257. """
  258. if self.location != MPIRedirect.local.name:
  259. return getProxy(self.location).getStateAtTime(self.model_id,
  260. request_time)
  261. elif first_call:
  262. # Shortcut if the call is local
  263. return self.state
  264. self.last_read_time = request_time
  265. while 1:
  266. for state in self.old_states:
  267. if state.time_last > request_time:
  268. return state.loadState()
  269. # State not yet available... wait some time before trying again...
  270. time.sleep(0.01)
  271. def extTransition(self, inputs):
  272. """
  273. DEFAULT External Transition Function.
  274. Accesses state and elapsed attributes, as well as inputs
  275. through the passed dictionary. Returns the new state.
  276. .. note:: Should only write to the *state* attribute.
  277. :param inputs: dictionary containing all ports and their corresponding outputs
  278. :returns: state -- the new state of the model
  279. """
  280. return self.state
  281. def intTransition(self):
  282. """
  283. DEFAULT Internal Transition Function.
  284. .. note:: Should only write to the *state* attribute.
  285. :returns: state -- the new state of the model
  286. .. versionchanged:: 2.1 The *elapsed* attribute is no longer guaranteed to be correct as this isn't required by the DEVS formalism.
  287. """
  288. return self.state
  289. def confTransition(self, inputs):
  290. """
  291. DEFAULT Confluent Transition Function.
  292. Accesses state and elapsed attributes, as well as inputs
  293. through the passed dictionary. Returns the new state.
  294. .. note:: Should only write to the *state* attribute.
  295. :param inputs: dictionary containing all ports and their corresponding outputs
  296. :returns: state -- the new state of the model
  297. """
  298. self.state = self.intTransition()
  299. self.state = self.extTransition(inputs)
  300. return self.state
  301. def outputFnc(self):
  302. """
  303. DEFAULT Output Function.
  304. Accesses only state attribute. Returns the output on the different ports as a dictionary.
  305. .. note:: Should **not** write to any attribute.
  306. :returns: dictionary containing output ports as keys and lists of output on that port as value
  307. .. versionchanged:: 2.1 The *elapsed* attribute is no longer guaranteed to be correct as this isn't required by the DEVS formalism.
  308. """
  309. return {}
  310. def timeAdvance(self):
  311. """
  312. DEFAULT Time Advance Function.
  313. .. note:: Should ideally be deterministic, though this is not mandatory for simulation.
  314. :returns: the time advance of the model
  315. .. versionchanged:: 2.1 The *elapsed* attribute is no longer guaranteed to be correct as this isn't required by the DEVS formalism.
  316. """
  317. # By default, return infinity
  318. return float('inf')
  319. def preActivityCalculation(self):
  320. """
  321. DEFAULT pre-transition activity fetcher. The returned value is passed to the *postActivityCalculation* function
  322. :returns: something -- passed to the *postActivityCalculation*
  323. """
  324. return time.time()
  325. def postActivityCalculation(self, prevalue):
  326. """
  327. DEFAULT post-transition activity fetcher. The returned value will be passed on to the relocator and MUST be an addable (e.g. integer, float, ...)
  328. :param prevalue: the value returned from the *preActivityCalculation* method
  329. :returns: addable (float, integer, ...) -- passed to the relocator
  330. """
  331. return time.time() - prevalue
  332. def flattenConnections(self):
  333. """
  334. Flattens the pickling graph, by removing backreference from the ports.
  335. """
  336. # It doesn't really matter what gets written in these hostDEVS attributes,
  337. # as it will never be used. Though for readability, the model_id will be used
  338. # to make it possible to do some debugging when necessary.
  339. for port in self.IPorts:
  340. port.host_DEVS = self.model_id
  341. for port in self.OPorts:
  342. port.host_DEVS = self.model_id
  343. def unflattenConnections(self):
  344. """
  345. Unflattens the picking graph, by reconstructing backreferences from the ports.
  346. """
  347. for port in self.IPorts:
  348. port.host_DEVS = self
  349. for port in self.OPorts:
  350. port.host_DEVS = self
  351. def finalize(self, name, model_counter, model_ids, locations, select_hierarchy):
  352. """
  353. Finalize the model hierarchy by doing all pre-simulation configuration
  354. .. note:: Parameters *model_ids* and *locations* are updated by reference.
  355. :param name: the name of the hierarchy above
  356. :param model_counter: the model ID counter
  357. :param model_ids: a list with all model_ids and their model
  358. :param locations: dictionary of locations and where every model runs
  359. :param select_hierarchy: hierarchy to perform selections in Classic DEVS
  360. :returns: int -- the new model ID counter
  361. """
  362. # Give a name
  363. self.full_name = name + str(self.getModelName())
  364. # Give a unique ID to the model itself
  365. self.model_id = model_counter
  366. self.select_hierarchy = select_hierarchy + [self]
  367. # Add the element to its designated place in the model_ids list
  368. model_ids.append(self)
  369. # Do a quick check, since this is vital to correct operation
  370. if model_ids[self.model_id] != self:
  371. raise DEVSException("Something went wrong while initializing models: IDs don't match")
  372. locations[self.location].append(self.model_id)
  373. # Return the unique ID counter, incremented so it stays unique
  374. return model_counter + 1
  375. def getModelLoad(self, lst):
  376. """
  377. Add this atomic model to the load of its location
  378. :param lst: list containing all locations and their current load
  379. :returns: int -- number of children in this subtree
  380. """
  381. lst[self.location] += 1
  382. self.num_children = 1
  383. return self.num_children
  384. class CoupledDEVS(BaseDEVS):
  385. """
  386. Abstract base class for all coupled-DEVS descriptive classes.
  387. """
  388. def __init__(self, name=None):
  389. """
  390. Constructor.
  391. :param name: the name of the coupled model, can be None for an automatically generated name
  392. """
  393. # Prevent any attempt to instantiate this abstract class
  394. if self.__class__ == CoupledDEVS:
  395. raise DEVSException("Cannot instantiate abstract class '%s' ... "
  396. % (self.__class__.__name__))
  397. # The minimal constructor shall first call the superclass
  398. # (i.e., BaseDEVS) constructor.
  399. BaseDEVS.__init__(self, name)
  400. # All components of this coupled model (the submodels)
  401. self.component_set = []
  402. def forceSequential(self):
  403. """
  404. Force a sequential simulation
  405. """
  406. self.setLocation(0, force=True)
  407. def select(self, imm_children):
  408. """
  409. DEFAULT select function, only used when using Classic DEVS simulation
  410. :param imm_children: list of all children that want to transition
  411. :returns: child -- a single child that is allowed to transition
  412. """
  413. return imm_children[0]
  414. def getModelLoad(self, lst):
  415. """
  416. Fetch the number of atomic models at this model
  417. :param lst: list containing all locations and their current load
  418. :returns: number of atomic models in this subtree, including non-local ones
  419. """
  420. children = 0
  421. for i in self.component_set:
  422. children += i.getModelLoad(lst)
  423. self.num_children = children
  424. return self.num_children
  425. def finalize(self, name, model_counter, model_ids, locations, select_hierarchy):
  426. """
  427. Finalize the model hierarchy by doing all pre-simulation configuration
  428. .. note:: Parameters *model_ids* and *locations* are updated by reference.
  429. :param name: the name of the hierarchy above
  430. :param model_counter: the model ID counter
  431. :param model_ids: a list with all model_ids and their model
  432. :param locations: dictionary of locations and where every model runs
  433. :param select_hierarchy: hierarchy to perform selections in Classic DEVS
  434. :returns: int -- the new model ID counter
  435. """
  436. # Set name, even though it will never be requested
  437. self.full_name = name + str(self.getModelName())
  438. for i in self.component_set:
  439. model_counter = i.finalize(self.full_name + ".", model_counter,
  440. model_ids, locations, select_hierarchy + [self])
  441. return model_counter
  442. def flattenConnections(self):
  443. """
  444. Flattens the pickling graph, by removing backreference from the ports.
  445. """
  446. for i in self.component_set:
  447. i.flattenConnections()
  448. def unflattenConnections(self):
  449. """
  450. Unflattens the pickling graph, by reconstructing backreference from the ports.
  451. """
  452. for i in self.component_set:
  453. i.unflattenConnections()
  454. def addSubModel(self, model, location = None):
  455. """
  456. Adds a specified model to the current coupled model as its child. This
  457. is the function that must be used to make distributed simulation
  458. possible.
  459. :param model: the model to be added as a child
  460. :param location: the location at which the child must run
  461. :returns: model -- the model that was created as a child
  462. .. versionchanged:: 2.1.3
  463. 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.
  464. """
  465. model.parent = self
  466. if location is not None:
  467. location = int(location)
  468. model.location = location if location is not None else self.location
  469. if model.location is not None and isinstance(model, CoupledDEVS):
  470. # Set the location of all children
  471. for i in model.component_set:
  472. i.setLocation(model.location)
  473. if hasattr(self, "full_name"):
  474. # Full Name is only created when starting the simulation, so we are currently in a running simulation
  475. # Dynamic Structure change
  476. self.server.getSelfProxy().dsScheduleModel(model)
  477. else:
  478. self.component_set.append(model)
  479. return model
  480. def removeSubModel(self, model):
  481. """
  482. Remove a specified model from the current coupled model, only callable while in a simulation.
  483. :param model: the model to remove as a child
  484. """
  485. if not hasattr(self, "full_name"):
  486. raise DEVSException("removeSubModel can only be called _during_ a simulation run")
  487. self.server.getSelfProxy().dsUnscheduleModel(model)
  488. def disconnectPorts(self, p1, p2):
  489. """
  490. Disconnect two ports
  491. .. note:: If these ports are connected multiple times, **only one** of them will be removed.
  492. :param p1: the port at the start of the connection
  493. :param p2: the port at the end of the connection
  494. """
  495. if not hasattr(self, "full_name"):
  496. raise DEVSException("removeSubModel can only be called _during_ a simulation run")
  497. new_connection = []
  498. found = False
  499. for p in p1.outline:
  500. if p == p2 and not found:
  501. found = True
  502. else:
  503. new_connection.append(p)
  504. p1.outline = new_connection
  505. new_connection = []
  506. found = False
  507. for p in p2.inline:
  508. if p == p1 and not found:
  509. found = True
  510. else:
  511. new_connection.append(p)
  512. p2.inline = new_connection
  513. self.server.getSelfProxy().dsDisconnectPorts(p1, p2)
  514. def connectPorts(self, p1, p2, z = None):
  515. """
  516. Connects two ports together. The coupling is to begin at p1 and
  517. to end at p2.
  518. :param p1: the port at the start of the new connection
  519. :param p2: the port at the end of the new connection
  520. :param z: the translation function for the events
  521. either input-to-input, output-to-input or output-to-output.
  522. """
  523. # For a coupling to be valid, two requirements must be met:
  524. # 1- at least one of the DEVS the ports belong to is a child of the
  525. # coupled-DEVS (i.e., self), while the other is either the
  526. # coupled-DEVS itself or another of its children. The DEVS'
  527. # 'parenthood relationship' uniquely determine the type of coupling;
  528. # 2- the types of the ports are consistent with the 'parenthood' of the
  529. # associated DEVS. This validates the coupling determined above.
  530. # Internal Coupling:
  531. if ((p1.host_DEVS.parent == self and p2.host_DEVS.parent == self) and
  532. (p1.type() == 'OUTPORT' and p2.type() == 'INPORT')):
  533. if p1.host_DEVS is p2.host_DEVS:
  534. raise DEVSException(("In coupled model '%s', connecting ports" +
  535. " '%s' and '%s' belong to the same model" +
  536. " '%s'. " +
  537. " Direct feedback coupling not allowed") % (
  538. self.getModelFullName(),
  539. p1.getPortFullName(),
  540. p2.getPortFullName(),
  541. p1.host_DEVS.getModelFullName()))
  542. else:
  543. p1.outline.append(p2)
  544. p2.inline.append(p1)
  545. # External input couplings:
  546. elif ((p1.host_DEVS == self and p2.host_DEVS.parent == self) and
  547. (p1.type() == p2.type() == 'INPORT')):
  548. p1.outline.append(p2)
  549. p2.inline.append(p1)
  550. # Eternal output couplings:
  551. elif ((p1.host_DEVS.parent == self and p2.host_DEVS == self) and
  552. (p1.type() == p2.type() == 'OUTPORT')):
  553. p1.outline.append(p2)
  554. p2.inline.append(p1)
  555. # Other cases (illegal coupling):
  556. else:
  557. raise DEVSException(("Illegal coupling in coupled model '%s' " +
  558. "between ports '%s' and '%s'") % (
  559. self.getModelName(), p1.getPortName(),
  560. p2.getPortName()))
  561. p1.z_functions[p2] = z
  562. if hasattr(self, "server"):
  563. # TODO modify
  564. self.server.getSelfProxy().dsConnectPorts(p1, p2)
  565. def setLocation(self, location, force=False):
  566. """
  567. Sets the location of this coupled model and its submodels if they don't have their own preference.
  568. :param location: the location to set
  569. :param force: whether or not to force this location, even if another is already defined
  570. """
  571. if self.location is None or force:
  572. self.location = location
  573. for child in self.component_set:
  574. child.setLocation(location, force)
  575. class RootDEVS(BaseDEVS):
  576. """
  577. The artificial RootDEVS model is the only 'coupled' model in the simulation after direct connection is performed.
  578. """
  579. def __init__(self, components, models, scheduler_type):
  580. """
  581. Basic constructor.
  582. :param components: the atomic DEVS models that are the cildren, only those that are ran locally should be mentioned
  583. :param models: all models that have to be passed to the scheduler, thus all models, even non-local ones
  584. :param scheduler_type: type of scheduler to use (string representation)
  585. """
  586. BaseDEVS.__init__(self, "ROOT model")
  587. self.component_set = components
  588. self.time_next = (float('inf'), 1)
  589. self.local_model_ids = set()
  590. for i in self.component_set:
  591. self.local_model_ids.add(i.model_id)
  592. self.models = models
  593. self.scheduler_type = scheduler_type
  594. def redoDirectConnection(self, ports):
  595. """
  596. Redo direct connection for a specified port, and all ports connected to it.
  597. :param ports: the ports that have changed.
  598. """
  599. # Find all changed ports and redo their direct connection
  600. worklist = list(ports)
  601. for outport in worklist:
  602. worklist.extend(outport.inline)
  603. for p in set(worklist):
  604. directConnectPort(p, self.listeners)
  605. def directConnect(self):
  606. """
  607. Perform direct connection on the models again
  608. """
  609. directConnect(self.models, self.listeners)
  610. def setScheduler(self, scheduler_type):
  611. """
  612. Set the scheduler to the desired type. Will overwite the previously present scheduler.
  613. :param scheduler_type: type of scheduler to use (string representation)
  614. """
  615. if isinstance(scheduler_type, tuple):
  616. try:
  617. exec("from pypdevs.schedulers.%s import %s" % scheduler_type)
  618. except:
  619. exec("from %s import %s" % scheduler_type)
  620. nr_models = len(self.models)
  621. self.scheduler = eval("%s(self.component_set, EPSILON, nr_models)"
  622. % scheduler_type[1])
  623. else:
  624. raise DEVSException("Unknown Scheduler: " + str(scheduler_type))
  625. def setGVT(self, gvt, activities, last_state_only):
  626. """
  627. Sets the GVT of this coupled model
  628. :param gvt: the time to which the GVT should be set
  629. :param activities: dictionary containing all activities for the models
  630. :param last_state_only: whether or not to use the last state for activity
  631. """
  632. for i in self.component_set:
  633. i.setGVT(gvt, activities, last_state_only)
  634. def fetchActivity(self, time, activities):
  635. """
  636. Fetch the activity of the model up to a certain time
  637. :param time: the time up to which the activity should be calculated
  638. :param activities: dictionary containing all activities for the models
  639. """
  640. for i in self.component_set:
  641. i.fetchActivity(time, activities)
  642. def revert(self, time, memorize):
  643. """
  644. Revert the coupled model to the specified time, all submodels will also
  645. be reverted.
  646. :param time: the time up to which revertion should happen
  647. :param memorize: whether or not the saved states should still be kept for memoization
  648. """
  649. reschedules = set()
  650. controller_revert = False
  651. for child in self.component_set:
  652. if child.time_last >= time:
  653. controller_revert |= child.revert(time, memorize)
  654. # Was reverted, so reschedule
  655. reschedules.add(child)
  656. # Always clear the inputs, as it is possible that there are only
  657. # partial results, which doesn't get found in the time_last >= time
  658. child.my_input = {}
  659. self.scheduler.massReschedule(reschedules)
  660. self.setTimeNext()
  661. return controller_revert
  662. def setTimeNext(self):
  663. """
  664. Reset the timeNext
  665. """
  666. try:
  667. self.time_next = self.scheduler.readFirst()
  668. except IndexError:
  669. # No element found in the scheduler, so put it to INFINITY
  670. self.time_next = (float('inf'), 1)
  671. class Port(object):
  672. """
  673. Class for DEVS model ports (both input and output). This class provides basic port attributes and query methods.
  674. """
  675. def __init__(self, is_input, name=None):
  676. """
  677. Constructor. Creates an input port if isInput evaluates to True, and
  678. an output port otherwise.
  679. :param is_input: whether or not this is an input port
  680. :param name: the name of the port. If None is provided, a unique ID is generated
  681. """
  682. self.inline = []
  683. self.outline = []
  684. self.host_DEVS = None
  685. self.msg_count = 0
  686. # The name of the port
  687. self.name = name
  688. self.is_input = is_input
  689. self.z_functions = {}
  690. def __str__(self):
  691. try:
  692. return self.getPortFullName()
  693. except:
  694. return "%s.%s" % (self.host_DEVS.getModelName(), self.getPortName())
  695. def getPortName(self):
  696. """
  697. Returns the name of the port
  698. :returns: local name of the port
  699. """
  700. return self.name
  701. def getPortFullName(self):
  702. """
  703. Returns the complete name of the port
  704. :returns: fully qualified name of the port
  705. """
  706. return "%s.%s" % (self.host_DEVS.getModelFullName(), self.getPortName())
  707. def type(self):
  708. """
  709. Returns the 'type' of the object
  710. :returns: either 'INPORT' or 'OUTPORT'
  711. """
  712. if self.is_input:
  713. return 'INPORT'
  714. else:
  715. return 'OUTPORT'
  716. def appendZ(first_z, new_z):
  717. if first_z is None:
  718. return new_z
  719. elif new_z is None:
  720. return first_z
  721. else:
  722. return lambda x: new_z(first_z(x))
  723. class ExternalWrapper(AtomicDEVS):
  724. def __init__(self, function):
  725. AtomicDEVS.__init__(self, "Fake")
  726. self.f = function
  727. self.model_id = None
  728. self.full_name = None
  729. def extTransition(self, inputs):
  730. # Fake object is created with a single fake port, so unpack that
  731. self.f(self.my_input.values()[0])
  732. def directConnectPort(outport, listeners):
  733. # The new contents of the line
  734. outport.routing_outline = []
  735. worklist = [(p, outport.z_functions.get(p, None))
  736. for p in outport.outline]
  737. for outline, z in worklist:
  738. if outline in listeners.keys():
  739. # This port is being listened on, so just add it as a fake model
  740. fake_port = Port(is_input=False,name="Fake")
  741. fake_port.host_DEVS = ExternalWrapper(listeners[outline])
  742. outport.routing_outline.append((fake_port, z))
  743. # If it is a coupled model, we must expand this model
  744. if isinstance(outline.host_DEVS, CoupledDEVS):
  745. for inline in outline.outline:
  746. # Add it to the current iterating list, so we can just continue
  747. entry = (inline, appendZ(z, outline.z_functions.get(inline, None)))
  748. worklist.append(entry)
  749. # If it is a Coupled model, we should just continue
  750. # expanding it and not add it to the finished line
  751. if not isinstance(inline.host_DEVS, CoupledDEVS):
  752. entry = (inline, appendZ(z, outline.z_functions.get(inline, None)))
  753. outport.routing_outline.append(entry)
  754. else:
  755. for ol, z in outport.routing_outline:
  756. if ol == outline:
  757. break
  758. else:
  759. # Add to the new line if it isn't already there
  760. # Note that it isn't really mandatory to check for this,
  761. # it is a lot cleaner to do so.
  762. # This will greatly increase the complexity of the connector though
  763. outport.routing_outline.append((outline, z))
  764. def directConnect(component_set, listeners):
  765. """
  766. Perform direct connection on this CoupledDEVS model
  767. :param component_set: the iterable to direct connect
  768. :returns: the direct connected component_set
  769. """
  770. new_list = []
  771. # Search for root model
  772. root = component_set[0]
  773. while root.parent is not None:
  774. root = root.parent
  775. component_set = [root]
  776. for i in component_set:
  777. if isinstance(i, CoupledDEVS):
  778. component_set.extend(i.component_set)
  779. else:
  780. # Found an atomic model
  781. new_list.append(i)
  782. # Also perform direct connection on all ports of the Coupled DEVS models, should injection ever be wanted
  783. for i in component_set:
  784. # Remap the output ports
  785. for outport in i.OPorts:
  786. directConnectPort(outport, listeners)
  787. if isinstance(i, CoupledDEVS):
  788. for inport in i.IPorts:
  789. directConnectPort(inport, listeners)
  790. return new_list