[Prev] [Next] [TOC] [Chapters]

7 The Simulation Library

OMNeT++ has an extensive C++ class library available to the user for implementing simulation models and model components. Part of the class library's functionality has already been covered in the previous chapters, including discrete event simulation basics, the simple module programming model, module parameters and gates, scheduling events, sending and receiving messages, channel operation and programming model, finite state machines, dynamic module creation, signals, and more.

This chapter discusses the rest of the simulation library. Topics will include logging, random number generation, queues, topology discovery and routing support, and statistics and result collection. This chapter also covers some of the conventions and internal mechanisms of the simulation library to allow one extending it and using it to its full potential.

7.1 Fundamentals

7.1.1 Using the Library

Classes in the OMNeT++ simulation library are part of the omnetpp namespace. To use the OMNeT++ API, one must include the omnetpp.h header file and either import the namespace with using namespace omnetpp, or qualify names with the omnetpp:: prefix.

Thus, simulation models will contain the

#include <omnetpp.h>

line, and often also

using namespace omnetpp;

When writing code that should work with various versions of OMNeT++, it is often useful to have compile-time access to the OMNeT++ version in a numeric form. The OMNETPP_VERSION macro exists for that purpose, and it is defined by OMNeT++ to hold the version number in the form major*256+minor. For example, in OMNeT++ 4.6 it was defined as

#define OMNETPP_VERSION 0x406

7.1.2 The cObject Base Class

Most classes in the simulation library are derived from cObject, or its subclasses cNamedObject and cOwnedObject. cObject defines several virtual member functions that are either inherited or redefined by subclasses. Otherwise, cObject is a zero-overhead class as far as memory consumption goes: it purely defines an interface but has no data members. Thus, having cObject a base class does not add anything to the size of a class if it already has at least one virtual member function.

Figure: cObject is the base class for most of the simulation library

The subclasses cNamedObject and cOwnedObject add data members to implement more functionality. The following sections discuss some of the practically important functonality defined by cObject.

7.1.2.1 Name and Full Name

The most useful and most visible member functions of cObject are getName() and getFullName(). The idea behind them is that many objects in OMNeT++ have names by default (for example, modules, parameters and gates), and even for other objects, having a printable name is a huge gain when it comes to logging and debugging.

getFullName() is important for gates and modules, which may be part of gate or module vectors. For them, getFullName() returns the name with the index in brackets, while getName() only returns the name of the module or gate vector. That is, for a gate out[3] in the gate vector out[10], getName() returns "out", and getFullName() returns "out[3]". For other objects, getFullName() simply returns the same string as getName(). An example:

cGate *gate = gate("out", 3);  // out[3]
EV << gate->getName();  // prints "out"
EV << gate->getFullName();  // prints "out[3]"

cObject merely defines these member functions, but they return an empty string. Actual storage for a name string and a setName() method is provided by the class cNamedObject, which is also an (indirect) base class for most library classes. Thus, one can assign names to nearly all user-created objects. It it also recommended to do so, because a name makes an object easier to identify in graphical runtimes like Tkenv or Qtenv.

By convention, the object name is the first argument to the constructor of every class, and it defaults to the empty string. To create an object with a name, pass the name string (a const char* pointer) as the first argument of the constructor. For example:

cMessage *timeoutMsg = new cMessage("timeout");

To change the name of an object, use setName():

timeoutMsg->setName("timeout");

Both the constructor and setName() make an internal copy of the string, instead of just storing the pointer passed to them.

For convenience and efficiency reasons, the empty string "" and nullptr are treated as interchangeable by library objects. That is, "" is stored as nullptr but returned as "". If one creates a message object with either nullptr or "" as its name string, it will be stored as nullptr, and getName() will return a pointer to a static "".

7.1.2.2 Hierarchical Name

getFullPath() returns the object's hierarchical name. This name is produced by prepending the full name (getFullName()) with the parent or owner object's getFullPath(), separated by a dot. For example, if the out[3] gate in the previous example belongs to a module named classifier, which in turn is part of a network called Queueing, then the gate's getFullPath() method will return "Queueing.classifier.out[3]".

cGate *gate = gate("out", 3);  // out[3]
EV << gate->getName();  // prints "out"
EV << gate->getFullName();  // prints "out[3]"
EV << gate->getFullPath();  // prints "Queueing.classifier.out[3]"

The getFullName() and getFullPath() methods are extensively used in graphical runtime environments (Tkenv, Qtenv), and also when assembling runtime error messages.

In contrast to getName() and getFullName() which return const char * pointers, getFullPath() returns std::string. This makes no difference when logging via EV<<, but when getFullPath() is used as a "%s" argument to sprintf(), one needs to write getFullPath().c_str().

char buf[100];
sprintf("msg is '%80s'", msg->getFullPath().c_str()); // note c_str()

7.1.2.3 Class Name

The getClassName() member function returns the class name as a string, including the namespace. getClassName() internally relies on C++ RTTI.

An example:

const char *className = msg->getClassName(); // returns "omnetpp::cMessage"

7.1.2.4 Cloning Objects

The dup() member function creates an exact copy of the object, duplicating contained objects also if necessary. This is especially useful in the case of message objects.

cMessage *copy = msg->dup();

dup() delegates to the copy constructor. Classes also declare an assignment operator (operator=()) which can be used to copy contents of an object into another object of the same type. dup(), the copy constructor and the assignment operator all perform deep coping: objects contained in the copied object will also be duplicated if necessary.

operator=() differs from the other two in that it does not copy the object's name string, i.e. does not invoke setName(). The rationale is that the name string is often used for identifying the particular object instance, as opposed to being considered as part of its contents.

7.1.3 Iterators

There are several container classes in the library (cQueue, cArray etc.) For many of them, there is a corresponding iterator class that one can use to loop through the objects stored in the container.

For example:

cQueue queue;

//...
for (cQueue::Iterator it(queue); !it.end(); ++it) {
    cObject *containedObject = *it;
    //...
}

7.1.4 Runtime Errors

When library objects detect an error condition, they throw a C++ exception. This exception is then caught by the simulation environment which pops up an error dialog or displays the error message.

At times it can be useful to be able stop the simulation at the place of the error (just before the exception is thrown) and use a C++ debugger to look at the stack trace and examine variables. Enabling the debug-on-errors or the debugger-attach-on-error configuration option lets you do that -- check it in section [11.12].

7.2 Logging from Modules

In a simulation there are often thousands of modules which simultaneously carry out non-trivial tasks. In order to understand a complex simulation, it is essential to know the inputs and outputs of algorithms, the information on which decisions are based, and the performed actions along with their parameters. In general, logging facilitates understanding which module is doing what and why.

OMNeT++ makes logging easy and consistent among simulation models by providing its own C++ API and configuration options. The API provides efficient logging with several predefined log levels, global compile-time and runtime filters, per-component runtime filters, automatic context information, log prefixes and other useful features. In the following sections, we look at how to write log statements using the OMNeT++ logging API.

7.2.1 Log Output

The exact way log messages are displayed to the user depends on the user interface. In the command-line user interface (Cmdenv), the log is simply written to the standard output. In the graphical user interfaces, Tkenv and Qtenv, the main window displays the log output of all modules by default. One can also open new output windows on a per module basis, these windows automatically filter for the log messages of the selected module.

7.2.2 Log Levels

All logging must be categorized into one of the predefined log levels. The assigned log level determines how important and how detailed a log statement is. When deciding which log level is appropriate for a particular log statement, keep in mind that they are meant to be local to components. There's no need for a global agreement among all components, because OMNeT++ provides per component filtering. Log levels are mainly useful because log output can be filtered based on them.

7.2.3 Log Statements

OMNeT++ provides several C++ macros for the actual logging. Each one of these macros act like a C++ stream, so they can be used similarly to std::cout with operator<< (shift operator).

The actual logging is as simple as writing information into one of these special log streams as follows:

EV_ERROR << "Connection to server is lost.\n";
EV_WARN << "Queue is full, discarding packet.\n";
EV_INFO << "Packet received , sequence number = " << seqNum << "." << endl;
EV_TRACE << "routeUnicastPacket(" << packet << ");" << endl;

The above C++ macros work well from any C++ class, including OMNeT++ modules. In fact, they automatically capture a number of context specific information such as the current event, current simulation time, context module, this pointer, source file and line number. The final log lines will be automatically extended with a prefix that is created from the captured information (see section [10.6]).

In static class member functions or in non-class member functions an extra EV_STATICCONTEXT macro must be present to make sure that normal log macros compile.

