Skip to content

4. Building DEVS Models

Román Cárdenas edited this page Jul 23, 2022 · 15 revisions

The following UML class diagram contains EVERYTHING you need to develop DEVS models with Cadmium.

UML Class Diagram of Cadmium 2 Models.

Ports: interfaces for sending and receiving messages

Cadmium uses ports to model the generation and propagation of events. The cadmium::PortInterface class is an interface that describes the basic behavior of a port, regardless of other implementation details. You won't have to deal with cadmium::PortInterface objects. The only thing that you need to know is that you can use the following methods to check the status of a port:

  • bool empty() const: it checks whether or not the port contains one or more messages.
  • std::size_t size() const: it returns the number of messages in the port.

cadmium::Port<T> objects are ports that can only hold message of the type T. For example, a port of type cadmium::Port<int> only accepts integer number messages. You can use the following methods for dealing with a cadmium::Port<T> object:

  • const std::vector<T>& getBag() const: it returns a reference to a vector with all the messages in the port. You will use this method in the external transition function of your atomic models to read input events.
  • void addMessage(T msg): it adds a new message to the port. You will use this method in the output function of your atomic models to send output events.

Most of the times, you will only deal with objects of the class cadmium::Port<T>. However, if the message type of the port is complex/has a lot of fields, you may consider to use a cadmium::BigPort<T>. Big ports store messages using pointers to the message. It is expensive to create a new pointer to a message. However, pointers are very cheap to clone. There is not a magic rule to choose a cadmium::Port<T> or a cadmium::BigPort<T>. If you want to send simple messages (e.g., a number, a boolean, etc.), use cadmium::Port<T>. On the other hand, if you want to send a big structure with vectors with thousands of elements, use cadmium::BigInPort<T>. If you are not sure, just try with both and check which port implementation is the fastest.

Important consideration port message types

We need to define the insertion operator (<<) for the message type of a port. The insertion operator in C++ allows us to print a data structure as a string. Cadmium needs you to define this operator so it can log messages during the simulation. If you use built-in types such as integer numbers, you don't need to worry about this. C++ already implements the insertion operator for you. Otherwise, if you want to store messages with your own data structures, you will need to implement it.

Components: a common interface for atomic and coupled models

In Cadmium, atomic and coupled models inherit from the cadmium::Component class. You will never deal directly with cadmium::Component objects. However, they provide useful methods for both atomic and coupled models.

Adding ports to a component

Components receive input events via one of their input ports. Alternatively, they send output events via one of their output ports. You can use the following methods of a DEVS component to create input and output ports:

  • std::shared_ptr<cadmium::Port<T>> addInPort<T>(std::string portId): the component creates a new port that only accepts messages of type T. This new port is added to the input ports of the component. Finally, the component returns a pointer to this new port. We can then store this pointer in an attribute of our model so we can read incoming messages easily.
  • std::shared_ptr<cadmium::BigPort<T>> addInBigPort<T>(std::string portId): this method is equivalent to addInPort<T>. However, the component creates a big port. Use this method if you want to receive messages that are expensive to copy.
  • std::shared_ptr<cadmium::Port<T>> addOutPort<T>(std::string portId): the component creates a new port that only accepts messages of type T. This new port is added to the output ports of the component. Finally, the component returns a pointer to this new port. We can then store this pointer in an attribute of our model so we can add output messages easily.
  • std::shared_ptr<cadmium::BigPort<T>> addOutBigPort<T>(std::string portId): this method is equivalent to addOutPort<T>. However, the component creates a big port. Use this method if you want to send messages that are expensive to copy.

You can use the following methods to check if your component has input/output events in its ports:

  • bool inEmpty() const: it returns true if all the input ports are empty.
  • bool outEmpty() const: it returns true if all the output ports are empty.

Important considerations regarding ports

Your component can have multiple input/output ports, each of them with different message types. You can even have ports and big ports simultaneously. However, each port must have a unique ID. When adding new input/output ports, make sure that each port has a different name. Otherwise, your component won't work.

You must NEVER try to create new ports by your own. Adding an input/output port to a component is not as straightforward as someone may think. Always use the previously mentioned methods to create ports. The component will take care of all these details for you and it will forward you a pointer to the newly created port so you can use it.

Atomic models

cadmium::Atomic<S> is an abstract class that allows us to describe atomic DEVS models. Our atomic models will inherit from this class. The S template argument corresponds to the data type we want to use to represent the state of our model. Let us assume that you want to implement your atomic model MyAtomic. The state of this model represented by the MyAtomicState structure. Your model has one input port for messages of type int and an output big port for messages of type BigMessage. The first lines of your implementation of the MyAtomic class would look like this:

include <cadmium/core/modeling/atomic.hpp>  // We need to import the cadmium::Atomic<S> class

