-
Notifications
You must be signed in to change notification settings - Fork 13
5. Examples of DEVS Models
Here we illustrate how to develop DEVS models with Cadmium 2. All the examples explained here are implemented and available in the example folder of the repo.
The Generator-Processor-Transducer model is a classic example in the DEVS community. This model consists of three atomic models (Generator, Processor, and Transducer) and a coupled model (GPT) that interconnects the atomic models. The GPT model looks as follows:

The Generator model creates new a new job every jobPeriod seconds and sends them via its outGenerated port. The Processor model receives newly generated jobs through its inGenerated port and processes them in a first-come-first-served basis. The Processor model tasks processingTime seconds to process a new job. The Processor model sends processed jobs via its outProcessed port. The Transducer model monitors the generated and processed jobs and computes some statistics (e.g., job processing throughput). After obsTime seconds, it sends a message via its outStop port to tell the Generator model to stop creating new jobs. The Generator model receives this message via its inStop port.
We must first implement all the custom data types of the ports in our model. In this scenario, there are only two port data types: bool and Job. The bool data type is a built-in data type of C++, so we don't have to worry about it. However, the Job data type is specific of our model, and we must first define it.
struct Job {
int id; //!< Job ID number.
double timeGenerated; //!< Time in which the job was created.
double timeProcessed; //!< Time in which the job was processed. If -1, the job has not been processed yet.
Job(int id, double timeGenerated): id(id), timeGenerated(timeGenerated), timeProcessed(-1) {}
};The id field corresponds to the unique ID of the job. The time in which the job was created is timeGenerated, and timeProcessed defines the time in which the job was processed. When creating a new Job message, the Generator model must define an ID and a generation time. However, as the job has not been processed yet, timeProcessed is initially set to -1.
In Cadmium 2, all the port data types of your model must implement the insertion (<<) operator. In this way, Cadmium will be able to log messages during a simulation. We implement this operator as follows:
#include <iostream>
std::ostream& operator<<(std::ostream& out, const Job& j) {
out << "{" << j.id << "," << j.timeGenerated << "," << j.timeProcessed << "}";
return out;
}With this operator, a job which ID is 1 that has been generated at t=3.5 but has not been processed yet will be represented as "{1,3.5,-1}". You can implement the << operator as you want.
Now it's time to implement all the atomic models in out model. We will illustrate the whole process with the Generator model as an example. The Processor and Transducer models are implemented similarly.
First, we must define the data type used to represent the Generator atomic model state. The GeneratorState struct represents the state of the Generator model:
struct GeneratorState {
double clock; //!< Current simulation time.
double sigma; //!< Time to wait before triggering the next internal transition function.
int jobCount; //!< Number of jobs generated by the Generator model so far.
GeneratorState(): clock(), sigma(), jobCount() {}
};The clock and sigma attributes are useful for keeping track of the current simulation time and the time remaining before triggering the next internal transition of our atomic model. Finally, jobCount tells us how many jobs the Generator model has produced so far. By default, all the state attributes are initialized to 0.
In Cadmium 2, all the atomic model state data types of your model must implement the insertion (<<) operator. In this way, Cadmium will be able to log model states during a simulation. We implement this operator as follows:
std::ostream& operator<<(std::ostream& out, const GeneratorState& s) {
out << s.jobCount;
return out;
}With this operator, the state of the Generator is represented as the number of jobs generated so far. Now, let's implement our Generator model:
#include <cadmium/core/modeling/atomic.hpp> //! We need to import the cadmium::Atomic<S> class
class Generator: public cadmium::Atomic<GeneratorState> { //! Atomic models MUST inherit from the cadmium::Atomic<S> class
private:
double jobPeriod; //!< Time to wait between Job generations.
public:
std::shared_ptr<cadmium::Port<bool>> inStop; //!< Input Port for receiving stop generating Job objects.
std::shared_ptr<cadmium::Port<Job>> outGenerated; //!< Output Port for sending new Job objects to be processed.
Generator(const std::string& id, double jobPeriod): cadmium::Atomic<GeneratorState>(id, GeneratorState()), jobPeriod(jobPeriod) {
inStop = addInPort<bool>("inStop");
outGenerated = addOutPort<Job>("outGenerated");
}
...Atomic models must inherit from the cadmium::Atomic<S> class. S is a template argument that tells Cadmium which data type is used to represent the atomic model state. We must include<cadmium/core/modeling/atomic.hpp> to use the cadmium::Atomic<S> class. The jobPeriod private attribute corresponds to the job generation period. Generator will create a new job every jobPeriod seconds.
All the ports of our model are defined as public attributes. Ports are implemented in the cadmium::Port<T> class, where T is a template argument that indicates the port data type. The Generator model has two ports: inStop (i.e., the input port for receiving commands from the Transducer to stop producing jobs) and outGenerated (i.e., the output port for sending newly generated jobs to the Processor.
You might be wondering why we use the std::shared_ptr<cadmium::Port<T>> (it is too complex!) instead of cadmium::Port<T> directly (this is way easier!). Well, there is a very good reason why we use shared pointers. While a port only belongs to one DEVS component, coupled models access to the ports of their subcomponents to propagate messages. Thus, we need a safe and efficient way for accessing to the same port from different parts of our code. We NEVER work with cadmium::Port<T>, we ALWAYS use std::shared_ptr<cadmium::Port<T>>. The built-in std::shared_ptr<T> data type corresponds to shared pointers. Shared pointers help us to safely share references to data across our software. In Cadmium, components NEVER access directly to their ports. Instead, they ALWAYS access indirectly using shared pointer to their ports. In this way, we make the life way easier to coupled models to propagate messages across DEVS components.
Now that the std::shared_ptr<cadmium::Port<T>> thing is clear, let's continue with the implementation of the Generator model. As Generator inherits from the cadmium::Atomic<S> class, we MUST override all the pure virtual methods of cadmium::Atomic<S>. These are the output, internalTransition, externalTransition, and timeAdvance methods.
The timeAdvance function returns the time to wait before triggering the model's output and internalTransition functions. In this case, as the state of the Generator model keeps track of this time in GeneratorState::sigma, we just have to return the value of sigma:
...
double timeAdvance(const GeneratorState& s) const override {
return s.sigma;
}
...Note that s is a constant reference to the current state of the model. We can read the state and return a value depending on in, but under no circumstances can we modify the model's state in the timeAdvance function. Then, we implement the output and internalTransition functions:
...
void output(const GeneratorState& s, const cadmium::PortSet& y) const override {
outGenerated->addMessage(Job(s.jobCount, s.clock + s.sigma));
}
void internalTransition(GeneratorState& s) const override {
s.clock += s.sigma;
s.sigma = jobPeriod;
s.jobCount += 1;
}
...Every time the output function is triggered, the Generator model creates a new Job and adds it to the outGenerated port. The job ID is set the number of jobs created so far by the Generator and Job::timeGenerated is set to the current simulation time. Simulation time is obtained from the current state of Generator. Again, s is a reference to the current model state. Note that the output function is triggered just before executing the internalTransition function. Therefore, s.clock does not consider yet the time passed since the last state transition. This is why we set Job::timeGenerated to s.clock + s.sigma. This "trick" in output functions is pretty common, and you will probably use it in most of your models if you need to provide timing information in your output messages.
NOTE: The y thing is a legacy part from an earlier version of Cadmium 2, where ports could not be attributes of the model. You still can get a pointer to outGenerated with y.getPort<Job>("outGenerated"). However, the presented methodology is way simpler and y will eventually be removed.
The internalTransition function is triggered right after executing the output function. The internalTransition CAN (in fact, MUST) modify the model state. That is why s is not a constant reference and we can modify it. Every time we include clock and sigma in our mode state, we must update clock to consider the time that passed since the last state transition. As Generator creates new jobs periodically, we set sigma to jobPeriod. Finally, we increment by one jobCount, as we have just sent a new job when triggering the output function.
Every time the Generator model receives an input message, it triggers its externalTransition function:
...
void externalTransition(GeneratorState& s, double e, const cadmium::PortSet& x) const override {
s.clock += e;
s.sigma = std::max(s.sigma - e, 0.);
if (!inStop->empty() && *inStop->getBag().back()) {
s.sigma = std::numeric_limits<double>::infinity();
}
}
}Again, the externalTransition CAN (in fact, MUST) modify the model state. That is why s is not a constant reference and we can modify it. Every time we include clock and sigma in our mode state, we must update these to consider the time that elapsed since the last state transition (i.e., e). We always add e to clock and subtract e to sigma. I decided to check that sigma is always greater than or equal to 0 in case something goes wrong. However, this should never happen, and therefore you can get rid of the std::max thing.
After updating clock and sigma, we are ready to read all input messages. In this case, Generator only has one input port (inStop). inStop->empty() returns true if the inStop port is empty. Thus, if the port is not empty, we get the last message in the port and check if we receive the command of stopping with *inStop->getBag().back(). If so, we set sigma to infinity and stop creating new jobs.
NOTE: The x thing is a legacy part from an earlier version of Cadmium 2, where ports could not be attributes of the model. You still can get a pointer to inStop with x.getPort<bool>("inStop"). However, the presented methodology is way simpler and x will eventually be removed.
Special attention to the external delta of Transducer, as it iterates over all the messages
void externalTransition(TransducerState& s, double e, const cadmium::PortSet& x) const override {
s.clock += e;
s.sigma -= e;
for (auto& job: inGenerated->getBag()) {
s.nJobsGenerated += 1;
std::cout << "Job " << job->id << " generated at t = " << s.clock << std::endl;
}
for (auto& job: inProcessed->getBag()) {
s.nJobsProcessed += 1;
s.totalTA += job->timeProcessed - job->timeGenerated;
std::cout << "Job " << job->id << " processed at t = " << s.clock << std::endl;
}
}#include <cadmium/core/modeling/coupled.hpp>
class GPT : public Coupled {
public:
//! (optional) shared pointers to subcomponents
std::shared_ptr<Generator> generator; //!< Generator model
std::shared_ptr<Processor> processor; //!< Processor model
std::shared_ptr<Transducer> transducer; //!< Transducer model
GPT(const std::string& id, double jobPeriod, double processingTime, double obsTime): Coupled(id) {
generator = addComponent<Generator>("generator", jobPeriod);
processor = addComponent<Processor>("processor", processingTime);
transducer = addComponent<Transducer>("transducer", obsTime);
addCoupling(generator->outGenerated, processor->inGenerated);
addCoupling(generator->outGenerated, transducer->inGenerated);
addCoupling(processor->outProcessed, transducer->inProcessed);
addCoupling(transducer->outStop, generator->inStop);
}
};
class EF: public cadmium::Coupled {
public:
std::shared_ptr<cadmium::Port<Job>> inProcessed; //!< Input Port for processed Job objects.
std::shared_ptr<cadmium::Port<Job>> outGenerated; //!< Output Port for sending new Job objects to be processed.
EF(const std::string& id, double jobPeriod, double obsTime): cadmium::Coupled(id) {
inProcessed = addInPort<Job>("inProcessed");
outGenerated = addOutPort<Job>("outGenerated");
auto generator = addComponent<Generator>("generator", jobPeriod);
auto transducer = addComponent<Transducer>("transducer", obsTime);
addCoupling(inProcessed, transducer->inProcessed);
addCoupling(transducer->outStop, generator->inStop);
addCoupling(generator->outGenerated, transducer->inGenerated);
addCoupling(generator->outGenerated, outGenerated);
}
};class EFP : public cadmium::Coupled {
public:
EFP(const std::string& id, double jobPeriod, double processingTime, double obsTime) : cadmium::Coupled(id) {
auto ef = addComponent<EF>("ef", jobPeriod, obsTime);
auto processor = addComponent<Processor>("processor", processingTime);
addCoupling(ef->outGenerated, processor->inGenerated);
addCoupling(processor->outProcessed, ef->inProcessed);
}
};