Skip to content

4. Building DEVS Models

Román Cárdenas edited this page Jul 21, 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 check if a port contains one or more messages via the bool empty() const method. If you want to know the number of messages in the port, you can use the std::size_t size() const method.

cadmium::Port<T> objects are ports that can only holds message of the type T. For example, a port of type cadmium::Port<int> only accepts integer number messages. You can read the messages in the port with the const std::vector<T>& getBag() const method. This method returns a vector with all the messages in the port. If the port type is cadmium::Port<int>, the return vector is of type std::vector<int> (i.e., it only contains integer numbers). On the other hand, if you want to add a new message to the port, you can use the void addMessage(T msg) method. If the port type is cadmium::Port<int>, you can only add integer numbers. Otherwise, your program won't compile.

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/it is computationally expensive to clone, you may consider to use a cadmium::BigPort<T>. Big ports store messages using pointers to the message. Pointer are cheap to copy. However, they are expensive to create. 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: you must NOT create ports directly

Cadmium DEVS models NEVER work with cadmium::Port<T> data structures directly. Instead, they use std::shared_ptr<cadmium::Port<T>> pointers. Shared pointers are very useful for internal reasons of the library. In the next section, we tell you how to properly create ports.

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. Components have input ports and output ports.

Atomic models

To do.

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.

    // ... the atomic model definition continues here

    double timeAdvance(const PhaseSigmaState& state) const override {
        return state.sigma;
    }

    void output(PhaseSigmaState& state) const override {
        outNewPhase->addMessage(state.phase);
    }

    void internalTransition(PhaseSigmaState& state) override {
        state.sigma = std::numeric_limits<double>::infinity();
    }

    void externalTransition(PhaseSigmaState& state, double e) override {
        state.sigma = std::numeric_limits<double>::infinity();
    }

    // We won't override the confluentTransition function, as we want to use the default one.
}

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