Skip to content

4. Building DEVS Models

Román Cárdenas edited this page Feb 1, 2023 · 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 directly. 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 returns true if 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. This reference is constant, which means that you won't be able to add any message to the bag.
  • 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 no 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 for you.

Important consideration for 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 represent 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:

  • 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 reference to this new port. We can then store this reference in an attribute of our model so we can read incoming messages easily.
  • 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.
  • 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 reference to this new port. We can then store this reference in an attribute of our model so we can add output messages easily.
  • 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 reference to the newly created port so you can use it safely.

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, one input big port for messages of type BigMessage, 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.
    cadmium::Port<int> inIntegers;  
    cadmium::BigPort<BigMessage> inBigMessages;
    cadmium::BigPort<BigMessage> outBigMessages;
    
    // we need to provide an initial state to our model!
    MyAtomic(std::string id, MyState initialState): cadmium::Atomic<MyState>(id, initialState) {
        inIntegers = addInPort<int>("inIntegers");  // Adding input ports is as simple as this!
        inBigMessages = addInBigPort<BigMessage>("inBigMessages");  // We can have input ports of different types
        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 considerations about 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 represent 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

cadmium::Coupled is a base class that allows us to describe coupled DEVS models. Our coupled models will inherit from this class. Defining coupled models in Cadmium is very easy, you just need to learn these two new methods:

  • std::shared_ptr<C> addComponent(Args&& ...args): this method creates a new DEVS component of type C and adds it to the component set of the coupled model. C must be either an atomic or a coupled model. This function has a variadic number of arguments. It adapts to the constructor function of the model being built. Then, it returns a pointer to the newly created component.
  • void addCoupling(std::shared_ptr<cadmium::PortInterface> portFrom, std::shared_ptr<cadmium::PortInterface> portTo): It adds a new coupling between a source port portFrom and a destination port portTo. This method internally checks that both ports are compatible (i.e., their message types is the same) and that the coupling is valid (e.g., the ports belong to a component). If something is wrong, Cadmium will complain and tell you what needs to be fixed.

Let us assume that you want to implement your coupled model MyCoupled. This model has one input port for messages of type int and an output big port for messages of type BigMessage. It also has two components of type MyAtomic. Your coupled model will look like this:

#include <cadmium/core/modeling/coupled>  // We need to import the cadmium::Coupled class

// MyCoupled inherits from cadmium::Coupled.
class MyCoupled: public cadmium::Coupled {  
  public:
    // We expose input and output ports of our model as public attributes.
    // Note that they are pointers to ports, not ports directly!
    cadmium::Port<int> inIntegers;  
    cadmium::BigPort<BigMessage> outBigMessages;
    
    MyCoupled(std::string id): cadmium::Coupled(id) {
        inIntegers = addInPort<int>("inIntegers");  // Adding input ports is as simple as this!
        outBigMessages = addOutBigPort<BigMessage>("outBigMessages");  // Big ports are also easy to use.
        
        // Adding a new component is that easy!
        // We need to say explicitly which model type we want to use for our new component
        // The number of parameters to create a new model depends on the model type.
        // This "magic" method adapts to your model constructor functions!
        auto myAtomic1 = addComponent<MyAtomic>("myAtomic1", MyState(...));
        auto myAtomic2 = addComponent<MyAtomic>("myAtomic2", MyState(...));
        
        // Do you want an EIC? Try this:
        addCoupling(inIntegers, myAtomic1->inIntegers);
        // An IC instead? here you are:
        addCoupling(myAtomic1->outBigMessages, myAtomic2->inBigMessages);
        // EOC as very similar:
        addCoupling(myAtomic->outBigMessages, outBigMessages);
    }
}

And that's it! we are ready to deploy our first DEVS models with Cadmium.