Examples for Dynamic Structure DEVS

We used the same approach to Dynamic Structure as adevs, meaning that a modelTransition method will be called at every step in simulated time on every model that transitioned.

A small trafficLight model is included in the examples folder of the PyPDEVS distribution. In this section, we introduce the use of the new constructs.

Dynamic structure is possible for both Classic and Parallel DEVS, but only for local simulation.

Starting point

We will start from a very simple Coupled DEVS model, which contains 2 Atomic DEVS models. The first model sends messages on its output port, which are then routed to the second model. Note that, for compactness, we include the experiment part in the same file as the model.

This can be expressed as follows (base_dsdevs.py):

from pypdevs.DEVS import AtomicDEVS, CoupledDEVS
from pypdevs.simulator import Simulator

class Root(CoupledDEVS):
    def __init__(self):
        CoupledDEVS.__init__(self, "Root")
        self.models = []
        # First model
        self.models.append(self.addSubModel(Generator()))
        # Second model
        self.models.append(self.addSubModel(Consumer(0)))
        # And connect them
        self.connectPorts(self.models[0].outport, self.models[1].inport)

class Generator(AtomicDEVS):
    def __init__(self):
        AtomicDEVS.__init__(self, "Generator")
        # Keep a counter of how many events were sent
        self.outport = self.addOutPort("outport")
        self.state = 0

    def intTransition(self):
        # Increment counter
        return self.state + 1

    def outputFnc(self):
        # Send the amount of messages sent on the output port
        return {self.outport: [self.state]}

    def timeAdvance(self):
        # Fixed 1.0
        return 1.0

class Consumer(AtomicDEVS):
    def __init__(self, count):
        AtomicDEVS.__init__(self, "Consumer_%i" % count)
        self.inport = self.addInPort("inport")

    def extTransition(self, inputs):
        for inp in inputs[self.inport]:
            print("Got input %i on model %s" % (inp, self.name))

sim = Simulator(Root())
sim.setTerminationTime(5)
sim.simulate()

All undefined functions will just use the default implementation. As such, this model will simply print messages:

Got input 0 on model Consumer_0
Got input 1 on model Consumer_0
Got input 2 on model Consumer_0
Got input 3 on model Consumer_0
Got input 4 on model Consumer_0

We will now extend this model to include dynamic structure.

Dynamic Structure

To allow for dynamic structure, we need to augment both our experiment and our model.

Experiment

First, the dynamic structure configuration parameter should be enabled in the experiment. For performance reasons, this feature is disabled by default. It can be enabled as follows:

sim = Simulator(Root())
sim.setTerminationTime(5)
sim.setDSDEVS(True)
sim.simulate()

Model

With dynamic structure enabled, models can define a new method: modelTransition(state). This method is called on all Atomic DEVS models that executed a transition in that time step.

This method is invoked on the model, and can thus access the state of the model. For now, ignore the state parameter. In this method, all modifications on the model are allowed, as it was during model construction. That is, it is possible to invoke the following methods:

setInPort(portname)
setOutPort(portname)
addSubModel(model)
connectPorts(output, input)

But apart from these known methods, it is also possible to delete existing constructs, with the operations:

removePort(port)
removeSubModel(model)
disconnectPorts(output, input)

On Atomic DEVS models, only the port operations are available. On Coupled DEVS models, all of these are available. Removing a port or submodel will automatically disconnect all its connections.

The method will also return a boolean, indicating whether or not to propagate the structural changes on to the parent model. If it is True, the method is invoked on the parent as well. Note that the root model should not return True. Propagation is necessary, as models are only allowed to change the structure of their subtree.

Note

In the latest implementation, modifying the structure outside of your own subtree has no negative consequences. However, it should be seen as a best practice to only modify yourself.

For example, to create a second receiver as soon as the generator has output 3 messages, you can modify the following methods (simple_dsdevs.py):

class Generator(AtomicDEVS):
    ...
    def modelTransition(self, state):
        # Notify parent of structural change if state equals 3
        return self.state == 3

class Root(CoupledDEVS):
    ...
    def modelTransition(self, state):
        # We are notified, so are required to add a new model and link it
        self.models.append(self.addSubModel(Consumer(1)))
        self.connectPorts(self.models[0].outport, self.models[-1].inport)

        ## Optionally, we could also remove the Consumer(0) instance as follows:
        # self.removeSubModel(self.models[1])

        # Always returns False, as this is top-level
        return False

This would give the following output (or similar, due to concurrency):

Got input 0 on model Consumer_0
Got input 1 on model Consumer_0
Got input 2 on model Consumer_0
Got input 3 on model Consumer_0
Got input 3 on model Consumer_1
Got input 4 on model Consumer_0
Got input 4 on model Consumer_1

Note

As structural changes are not a common operation, their performance is not optimized extensively. To make matters worse, many structural optimizations done by PythonPDEVS will automatically be redone after each structural change.

Passing state

Finally, we come to the state parameter of the modelTransition call. In some cases, it will be necessary to pass arguments to the parent, to notify it of how the structure should change. This is useful if the child knows information that is vital to the change. Since Coupled DEVS models cannot hold state, and should not directly access the state of their children, we can use the state parameter for this.

The state parameter is simply a dictionary object, which is passed between all the different modelTransition calls. Simply put, it is an object shared by all calls.

For example, if we would want the structural change from before to create a new consumer every time, with an ID provided by the Generator, this can be done as follows (state_dsdevs.py):

class Generator(AtomicDEVS):
    ...
    def modelTransition(self, state):
        # We pass on the ID that we would like to create, which is equal to our counter
        state["ID"] = self.state
        # Always create a new element
        return True

class Root(CoupledDEVS):
    ...
    def modelTransition(self, state):
        # We are notified, so are required to add a new model and link it
        # We can use the ID provided by the model below us
        self.models.append(self.addSubModel(Consumer(state["ID"])))
        self.connectPorts(self.models[0].outport, self.models[-1].inport)

        # Always returns False, as this is top-level
        return False

This would then create the output (or similar, due to concurrency):

Got input 0 on model Consumer_0
Got input 1 on model Consumer_0
Got input 1 on model Consumer_1
Got input 2 on model Consumer_0
Got input 2 on model Consumer_1
Got input 2 on model Consumer_2
Got input 3 on model Consumer_0
Got input 3 on model Consumer_1
Got input 3 on model Consumer_2
Got input 3 on model Consumer_3

More complex example

In the PyPDEVS distribution, a more complex example is provided. That example provides a model of two traffic lights, with a policeman who periodically changes the traffic light he is interrupting.