// MyAtomic inherits from Atomic<MyState> (i.e., it is an atomic model which state corresponds to a MyState object).
class MyAtomic: public cadmium::Atomic<MyState> {  
  public:
    // We expose input and output ports of our model as public attributes.
    // Note that they are pointers to ports, not ports directly!
    std::shared_ptr<cadmium::Port<int>> inIntegers;  
    std::shared_ptr<cadmium::BigPort<BigMessage>> outBigMessages;
    
    // we need to provide an initial state to our model!
    MyAtomic(std::string id): cadmium::Atomic<MyState>(id, MyState(...)) {
        inIntegers = addInPort<int>("inIntegers");  // Adding input ports is as simple as this!
        outBigMessages = addOutBigPort<BigMessage>("outBigMessages");  // Big ports are also easy to use.
    }
    ...

Next, let's define the actual behavior of the atomic model. To do so, we MUST OVERRIDE the following methods of the class cadmium::Atomic<S>:

    ...
    double timeAdvance(const MyState& state) const override {
        return <time to wait before triggering the internal transition function>;
    }

    void output(MyState& state) const override {
        // Here, we can add message to output ports:
        outBigMessages->addMessage(<message to be sent>);
    }

    void internalTransition(MyState& state) const override {
        state = <new state due to internal transition>;
    }

    void externalTransition(MyState& state, double e) const override {
        we can read input messages from a port like this:
        for (const auto& m: inIntegers.getBag()) {
            // m is an input message!
        }
        state = <new state due to external transition>;
    }
   
    // We don't need to override the confluentTransition function. This is the default behavior.
    void confluentTransition(MyState& state, double e) const override {
        externalTransition(internalTransition(state), 0);
    }
}

Important consideration atomic model state types

We need to define the insertion operator (<<) for the state type of an atomic model. The insertion operator in C++ allows us to print a data structure as a string. Cadmium needs you to define this operator so it can log model states during the simulation. If you use built-in types such as integer numbers, you don't need to worry about this. C++ already implements the insertion operator for you. Otherwise, if you want to define atomic model states with your own data structures, you will need to implement it.

Coupled models

To do.

First, let's look at the steps we need to follow to develop an atomic DEVS model.

Building Atomic DEVS Models

Defining the state type S

Our atomic model must inherit from the cadmium::Atomic<S> class. S is a template argument to determine which data type is used to represent the state of the atomic model. If you want it to be an integer number, your model must inherit from cadmium::Atomic<int>. However, most of the times you will want to use your custom class. Let us assume that you want to represent the state in a phase-sigma fashion. Then, we have to define the PhaseSigmaState structure:

#include <limits>

struct PhaseSigmaState {
    std::string phase;  // phase (or state) of the atomic model
    double sigma;       // time to wait until the next internal transition
    PhaseSigmaState(std::string phase, double sigma): phase(phase), sigma(sigma) {}
}

We must overwrite the insertion (<<) operator for our state data structure. The insertion operator allows us to tell Cadmium how to represent the state of our model as a string. In our case, we define << as follows:

std::ostream& operator<<(std::ostream &out, const PhaseSigmaState& s) {
	out << "(" << s.phase << "," << s.sigma << ")";
	return out;
}

Defining the behavior of the atomic model

Now, let's define the behavior of our atomic model. We will define a very simple model. It has two input ports: inNewSigma and inNewPhase. The model also has one output port: outNewPhase. This port only outputs strings.

include <cadmium/core/modeling/atomic.hpp>

class MyAtomic: public cadmium::Atomic<PhaseSigmaState> {
  public:
    std::shared_ptr<cadmium::Port<double>> inNewSigma;
    std::shared_ptr<cadmium::Port<std::string>> inNewPhase;
    std::shared_ptr<cadmium::Port<std::string>> outNewPhase;

    MyAtomic(): cadmium::Atomic<PhaseSigmaState>(PhaseSigmaState("initial", 0.)) {
        inNewSigma = addInPort<double>("inNewSigma");
        inNewPhase = addInPort<std::string>("inNewPhase");
        outNewPhase = addOutPort<std::string>("outNewPhase");
    }
    // it continues below...

We define the ports of our model as public attributes of our new class. Note that we NEVER work with cadmium::Port<T> data structures directly. We ALWAYS work with std::shared_ptr<cadmium::Port<T>> structures. The ports are created in the constructor function. We use the methods addInPort<T> and addOutPort<T> to create the new ports. T is a template argument that defines the data type of the messages that the port accepts. For instance, inNewSigma port only accepts string messages. If you try to send an integer number via the inNewSigma port, your code will not compile.

When receiving a new message, the model sets its phase to the value of the message. When receiving a new message, the model sets its sigma to the value of the message.

To do.

Clone this wiki locally