void findModule(const char *name, cModule *from)
{
    EV_STATICCONTEXT;
    EV_TRACE << "findModule(" << name << ", " << from << ");" << endl;

7.2.4 Log Categories

Sometimes it might be useful to further classify log statements into user defined log categories. In the OMNeT++ logging API, a log category is an arbitrary string provided by the user.

For example, a module test may check for a specific log message in the test's output. Putting the log statement into the test category ensures that extra care is taken when someone changes the wording in the statement to match the one in the test.

Similarily to the normal C++ log macros, there are separate log macros for each log level which also allow specifying the log category. Their name is the same as the normal variants' but simply extended with the _C suffix. They take the log category as the first parameter before any shift operator calls:

EV_INFO_C("test") << "Received " << numPacket << " packets in total.\n";

7.2.5 Composition and New lines

Occasionally it's easier to produce a log line using multiple statements. Mostly because some computation has to be done between the parts. This can be achieved by omitting the new line from the log statements which are to be continued. And then subsequent log statements must use the same log level, otherwise an implicit new line would be inserted.

EV_INFO << "Line starts here, ";
... // some other code without logging
EV_INFO << "and it continues here" << endl;

Assuming a simple log prefix that prints the log level in brakets, the above code fragment produces the following output in Cmdenv:

[INFO] Line starts here, and it continues here

Sometimes it might be useful to split a line into multiple lines to achieve better formatting. In such cases, there's no need to write multiple log statements. Simply insert new lines into the sequence of shift operator calls:

EV_INFO << "First line" << endl << "second line" << endl;

In the produced output, each line will have the same log prefix, as shown below:

[INFO] First line
[INFO] Second line

The OMNeT++ logging API also supports direct printing to a log stream. This is mainly useful when printing is really complicated algorithmically (e.g. printing a multi-dimensional value). The following code could produce multiple log lines each having the same log prefix.

void Matrix::print(std::stream &output) { ... }
void Matrix::someFunction()
{
   print(EV_INFO);

7.2.6 Implementation

OMNeT++ does its best to optimize the performance of logging. The implementation fully supports conditinal compilation of log statements based on their log level. It automatically checks whether the log is recorded anywhere. It also checks global and per-component runtime log levels. The latter is efficiently cached in the components for subsequent checks. See section [10.6] for more details on how to configure these log levels.

The implementation of the C++ log macros makes use of the fact that the operator<< is bound more loosely than the conditional operator (?:). This solves conditional compilation, and also helps runtime checks by redirecting the output to a null stream. Unfortunately the operator<< calls are still evaluated on the null stream, even if the log level is disabled.

Rarely just the computation of log statement parameters may be very expensive, and thus it must be avoided if possible. In this case, it is a good idea to make the log statement conditional on whether the output is actually being displayed or recorded anywhere. The cEnvir::isLoggingEnabled() call returns false when the output is disabled, such as in “express” mode. Thus, one can write code like this:

if (!getEnvir()->isLoggingEnabled())
    EV_DEBUG << "CRC: " << computeExpensiveCRC(packet) << endl;

7.3 Random Number Generators

Random numbers in simulation are usually not really random. Rather, they are produced using deterministic algorithms. Based on some internal state, the algorithm performs some deterministic computation to produce a “random” number and the next state. Such algorithms and their implementations are called random number generators or RNGs, or sometimes pseudo random number generators or PRNGs to highlight their deterministic nature. The algorithm's internal state is usually initialized from a smaller seed value.

Starting from the same seed, RNGs always produce the same sequence of random numbers. This is a useful property and of great importance, because it makes simulation runs repeatable.

RNGs are rarely used directly, because they produce uniformly distributed random numbers. When non-uniform random numbers are needed, mathematical transformations are used to produce random numbers from RNG input that correspond to specific distributions. This is called random variate generation, and it will be covered in the next section, [7.4].

It is often advantageous for simulations to use random numbers from multiple RNG instances. For example, a wireless network simulation may use one RNG for generating traffic, and another RNG for simulating transmission errors in the noisy wireless channel. Since seeds for individual RNGs can be configured independently, this arrangement allows one e.g. to perform several simulation runs with the same traffic but with bit errors occurring in different places. A simulation technique called variance reduction is also related to the use of different random number streams. OMNeT++ makes it easy to use multiple RNGs in various flexible configurations.

When assigning seeds, it is important that different RNGs and also different simulation runs use non-overlapping series of random numbers. Overlap in the generated random number sequences can introduce unwanted correlation in the simulation results.

7.3.1 RNG Implementations

OMNeT++ comes with the following RNG implementations.

7.3.1.1 Mersenne Twister

By default, OMNeT++ uses the Mersenne Twister RNG (MT) by M. Matsumoto and T. Nishimura [Matsumoto98]. MT has a period of 219937-1, and 623-dimensional equidistribution property is assured. MT is also very fast: as fast or faster than ANSI C's rand().

7.3.1.2 The "Minimal Standard" RNG

OMNeT++ releases prior to 3.0 used a linear congruential generator (LCG) with a cycle length of 231-2, described in [Jain91], pp. 441-444,455. This RNG is still available and can be selected from omnetpp.ini (Chapter [11]). This RNG is only suitable for small-scale simulation studies. As shown by Karl Entacher et al. in [Entacher02], the cycle length of about 231 is too small (on todays fast computers it is easy to exhaust all random numbers), and the structure of the generated “random” points is too regular. The [Hellekalek98] paper provides a broader overview of issues associated with RNGs used for simulation, and it is well worth reading. It also contains useful links and references on the topic.

7.3.1.3 The Akaroa RNG

When a simulation is executed under Akaroa control (see section [11.21]), it is also possible to let OMNeT++ use Akaroa's RNG. This needs to be configured in omnetpp.ini (section [10.5]).

7.3.1.4 Other RNGs

OMNeT++ allows plugging in your own RNGs as well. This mechanism, based on the cRNG interface, is described in section [17.5]. For example, one candidate to include could be L'Ecuyer's CMRG [LEcuyer02] which has a period of about 2191 and can provide a large number of guaranteed independent streams.

7.3.2 Global and Component-Local RNGs

OMNeT++ can be configured to make several RNGs available for the simulation model. These global or physical RNGs are numbered from 0 to numRNGs-1, and can be seeded independently.

However, usually model code doesn't directly work with those RNGs. Instead, there is an indirection step introduced for additional flexibility. When random numbers are drawn in a model, the code usually refers to component-local or logical RNG numbers. These local RNG numbers are mapped to global RNG indices to arrive at actual RNG instances. This mapping occurs on per-component basis. That is, each module and channel object contains a mapping table similar to the following:

Local RNG index Global RNG
0 --> 0
1 --> 0
2 --> 2
3 --> 1
4 --> 1
5 --> 3

In the example, the module or channel in question has 6 local (logical) RNGs that map to 4 global (physical) RNGs.

The local-to-global mapping, as well as the number of number of global RNGs and their seeding can be configured in omnetpp.ini (see section [10.5]).

The mapping can be set up arbitrarily, with the default being identity mapping (that is, local RNG k refers to global RNG k.) The mapping allows for flexibility in RNG and random number streams configuration -- even for simulation models which were not written with RNG awareness. For example, even if modules in a simulation only use the default, local RNG number 0, one can set up mapping so that different groups of modules use different physical RNGs.

In theory, RNGs could also be instantiated and used directly from C++ model code. However, doing so is not recommended, because the model would lose configurability via omnetpp.ini.

7.3.3 Accessing the RNGs

RNGs are represented with subclasses of the abstract class cRNG. In addition to random number generation methods like intRand() and doubleRand(), the cRNG interface also includes methods like selfTest() for basic integrity checking and getNumbersDrawn() to query the number of random numbers generated.

RNGs can be accessed by local RNG number via cComponent's getRNG(k) method. To access global global RNGs directly by their indices, one can use cEnvir's getRNG(k) method. However, RNGs rarely need to be accessed directly. Most simulations will only use them via random variate generation functions, described in the next section.

7.4 Generating Random Variates

Random numbers produced by RNGs are uniformly distributed. This section describes how to obtain streams of non-uniformly distributed random numbers from various distributions.

The simulation library supports the following distributions:

Distribution Description
Continuous distributions
uniform(a, b) uniform distribution in the range [a,b)
exponential(mean) exponential distribution with the given mean
normal(mean, stddev) normal distribution with the given mean and standard deviation
truncnormal(mean, stddev) normal distribution truncated to nonnegative values
gamma_d(alpha, beta) gamma distribution with parameters alpha>0, beta>0
beta(alpha1, alpha2) beta distribution with parameters alpha1>0, alpha2>0
erlang_k(k, mean) Erlang distribution with k>0 phases and the given mean
chi_square(k) chi-square distribution with k>0 degrees of freedom
student_t(i) student-t distribution with i>0 degrees of freedom
cauchy(a, b) Cauchy distribution with parameters a,b where b>0
triang(a, b, c) triangular distribution with parameters a<=b<=c, a!=c
lognormal(m, s) lognormal distribution with mean m and variance s>0
weibull(a, b) Weibull distribution with parameters a>0, b>0
pareto_shifted(a, b, c) generalized Pareto distribution with parameters a, b and shift c
Discrete distributions
intuniform(a, b) uniform integer from a..b
bernoulli(p) result of a Bernoulli trial with probability 0<=p<=1 (1 with probability p and 0 with probability (1-p))
binomial(n, p) binomial distribution with parameters n>=0 and 0<=p<=1
geometric(p) geometric distribution with parameter 0<=p<=1
negbinomial(n, p) negative binomial distribution with parameters n>0 and 0<=p<=1
poisson(lambda) Poisson distribution with parameter lambda

Some notes:

There are several ways to generate random numbers from these distributions, as described in the next sections.

7.4.1 Component Methods

The preferred way is to use methods defined on cComponent, the common base class of modules and channels:

double uniform(double a, double b, int rng=0) const;
double exponential(double mean, int rng=0) const;
double normal(double mean, double stddev, int rng=0) const;
...

These methods work with the component's local RNGs, and accept the RNG index (default 0) in their extra int parameter.

Since most simulation code is located in methods of simple modules, these methods can be usually called in a concise way, without an explicit module or channel pointer. An example:

scheduleAt(simTime() + exponential(1.0), msg);

There are two additional methods, intrand() and dblrand(). intrand(n) generates random integers in the range [0, n-1], and dblrand() generates a random double on [0,1). They also accept an additional local RNG index that defaults to 0.

7.4.2 Random Number Stream Classes

It is sometimes useful to be able to pass around random variate generators as objects. The classes cUniform, cExponential, cNormal, etc. fulfill this need.

These classes subclass from the cRandom abstract class. cRandom was designed to encapsulate random number streams. Its most important method is draw() that returns a new random number from the stream. cUniform, cExponential and other classes essentially bind the distribution's parameters and an RNG to the generation function.

Figure: Random number stream classes

Let us see for example cNormal. The constructor expects an RNG (cRNG pointer) and the parameters of the distribution, mean and standard deviation. It also has a default constructor, as it is a requirement for Register_Class(). When the default constructor is used, the parameters can be set with setRNG(), setMean() and setStddev(). setRNG() is defined on cRandom. The draw() method, of course, is redefined to return a random number from the normal distribution.

An example that shows the use of a random number stream as an object:

cNormal *normal = new cNormal(getRNG(0), 0, 1); // unit normal distr.
printRandomNumbers(normal, 10);
...

void printRandomNumbers(cRandom *rand, int n)
{
    EV << "Some numbers from a " << rand->getClassName() << ":" << endl;
    for (int i = 0; i < n; i++)
        EV << rand->draw() << endl;
}

Another important property of cRandom is that it can encapsulate state. That is, subclasses can be implemented that, for example, return autocorrelated numbers, numbers from a stochastic process, or simply elements of a stored sequence (e.g. one loaded from a trace file).

7.4.3 Generator Functions

Both the cComponent methods and the random number stream classes described above have been implemented with the help of standalone generator functions. These functions take a cRNG pointer as their first argument.

double uniform(cRNG *rng, double a, double b);
double exponential(cRNG *rng, double mean);
double normal(cRNG *rng, double mean, double stddev);
...

7.4.4 Random Numbers from Histograms

One can also specify a distribution as a histogram. The cHistogram, cKSplit and cPSquare classes can be used to generate random numbers from histograms. This feature is documented later, with the statistical classes.

7.4.5 Adding New Distributions

One can easily add support for new distributions. We recommend that you write a standalone generator function first. Then you can add a cRandom subclass that wraps it, and/or module (channel) methods that invoke it with the module's local RNG. If the function is registered with the Define_NED_Function() macro (see [7.11]), it will be possible to use the new distribution in NED files and ini files, as well.

If you need a random number stream that has state, you need to subclass from cRandom.

7.5 Container Classes

7.5.1 Queue class: cQueue

7.5.1.1 Basic Usage

cQueue is a container class that acts as a queue. cQueue can hold objects of type derived from cObject (almost all classes from the OMNeT++ library), such as cMessage, cPar, etc. Normally, new elements are inserted at the back, and removed from the front.

Figure: cQueue: insertion and removal

The member functions dealing with insertion and removal are insert() and pop().

cQueue queue("my-queue");
cMessage *msg;

// insert messages
for (int i = 0; i < 10; i++) {
    msg = new cMessage;
    queue.insert(msg);
}

// remove messages
while(!queue.isEmpty()) {
    msg = (cMessage *)queue.pop();
    delete msg;
}

The length() member function returns the number of items in the queue, and empty() tells whether there is anything in the queue.

There are other functions dealing with insertion and removal. The insertBefore() and insertAfter() functions insert a new item exactly before or after a specified one, regardless of the ordering function.

The front() and back() functions return pointers to the objects at the front and back of the queue, without affecting queue contents.

The pop() function can be used to remove items from the tail of the queue, and the remove() function can be used to remove any item known by its pointer from the queue:

queue.remove(msg);

7.5.1.2 Priority Queue

By default, cQueue implements a FIFO, but it can also act as a priority queue, that is, it can keep the inserted objects ordered. To use this feature, one needs to provide a comparison function that takes two cObject pointers, and returns -1, 0 or 1 (see the reference for details). An example of setting up an ordered cQueue:

cQueue queue("queue", someCompareFunc);

If the queue object is set up as an ordered queue, the insert() function uses the ordering function: it searches the queue contents from the head until it reaches the position where the new item needs to be inserted, and inserts it there.

7.5.1.3 Iterators

The cQueue::Iterator class lets one iterate over the contents of the queue and examine each object.

The cQueue::Iterator constructor expects the queue object in the first argument. Normally, forward iteration is assumed, and the iteration is initialized to point at the front of the queue. For reverse iteration, specify reverse=true as the optional second argument. After that, the class acts as any other OMNeT++ iterator: one can use the ++ and -- operators to advance it, the * operator to get a pointer to the current item, and the end() member function to examine whether the iterator has reached the end (or the beginning) of the queue.

Forward iteration:

for (cQueue::Iterator iter(queue); !iter.end(), iter++) {
    cMessage *msg = (cMessage *) *iter;
    //...
}

Reverse iteration:

for (cQueue::Iterator iter(queue, true); !iter.end(), iter--) {
    cMessage *msg = (cMessage *) *iter;
    //...
}

7.5.2 Expandable Array: cArray

7.5.2.1 Basic Usage

cArray is a container class that holds objects derived from cObject. cArray implements a dynamic-size array: its capacity grows automatically when it becomes full. cArray stores pointers of objects inserted instead of making copies.

Creating an array:

cArray array("array");

Adding an object at the first free index:

cMsgPar *p = new cMsgPar("par");
int index = array.add(p);

Adding an object at a given index (if the index is occupied, you will get an error message):

cMsgPar *p = new cMsgPar("par");
int index = array.addAt(5,p);

Finding an object in the array:

int index = array.find(p);

Getting a pointer to an object at a given index:

cPar *p = (cPar *) array[index];

You can also search the array or get a pointer to an object by the object's name:

int index = array.find("par");
Par *p = (cPar *) array["par"];

You can remove an object from the array by calling remove() with the object name, the index position or the object pointer:

array.remove("par");
array.remove(index);
array.remove(p);

The remove() function doesn't deallocate the object, but it returns the object pointer. If you also want to deallocate it, you can write:

delete array.remove(index);

7.5.2.2 Iteration

cArray has no iterator, but it is easy to loop through all the indices with an integer variable. The size() member function returns the largest index plus one.

for (int i = 0; i < array.size(); i++) {
  if (array[i]) { // is this position used?
    cObject *obj = array[i];
    EV << obj->getName() << endl;
  }
}

7.6 Routing Support: cTopology

7.6.1 Overview

The cTopology class was designed primarily to support routing in communication networks.

A cTopology object stores an abstract representation of the network in a graph form:

One can specify which modules to include in the graph. Compound modules may also be selected. The graph will include all connections among the selected modules. In the graph, all nodes are at the same level; there is no submodule nesting. Connections which span across compound module boundaries are also represented as one graph edge. Graph edges are directed, just as module gates are.

If you are writing a router or switch model, the cTopology graph can help you determine what nodes are available through which gate and also to find optimal routes. The cTopology object can calculate shortest paths between nodes for you.

The mapping between the graph (nodes, edges) and network model (modules, gates, connections) is preserved: one can find the corresponding module for a cTopology node and vica versa.

7.6.2 Basic Usage

One can extract the network topology into a cTopology object with a single method call. There are several ways to specify which modules should be included in the topology:

First, you can specify which node types you want to include. The following code extracts all modules of type Router or Host. (Router and Host can be either simple or compound module types.)

cTopology topo;
topo.extractByModuleType("Router", "Host", nullptr);

Any number of module types can be supplied; the list must be terminated by nullptr.

A dynamically assembled list of module types can be passed as a nullptr-terminated array of const char* pointers, or in an STL string vector std::vector<std::string>. An example for the former:

cTopology topo;
const char *typeNames[3];
typeNames[0] = "Router";
typeNames[1] = "Host";
typeNames[2] = nullptr;
topo.extractByModuleType(typeNames);

Second, you can extract all modules which have a certain parameter:

topo.extractByParameter("ipAddress");

You can also specify that the parameter must have a certain value for the module to be included in the graph:

cMsgPar yes = "yes";
topo.extractByParameter("includeInTopo", &yes);

The third form allows you to pass a function which can determine for each module whether it should or should not be included. You can have cTopology pass supplemental data to the function through a void* pointer. An example which selects all top-level modules (and does not use the void* pointer):

int selectFunction(cModule *mod, void *)
{
  return mod->getParentModule() == getSimulation()->getSystemModule();
}

topo.extractFromNetwork(selectFunction, nullptr);

A cTopology object uses two types: cTopology::Node for nodes and cTopology::Link for edges. (cTopology::LinkIn and cTopology::LinkOut are aliases for cTopology::Link; we'll talk about them later.)

Once you have the topology extracted, you can start exploring it. Consider the following code (we'll explain it shortly):

for (int i = 0; i < topo.getNumNodes(); i++) {
  cTopology::Node *node = topo.getNode(i);
  EV << "Node i=" << i << " is " << node->getModule()->getFullPath() << endl;
  EV << " It has " << node->getNumOutLinks() << " conns to other nodes\n";
  EV << " and " << node->getNumInLinks() << " conns from other nodes\n";

  EV << " Connections to other modules are:\n";
  for (int j = 0; j < node->getNumOutLinks(); j++) {
    cTopology::Node *neighbour = node->getLinkOut(j)->getRemoteNode();
    cGate *gate = node->getLinkOut(j)->getLocalGate();
    EV << " " << neighbour->getModule()->getFullPath()
       << " through gate " << gate->getFullName() << endl;
  }
}

The getNumNodes() member function returns the number of nodes in the graph, and getNode(i) returns a pointer to the ith node, a cTopology::Node structure.

The correspondence between a graph node and a module can be obtained by getNodeFor() method:

cTopology::Node *node = topo.getNodeFor(module);
cModule *module = node->getModule();

The getNodeFor() member function returns a pointer to the graph node for a given module. (If the module is not in the graph, it returns nullptr). getNodeFor() uses binary search within the cTopology object so it is relatively fast.

cTopology::Node's other member functions let you determine the connections of this node: getNumInLinks(), getNumOutLinks() return the number of connections, getLinkIn(i) and getLinkOut(i) return pointers to graph edge objects.

By calling member functions of the graph edge object, you can determine the modules and gates involved. The getRemoteNode() function returns the other end of the connection, and getLocalGate(), getRemoteGate(), getLocalGateId() and getRemoteGateId() return the gate pointers and ids of the gates involved. (Actually, the implementation is a bit tricky here: the same graph edge object cTopology::Link is returned either as cTopology::LinkIn or as cTopology::LinkOut so that “remote” and “local” can be correctly interpreted for edges of both directions.)

7.6.3 Shortest Paths

The real power of cTopology is in finding shortest paths in the network to support optimal routing. cTopology finds shortest paths from all nodes to a target node. The algorithm is computationally inexpensive. In the simplest case, all edges are assumed to have the same weight.

A real-life example assumes we have the target module pointer; finding the shortest path to the target looks like this:

cModule *targetmodulep =...;
cTopology::Node *targetnode = topo.getNodeFor(targetmodulep);
topo.calculateUnweightedSingleShortestPathsTo(targetnode);

This performs the Dijkstra algorithm and stores the result in the cTopology object. The result can then be extracted using cTopology and cTopology::Node methods. Naturally, each call to calculateUnweightedSingleShortestPathsTo() overwrites the results of the previous call.

Walking along the path from our module to the target node:

cTopology::Node *node = topo.getNodeFor(this);

if (node == nullptr) {
  EV << "We (" << getFullPath() << ") are not included in the topology.\n";
}
else if (node->getNumPaths()==0) {
  EV << "No path to destination.\n";
}
else {
  while (node != topo.getTargetNode()) {
    EV << "We are in " << node->getModule()->getFullPath() << endl;
    EV << node->getDistanceToTarget() << " hops to go\n";
    EV << "There are " << node->getNumPaths()
       << " equally good directions, taking the first one\n";
    cTopology::LinkOut *path = node->getPath(0);
    EV << "Taking gate " << path->getLocalGate()->getFullName()
       << " we arrive in " << path->getRemoteNode()->getModule()->getFullPath()
       << " on its gate " << path->getRemoteGate()->getFullName() << endl;
    node = path->getRemoteNode();
  }
}

The purpose of the getDistanceToTarget() member function of a node is self-explanatory. In the unweighted case, it returns the number of hops. The getNumPaths() member function returns the number of edges which are part of a shortest path, and path(i) returns the ith edge of them as cTopology::LinkOut. If the shortest paths were created by the ...SingleShortestPaths() function, getNumPaths() will always return 1 (or 0 if the target is not reachable), that is, only one of the several possible shortest paths are found. The ...MultiShortestPathsTo() functions find all paths, at increased run-time cost. The cTopology's getTargetNode() function returns the target node of the last shortest path search.

You can enable/disable nodes or edges in the graph. This is done by calling their enable() or disable() member functions. Disabled nodes or edges are ignored by the shortest paths calculation algorithm. The isEnabled() member function returns the state of a node or edge in the topology graph.

One usage of disable() is when you want to determine in how many hops the target node can be reached from our node through a particular output gate. To compute this, you compute the shortest paths to the target from the neighbor node while disabling the current node to prevent the shortest paths from going through it:

cTopology::Node *thisnode = topo.getNodeFor(this);
thisnode->disable();
topo.calculateUnweightedSingleShortestPathsTo(targetnode);
thisnode->enable();

for (int j = 0; j < thisnode->getNumOutLinks(); j++) {
  cTopology::LinkOut *link = thisnode->getLinkOut(i);
  EV << "Through gate " << link->getLocalGate()->getFullName() << " : "
     << 1 + link->getRemoteNode()->getDistanceToTarget() << " hops" << endl;
}

In the future, other shortest path algorithms will also be implemented:

unweightedMultiShortestPathsTo(cTopology::Node *target);
weightedSingleShortestPathsTo(cTopology::Node *target);
weightedMultiShortestPathsTo(cTopology::Node *target);

7.6.4 Manipulating the graph

cTopology also has methods that let one manipulate the stored graph, or even, build a graph from scratch. These methods are addNode(), deleteNode(), addLink() and deleteLink().

When extracting the topology from the network, cTopology uses the factory methods createNode() and createLink() to instantiate the node and link objects. These methods may be overridden by subclassing cTopology if the need arises, for example when it is useful to be able to store additional information in those objects.

7.7 Pattern Matching

Since version 4.3, OMNeT++ contains two utility classes for pattern matching, cPatternMatcher and cMatchExpression.

cPatternMatcher is a glob-style pattern matching class, adopted to special OMNeT++ requirements. It recognizes wildcards, character ranges and numeric ranges, and supports options such as case sensitive and whole string matching. cMatchExpression builds on top of cPatternMatcher and extends it in two ways: first, it lets you combine patterns with AND, OR, NOT into boolean expressions, and second, it applies the pattern expressions to objects instead of text. These classes are especially useful for making model-specific configuration files more concise or more powerful by introducing patterns.

7.7.1 cPatternMatcher

cPatternMatcher holds a pattern string and several option flags, and has a matches() boolean function that determines whether the string passed as argument matches the pattern with the given flags. The pattern and the flags can be set via the constructor or by calling the setPattern() member function.

The pattern syntax is a variation on Unix glob-style patterns. The most apparent differences to globbing rules are the distinction between * and **, and that character ranges should be written with curly braces instead of square brackets; that is, any-letter is expressed as {a-zA-Z} and not as [a-zA-Z], because square brackets are reserved for the notation of module vector indices.

The following option flags are supported:

Patterns may contain the following elements:

Sets and negated sets can contain several character ranges and also enumeration of characters, for example {_a-zA-Z0-9} or {xyzc-f}. To include a hyphen in the set, place it at a position where it cannot be interpreted as character range, for example {a-z-} or {-a-z}. To include a close brace in the set, it must be the first character: {}a-z}, or for a negated set: {^}a-z}. A backslash is always taken as literal backslash (and NOT as escape character) within set definitions. When doing case-insensitive match, avoid ranges that include both alpha and non-alpha characters, because they might cause funny results.

For numeric ranges and numeric index ranges, ranges are inclusive, and both the start and the end of the range are optional; that is, {10..}, {..99} and {..} are all valid numeric ranges (the last one matches any number). Only nonnegative integers can be matched. Caveat: {17..19} will match "a17", "117" and also "963217"!

The cPatternMatcher constructor and the setPattern() member function have similar signatures:

cPatternMatcher(const char *pattern, bool dottedpath, bool fullstring,
                bool casesensitive);
void setPattern(const char *pattern, bool dottedpath, bool fullstring,
                bool casesensitive);

The matcher function:

bool matches(const char *text);

There are also some more utility functions for printing the pattern, determining whether a pattern contains wildcards, etc.

Example:

cPatternMatcher matcher("**.host[*]", true, true, true);
EV << matcher.matches("Net.host[0]") << endl;  // -> true
EV << matcher.matches("Net.area1.host[0]") << endl;  // -> true
EV << matcher.matches("Net.host") << endl;  // -> false
EV << matcher.matches("Net.host[0].tcp") << endl;  // -> false

7.7.2 cMatchExpression

The cMatchExpression class builds on top of cPatternMatcher, and lets one determine whether an object matches a given pattern expression.

A pattern expression consists of elements in the fieldname(pattern) syntax; they check whether the string representation of the given field of the object matches the pattern. For example, srcAddr(192.168.0.*) will match if the srcAddr field of the object starts with 192.168.0. A naked pattern (without field name and parens) is also accepted, and it will be matched against the default field of the object, which will usually be its name.

These elements can be combined with the AND, OR, NOT operators, accepted in both lowercase and uppercase. AND has higher precedence than OR, but parentheses can be used to change the evaluation order.

Pattern examples:

The cMatchExpression class has a constructor and setPattern() method similar to those of cPatternMatcher:

cMatchExpression(const char *pattern, bool dottedpath, bool fullstring,
                bool casesensitive);
void setPattern(const char *pattern, bool dottedpath, bool fullstring,
                bool casesensitive);

However, the matcher function takes a cMatchExpression::Matchable instead of string:

bool matches(const Matchable *object);

This means that objects to be matched must either be subclassed from cMatchExpression::Matchable, or be wrapped into some adapter class that does. cMatchExpression::Matchable is a small abstract class with only a few pure virtual functions:

/**
 * Objects to be matched must implement this interface
 */
class SIM_API Matchable
{
  public:
    /**
     * Return the default string to match. The returned pointer will not be
     * cached by the caller, so it is OK to return a pointer to a static buffer.
     */
    virtual const char *getAsString() const = 0;

    /**
     * Return the string value of the given attribute, or nullptr if the object
     * doesn't have an attribute with that name. The returned pointer will not
     * be cached by the caller, so it is OK to return a pointer to a static buffer.
     */
    virtual const char *getAsString(const char *attribute) const = 0;

    /**
     * Virtual destructor.
     */
    virtual ~Matchable() {}
};

To be able to match instances of an existing class that is not already a Matchable, one needs to write an adapter class. An adapter class that we can look at as an example is cMatchableString. cMatchableString makes it possible to match strings with a cMatchExpression, and is part of OMNeT++:

/**
 * Wrapper to make a string matchable with cMatchExpression.
 */
class cMatchableString : public cMatchExpression::Matchable
{
  private:
    std::string str;
  public:
    cMatchableString(const char *s) {str = s;}
    virtual const char *getAsString() const {return str.c_str();}
    virtual const char *getAsString(const char *name) const {return nullptr;}
};

An example:

cMatchExpression expr("foo* or bar*", true, true, true);
cMatchableString str1("this is a foo");
cMatchableString str2("something else");
EV << expr.matches(&str1) << endl; // -> true
EV << expr.matches(&str2) << endl; // -> false

Or, by using temporaries:

EV << expr.matches(&cMatchableString("this is a foo")) << endl; // -> true
EV << expr.matches(&cMatchableString("something else")) << endl; // -> false

7.8 Collecting Summary Statistics and Histograms

There are several statistic and result collection classes: cStdDev, cHistogram, cPSquare and cKSplit. They are all derived from the abstract base class cStatistic; histogram-like classes derive from cAbstractHistogram.

Figure: Statistics classes

All classes use the double type for representing observations, and compute all metrics in the same data type (except the observation count, which is int64_t.)

For weighted statistics, weights are also doubles. Being able to handle non-integer weights is important because weighted statistics are often used for computing time averages, e.g. average queue length or average channel utilization.

7.8.1 cStdDev

The cStdDev class is meant to collect summary statistics of observations. If you also need to compute a histogram, use cHistogram (or cKSplit/cPSquare) instead, because those classes already include the functionality of cStdDev.

cStdDev can collect unweighted or weighted statistics. This needs to be decided in the constructor call, and cannot be changed later. Specify true as the second argument for weighted statistics.

cStdDev unweighted("packetDelay");  // unweighted
cStdDev weighted("queueLength", true); // weighted

Observations are added to the statistics by using the collect() or the collectweighted() methods. The latter takes two parameters, the value and the weight.

for (double value : values)
    unweighted.collect(value);

for (double value : values2) {
    double weight = ...
    weighted.collectWeighted(value, weight);
}

Statistics can be obtained from the object with the following methods: getCount(), getMin(), getMax(), getMean(), getStddev(), getVariance().

There are two getter methods that only work for unweighted statistics: getSum() and getSqrSum(). Plain (unweighted) sum and sum of squares are not computed for weighted observations, and it is an error to call these methods in the weighted case.

Other getter methods are primarily meant for weighted statistics: getSumWeights(), getWeightedSum(), getSqrSumWeights(), getWeightedSqrSum(). When called on unweighted statistics, these methods simply assume a weight of 1.0 for all observations.

An example:

EV << "count = " << unweighted.getCount() << endl;
EV << "mean = " << unweighted.getMean() << end;
EV << "stddev = " << unweighted.getStddev() << end;
EV << "min = " << unweighted.getMin() << end;
EV << "max = " << unweighted.getMax() << end;

7.8.2 cHistogram

cHistogram is able to represent both uniform and non-uniform bin histograms, and supports both weighted and unweighted observations. The histogram can be modified dynamically: it can be extended with new bins, and adjacent bins can be merged. In addition to the bin values (which mean count in the unweighted case, and sum of weights in the weighted case), the histogram object also keeps the number (or sum of weights) of the lower and upper outliers (“underflows” and “overflows”.)

Figure: Histograms keep track of outliers as well

Setting up and managing the bins based on the collected observations is usually delegated to a strategy object. However, for most use cases, histogram strategies is not something the user needs to be concerned with. The default constructor of cHistogram sets up the histogram with a default strategy that usually produces a good quality histogram without requiring manual configuration or a-priori knowledge about the distribution. For special use cases, there are other histogram strategies, and it is also possible to write new ones.

7.8.2.1 Creating a Histogram

cHistogram has several constructors variants. Like with cStdDev, it needs to be decided in the constructor call by a boolean argument whether the histogram should collect unweighted (false) or weighted (true) statistics; the default is unweighted. Another argument is a number of bins hint. (The actual number of bins produced might slightly differ, due to dynamic range extensions and bin merging performed by some strategies.)

cHistogram unweighted1("packetDelay");  // unweighted
cHistogram unweighted2("packetDelay", 10);  // unweighted, with ~10 bins
cHistogram weighted1("queueLength", true); // weighted
cHistogram weighted2("queueLength", 10, true); // weighted, with ~10 bins

It is also possible to provide a strategy object in a constructor call. (The strategy object may also be set later though, using setStrategy(). It must be called before the first observation is collected.)

cHistogram autoRangeHist("queueLength", new cAutoRangeHistogramStrategy());

This constructor can also be used to create a histogram without a strategy object, which is useful if you want to set up the histogram bins manually.

cHistogram hist("queueLength", nullptr, true); // weighted, no strategy

cHistogram also has methods where you can provide constraints and hints for setting up the bins: setMode(), setRange(), setRangeExtensionFactor(), setAutoExtend(), setNumBinsHint(), setBinSizeHint(). These methods delegate to similar methods of cAutoRangeHistogramStrategy.

7.8.2.2 Collecting Observations

Observations are added to the histogram in the same way as with cStdDev: using the collect() and collectWeighted() methods.

7.8.2.3 Querying the Bins

Histogram bins can be accessed with three member functions: getNumBins() returns the number of bins, getBinEdge(int k) returns the kth bin edge, getBinValue(int k) returns the count or sum of weights in bin k, and getBinPDF(int k) returns the PDF value in the bin (i.e. between getBinEdge(k) and getBinEdge(k+1)). The getBinInfo(k) method returns multiple bin data (edges, value, relative frequency) packed together in a struct. Four other methods, getUnderflowSumWeights(), getOverflowSumWeights(), getNumUnderflows(), getNumOverflows(), provide access to the outliers.

These functions, being defined on cHistogramBase, are not only available on cHistogram but also for cPSquare and cKSplit.

For cHistogram, bin edges and bin values can also be accessed as a vector of doubles, using the getBinEdges() and getBinValues() methods.

Figure: Bin edges and bins of an N-bin histogram

An example:

EV << "[" << hist.getMin() << "," << hist.getBinEdge(0) << "): " 
   << hist.getUnderflowSumWeights() << endl;
int numBins = hist.getNumBins();
for (int i = 0; i < numBins; i++) {
  EV << "[" << hist.getBinEdge(i) << "," << hist.getBinEdge(i+1) << "): " 
     << hist.getBinValue(i) << endl;
}
EV << "[" << hist.getBinEdge(numBins) << "," << hist.getMax() << "]: " 
   << hist.getOverflowSumWeights() << endl;

The getPDF(x) and getCDF(x) member functions return the value of the Probability Density Function and the Cumulated Density Function at a given x, respectively.

Note that bins may not be immediately available during observation collection, because some histogram strategies use precollection to gather information about the distribution before setting up the bins. Use binsAlreadySetUp() to figure out whether bins are set up already. Setting up the bins can be forced with the setupBins() method.

7.8.2.4 Setting Up and Managing the Bins

The cHistogram class has several methods for creating and manipulating bins. These methods are primarily intended to be called from strategy classes, but are also useful if you want to manage the bins manually, i.e. without a strategy class.

For setting up the bins, you can either use createUniformBins() with the range (lo, hi) and the step size as parameters, or specify all bin edges explicitly in a vector of doubles to setBinEdges().

When the bins have already been set up, the histogram can be extended with new bins down or up using the prependBins() and appendBins() methods that take a list of new bin edges to add. There is also an extendBinsTo() method that extends the histogram with equal-sized bins at either end to make sure that a supplied value falls into the histogram range. Of course, extending the histogram is only possible if there are no outliers in that direction. (The positions of the outliers is not preserved, so it is not known how many would fall in each of the newly created bins.)

If the histogram has too many bins, adjacent ones (pairs, triplets, or groups of size n) can be merged, using the mergeBins() method.

Example code which sets up a histogram with uniform bins:

cHistogram hist("queueLength", nullptr); // create w/o strategy object
hist.createUniformBins(0, 100, 10); // 10 bins over (0,100)

The following code achieves the same, but uses setBinEdges():

std::vector<double> edges = {0,10,20,30,40,50,60,70,80,90,100}; // C++11
cHistogram hist("queueLength", nullptr);
hist.setBinEdges(edges);

7.8.2.5 Strategy Concept

Histogram strategies subclass from cIHistogramStrategy, and are responsible for setting up and managing the bins.

A cHistogram is created with a cDefaultHistogramStrategy by default, which works well in most cases. Other cHistogram constructors allow passing in an arbitrary histogram strategy.

The collect() and collectWeighted() methods of a cHistogram delegate to similar methods of the strategy object, which in turn decides when and how to set up the bins, and how to manage the bins later. (Setting up the bins may be postponed until a few observations have been collected, in order to gather more information for it.) The histogram strategy uses public histogram methods like createUniformBins() to create and manage the bins.

7.8.2.6 Available Histogram Strategies

The following histogram strategy classes exist.

cFixedRangeHistogramStrategy sets up uniform bins over a predetermined interval. The number of bins and the histogram mode (integers or reals) also need to be configured. This strategy does not use precollection, as all input for setting up the bins must be explicitly provided by the user.

cDefaultHistogramStrategy is used by the default setup of cHistogram. This strategy uses precollection to gather input information about the distribution before setting up the bins. Precollection is used to determine the initial histogram range and the histogram mode (integers vs. reals). In integers mode, bin edges will be whole numbers.

To keep up with distributions that change over time, this histogram strategy can auto-extend the histogram range by adding new bins as needed. It also performs bin merging when necessary, to keep the number of bins reasonably low.

cAutoRangeHistogramStrategy is a generic, very configurable, precollection-based histogram strategy which creates uniform bins, and creates quality histograms for practical distributions.

Several constraints and hints can be specified for setting up the bins: range lower and/or upper endpoint, bin size, number of bins, mode (integers vs. reals), and whether bin size rounding is to be used.

This histogram strategy can auto-extend the histogram range by adding new bins at either end. One can also set up an upper limit to the number of histogram bins to prevent it from growing indefinitely. Bin merging can also be enabled: it will cause every two (or N) adjacent bins to be merged to reduce the number of bins if their number grows too high.

7.8.2.7 Random Number Generation from Distributions

The random() member function generates random numbers from the distribution stored by the object:

double rnd = histogram.random();

7.8.2.8 Storing and Loading Distributions

The statistic classes have loadFromFile() member functions that read the histogram data from a text file. If you need a custom distribution that cannot be written (or it is inefficient) as a C++ function, you can describe it in histogram form stored in a text file, and use a histogram object with loadFromFile().

You can also use saveToFile() that writes out the distribution collected by the histogram object:

FILE *f = fopen("histogram.dat","w");
histogram.saveToFile(f); // save the distribution
fclose(f);

cHistogram restored;
FILE *f2 = fopen("histogram.dat","r");
restored.loadFromFile(f2); // load stored distribution
fclose(f2);

7.8.3 cPSquare

The cPSquare class implements the P2 algorithm described in [JCh85]. P2 is a heuristic algorithm for dynamic calculation of the median and other quantiles. The estimates are produced dynamically as the observations arrive. The observations are not stored; therefore, the algorithm has a very small and fixed storage requirement regardless of the number of observations. The P2 algorithm operates by adaptively shifting bin edges as observations arrive.

cPSquare only needs the number of cells, for example in the constructor:

cPSquare psquare("endToEndDelay", 20);

Afterwards, observations can be added and the resulting histogram can be queried with the same cAbstractHistogram methods as with cHistogram.

7.8.4 cKSplit

7.8.4.1 Motivation

The k-split algorithm is an on-line distribution estimation method. It was designed for on-line result collection in simulation programs. The method was proposed by Varga and Fakhamzadeh in 1997. The primary advantage of k-split is that without having to store the observations, it gives a good estimate without requiring a-priori information about the distribution, including the sample size. The k-split algorithm can be extended to multi-dimensional distributions, but here we deal with the one-dimensional version only.

7.8.4.2 The k-split Algorithm

The k-split algorithm is an adaptive histogram-type estimate which maintains a good partitioning by doing cell splits. We start out with a histogram range [xlo, xhi) with k equal-sized histogram cells with observation counts n1,n2, .. nk. Each collected observation increments the corresponding observation count. When an observation count ni reaches a split threshold, the cell is split into k smaller, equal-sized cells with observation counts ni,1, ni,2, .. ni,k initialized to zero. The ni observation count is remembered and is called the mother observation count to the newly created cells. Further observations may cause cells to be split further (e.g. ni,1,1,...ni,1,k etc.), thus creating a k-order tree of observation counts where leaves contain live counters that are actually incremented by new observations, and intermediate nodes contain mother observation counts for their children. If an observation falls outside the histogram range, the range is extended in a natural manner by inserting new level(s) at the top of the tree. The fundamental parameter to the algorithm is the split factor k. Experience has shown that k=2 works best.

Figure: Illustration of the k-split algorithm, k=2. The numbers in boxes represent the observation count values

For density estimation, the total number of observations that fell into each cell of the partition has to be determined. For this purpose, mother observations in each internal node of the tree must be distributed among its child cells and propagated up to the leaves.

Let n...,i be the (mother) observation count for a cell, s...,i be the total observation count in a cell n...,i plus the observation counts in all its sub-, sub-sub-, etc. cells), and m...,i the mother observations propagated to the cell. We are interested in the ñ...,i = n...,i + m...,i estimated amount of observations in the tree nodes, especially in the leaves. In other words, if we have ñ...,i estimated observation amount in a cell, how to divide it to obtain m...,i,1, m...,i,2 .. m...,i,k that can be propagated to child cells. Naturally, m...,i,1 + m...,i,2 + .. + m...,i,k = ñ...,i.

Two natural distribution methods are even distribution (when m...,i,1 = m...,i,2 = .. = m...,i,k) and proportional distribution (when m...,i,1 : m...,i,2 : .. : m...,i,k = s...,i,1 : s...,i,2 : .. : s...,i,k). Even distribution is optimal when the s...,i,j values are very small, and proportional distribution is good when the s...,i,j values are large compared to m...,i,j. In practice, a linear combination of them seems appropriate, where λ=0 means even and λ=1 means proportional distribution:

m..,i,j = (1-λ)ñ..,i/k + λ ñ..,i s...,i,j / s..,i where λ is in [0,1]

Figure: Density estimation from the k-split cell tree. We assume λ=0, i.e. we distribute mother observations evenly.

Note that while n...,i are integers, m...,i and thus ñ...,i are typically real numbers. The histogram estimate calculated from k-split is not exact, because the frequency counts calculated in the above manner contain a degree of estimation themselves. This introduces a certain cell division error; the λ parameter should be selected so that it minimizes that error. It has been shown that the cell division error can be reduced to a more-than-acceptable small value.
Strictly speaking, the k-split algorithm is semi-online, because its needs some observations to set up the initial histogram range. Because of the range extension and cell split capabilities, the algorithm is not very sensitive to the choice of the initial range, so very few observations are sufficient for range estimation (say Npre=10). Thus we can regard k-split as an on-line method.

K-split can also be used in semi-online mode, when the algorithm is only used to create an optimal partition from a larger number of Npre observations. When the partition has been created, the observation counts are cleared and the Npre observations are fed into k-split once again. This way all mother (non-leaf) observation counts will be zero and the cell division error is eliminated. It has been shown that the partition created by k-split can be better than both the equi-distant and the equal-frequency partition.

OMNeT++ contains an implementation of the k-split algorithm, the cKSplit class.

7.8.4.3 The cKSplit Class

The cKSplit class is an implementation of the k-split method. It is a subclass of cAbstractHistogram, so configuring, adding observations and querying histogram cells is done the same way as with other histogram classes.

Specific member functions allow one to fine-tune the k-split algorithm. setCritFunc() and setDivFunc() let one replace the split criteria and the cell division function, respectively. setRangeExtension() lets one enable/disable range extension. (If range extension is disabled, out-of-range observations will simply be counted as underflows or overflows.)

The class also allows one to access the k-split data structure, directly, via methods like getTreeDepth(), getRootGrid(), getGrid(i), and others.

7.9 Recording Simulation Results

7.9.1 Output Vectors: cOutVector

Objects of type cOutVector are responsible for writing time series data (referred to as output vectors) to a file. The record() method is used to output a value (or a value pair) with a timestamp. The object name will serve as the name of the output vector.

The vector name can be passed in the constructor,

cOutVector responseTimeVec("response time");

but in the usual arrangement you'd make the cOutVector a member of the module class and set the name in initialize(). You'd record values from handleMessage() or from a function called from handleMessage().

The following example is a Sink module which records the lifetime of every message that arrives to it.

class Sink : public cSimpleModule
{
  protected:
    cOutVector endToEndDelayVec;

    virtual void initialize();
    virtual void handleMessage(cMessage *msg);
};

Define_Module(Sink);

void Sink::initialize()
{
    endToEndDelayVec.setName("End-to-End Delay");
}

void Sink::handleMessage(cMessage *msg)
{
    simtime_t eed = simTime() - msg->getCreationTime();
    endToEndDelayVec.record(eed);
    delete msg;
}

There is also a recordWithTimestamp() method, to make it possible to record values into output vectors with a timestamp other than simTime(). Increasing timestamp order is still enforced though.

All cOutVector objects write to a single output vector file that has a file extension .vec.

The format and processing of output vector files is described in section [12].

You can configure output vectors from omnetpp.ini: you can disable individual vectors, or limit recording to certain simulation time intervals (see sections [12.2.2], [12.2.5]).

If the output vector object is disabled or the simulation time is outside the specified interval, record() doesn't write anything to the output file. However, if you have a Tkenv or Qtenv inspector window open for the output vector object, the values will be displayed there, regardless of the state of the output vector object.

7.9.2 Output Scalars

While output vectors are to record time series data and thus they typically record a large volume of data during a simulation run, output scalars are supposed to record a single value per simulation run. You can use output scalars

Output scalars are recorded with the record() method of cSimpleModule, and you will usually want to insert this code into the finish() function. An example:

void Transmitter::finish()
{
    double avgThroughput = totalBits / simTime();
    recordScalar("Average throughput", avgThroughput);
}

You can record whole statistic objects by calling their record() methods, declared as part of cStatistic. In the following example we create a Sink module which calculates the mean, standard deviation, minimum and maximum values of a variable, and records them at the end of the simulation.

class Sink : public cSimpleModule
{
  protected:
    cStdDev eedStats;

    virtual void initialize();
    virtual void handleMessage(cMessage *msg);
    virtual void finish();
};

Define_Module(Sink);

void Sink::initialize()
{
    eedStats.setName("End-to-End Delay");
}

void Sink::handleMessage(cMessage *msg)
{
    simtime_t eed = simTime() - msg->getCreationTime();
    eedStats.collect(eed);
    delete msg;
}

void Sink::finish()
{
    recordScalar("Simulation duration", simTime());
    eedStats.record();
}

The above calls record the data into an output scalar file, a line-oriented text file that has the file extension .sca. The format and processing of output vector files is described in chapter [12].

7.10 Watches and Snapshots

7.10.1 Basic Watches

Unfortunately, variables of type int, long, double do not show up by default in Tkenv/Qtenv; neither do STL classes (std::string, std::vector, etc.) or your own structs and classes. This is because the simulation kernel, being a library, knows nothing about types and variables in your source code.

OMNeT++ provides WATCH() and a set of other macros to allow variables to be inspectable in Tkenv/Qtenv and to be output into the snapshot file. WATCH() macros are usually placed into initialize() (to watch instance variables) or to the top of the activity() function (to watch its local variables); the point being that they should only be executed once.

long packetsSent;
double idleTime;

WATCH(packetsSent);
WATCH(idleTime);

Of course, members of classes and structs can also be watched:

WATCH(config.maxRetries);

The Tkenv and Qtenv runtime environments let you inspect and also change the values of inspected variables.

The WATCH() macro can be used with any type that has a stream output operator (operator<<) defined. By default, this includes all primitive types and std::string, but since you can write operator<< for your classes/structs and basically any type, WATCH() can be used with anything. The only limitation is that since the output should more or less fit on single line, the amount of information that can be conveniently displayed is limited.

An example stream output operator:

std::ostream& operator<<(std::ostream& os, const ClientInfo& cli)
{
    os << "addr=" << cli.clientAddr << "  port=" << cli.clientPort; // no endl!
    return os;
}

And the WATCH() line:

WATCH(currentClientInfo);

7.10.2 Read-write Watches

Watches for primitive types and std::string allow for changing the value from the GUI as well, but for other types you need to explicitly add support for that. What you need to do is define a stream input operator (operator>>) and use the WATCH_RW() macro instead of WATCH().

The stream input operator:

std::ostream& operator>>(std::istream& is, ClientInfo& cli)
{
    // read a line from "is" and parse its contents into "cli"
    return is;
}

And the WATCH_RW() line:

WATCH_RW(currentClientInfo);

7.10.3 Structured Watches

WATCH() and WATCH_RW() are basic watches; they allow one line of (unstructured) text to be displayed. However, if you have a data structure generated from message definitions (see Chapter [5]), then there is a better approach. The message compiler automatically generates meta-information describing individual fields of the class or struct, which makes it possible to display the contents on field level.

The WATCH macros to be used for this purpose are WATCH_OBJ() and WATCH_PTR(). Both expect the object to be subclassed from cObject; WATCH_OBJ() expects a reference to such class, and WATCH_PTR() expects a pointer variable.

ExtensionHeader hdr;
ExtensionHeader *hdrPtr;
...
WATCH_OBJ(hdr);
WATCH_PTR(hdrPtr);

CAUTION: With WATCH_PTR(), the pointer variable must point to a valid object or be nullptr at all times, otherwise the GUI may crash while trying to display the object. This practically means that the pointer should be initialized to nullptr even if not used, and should be set to nullptr when the object to which it points is deleted.

delete watchedPtr;
watchedPtr = nullptr;  // set to nullptr when object gets deleted

7.10.4 STL Watches

The standard C++ container classes (vector, map, set, etc) also have structured watches, available via the following macros:

WATCH_VECTOR(), WATCH_PTRVECTOR(), WATCH_LIST(), WATCH_PTRLIST(), WATCH_SET(), WATCH_PTRSET(), WATCH_MAP(), WATCH_PTRMAP().

The PTR-less versions expect the data items ("T") to have stream output operators (operator <<), because that is how they will display them. The PTR versions assume that data items are pointers to some type which has operator <<. WATCH_PTRMAP() assumes that only the value type (“second”) is a pointer, the key type (“first”) is not. (If you happen to use pointers as key, then define operator << for the pointer type itself.)

Examples:

std::vector<int> intvec;
WATCH_VECTOR(intvec);

std::map<std::string,Command*> commandMap;
WATCH_PTRMAP(commandMap);

7.10.5 Snapshots

The snapshot() function outputs textual information about all or selected objects of the simulation (including the objects created in module functions by the user) into the snapshot file.

bool snapshot(cObject *obj=nullptr, const char *label=nullptr);

The function can be called from module functions, like this:

snapshot();     // dump the network
snapshot(this); // dump this simple module and all its objects
snapshot(getSimulation()->getFES()); // dump the future events set

snapshot() will append to the end of the snapshot file. The snapshot file name has an extension of .sna.

The snapshot file output is detailed enough to be used for debugging the simulation: by regularly calling snapshot(), one can trace how the values of variables, objects changed over the simulation. The arguments: label is a string that will appear in the output file; obj is the object whose inside is of interest. By default, the whole simulation (all modules etc) will be written out.

If you run the simulation with Tkenv or Qtenv, you can also create a snapshot from the menu.

An example snapshot file (some abbreviations have been applied):

<?xml version="1.0" encoding="ISO-8859-1"?>
<snapshot object="simulation" label="Long queue" simtime="9.038229311343"
network="FifoNet">
  <object class="cSimulation" fullpath="simulation">
    <info></info>
    <object class="cModule" fullpath="FifoNet">
      <info>id=1</info>
      <object class="fifo::Source" fullpath="FifoNet.gen">
        <info>id=2</info>
        <object class="cPar" fullpath="FifoNet.gen.sendIaTime">
          <info>exponential(0.01s)</info>
        </object>
        <object class="cGate" fullpath="FifoNet.gen.out">
          <info>--> fifo.in</info>
        </object>
      </object>
      <object class="fifo::Fifo" fullpath="FifoNet.fifo">
        <info>id=3</info>
        <object class="cPar" fullpath="FifoNet.fifo.serviceTime">
          <info>0.01</info>
        </object>
        <object class="cGate" fullpath="FifoNet.fifo.in">
          <info><-- gen.out</info>
        </object>
        <object class="cGate" fullpath="FifoNet.fifo.out">
          <info>--> sink.in</info>
        </object>
        <object class="cQueue" fullpath="FifoNet.fifo.queue">
          <info>length=13</info>
          <object class="cMessage" fullpath="FifoNet.fifo.queue.job">
            <info>src=FifoNet.gen (id=2)  dest=FifoNet.fifo (id=3)</info>
          </object>
          <object class="cMessage" fullpath="FifoNet.fifo.queue.job">
            <info>src=FifoNet.gen (id=2)  dest=FifoNet.fifo (id=3)</info>
          </object>
        </object>
      <object class="fifo::Sink" fullpath="FifoNet.sink">
        <info>id=4</info>
        <object class="cGate" fullpath="FifoNet.sink.in">
          <info><-- fifo.out</info>
        </object>
      </object>
    </object>
    <object class="cEventHeap" fullpath="simulation.scheduled-events">
      <info>length=3</info>
      <object class="cMessage" fullpath="simulation.scheduled-events.job">
        <info>src=FifoNet.fifo (id=3)  dest=FifoNet.sink (id=4)</info>
      </object>
      <object class="cMessage" fullpath="...sendMessageEvent">
        <info>at T=9.0464.., in dt=0.00817..; selfmsg for FifoNet.gen (id=2)</info>
      </object>
      <object class="cMessage" fullpath="...end-service">
        <info>at T=9.0482.., in dt=0.01; selfmsg for FifoNet.fifo (id=3)</info>
      </object>
    </object>
  </object>
</snapshot>

7.10.6 Getting Coroutine Stack Usage

It is important to choose the correct stack size for modules. If the stack is too large, it unnecessarily consumes memory; if it is too small, stack violation occurs.

OMNeT++ contains a mechanism that detects stack overflows. It checks the intactness of a predefined byte pattern (0xdeadbeef) at the stack boundary, and reports “stack violation” if it was overwritten. The mechanism usually works fine, but occasionally it can be fooled by large -- and not fully used -- local variables (e.g. char buffer[256]): if the byte pattern happens to fall in the middle of such a local variable, it may be preserved intact and OMNeT++ does not detect the stack violation.

To be able to make a good guess about stack size, you can use the getStackUsage() call which tells you how much stack the module actually uses. It is most conveniently called from finish():

void FooModule::finish()
{
  EV << getStackUsage() << " bytes of stack used\n";
}

The value includes the extra stack added by the user interface library (see extraStackforEnvir in envir/omnetapp.h), which is currently 8K for Cmdenv and at least 16K for Tkenv.

getStackUsage() also works by checking the existence of predefined byte patterns in the stack area, so it is also subject to the above effect with local variables.

7.11 Defining New NED Functions

It is possible to extend the NED language with new functions beyond the built-in ones. New functions are implemented in C++, and then compiled into the simulation model. When a simulation program starts up, the new functions are registered in the NED runtime, and become available for use in NED and ini files.

There are two methods to define NED functions. The Define_NED_Function() macro is the more flexible, preferred method of the two. Define_NED_Math_Function() is the older one, and it supports only certain cases. Both macros have several variants.

7.11.1 Define_NED_Function()

The Define_NED_Function() macro lets you define new functions that can accept arguments of various data types (bool, double, string, etc.), supports optional arguments and also variable argument lists (variadic functions).

The macro has two variants:

Define_NED_Function(FUNCTION,SIGNATURE);
Define_NED_Function2(FUNCTION,SIGNATURE,CATEGORY,DESCRIPTION);

The two variants are basically equivalent; the only difference is that the second one allows you to specify two more parameters, CATEGORY and DESCRIPTION. These two parameters expect human-readable strings that are displayed when listing the available NED functions.

The common parameters, FUNCTION and SIGNATURE are the important ones. FUNCTION is the name of (or pointer to) the C++ function that implements the NED function, and SIGNATURE is the function signature as a string; it defines the name, argument types and return type of the NED function.

You can list the available NED functions by running opp_run or any simulation executable with the -h nedfunctions option. The result will be similar to what you can see in Appendix [22].

$ opp_run -h nedfunctions
OMNeT++ Discrete Event Simulation...
Functions that can be used in NED expressions and in omnetpp.ini:

 Category "conversion":
  double : double double(any x)
    Converts x to double, and returns the result. A boolean argument becomes
    0 or 1; a string is interpreted as number; an XML argument causes an error.
 ...

Seeing the above output, it should now be obvious what the CATEGORY and DESCRIPTION macro arguments are for. OMNeT++ uses the following category names: "conversion", "math", "misc", "ned", "random/continuous", "random/discrete", "strings", "units", "xml". You can use these category names for your own functions as well, when appropriate.

7.11.1.1 The Signature

The signature string has the following syntax:

returntype functionname(argtype1 argname1, argtype2 argname2, ...)

The functionname part defines the name of the NED function, and it must meet the syntactical requirements for NED identifiers (start with a letter or underscore, not be a reserved NED keyword, etc.)

The argument types and return type can be one of the following: bool, int (maps to C/C++ long), double, quantity, string, xml or any; that is, any NED parameter type plus quantity and any. quantity means double with an optional measurement unit (double and int only accept dimensionless numbers), and any stands for any type. The argument names are presently ignored.

To make arguments optional, append a question mark to the argument name. Like in C++, optional arguments may only occur at the end of the argument list, i.e. all arguments after an optional argument must also be optional. The signature string does not have syntax for supplying default values for optional arguments; that is, default values have to be built into the C++ code that implements the NED function. To let the NED function accept any number of additional arguments of arbitrary types, add an ellipsis (...) to the signature as the last argument.

Some examples:

"int factorial(int n)"
"bool isprime(int n)"
"double sin(double x)"
"string repeat(string what, int times)"
"quantity uniform(quantity a, quantity b, long rng?)"
"any choose(int index, ...)"

The first three examples define NED functions with the names factorial, isprime and sin, with the obvious meanings. The fourth example can be the signature for a function that repeats a string n times, and returns the concatenated result. The fifth example is the signature of the existing uniform() NED function; it accepts numbers both with and without measurement units (of course, when invoked with measurement units, both a and b must have one, and the two must be compatible -- this should be checked by the C++ implementation). uniform() also accepts an optional third argument, an RNG index. The sixth example can be the signature of a choose() NED function that accepts an integer plus any number of additional arguments of any type, and returns the indexth one among them.

7.11.1.2 Implementing the NED Function

The C++ function that implements the NED function must have the following signature, as defined by the NEDFunction typedef:

cNEDValue function(cComponent *context, cNEDValue argv[], int argc);

As you can see, the function accepts an array of cNEDValue objects, and returns a cNEDValue; the argc-argv style argument list should be familiar to you from the declaration of the C/C++ main() function. cNEDValue is a class that is used during the evaluation of NED expressions, and represents a value together with its type. The context argument contains the module or channel in the context of which the NED expression is being evaluated; it is useful for implementing NED functions like getParentModuleIndex().

The function implementation does not need to worry too much about checking the number and types of the incoming arguments, because the NED expression evaluator already does that: inside the function you can be sure that the number and types of arguments correspond to the function signature string. Thus, argc is mostly useful only if you have optional arguments or a variable argument list. The NED expression evaluator also checks that the value you return from the function corresponds to the signature.

cNEDValue can store all the needed data types (bool, double, string, etc.), and is equipped with the functions necessary to conveniently read and manipulate the stored value. The value can be read via functions like boolValue(), longValue(), doubleValue(), stringValue() (returns const char *), stdstringValue() (returns const std::string&) and xmlValue() (returns cXMLElement*), or by simply casting the object to the desired data type, making use of the provided typecast operators. Invoking a getter or typecast operator that does not match the stored data type will result in a runtime error. For setting the stored value, cNEDValue provides a number of overloaded set() functions, assignment operators and constructors.

Further cNEDValue member functions provide access to the stored data type; yet other functions are associated with handling quantities, i.e. doubles with measurement units. There are member functions for getting and setting the number part and the measurement unit part separately; for setting the two components together; and for performing unit conversion.

Equipped with the above information, we can already write a simple NED function that returns the length of a string:

static cNEDValue ned_strlen(cComponent *context, cNEDValue argv[], int argc)
{
    return (long)argv[0].stdstringValue().size();
}

Define_NED_Function(ned_strlen, "int length(string s)");

Note that since Define_NED_Function() expects the C++ function to be already declared, we place the function implementation in front of the Define_NED_Function() line. We also declare the function to be static, because its name doesn't need to be visible for the linker. In the function body, we use std::string's size() method to obtain the length of the string, and cast the result to long; the C++ compiler will convert that into a cNEDValue using cNEDValue's long constructor. Note that the int keyword in the signature maps to the C++ type long.

The following example defines a choose() NED function that returns its kth argument that follows the index (k) argument.

static cNEDValue ned_choose(cComponent *context, cNEDValue argv[], int argc)
{
    int index = (int)argv[0];
    if (index < 0 || index >= argc-1)
        throw cRuntimeError("choose(): index %d is out of range", index);
    return argv[index+1];
}

Define_NED_Function(ned_choose, "any choose(int index, ...)");

Here, the value of argv[0] is read using the typecast operator that maps to longValue(). (Note that if the value of the index argument does not fit into an int, the conversion will result in data loss!) The code also shows how to report errors (by throwing a cRuntimeError.)

The third example shows how the built-in uniform() NED function could be reimplemented by the user:

static cNEDValue ned_uniform(cComponent *context, cNEDValue argv[], int argc)
{
    int rng = argc==3 ? (int)argv[2] : 0;
    double argv1converted = argv[1].doubleValueInUnit(argv[0].getUnit());
    double result = uniform((double)argv[0], argv1converted, rng);
    return cNEDValue(result, argv[0].getUnit());
    // or: argv[0].setPreservingUnit(result); return argv[0];
}

Define_NED_Function(ned_uniform, "quantity uniform(quantity a, quantity b, int rng?)");

The first line of the function body shows how to supply default values for optional arguments; for the rng argument in this case. The next line deals with unit conversion. This is necessary because the a and b arguments are both quantities and may come in with different measurement units. We use the doubleValueInUnit() function to obtain the numeric value of b in a's measurement unit. If the two units are incompatible or only one of the parameters have a unit, an error will be raised. If neither parameters have a unit, doubleValueInUnit() simply returns the stored double. Then we call the uniform() C++ function to actually generate a random number, and return it in a temporary object with a's measurement unit. Alternatively, we could have overwritten the numeric part of a with the result using setPreservingUnit(), and returned just that. If there is no measurement unit, getUnit() will return nullptr, which is understood by both doubleValueInUnit() and the cNEDValue constructor.

7.11.1.3 cNEDValue In More Detail

In the previous section we have given an overview and demonstrated the basic use of the cNEDValue class; here we go into further details.

The stored data type can be obtained with the getType() function. It returns an enum (cNEDValue::Type) that has the following values: UNDEF, BOOL, DBL, STR, XML. UNDEF is synonymous with unset; the others have the obvious meanings. There is no separate QUANTITY type: quantities are also represented with the DBL type, which has an optional associated measurement unit. Note that LONG is also missing; the reason is that the NED expression evaluator currently (as of OMNeT++ 4.2) stores all numbers as doubles.

The getTypeName() static function returns the string equivalent of a cNEDValue::Type. The utility functions isSet() and isNumeric() check that the type is (not) UNDEF and DBL, respectively.

cNEDValue value = 5.0;
cNEDValue::Type type = value.getType(); // ==> DBL
EV << cNEDValue::getTypeName(type) << endl; // ==> "double"

We have already seen that the DBL type serves both the double and quantity types of the NED function signature, by storing an optional measurement unit (a string) in addition to the double variable. A cNEDValue can be set to a quantity by creating it with a two-argument constructor that accepts a double and a const char * for unit, or by invoking a similar two-argument set() function. The measurement unit can be read with getUnit(), and overwritten with setUnit(). If you assign a double to a cNEDValue or invoke the one-argument set(double) method on it, that will clear the measurement unit. If you want to overwrite the number part but preserve the original unit, you need to use the setPreservingUnit(double) method.

There are several functions that perform unit conversion. The doubleValueInUnit() method accepts a measurement unit, and attempts to return the number in that unit. The convertTo() method also accepts a measurement unit, and tries to permanently convert the value to that unit; that is, if successful, it changes both the number and the measurement unit part of the object. The convertUnit() static cNEDValue member function accepts three arguments: a quantity as a double and a unit, and a target unit; and returns the number in the target unit. A parseQuantity() static member function parses a string that contains a quantity (e.g. "5min 48s"), and return both the numeric value and the measurement unit. Another version of parseQuantity() tries to return the value in a unit you specify. All functions raise an error if the unit conversion is not possible, e.g. due to incompatible units.

For performance reasons, setUnit(), convertTo() and all other functions that accept and store a measurement unit will only store the const char* pointer, but do not copy the string itself. Consequently, the passed measurement unit pointers must stay valid for at least the lifetime of the cNEDValue object, or even longer if the same pointer propagates to other cNEDValue objects. It is recommended that you only pass pointers that stay valid during the entire simulation. It is safe to use: (1) string constants from the code; (2) unit strings from other cNEDValues; and (3) pooled strings e.g. from a cStringPool or from cNEDValue's static getPooled() function.

Example code:

// manipulating the number and the measurement unit
cNEDValue value(250,"ms");    // initialize to 250ms
value = 300.0;                // ==> 300 (clears the unit!)
value.set(500,"ms");          // ==> 500ms
value.setUnit("s");           // ==> 500s (overwrites the unit)
value.setPreservingUnit(180); // ==> 180s (overwrites the number)
value.setUnit(nullptr);       // ==> 180 (clears the unit)

// unit conversion
value.set(500, "ms");         // ==> 500ms
value.convertTo("s");         // ==> 0.5s
double us = value.doubleValueInUnit("us"); // ==> 500000 (value is unchanged)
double bps = cNEDValue::convertUnit(128, "kbps", "bps"); // ==> 128000
double ms = cNEDValue::convertUnit("2min 15.1s", "ms"); // ==> 135100

// getting persistent measurement unit strings
const char *unit = argv[0].stringValue(); // cannot be trusted to persist
value.setUnit(cNEDValue::getPooled(unit)); // use a persistent copy for setUnit()

7.11.2 Define_NED_Math_Function()

The Define_NED_Math_Function() macro lets you register a C/C++ “mathematical” function as a NED function. The registered C/C++ function may take up to four double arguments, and must return a double; the NED signature will be the same. In other words, functions registered this way cannot accept any NED data type other than double; cannot return anything else than double; cannot accept or return values with measurement units; cannot have optional arguments or variable argument lists; and are restricted to four arguments at most. In exchange for these restrictions, the C++ implementation of the functions is a lot simpler.

Accepted function signatures for Define_NED_Math_Function():

double f();
double f(double);
double f(double, double);
double f(double, double, double);
double f(double, double, double, double);

The simulation kernel uses Define_NED_Math_Function() to expose commonly used <math.h> functions in the NED language. Most <math.h> functions (sin(), cos(), fabs(), fmod(), etc.) can be directly registered without any need for wrapper code, because their signatures is already one of the accepted ones listed above.

The macro has the following variants:

Define_NED_Math_Function(NAME,ARGCOUNT);
Define_NED_Math_Function2(NAME,FUNCTION,ARGCOUNT);
Define_NED_Math_Function3(NAME,ARGCOUNT,CATEGORY,DESCRIPTION);
Define_NED_Math_Function4(NAME,FUNCTION,ARGCOUNT,CATEGORY,DESCRIPTION);

All macros accept the NAME and ARGCOUNT parameters; they are the intended name of the NED function and the number of double arguments the function takes (0..3). NAME should be provided without quotation marks (they will be added inside the macro.) Two macros also accept a FUNCTION parameter, which is the name of (or pointer to) the implementation C/C++ function. The macros that don't have a FUNCTION parameter simply use the NAME parameter for that as well. The last two macros accept CATEGORY and DESCRIPTION, which have exactly the same role as with Define_NED_Function().

Examples:

Define_NED_Math_Function3(sin, 1, "math", "Trigonometric function; see <math.h>");
Define_NED_Math_Function3(cos, 1, "math", "Trigonometric function; see <math.h>");
Define_NED_Math_Function3(pow, 2, "math", "Power-of function; see <math.h>");

7.12 Deriving New Classes

7.12.1 cObject or Not?

If you plan to implement a completely new class (as opposed to subclassing something already present in OMNeT++), you have to ask yourself whether you want the new class to be based on cObject or not. Note that we are not saying you should always subclass from cObject. Both solutions have advantages and disadvantages, which you have to consider individually for each class.

cObject already carries (or provides a framework for) significant functionality that is either relevant to your particular purpose or not. Subclassing cObject generally means you have more code to write (as you have to redefine certain virtual functions and adhere to conventions) and your class will be a bit more heavy-weight. However, if you need to store your objects in OMNeT++ objects like cQueue or you want to store OMNeT++ classes in your object, then you must subclass from cObject.

The most significant features of cObject are the name string (which has to be stored somewhere, so it has its overhead) and ownership management (see section [7.13]), which also provides advantages at some cost.

As a general rule, small struct-like classes like IPAddress or MACAddress are better not subclassed from cObject. If your class has at least one virtual member function, consider subclassing from cObject, which does not impose any extra cost because it doesn't have data members at all, only virtual functions.

7.12.2 cObject Virtual Methods

Most classes in the simulation class library are descendants of cObject. When deriving a new class from cObject or a cObject descendant, one must redefine certain member functions so that objects of the new class can fully co-operate with the simulation library classes. A list of those methods is presented below.

The following methods must be implemented:

If the new class contains other objects subclassed from cObject, either via pointers or as a data member, the following function should be implemented:

Implementation of the following methods is recommended:

It is customary to implement the copy constructor and the assignment operator so that they delegate to the same function of the base class, and invoke a common private copy() function to copy the local members.

7.12.3 Class Registration

You should also use the Register_Class() macro to register the new class. It is used by the createOne() factory function, which can create any object given the class name as a string. createOne() is used by the Envir library to implement omnetpp.ini options such as rng-class="..." or scheduler-class="...". (see Chapter [17])

For example, an omnetpp.ini entry such as

rng-class = "cMersenneTwister"

would result in something like the following code to be executed for creating the RNG objects:

cRNG *rng = check_and_cast<cRNG*>(createOne("cMersenneTwister"));

But for that to work, we needed to have the following line somewhere in the code:

Register_Class(cMersenneTwister);

createOne() is also needed by the parallel distributed simulation feature (Chapter [16]) to create blank objects to unmarshal into on the receiving side.

7.12.4 Details

We'll go through the details using an example. We create a new class NewClass, redefine all above mentioned cObject member functions, and explain the conventions, rules and tips associated with them. To demonstrate as much as possible, the class will contain an int data member, dynamically allocated non-cObject data (an array of doubles), an OMNeT++ object as data member (a cQueue), and a dynamically allocated OMNeT++ object (a cMessage).

The class declaration is the following. It contains the declarations of all methods discussed in the previous section.

//
// file: NewClass.h
//
#include <omnetpp.h>

class NewClass : public cObject
{
  protected:
    int size;
    double *array;
    cQueue queue;
    cMessage *msg;
    ...
  private:
    void copy(const NewClass& other); // local utility function
  public:
    NewClass(const char *name=nullptr, int d=0);
    NewClass(const NewClass& other);
    virtual ~NewClass();
    virtual NewClass *dup() const;
    NewClass& operator=(const NewClass& other);

    virtual void forEachChild(cVisitor *v);
    virtual std::string info();
};

We'll discuss the implementation method by method. Here is the top of the .cc file:

//
// file: NewClass.cc
//
#include <stdio.h>
#include <string.h>
#include <iostream.h>
#include "newclass.h"

Register_Class(NewClass);

NewClass::NewClass(const char *name, int sz) : cObject(name)
{
    size = sz;
    array = new double[size];
    take(&queue);
    msg = nullptr;
}

The constructor (above) calls the base class constructor with the name of the object, then initializes its own data members. You need to call take() for cOwnedObject-based data members.

NewClass::NewClass(const NewClass& other) : cObject(other)
{
    size = -1; // needed by copy()
    array = nullptr;
    msg = nullptr;
    take(&queue);
    copy(other);
}

The copy constructor relies on the private copy() function. Note that pointer members have to be initialized (to nullptr or to an allocated object/memory) before calling the copy() function.

You need to call take() for cOwnedObject-based data members.

NewClass::~NewClass()
{
    delete [] array;
    if (msg->getOwner()==this)
        delete msg;
}

The destructor should delete all data structures the object allocated. cOwnedObject-based objects should only be deleted if they are owned by the object -- details will be covered in section [7.13].

NewClass *NewClass::dup() const
{
    return new NewClass(*this);
}

The dup() function is usually just one line, like the one above.

NewClass& NewClass::operator=(const NewClass& other)
{
    if (&other==this)
        return *this;

    cOwnedObject::operator=(other);
    copy(other);
    return *this;
}

The assignment operator (above) first makes sure that will not try to copy the object to itself, because that can be disastrous. If so (that is, &other==this), the function returns immediately without doing anything.

The base class part is copied via invoking the assignment operator of the base class. Then the method copies over the local members using the copy() private utility function.

void NewClass::copy(const NewClass& other)
{
    if (size != other.size) {
        size = other.size;
        delete array;
        array = new double[size];
    }
    for (int i = 0; i < size; i++)
        array[i] = other.array[i];

    queue = other.queue;
    queue.setName(other.queue.getName());

    if (msg && msg->getOwner()==this)
        delete msg;

    if (other.msg && other.msg->getOwner()==const_cast<cMessage*>(&other))
        take(msg = other.msg->dup());
    else
        msg = other.msg;
}

Complexity associated with copying and duplicating the object is concentrated in the copy() utility function.

Data members are copied in the normal C++ way. If the class contains pointers, you will most probably want to make a deep copy of the data where they point, and not just copy the pointer values.

If the class contains pointers to OMNeT++ objects, you need to take ownership into account. If the contained object is not owned then we assume it is a pointer to an “external” object, consequently we only copy the pointer. If it is owned, we duplicate it and become the owner of the new object. Details of ownership management will be covered in section [7.13].

void NewClass::forEachChild(cVisitor *v)
{
    v->visit(queue);
    if (msg)
        v->visit(msg);
}

The forEachChild() function should call v->visit(obj) for each obj member of the class. See the API Reference for more information about forEachChild().

std::string NewClass::info()
{
    std::stringstream out;
    out << "data=" << data << ", array[0]=" << array[0];
    return out.str();

}

The info() method should produce a concise, one-line string about the object. You should try not to exceed 40-80 characters, since the string will be shown in tooltips and listboxes.

See the virtual functions of cObject and cOwnedObject in the class library reference for more information. The sources of the Sim library (include/, src/sim/) can serve as further examples.

7.13 Object Ownership Management

7.13.1 The Ownership Tree

OMNeT++ has a built-in ownership management mechanism which is used for sanity checks, and as part of the infrastructure supporting Tkenv/Qtenv inspectors.

Container classes like cQueue own the objects inserted into them, but this is not limited to objects inserted into a container: every cOwnedObject-based object has an owner all the time. From the user's point of view, ownership is managed transparently. For example, when you create a new cMessage, it will be owned by the simple module. When you send it, it will first be handed over to (i.e. change ownership to) the FES, and, upon arrival, to the destination simple module. When you encapsulate the message in another one, the encapsulating message will become the owner. When you decapsulate it again, the currently active simple module becomes the owner.

The getOwner() method, defined in cObject, returns the owner of the object:

cOwnedObject *o = msg->getOwner();
EV << "Owner of " << msg->getName() << " is: " <<
   << "(" << o->getClassName() << ") " << o->getFullPath() << endl;

The other direction, enumerating the objects owned can be implemented with the forEachChild() method by it looping through all contained objects and checking the owner of each object.

7.13.1.1 Why Do We Need This?

The traditional concept of object ownership is associated with the “right to delete” objects. In addition to that, keeping track of the owner and the list of objects owned also serves other purposes in OMNeT++:

Some examples of programming errors that can be caught by the ownership facility:

For example, the send() and scheduleAt() functions check that the message being sent/scheduled is owned by the module. If it is not, then it signals a programming error: the message is probably owned by another module (already sent earlier?), or currently scheduled, or inside a queue, a message or some other object -- in either case, the module does not have any authority over it. When you get the error message ("not owner of object"), you need to carefully examine the error message to determine which object has ownership of the message, and correct the logic that caused the error.

The above errors are easy to make in the code, and if not detected automatically, they could cause random crashes which are usually very difficult to track down. Of course, some errors of the same kind still cannot be detected automatically, like calling member functions of a message object which has been sent to (and so is currently owned by) another module.

7.13.2 Managing Ownership

Ownership is managed transparently for the user, but this mechanism has to be supported by the participating classes themselves. It will be useful to look inside cQueue and cArray, because they might give you a hint what behavior you need to implement when you want to use non-OMNeT++ container classes to store messages or other cOwnedObject-based objects.

7.13.2.1 Insertion

cArray and cQueue have internal data structures (array and linked list) to store the objects which are inserted into them. However, they do not necessarily own all of these objects. (Whether they own an object or not can be determined from that object's getOwner() pointer.)

The default behaviour of cQueue and cArray is to take ownership of the objects inserted. This behavior can be changed via the takeOwnership flag.

Here is what the insert operation of cQueue (or cArray) does:

The corresponding source code:

void cQueue::insert(cOwnedObject *obj)
{
    // insert into queue data structure
    ...

    // take ownership if needed
    if (getTakeOwnership())
        take(obj);

}

7.13.2.2 Removal

Here is what the remove family of operations in cQueue (or cArray) does:

After the object was removed from a cQueue/cArray, you may further use it, or if it is not needed any more, you can delete it.

The release ownership phrase requires further explanation. When you remove an object from a queue or array, the ownership is expected to be transferred to the simple module's local objects list. This is accomplished by the drop() function, which transfers the ownership to the object's default owner. getDefaultOwner() is a virtual method defined in cOwnedObject, and its implementation returns the currently executing simple module's local object list.

As an example, the remove() method of cQueue is implemented like this:

cOwnedObject *cQueue::remove(cOwnedObject *obj)
{
    // remove object from queue data structure
    ...

    // release ownership if needed
    if (obj->getOwner()==this)
        drop(obj);

    return obj;
}

7.13.2.3 Destructor

The concept of ownership is that the owner has the exclusive right and duty to delete the objects it owns. For example, if you delete a cQueue containing cMessages, all messages it contains and owns will also be deleted.

The destructor should delete all data structures the object allocated. From the contained objects, only the owned ones are deleted -- that is, where obj->getOwner()==this.

7.13.2.4 Object Copying

The ownership mechanism also has to be taken into consideration when a cArray or cQueue object is duplicated (using dup() or the copy constructor.) The duplicate is supposed to have the same content as the original; however, the question is whether the contained objects should also be duplicated or only their pointers taken over to the duplicate cArray or cQueue. A similar question arises when an object is copied using the assignment operator (operator=()).

The convention followed by cArray/cQueue is that only owned objects are copied, and the contained but not owned ones will have their pointers taken over and their original owners left unchanged.



[Prev] [Next] [TOC] [Chapters]