examples.rst 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297
  1. ..
  2. Copyright 2014 Modelling, Simulation and Design Lab (MSDL) at
  3. McGill University and the University of Antwerp (http://msdl.cs.mcgill.ca/)
  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. http://www.apache.org/licenses/LICENSE-2.0
  8. Unless required by applicable law or agreed to in writing, software
  9. distributed under the License is distributed on an "AS IS" BASIS,
  10. WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  11. See the License for the specific language governing permissions and
  12. limitations under the License.
  13. Examples for Parallel DEVS
  14. ==========================
  15. A small *trafficModel* and corresponding *trafficExperiment* file is included in the *examples* folder of the PyPDEVS distribution. This (completely working) example is slightly too big to use as a first introduction to PyPDEVS and therefore this page will start with a very simple example.
  16. For this, we will first introduce a simplified queue model, which will be used as the basis of all our examples. The complete model can be downloaded: :download:`queue_example.py <queue_example.py>`.
  17. This section should provide you with all necessary information to get you started with creating your very own PyPDEVS simulation. More advanced features are presented in the next section.
  18. Generator
  19. ---------
  20. Somewhat simpler than a queue even, is a generator. It will simply create a message to send after a certain delay and then it will stop doing anything.
  21. Informally, this would result in a DEVS specification as:
  22. * Time advance function returns the waiting time to generate the message, infinity after the message was created
  23. * Output function outputs the generated message
  24. * Internal transition function marks the generator as done
  25. * External transition function will never happen (as there are no inputs)
  26. * Confluent transition function will never happen (as no external transition will ever happen)
  27. In PythonPDEVS, this simply becomes the class::
  28. class Generator(AtomicDEVS):
  29. def __init__(self):
  30. AtomicDEVS.__init__(self, "Generator")
  31. self.state = True
  32. self.outport = self.addOutPort("outport")
  33. def timeAdvance(self):
  34. if self.state:
  35. return 1.0
  36. else:
  37. return INFINITY
  38. def outputFnc(self):
  39. # Our message is simply the integer 5, though this could be anything
  40. # It is mandatory to output lists (which signify the 'bag')
  41. return {self.outport: [5]}
  42. def intTransition(self):
  43. self.state = False
  44. return self.state
  45. Note that all functions have a *default* behaviour, which is sufficient if the function will never be called. In most situations, the *confTransition* function is sufficient and rarely requires to be overwritten.
  46. It is possible to simulate this model, though nothing spectacular will happen. For this reason, we will postpone actual simulation examples.
  47. Simple queue
  48. ------------
  49. To couple the *Generator* model up to something useful, we will now create a simple queue model. It doesn't do any real computation and just forwards the message after a certain (fixed) time delay. For simplicity, we allow the queue to *drop* the previous message if a message was already being processed.
  50. Informally, this would result in a DEVS specification as:
  51. * Time advance function returns the processing time if a message is being processed, or INFINITY otherwise
  52. * Output function outputs the message
  53. * Internal transition function removes the message from the queue
  54. * External transition function adds the message to the queue
  55. * Confluent transition function can simply default to the internal transition function, immeditaly followed by the external transition function
  56. To implement this in PythonPDEVS, one simply has to write::
  57. class Queue(AtomicDEVS):
  58. def __init__(self):
  59. AtomicDEVS.__init__(self, "Queue")
  60. self.state = None
  61. self.processing_time = 1.0
  62. self.inport = self.addInPort("input")
  63. self.outport = self.addOutPort("output")
  64. def timeAdvance(self):
  65. if self.state is None:
  66. return INFINITY
  67. else:
  68. return self.processing_time
  69. def outputFnc(self):
  70. return {self.outport: [self.state]}
  71. def extTransition(self, inputs):
  72. # To access them, you will need to access each element seperately
  73. # In this case, we only use the first element from the bag as we
  74. # know that it will only contain one element
  75. self.state = inputs[self.inport][0]
  76. return self.state
  77. def intTransition(self):
  78. self.state = None
  79. return self.state
  80. And we are done with our basic queue model.
  81. However, there is currently no means of testing it, as simply simulating this model will have no effect, due to no messages arriving. We will thus have to couple it with the *Generator* we previously made.
  82. Coupling
  83. --------
  84. To couple up the *Generator* to the *Queue*, all we have to do is create a *CoupledDEVS* class and simulate this class::
  85. class CQueue(CoupledDEVS):
  86. def __init__(self):
  87. CoupledDEVS.__init__(self, "CQueue")
  88. self.generator = self.addSubModel(Generator())
  89. self.queue = self.addSubModel(Queue())
  90. self.connectPorts(self.generator.outport, self.queue.inport)
  91. That is all for the coupled model. Note that it is not required for every port of a model to be connected to another port. For example the *outport* of the *Queue* is not connected. Any output that is put on this port is thus discarded.
  92. It is perfectly allowed to do model construction and connection in e.g. a loop or conditionally, as long as the required functions are called.
  93. .. note:: The DEVS formalism allows for an input-to-output translation function, but this is not implemented in PythonPDEVS.
  94. Simulation
  95. ----------
  96. Now that we have an actual coupled model that does something remotely useful, it is time to simulate it. Simulation is as simple as constructing a *Simulator* object with the model and calling *simulate()* on the simulator::
  97. model = CQueue()
  98. sim = Simulator(model)
  99. sim.simulate()
  100. Sadly, nothing seems to happen because no tracers are enabled. Note that it is possible to access the attributes of the model and see that they are actually changed as directed by the simulation::
  101. model = CQueue()
  102. print(model.generator.state)
  103. sim = Simulator(model)
  104. sim.simulate()
  105. print(model.generator.state)
  106. This code will simply print *True* in the beginning and *False* at the end, since the model is updated in-place in this situation. The model will **not** be simulated in-place if either simulation is distributed, or reinitialisation is enabled.
  107. Tracing
  108. -------
  109. To actually see some results from the simulation, it is advised to enable certain tracers. The simplest tracer is the *verbose* tracer, which will output some details in a human-readable format. Enabling the verbose tracer is as simple as setting the *setVerbose()* configuration to a destination file. For the verbose tracer, it is also possible to trace to stdout by using the *None* argument::
  110. model = CQueue()
  111. sim = Simulator(model)
  112. sim.setVerbose(None)
  113. sim.simulate()
  114. Saving the output to a file can de done by passing the file name as a string. Note that a file handle does **not** work::
  115. model = CQueue()
  116. sim = Simulator(model)
  117. sim.setVerbose("myOutputFile")
  118. sim.simulate()
  119. Multiple tracers can be defined simultaneously, all of which will be used. So to trace to the files *myOutputFile* and *myOutputFile* and simultaneously output to stdout, you could use::
  120. model = CQueue()
  121. sim = Simulator(model)
  122. sim.setVerbose("myOutputFile")
  123. sim.setVerbose(None)
  124. sim.setVerbose("myOutputFile2")
  125. sim.simulate()
  126. .. note:: There is no way to unset a single tracer. There is however a way to remove all currently registered tracers: *setRemoveTracers()*, though it is generally only useful in reinitialized simulations.
  127. An example output of the *verbose* tracer is::
  128. __ Current Time: 0.00 __________________________________________
  129. INITIAL CONDITIONS in model <CQueue.Generator>
  130. Initial State: True
  131. Next scheduled internal transition at time 1.00
  132. INITIAL CONDITIONS in model <CQueue.Queue>
  133. Initial State: None
  134. Next scheduled internal transition at time inf
  135. __ Current Time: 1.00 __________________________________________
  136. EXTERNAL TRANSITION in model <CQueue.Queue>
  137. Input Port Configuration:
  138. port <input>:
  139. 5
  140. New State: 5
  141. Next scheduled internal transition at time 2.00
  142. INTERNAL TRANSITION in model <CQueue.Generator>
  143. New State: False
  144. Output Port Configuration:
  145. port <outport>:
  146. 5
  147. Next scheduled internal transition at time inf
  148. __ Current Time: 2.00 __________________________________________
  149. INTERNAL TRANSITION in model <CQueue.Queue>
  150. New State: None
  151. Output Port Configuration:
  152. port <output>:
  153. 5
  154. Next scheduled internal transition at time inf
  155. .. note:: Several other tracers are available, such as *VCD*, *XML* and *Cell*. Their usage is very similar and is only useful in several situations. Only the *Cell* tracer requires further explanation and is mentioned in the *Advanced examples* section.
  156. Termination
  157. -----------
  158. Our previous example stopped simulation automatically, since both models returned a time advance equal to infinity.
  159. In several cases, it is desired to stop simulation after a certain period. The simplest example of this is when the *Generator* would keep generating messages after a certain delay. Without a termination condition, the simulation will keep going forever.
  160. Adding a termination time is as simple as setting one additional configuration option::
  161. sim.setTerminationTime(5.0)
  162. This will make the simulation stop as soon as simulation time 5.0 is reached.
  163. A termination time is sufficient in most situations, though it is possible to use a more advanced approach: using a termination function. Using the option::
  164. sim.setTerminationCondition(termFunc)
  165. With this additional option, the function *termFunc* will be evaluated at every timestep. If the function returns *True*, simulation will stop. The function will receive 2 parameters from the simulator: the model being simulated and the current simulation time.
  166. Should our generator save the number of messages it has generated, an example of such a termination function could be::
  167. def termFunc(clock, model):
  168. if model.generator.generated > 5:
  169. # The generator has generated more than 5 events
  170. # So stop
  171. return True
  172. elif clock[0] > 10:
  173. # Or if the clock has progressed past simulation time 10
  174. return True
  175. else:
  176. # Otherwise, we simply continue
  177. return False
  178. The *clock* parameter in the termination condition will be a **tuple** instead of a simple floating point number. The first field of the tuple is the current simulation time (and can be used as such). The second field is a so-called *age* field, containing the number of times the same simulation time has occured. This is passed on in the termination condition as it is required in some cases for distributed simulation.
  179. .. note:: Using a termination function is a lot slower than simply using a termination time. This option should therefore be avoided if at all possible.
  180. .. warning:: It is **only** allowed to read from the model in the termination function. Performing write actions to the model has unpredictable consequences!
  181. .. warning:: Running a termination function in a distributed simulation is slightly different, so please refer to the advanced section for this!
  182. Simulation time
  183. ---------------
  184. Accessing the global simulation time is a frequent operation, though it is not supported by DEVS out-of-the-box. Of course, the simulator internally keeps such a clock, though this is not meant to be accessed by the user directly as it is an implementation detail of PyPDEVS (and it might even change between releases!).
  185. If you require access to the simulation time, e.g. to put a timestamp on a message, this can be done by writing some additional code in the model that requires this time as follows::
  186. class MyModelState():
  187. def __init__(self):
  188. self.actual_state = ...
  189. self.current_time = 0.0
  190. class MyModel(AtomicDEVS):
  191. def __init__(self, ...):
  192. AtomicDEVS.__init__(self, "ExampleModel")
  193. self.state = MyModelState()
  194. ...
  195. def extTransition(self, inputs):
  196. self.state.current_time += self.elapsed
  197. ...
  198. return self.state
  199. def intTransition(self):
  200. self.state.current_time += self.timeAdvance()
  201. ...
  202. return self.state
  203. def confTransition(self, inputs):
  204. self.state.current_time += self.timeAdvance()
  205. ...
  206. return self.state
  207. In the *extTransition* method, we use the *elapsed* attribute to determine the time between the last transition and the current transition. However, in the *intTransition* we are **not** allowed to access it.
  208. A more detailed explanation can be found at :ref:`elapsed_time`.
  209. You are allowed to call the *timeAdvance* method again, as this is the time that was waited before calling the internal transition function (as defined in the DEVS formalism).
  210. This requires, however, that your timeAdvance is deterministic (as it should be).
  211. Deterministic timeAdvance functions are not trivial if you use random numbers, for which you should read up on :ref:`random_numbers` in PythonPDEVS.