User Manual

OMNeT++ version 4.0

Chapters

1 Introduction
2 Overview
3 The NED Language
4 Simple Modules
5 Messages
6 The Simulation Library
7 Building Simulation Programs
8 Configuring Simulations
9 Running Simulations
10 Network Graphics And Animation
11 Analyzing Simulation Results
12 Eventlog
13 Documenting NED and Messages
14 Parallel Distributed Simulation
15 Plug-in Extensions
16 Embedding the Simulation Kernel
17 Appendix: NED Reference
18 Appendix: NED Language Grammar
19 Appendix: NED XML Binding
20 Appendix: NED Functions
21 Appendix: Message Definitions Grammar
22 Appendix: Display String Tags
23 Appendix: Configuration Options
24 Appendix: Result File Formats
25 Appendix: Eventlog File Format


Table of Contents


  1 Introduction
    1.1 What is OMNeT++?
    1.2 Organization of this manual
    1.3 Credits

  2 Overview
    2.1 Modeling concepts
      2.1.1 Hierarchical modules
      2.1.2 Module types
      2.1.3 Messages, gates, links
      2.1.4 Modeling of packet transmissions
      2.1.5 Parameters
      2.1.6 Topology description method
    2.2 Programming the algorithms
    2.3 Using OMNeT++
      2.3.1 Building and running simulations
      2.3.2 What is in the distribution

  3 The NED Language
    3.1 NED overview
    3.2 Warmup
      3.2.1 The network
      3.2.2 Introducing a channel
      3.2.3 The App, Routing and Queue simple modules
      3.2.4 The Node compound module
      3.2.5 Putting it together
    3.3 Simple modules
    3.4 Compound modules
    3.5 Channels
    3.6 Parameters
    3.7 Gates
    3.8 Submodules
    3.9 Connections
    3.10 Multiple connections
      3.10.1 Connection patterns
    3.11 Submodule type as parameter
    3.12 Properties (metadata annotations)
    3.13 Inheritance
    3.14 Packages

  4 Simple Modules
    4.1 Simulation concepts
      4.1.1 Discrete Event Simulation
      4.1.2 The event loop
      4.1.3 Simple modules in OMNeT++
      4.1.4 Events in OMNeT++
      4.1.5 Simulation time
      4.1.6 FES implementation
    4.2 Defining simple module types
      4.2.1 Overview
      4.2.2 Constructor
      4.2.3 Constructor and destructor vs initialize() and finish()
      4.2.4 An example
      4.2.5 Using global variables
    4.3 Adding functionality to cSimpleModule
      4.3.1 handleMessage()
      4.3.2 activity()
      4.3.3 initialize() and finish()
      4.3.4 handleParameterChange()
      4.3.5 Reusing module code via subclassing
    4.4 Accessing module parameters
      4.4.1 Volatile and non-volatile parameters
      4.4.2 Changing a parameter's value
      4.4.3 Further cPar methods
      4.4.4 Emulating parameter arrays
    4.5 Accessing gates and connections
      4.5.1 Gate objects
      4.5.2 Connections
      4.5.3 The connection's channel
    4.6 Sending and receiving messages
      4.6.1 Sending messages
      4.6.2 Packet transmissions
      4.6.3 Delay, data rate, bit error rate, packet error rate
      4.6.4 Broadcasts and retransmissions
      4.6.5 Delayed sending
      4.6.6 Direct message sending
      4.6.7 Receiving messages
      4.6.8 The wait() function
      4.6.9 Modeling events using self-messages
    4.7 Stopping the simulation
      4.7.1 Normal termination
      4.7.2 Raising errors
    4.8 Finite State Machines in OMNeT++
    4.9 Walking the module hierarchy
    4.10 Direct method calls between modules
    4.11 Dynamic module creation
      4.11.1 When do you need dynamic module creation
      4.11.2 Overview
      4.11.3 Creating modules
      4.11.4 Deleting modules
      4.11.5 Module deletion and finish()
      4.11.6 Creating connections
      4.11.7 Removing connections

  5 Messages
    5.1 Messages and packets
      5.1.1 The cMessage class
      5.1.2 Self-messages
      5.1.3 Modelling packets
      5.1.4 Encapsulation
      5.1.5 Attaching parameters and objects
    5.2 Message definitions
      5.2.1 Introduction
      5.2.2 Declaring enums
      5.2.3 Message declarations
      5.2.4 Inheritance, composition
      5.2.5 Using existing C++ types
      5.2.6 Customizing the generated class
      5.2.7 Using STL in message classes
      5.2.8 Summary
      5.2.9 What else is there in the generated code?

  6 The Simulation Library
    6.1 Class library conventions
      6.1.1 Base class
      6.1.2 Setting and getting attributes
      6.1.3 getClassName()
      6.1.4 Name attribute
      6.1.5 getFullName() and getFullPath()
      6.1.6 Copying and duplicating objects
      6.1.7 Iterators
      6.1.8 Error handling
    6.2 Logging from modules
    6.3 Simulation time conversion
    6.4 Generating random numbers
      6.4.1 Random number generators
      6.4.2 Random number streams, RNG mapping
      6.4.3 Accessing the RNGs
      6.4.4 Random variates
      6.4.5 Random numbers from histograms
    6.5 Container classes
      6.5.1 Queue class: cQueue
      6.5.2 Expandable array: cArray
    6.6 Routing support: cTopology
      6.6.1 Overview
      6.6.2 Basic usage
      6.6.3 Shortest paths
    6.7 Statistics and distribution estimation
      6.7.1 cStatistic and descendants
      6.7.2 Distribution estimation
      6.7.3 The k-split algorithm
      6.7.4 Transient detection and result accuracy
    6.8 Recording simulation results
      6.8.1 Output vectors: cOutVector
      6.8.2 Output scalars
      6.8.3 Precision
    6.9 Watches and snapshots
      6.9.1 Basic watches
      6.9.2 Read-write watches
      6.9.3 Structured watches
      6.9.4 STL watches
      6.9.5 Snapshots
      6.9.6 Getting coroutine stack usage
    6.10 Deriving new classes
      6.10.1 cOwnedObject or not?
      6.10.2 cOwnedObject virtual methods
      6.10.3 Class registration
      6.10.4 Details
    6.11 Object ownership management
      6.11.1 The ownership tree
      6.11.2 Managing ownership

  7 Building Simulation Programs
    7.1 Overview
    7.2 Using gcc
      7.2.1 The opp_makemake tool
      7.2.2 Basic use
      7.2.3 Debug and release builds
      7.2.4 Using external C/C++ libraries
      7.2.5 Building directory trees
      7.2.6 Automatic include dirs
      7.2.7 Dependency handling
      7.2.8 Out-of-directory build
      7.2.9 Building shared and static libraries
      7.2.10 Recursive builds
      7.2.11 Customizing the Makefile
      7.2.12 Projects with multiple source trees
      7.2.13 A multi-directory example

  8 Configuring Simulations
    8.1 Configuring simulations
    8.2 The configuration file: omnetpp.ini
      8.2.1 An example
      8.2.2 File syntax
      8.2.3 File inclusion
    8.3 Sections
      8.3.1 The [General] section
      8.3.2 Named configurations
      8.3.3 Section inheritance
    8.4 Setting module parameters
      8.4.1 Using wildcard patterns
      8.4.2 Using the default values
    8.5 Parameter studies
      8.5.1 Basic use
      8.5.2 Named iteration variables
      8.5.3 Repeating runs with different seeds
    8.6 Parameter Studies and Result Analysis
      8.6.1 Output vectors and scalars
      8.6.2 Configuring output vectors
      8.6.3 Saving parameters as scalars
      8.6.4 Experiment-Measurement-Replication
    8.7 Configuring the random number generators
      8.7.1 Number of RNGs
      8.7.2 RNG choice
      8.7.3 RNG mapping
      8.7.4 Automatic seed selection
      8.7.5 Manual seed configuration

  9 Running Simulations
    9.1 Introduction
      9.1.1 Running a simulation executable
      9.1.2 Running a shared library
      9.1.3 Controlling the run
    9.2 Cmdenv: the command-line interface
      9.2.1 Example run
      9.2.2 Command-line switches
      9.2.3 Cmdenv ini file options
      9.2.4 Interpreting Cmdenv output
    9.3 Tkenv: the graphical user interface
      9.3.1 Command-line switches
    9.4 Batch execution
      9.4.1 Using Cmdenv
      9.4.2 Using shell scripts
      9.4.3 Using opp_runall
    9.5 Akaroa support: Multiple Replications in Parallel
      9.5.1 Introduction
      9.5.2 What is Akaroa
      9.5.3 Using Akaroa with OMNeT++
    9.6 Troubleshooting
      9.6.1 Unrecognized configuration option
      9.6.2 Stack problems
      9.6.3 Memory leaks and crashes
      9.6.4 Simulation executes slowly

  10 Network Graphics And Animation
    10.1 Display strings
      10.1.1 Display string syntax
      10.1.2 Display string placement
      10.1.3 Display string inheritance
      10.1.4 Display string tags used in submodule context
      10.1.5 Display string tags used in module background context
      10.1.6 Connection display strings
      10.1.7 Message display strings
    10.2 Parameter substitution
    10.3 Colors
      10.3.1 Color names
      10.3.2 Icon colorization
    10.4 The icons
      10.4.1 The image path
      10.4.2 Categorized icons
      10.4.3 Icon size
    10.5 Layouting
    10.6 Enhancing animation
      10.6.1 Changing display strings at runtime
      10.6.2 Bubbles

  11 Analyzing Simulation Results
    11.1 Result files
      11.1.1 Results
      11.1.2 Output vectors
      11.1.3 Format of output vector files
      11.1.4 Scalar results
    11.2 The Analysis Tool in the Simulation IDE
    11.3 Scave Tool
      11.3.1 Filter command
      11.3.2 Index command
      11.3.3 Summary command
    11.4 Alternative statistical analysis and plotting tools
      11.4.1 Spreadsheet programs
      11.4.2 GNU R
      11.4.3 MATLAB or Octave
      11.4.4 NumPy and MatPlotLib
      11.4.5 ROOT
      11.4.6 Gnuplot
      11.4.7 Grace

  12 Eventlog
    12.1 Introduction
    12.2 Configuration
      12.2.1 File Name
      12.2.2 Recording Intervals
      12.2.3 Recording Modules
      12.2.4 Recording Message Data
    12.3 Eventlog Tool
      12.3.1 Filter
      12.3.2 Echo

  13 Documenting NED and Messages
    13.1 Overview
    13.2 Documentation comments
      13.2.1 Private comments
      13.2.2 More on comment placement
    13.3 Text layout and formatting
      13.3.1 Paragraphs and lists
      13.3.2 Special tags
      13.3.3 Text formatting using HTML
      13.3.4 Escaping HTML tags
    13.4 Customizing and adding pages
      13.4.1 Adding a custom title page
      13.4.2 Adding extra pages
      13.4.3 Incorporating externally created pages

  14 Parallel Distributed Simulation
    14.1 Introduction to Parallel Discrete Event Simulation
    14.2 Assessing available parallelism in a simulation model
    14.3 Parallel distributed simulation support in OMNeT++
      14.3.1 Overview
      14.3.2 Parallel Simulation Example
      14.3.3 Placeholder modules, proxy gates
      14.3.4 Configuration
      14.3.5 Design of PDES Support in OMNeT++

  15 Plug-in Extensions
    15.1 Overview
    15.2 Plug-in descriptions
      15.2.1 Defining a new random number generator
      15.2.2 Defining a new scheduler
      15.2.3 Defining a new configuration provider
      15.2.4 Defining a new output scalar manager
      15.2.5 Defining a new output vector manager
      15.2.6 Defining a new snapshot manager
    15.3 Accessing the configuration
      15.3.1 Defining new configuration options
      15.3.2 Reading values from the configuration
    15.4 Implementing a new user interface

  16 Embedding the Simulation Kernel
    16.1 Architecture
    16.2 Embedding the OMNeT++ simulation kernel
      16.2.1 The main() function
      16.2.2 The simulate() function
      16.2.3 Providing an environment object
      16.2.4 Providing a configuration object
      16.2.5 Loading NED files
      16.2.6 How to eliminate NED files
      16.2.7 Assigning module parameters
      16.2.8 Extracting statistics from the model
      16.2.9 The simulation loop
      16.2.10 Multiple, coexisting simulations
      16.2.11 Installing a custom scheduler
      16.2.12 Multi-threaded programs

  17 Appendix: NED Reference
    17.1 Syntax
      17.1.1 NED file extension
      17.1.2 NED file encoding
      17.1.3 Reserved words
      17.1.4 Identifiers
      17.1.5 Case sensitivity
      17.1.6 Literals
      17.1.7 Comments
      17.1.8 Grammar
    17.2 Built-in definitions
    17.3 Packages
      17.3.1 Package declaration
      17.3.2 Directory structure, package.ned
    17.4 Components
      17.4.1 Simple modules
      17.4.2 Compound modules
      17.4.3 Networks
      17.4.4 Channels
      17.4.5 Module interfaces
      17.4.6 Channel interfaces
      17.4.7 Resolving the implementation C++ class
      17.4.8 Properties
      17.4.9 Parameters
      17.4.10 Gates
      17.4.11 Submodules
      17.4.12 Connections
      17.4.13 Inner types
      17.4.14 Name uniqueness
      17.4.15 Type name resolution
      17.4.16 Implementing an interface
      17.4.17 Inheritance
      17.4.18 Network build order
    17.5 Expressions
      17.5.1 Operators
      17.5.2 Referencing parameters and loop variables
      17.5.3 The index operator
      17.5.4 The sizeof() operator
      17.5.5 The xmldoc() operator
      17.5.6 Functions
      17.5.7 Units of measurement

  18 Appendix: NED Language Grammar

  19 Appendix: NED XML Binding

  20 Appendix: NED Functions

  21 Appendix: Message Definitions Grammar

  22 Appendix: Display String Tags
    22.1 Module and connection display string tags
    22.2 Message display string tags

  23 Appendix: Configuration Options
    23.1 Configuration Options
    23.2 Predefined Configuration Variables

  24 Appendix: Result File Formats
    24.1 Version
    24.2 Run Declaration
    24.3 Attributes
    24.4 Module Parameters
    24.5 Scalar Data
    24.6 Vector Declaration
    24.7 Vector Data
    24.8 Index Header
    24.9 Index Data
    24.10 Statistics Object
    24.11 Field
    24.12 Histogram Bin

  25 Appendix: Eventlog File Format



1 Introduction

1.1 What is OMNeT++?

OMNeT++ is an object-oriented modular discrete event network simulation framework. It has a generic architecture, so it can be (and has been) used in various problem domains:

OMNeT++ itself is not a simulator of anything concrete, but it rather provides infrastructure and tools for writing simulations. One of the fundamental ingredients of this infrastructure is a component architecture for simulation models. Models are assembled from reusable components termed modules. Well-written modules are truly reusable, and can be combined in various ways like LEGO blocks.

Modules can be connected with each other via gates (other systems would call them ports), and combined to form compound modules. The depth of module nesting is not limited. Modules communicate through message passing, where messages may carry arbitrary data structures. Modules can may messages along predefined paths via gates and connections, or directly to their destination; the latter is useful for wireless simulations, for example. Modules may have parameters, which can be used to customize module behaviour, and/or to parameterize the model's topology. Modules at the lowest level of the module hierarchy are called simple modules, and they encapsulate behaviour. Simple modules are programmed in C++, and make use of the simulation library.

OMNeT++ simulations can be run under various user interfaces. Graphical, animating user interfaces are highly useful for demonstration and debugging purposes, and command-line user interfaces are best for batch execution.

The simulator as well as user interfaces and tools are highly portable. They are tested on the most common operating systems (Linux, Mac OS/X, Windows), and they can be compiled out of the box or after trivial modifications on most Unix-like operating systems.

OMNeT++ also supports parallel distributed simulation. OMNeT++ can use several mechanisms for communication between partitions of a parallel distributed simulation, for example MPI or named pipes. The parallel simulation algorithm can easily be extended or new ones plugged in. Models do not need any special instrumentation to be run in parallel -- it is just a matter of configuration. OMNeT++ can even be used for classroom presentation of parallel simulation algorithms, because simulations can be run in parallel even under the GUI which provides detailed feedback on what is going on.

OMNEST is the commercially supported version of OMNeT++. OMNeT++ is only free for academic and non-profit use -- for commercial purposes one needs to obtain OMNEST licenses from Simulcraft Inc.

1.2 Organization of this manual

The manual is organized the following way:

1.3 Credits

OMNeT++ has been developed by András Varga (andras@omnetpp.org, andras.varga@omnest.com).

In the early stage of the project, several people have contributed to OMNeT++. Although most contributed code is no longer part of the OMNeT++, nevertheless I'd like to acknowledge the work of the following people. First of all, I'd like thank Dr György Pongor (pongor@hit.bme.hu), my advisor at the Technical University of Budapest who initiated the OMNeT++ as a student project.

My fellow student Ákos Kun started to program the first NED parser in 1992-93, but it was abandoned after a few months. The first version of nedc was finally developed in summer 1995, by three exchange students from TU Delft: Jan Heijmans, Alex Paalvast and Robert van der Leij. nedc was first called JAR after their initials until it got renamed to nedc. nedc was further developed and refactored several times until it finally retired and got replaced by nedtool in OMNeT++ 3.0. The second group of Delft exchange students (Maurits André, George van Montfort, Gerard van de Weerd) arrived in fall 1995. They performed some testing of the simulation library, and wrote some example simulations, for example the original version of Token Ring, and simulation of the NIM game which survived until OMNeT++ 3.0. These student exchanges were organized by Dr. Leon Rothkranz at TU Delft, and György Pongor at TU Budapest.

The diploma thesis of Zoltán Vass (spring 1996) was to prepare OMNeT++ for parallel execution over PVM to OMNeT++. This code has been replaced with the new Parallel Simulation Architecture in OMNeT++ 3.0. Gábor Lencse (lencse@hit.bme.hu) was also interested in parallel simulation, namely a method called Statistical Synchronization (SSM). He implemented the FDDI model (practically unchanged until now), and added some extensions into NED for SSM. These extensions have been removed since then (OMNeT++ 3.0 does parallel execution on different principles).

The P2 algorithm and the original implementation of the k-split algorithm was programmed in fall 1996 by Babak Fakhamzadeh from TU Delft. k-split was later reimplemented by András.

Several bugfixes and valuable suggestions for improvements came from the user community of OMNeT++. It would be impossible to mention everyone here, and the list is constantly growing -- instead, the README and ChangeLog files contain acknowledgements.

Between summer 2001 and fall 2004, the OMNeT++ CVS was hosted at the University of Karlsruhe. Credit for setting up and maintaining the CVS server goes to Ulrich Kaage. Ulrich can also be credited with converting the User Manual from Microsoft Word format to LaTeX, which was a huge undertaking and great help.



2 Overview

2.1 Modeling concepts

An OMNeT++ model consists of modules that communicate with message passing. The active modules are termed simple modules; they are written in C++, using the simulation class library. Simple modules can be grouped into compound modules and so forth; the number of hierarchy levels is not limited. The whole model, called network in OMNeT++, is itself a compound module. Messages can be sent either via connections that span between modules or directly to other modules. The concept of simple and compound modules is similar to DEVS atomic and coupled models. In Fig. below, boxes represent simple modules (gray background) and compound modules. Arrows connecting small boxes represent connections and gates.


Figure: Simple and compound modules

Modules communicate with messages which -- in addition to usual attributes such as timestamp -- may contain arbitrary data. Simple modules typically send messages via gates, but it is also possible to send them directly to their destination modules. Gates are the input and output interfaces of modules: messages are sent out through output gates and arrive through input gates. An input and an output gate can be linked with a connection. Connections are created within a single level of module hierarchy: within a compound module, corresponding gates of two submodules, or a gate of one submodule and a gate of the compound module can be connected. Connections spanning across hierarchy levels are not permitted, as it would hinder model reuse. Due to the hierarchical structure of the model, messages typically travel through a chain of connections, to start and arrive in simple modules. Compound modules act as 'cardboard boxes' in the model, transparently relaying messages between their inside and the outside world. Parameters such as propagation delay, data rate and bit error rate, can be assigned to connections. One can also define connection types with specific properties (termed channels) and reuse them in several places. Modules can have parameters. Parameters are mainly used to pass configuration data to simple modules, and to help define model topology. Parameters may take string, numeric or boolean values. Because parameters are represented as objects in the program, parameters -- in addition to holding constants -- may transparently act as sources of random numbers with the actual distributions provided with the model configuration, they may interactively prompt the user for the value, and they might also hold expressions referencing other parameters. Compound modules may pass parameters or expressions of parameters to their submodules.

OMNeT++ provides efficient tools for the user to describe the structure of the actual system. Some of the main features are:

2.1.1 Hierarchical modules

An OMNeT++ model consists of hierarchically nested modules, which communicate by passing messages to each another. OMNeT++ models are often referred to as networks. The top level module is the system module. The system module contains submodules, which can also contain submodules themselves (Fig. below). The depth of module nesting is not limited; this allows the user to reflect the logical structure of the actual system in the model structure.

Model structure is described in OMNeT++'s NED language.

Modules that contain submodules are termed compound modules, as opposed simple modules which are at the lowest level of the module hierarchy. Simple modules contain the algorithms in the model. The user implements the simple modules in C++, using the OMNeT++ simulation class library.

2.1.2 Module types

Both simple and compound modules are instances of module types. While describing the model, the user defines module types; instances of these module types serve as components for more complex module types. Finally, the user creates the system module as an instance of a previously defined module type; all modules of the network are instantiated as submodules and sub-submodules of the system module.

When a module type is used as a building block, there is no distinction whether it is a simple or a compound module. This allows the user to split a simple module into several simple modules embedded into a compound module, or vica versa, aggregate the functionality of a compound module into a single simple module, without affecting existing users of the module type.

Module types can be stored in files separately from the place of their actual usage. This means that the user can group existing module types and create component libraries. This feature will be discussed later, in Chapter [9].

2.1.3 Messages, gates, links

Modules communicate by exchanging messages. In an actual simulation, messages can represent frames or packets in a computer network, jobs or customers in a queuing network or other types of mobile entities. Messages can contain arbitrarily complex data structures. Simple modules can send messages either directly to their destination or along a predefined path, through gates and connections.

The ``local simulation time'' of a module advances when the module receives a message. The message can arrive from another module or from the same module (self-messages are used to implement timers).

Gates are the input and output interfaces of modules; messages are sent out through output gates and arrive through input gates.

Each connection (also called link) is created within a single level of the module hierarchy: within a compound module, one can connect the corresponding gates of two submodules, or a gate of one submodule and a gate of the compound module (Fig. below).

Due to the hierarchical structure of the model, messages typically travel through a series of connections, to start and arrive in simple modules. Such series of connections that go from simple module to simple module are called routes. Compound modules act as `cardboard boxes' in the model, transparently relaying messages between their inside and the outside world.

2.1.4 Modeling of packet transmissions

Connections can be assigned three parameters, which facilitate the modeling of communication networks, but can be useful in other models too: propagation delay, bit error rate and data rate, all three being optional. One can specify link parameters individually for each connection, or define link types and use them throughout the whole model.

Propagation delay is the amount of time the arrival of the message is delayed by when it travels through the channel.

Bit error rate specifies the probability that a bit is incorrectly transmitted, and allows for simple noisy channel modelling.

Data rate is specified in bits/second, and it is used for calculating transmission time of a packet.

When data rates are in use, the sending of the message in the model corresponds to the transmission of the first bit, and the arrival of the message corresponds to the reception of the last bit. This model is not always applicable, for example protocols like Token Ring and FDDI do not wait for the frame to arrive in its entirety, but rather start repeating its first bits soon after they arrive -- in other words, frames ``flow through'' the stations, being delayed only a few bits. If you want to model such networks, it is possible to change this default behaviour and deliver the message to the module when the first bit is received.

2.1.5 Parameters

Modules can have parameters. Parameters can be assigned either in the NED files or the configuration file omnetpp.ini.

Parameters may be used to customize simple module behaviour, and for parameterizing the model topology.

Parameters can take string, numeric or boolean values, or can contain XML data trees. Numeric values include expressions using other parameters and calling C functions, random variables from different distributions, and values input interactively by the user.

Numeric-valued parameters can be used to construct topologies in a flexible way. Within a compound module, parameters can define the number of submodules, number of gates, and the way the internal connections are made.

2.1.6 Topology description method

The user defines the structure of the model in NED language descriptions (Network Description).The NED language will be discussed in detail in Chapter [3].

2.2 Programming the algorithms

The simple modules of a model contain algorithms as C++ functions. The full flexibility and power of the programming language can be used, supported by the OMNeT++ simulation class library. The simulation programmer can choose between event-driven and process-style description, and can freely use object-oriented concepts (inheritance, polymorphism etc) and design patterns to extend the functionality of the simulator.

Simulation objects (messages, modules, queues etc.) are represented by C++ classes. They have been designed to work together efficiently, creating a powerful simulation programming framework. The following classes are part of the simulation class library:

The classes are also specially instrumented, allowing one to traverse objects of a running simulation and display information about them such as name, class name, state variables or contents. This feature has made it possible to create a simulation GUI where all internals of the simulation are visible.

2.3 Using OMNeT++

2.3.1 Building and running simulations

This section provides insight into working with OMNeT++ in practice: Issues such as model files, compiling and running simulations are discussed.

An OMNeT++ model consists of the following parts:

The simulation system provides the following components:

Simulation programs are built from the above components. First, .msg files are translated into C++ code using the opp_msgc. program. Then all C++ sources are compiled, and linked with the simulation kernel and a user interface library to form a simulation executable or shared library. NED files are loaded dynamically in their original text forms when the simulation program starts.

Running the simulation and analyzing the results

The simulation may be compiled as a standalone program executable, thus it can be run on other machines without OMNeT++ being present or it can be created as a shared library. In this case the OMNeT++ shared libraries must be present on that system. When the program is started, first it reads all NED files containing your model topology, then it reads a configuration file (usually called omnetpp.ini). This file contains settings that control how the simulation is executed, values for model parameters, etc. The configuration file can also prescribe several simulation runs; in the simplest case, they will be executed by the simulation program one after another.

The output of the simulation is written into data files: output vector files, output scalar files , and possibly the user's own output files. OMNeT++ contains an Integrated Development Environment that provides rich environment for analyzing these files. It is not expected that someone will process the result files using OMNeT++ alone: output files are text files in a format which can be read into math packages like Matlab or Octave, or imported into spreadsheets like OpenOffice Calc, Gnumeric or MS Excel (some preprocessing using sed, awk or perl might be required, this will be discussed later). All these external programs provide rich functionality for statistical analysis and visualization, and it is outside the scope of OMNeT++ to duplicate their efforts. This manual briefly describes some data plotting programs and how to use them with OMNeT++.

Output scalar files can be visualized with the OMNeT++ IDE too. It can draw bar charts, x-y plots (e.g. throughput vs offered load), or export data via the clipboard for more detailed analysis into spreadsheets and other programs.

User interfaces

The primary purpose of user interfaces is to make the internals of the model visible to the user, to control simulation execution, and possibly allow the user to intervene by changing variables/objects inside the model. This is very important in the development/debugging phase of the simulation project. Just as important, a hands-on experience allows the user to get a `feel' of the model's behaviour. The graphical user interface can also be used to demonstrate a model's operation.

The same simulation model can be executed with different user interfaces, without any change in the model files themselves. The user would test and debug the simulation with a powerful graphical user interface, and finally run it with a simple and fast user interface that supports batch execution.

Component libraries

Module types can be stored in files separate from the place of their actual use. This enables the user to group existing module types and create component libraries.

Universal standalone simulation programs

A simulation executable can store several independent models that use the same set of simple modules. The user can specify in the configuration file which model is to be run. This allows one to build one large executable that contains several simulation models, and distribute it as a standalone simulation tool. The flexibility of the topology description language also supports this approach.

2.3.2 What is in the distribution

If you installed the source distribution, the omnetpp directory on your system should contain the following subdirectories. (If you installed a precompiled distribution, some of the directories may be missing, or there might be additional directories, e.g. containing software bundled with OMNeT++.)

The simulation system itself:

  omnetpp/         OMNeT++ root directory
    bin/           OMNeT++ executables
    include/       header files for simulation models
    lib/           library files
    images/        icons and backgrounds for network graphics
    doc/           manuals, readme files, license, APIs, etc.
      manual/      manual in HTML
      migration/   how to migrate your models from 3.x to 4.0 version
      ned2/        DTD definition of the XML syntax for NED files
      tictoc-tutorial/  introduction into using OMNeT++
      api/         API reference in HTML
      nedxml-api/  API reference for the NEDXML library
      parsim-api/  API reference for the parallel simulation library
    migrate/       tools to help model migration from 3.x to 4.0 version
    src/           OMNeT++ sources
      sim/         simulation kernel
        parsim/    files for distributed execution
        netbuilder/files for dynamically reading NED files
      envir/       common code for user interfaces
      cmdenv/      command-line user interface
      tkenv/       Tcl/Tk-based user interface
      nedxml/      NEDXML library, nedtool, opp_msgc
      scave/       result analysis library
      eventlog/    eventlog processing library
      layout/      graph layouter for network graphics
      common/      common library
      utils/       opp_makemake, opp_test, etc.
    test/          regression test suite
      core/        tests for the simulation library
      anim/        tests for graphics and animation
      dist/        tests for the built-in distributions
      makemake/    tests for opp_makemake
      ...

The Eclipse-based Simulation IDE is in the ide directory.

    ide/           Simulation IDE
      features/    Eclipse feature definitions
      plugins/     IDE plugins (extensions to the IDE can be dropped here)
      ...

The Windows version of OMNeT++ contains a redistribution of the MinGW gcc compiler, together with a copy of MSYS that provides Unix tools commonly used in Makefiles. The MSYS directory also contains various 3rd party open-source libraries needed to compile and run OMNeT++.

    mingw/       MinGW gcc port
    msys/        MSYS plus libraries

Sample simulations are in the samples directory.

    samples/     directories for sample simulations
      aloha/     models the Aloha protocol
      cqn/       Closed Queueing Network
      ...

The contrib directory contains material from the OMNeT++ community.

    contrib/     directory for contributed material
      octave/    Octave scripts for result processing
      emacs/     NED syntax highlight for Emacs
      ...



3 The NED Language

3.1 NED overview

The user describes the structure of a simulation model in the NED language. NED stands for Network Description. NED lets the user declare simple modules, and connect and assemble them into compound modules. The user can label some compound modules as networks, self-contained simulation models. Channels are another component type, whose instances can also be used in compound modules.

The NED language has several features which let it scale well to large projects:

  • [Hierarchical] The traditional way to deal with complexity is via introducing hierarchies. In OMNeT++, any module which would be too complex as a single entity can be broken down into smaller modules, and used as a compound module.

  • [Component-Based] Simple modules and compound modules are inherently reusable, which not only reduces code copying, but more importantly, allows component libraries (like the INET Framework, MiXiM, Castalia, etc.) to exist.

  • [Interfaces] Module and channel interfaces can be used as a placeholder where normally a module or channel type would be used, and the concrete module or channel type is determined at network setup time by a parameter. Concrete module types have to ``implement'' the interface they can substitute. For example, given a compound module type named MobileHost contains a mobility submodule of the type IMobility (where IMobility is a module interface), the actual type of mobility may be chosen from the module types that implemented IMobility (RandomWalkMobility, TurtleMobility, etc.)

  • [Inheritance] Modules and channels can be subclassed. Derived modules and channels may add new parameters, gates, and (in the case of compound modules) new submodules and connections. They may set existing parameters to a specific value, and also set the gate size of a gate vector. This makes it possible, for example, to take a GenericTCPClientApp module and derive an FTPClientApp from it by setting certain parameters to a fixed value; or to derive a WebClientHost compound module from a BaseHost compound module by adding a WebClientApp submodule and connecting it to the inherited TCP submodule.

  • [Packages] The NED language features a Java-like package structure, to reduce the risk of name clashes between different models. NEDPATH (similar to Java's CLASSPATH) was also introduced to make it easier to specify dependencies among simulation models.

  • [Inner types] Channel types and module types used locally by a compound module can be defined within the compound module, in order to reduce namespace pollution.

  • [Metadata annotations] It is possible to annotate module or channel types, parameters, gates and submodules by adding properties. Metadata are not used by the simulation kernel directly, but they can carry extra information for various tools, the runtime environment, or even for other modules in the model. For example, a module's graphical representation (icon, etc) or the prompt string and measurement unit (milliwatt, etc) of a parameter are already specified as metadata annotations.

  • The NED language has an equivalent tree representation which can be serialized to XML; that is, NED files can be converted to XML and back without loss of data, including comments. This lowers the barrier for programmatic manipulation of NED files, for example extracting information, refactoring and transforming NED, generating NED from information stored in other system like SQL databases, and so on.

    3.2 Warmup

    In this section we introduce the NED language via a complete and reasonably real-life example: a communication network.

    Our hypothetical network consists of nodes. One each node there's an application running which generates packets at random intervals. The nodes are routers themselves as well. We assume that the application uses datagram-based communication, so that we can leave out the transport layer from the model.

    3.2.1 The network

    First we'll define the network, then in the next sections we'll continue to define the network nodes.

    Let the network topology be as in Figure below.


    Figure: The network

    The corresponding NED description would look like this:

    //
    // A network
    //
    network Network
    {
        submodules:
            node1: Node;
            node2: Node;
            node3: Node;
            ...
        connections:
            node1.port++ <--> {datarate=100Mbps;} <--> node2.port++;
            node2.port++ <--> {datarate=100Mbps;} <--> node4.port++;
            node4.port++ <--> {datarate=100Mbps;} <--> node6.port++;
            ...
    }
    

    The above code defines a network type named Network. Note that the NED language uses the familiar curly brace syntax, and ``//'' to denote comments.

    The network contains several nodes, named node1, node2, etc. from the NED module type Node. We'll define Node in the next sections.

    The second half of the declaration defines how the nodes are to be connected. The double arrow means bidirectional connection. The connection points of modules are called gates, and the port++ notation adds a new gate to the port[] gate vector. Gates and connections will be covered in more detail in sections [3.7] and [3.9]. Nodes are connected with a channel that has a data rate of 100Mbps.

    The above code would be placed into a file named Net6.ned. It is a convention to put every NED definition into its own file and to name the file accordingly, but it is not mandatory to do so.

    One can define any number of networks in the NED files, and for every simulation the user has to specify which network he wants to set up. The usual way of specifying the network is to put the network option into the configuration (by default the omnetpp.ini file):

    [General]
    network = Network
    

    3.2.2 Introducing a channel

    It is cumbersome to have to repeat the data rate for every connection. Luckily, NED provides a convenient solution: one can create a new channel type that encapsulates the data rate setting, and this channel type can be defined inside the network so that it does not litter the global namespace.

    The improved network will look like this:

    //
    // A Network
    //
    network Network
    {
        types:
            channel C extends ned.DatarateChannel {
                datarate = 100Mbps;
            }
        submodules:
            node1: Node;
            node2: Node;
            node3: Node;
            ...
        connections:
            node1.port++ <--> C <--> node2.port++;
            node2.port++ <--> C <--> node4.port++;
            node4.port++ <--> C <--> node6.port++;
            ...
    }
    

    Later sections will cover the concepts used (inner types, channels, the DatarateChannel built-in type, inheritance) in details.

    3.2.3 The App, Routing and Queue simple modules

    Simple modules are the basic building blocks for other (compound) modules. All active behavior in the model is encapsulated in simple modules. Behavior is defined with a C++ class; NED files only declare the externally visible interface of the module (gates, parameters).

    In our example, we could define Node as a simple module. However, its functionality is quite complex (traffic generation, routing, etc), so it is better to implement it with several smaller simple module types which we are going to assemble into a compound module. We'll have one simple module for traffic generation (App), one for routing (Routing), and one for queueing up packets to be sent out (Queue). For brevity, we omit the bodies of the latter two in the code below.

    simple App
    {
        parameters:
            int destAddress;
            ...
            @display("i=block/browser");
        gates:
            input in;
            output out;
    }
    
    simple Routing
    {
        ...
    }
    
    simple Queue
    {
        ...
    }
    

    By convention, the above simple module declarations go into the App.ned, Routing.ned and Queue.ned files.

    Let us see the first simple module type declaration. App has a parameter called destAddress (others have been omitted for now), and two gates named out and in for sending and receiving application packets.

    The argument of @display() is called a display string, and it defines the rendering of the module in graphical environments; "i=..." defines the default icon.

    Generally, @-words like @display are called properties in NED, and they are used to annotate various objects with metadata. Properties can be attached to files, modules, parameters, gates, connections, and other objects, and parameter values have a very flexible syntax.

    3.2.4 The Node compound module

    Now we can assemble App, Routing and Queue into the compound module Node. A compound module can be thought of as a ``cardboard box'' that groups other modules into a larger unit, which can further be used as a building block for other modules; networks are also a kind of compound module.


    Figure: The Node compound module

    module Node
    {
        parameters:
            @display("i=misc/node_vs,gold");
        gates:
            inout port[];
        submodules:
            app: App;
            routing: Routing;
            queue[sizeof(port)]: Queue;
        connections:
            routing.localOut --> app.in;
            routing.localIn <-- app.out;
            for i=0..sizeof(port)-1 {
                routing.out[i] --> queue[i].in;
                routing.in[i] <-- queue[i].out;
                queue[i].line <--> port[i];
            }
    }
    

    Compound modules, like simple modules, may have parameters and gates. Our Node module contains an address parameter, plus a gate vector of unspecified size, named port. The actual gate vector size will be determined implicitly by the number of neighbours when we create a network from nodes of this type. The type of port[] is inout, which allows bidirectional connections.

    The modules that make up the compound module are listed under submodules. Our Node compound module type has an app and a routing submodule, plus a queue[] submodule vector that contains one Queue module for each port, as specified by [sizeof(port)]. (It is legal to refer to [sizeof(port)] because the network is built in top-down order, and the node is already created and connected at network level when its submodule structure is built out.)

    In the connections section, the submodules are connected to each other and to the parent module. Single arrows are used to connect input and output gates, and double arrows connect inout gates, and a for loop is utilized to connect the routing module to each queue module, and to connect the outgoing/incoming link (line gate) of each queue to the corresponding port of the enclosing module.

    3.2.5 Putting it together

    We have seen all NED definitions, but how does it get used by OMNeT++? When the simulation program is started, it loads the NED files. The program should already contain the C++ classes that implement the needed simple modules, App, Routing and Queue; their C++ code is either part if the executable or gets loaded from shared library. The simulation program also loads the configuration (omnetpp.ini), and determines from it that the simulation model to be run is the Network network. Then the network gets instantiated for simulation.

    The simulation model is built in a top-down preorder fashion. This means that starting from an empty system module, all submodules are created, their parameters and vector sizes get assigned and they get fully connected before proceeding to go into the submodules to build their internals.



    * * *


    In the following sections we'll go through the elements of the NED language and look at them in more details.

    3.3 Simple modules

    Simple modules are the active components in the model. Simple modules are defined with the simple keyword.

    An example simple module:

    simple Queue
    {
        parameters:
            int capacity;
            @display("i=block/queue");
        gates:
            input in;
            output out;
    }
    

    Both the parameters and gates sections are optional, that is, they can be left out if there's no parameter or gate. In addition, the parameters keyword itself is optional too, it can be left out even if there are parameters or properties.

    Note that the NED definition doesn't contain any code to define the operation of the module: that part is expressed in C++. By default, OMNeT++ looks for C++ classes of the same name as the NED type (so here, Queue).

    One can explicitly specify the C++ class with the @class property. Classes with namespace qualifiers are also accepted, as shown in the following example that uses the mylib::Queue class:

    simple Queue
    {
        parameters:
            int capacity;
            @class(mylib::Queue);
            @display("i=block/queue");
        gates:
            input in;
            output out;
    }
    

    If you have several modules that are all in a common namespace, then a better alternative to @class is the @namespace property. The C++ namespace given with @namespace will be prepended to the normal class name. In the following example, the C++ classes will be mylib::App, mylib::Router and mylib::Queue:

    @namespace(mylib);
    
    simple App {
       ...
    }
    
    simple Router {
       ...
    }
    
    simple Queue {
       ...
    }
    

    As you've seen, @namespace can be specified on file level. Moreover, when placed in a file called package.ned, the namespace will apply to all files in the same directory and all directories below.

    The implementation C++ classes need to be subclassed from the cSimpleModule library class; chapter [4] of this manual describes in detail how to write them.

    Simple modules can be extended (or specialized) via subclassing. The motivation for subclassing can be to set some open parameters or gate sizes to a fixed value (see [3.6] and [3.7]), or to replace the C++ class with a different one. Now, by default the derived NED module type will inherit the C++ class from its base, so it is important to remember that you need to write out @class if you want it to use the new class.

    The following example shows how to specialize a module by setting a parameter to a fixed value (and leaving the C++ class unchanged):

    simple Queue
    {
       int capacity;
       ...
    }
    
    simple BoundedQueue extends Queue
    {
       capacity = 10;
    }
    

    In the next example, the author wrote a PriorityQueue C++ class, and wants to have a corresponding NED type, derived from Queue. However, it does not work as expected:

    simple PriorityQueue extends Queue // wrong! still uses the Queue C++ class
    {
    }
    

    The correct solution is to add a @class property to override the inherited C++ class:

    simple PriorityQueue extends Queue
    {
       @class(PriorityQueue);
    }
    

    Inheritance in general will be discussed in section [3.13].

    3.4 Compound modules

    A compound module groups other modules into a larger unit. A compound module may have gates and parameters like a simple module, but no active behavior (no C++ code) is associated with it.

    A compound module declaration may contain several sections, all of them optional:

    module Host
    {
       types:
           ...
       parameters:
           ...
       gates:
           ...
       submodules:
           ...
       connections:
           ...
    }
    

    Modules contained in a compound module are called submodules, and they are listed in the submodules section. One can create arrays of submodules (i.e. submodule vectors), and the submodule type may come from a parameter.

    Connections are listed under the connections section of the declaration. One can create connections using simple programming constructs (loop, conditional). Connection behaviour can be defined by associating a channel with the connection; the channel type may also come from a parameter.

    Module and channel types only used locally can be defined in the types section as inner types, so that they don't pollute the namespace.

    Compound modules may be extended via subclassing. Inheritance may add new submodules and new connections as well, not only parameters and gates; also, one may refer to inherited submodules, to inherited types etc. What is not possible is to "de-inherit" submodules or connections, or to modify inherited ones.

    In the following example, we show how one can assemble common protocols into a "stub" for wireless hosts, and add user agents via subclassing.

    module WirelessHostBase
    {
       gates:
           input radioIn;
       submodules:
           tcp: TCP;
           ip: IP;
           wlan: Ieee80211;
       connections:
           tcp.ipOut --> ip.tcpIn;
           tcp.ipIn <-- ip.tcpOut;
           ip.nicOut++ --> wlan.ipIn;
           ip.nicIn++ <-- wlan.ipOut;
           wlan.radioIn <-- radioIn;
    }
    
    module WirelessUser extends WirelessHostBase
    {
       submodules:
           webAgent: WebAgent;
       connections:
           webAgent.tcpOut --> tcp.appIn++;
           webAgent.tcpIn <-- tcp.appOut++;
    }
    

    The WirelessUser compound module can further be extended, for example with an Ethernet port:

    module DesktopUser extends WirelessUser
    {
       gates:
           inout ethg;
       submodules:
           eth: EthernetNic;
       connections:
           ip.nicOut++ --> eth.ipIn;
           ip.nicIn++ <-- eth.ipOut;
           eth.phy <--> ethg;
    }
    

    3.5 Channels

    Channels encapsulate parameters and behaviour associated with connections. Channels are like simple modules, in the sense that there are C++ classes behind them. The rules for finding the C++ class for a NED channel type is the same as with simple modules: the default class name is the NED type name unless there is a @class property (@namespace is also observed), and the C++ class is inherited when the channel is subclassed.

    Thus, the following channel type would expect a CustomChannel C++ class to be present:

    channel CustomChannel  // needs a CustomChannel C++ class
    {
    }
    

    The practical difference to modules is that you rarely need to write you own channel C++ class, because there are predefined channel types that you can subclass from, inheriting their C++ code. The predefined types are: ned.IdealChannel, ned.DelayChannel and ned.DatarateChannel. (``ned'' is the package name; you can get rid of it if you import the types with the import ned.* or similar directive. Packages and imports are described in section [3.14].)

    IdealChannel has no parameters, and lets through all messages without delay or any side effect. A connection without a channel object and a connection with an IdealChannel behave in the same way. Still, IdealChannel has its uses, for example when a channel object is required so that it can carry a new property or parameter that is going to be read by other parts of the simulation model.

    DelayChannel has two parameters:

    DatarateChannel has a few additional parameters compared to DelayChannel:

    The following example shows how to create a new channel type by specializing DatarateChannel:

    channel C extends ned.DatarateChannel
    {
        datarate = 100Mbps;
        delay = 100us;
        ber = 1e-10;
    }
    

    You may add parameters and properties to channels via subclassing, and modify existing ones. In the following example, we introduce length-based calculation of the propagation delay:

    channel DatarateChannel2 extends ned.DatarateChannel
    {
        double length @unit(m);
        delay = this.length / 200000km * 1s;
    }
    

    Parameters are primarily useful as input to the underlying C++ class, but even if you reuse the underlying C++ class of built-in channel types, they may be read and used by other parts of the model. For example, adding a cost parameter (or @cost property) may be observed by the routing algorithm and used for routing decisions. The following example shows a cost parameter, and annotation using a property (@backbone).

    channel Backbone extends ned.DatarateChannel
    {
        @backbone;
        double cost = default(1);
    }
    

    3.6 Parameters

    Parameters are variables that belong to a module. Parameters can be used in building the topology (number of nodes, etc), and to supply input to C++ code that implements simple modules and channels.

    Parameters can be of type double, int, bool, string and xml; they can also be declared volatile. For the numeric types, a unit of measurement can also be specified (@unit property), to increase type safety.

    Parameters can get their value from NED files or from the configuration (omnetpp.ini). A default value can also be given (default(...)), which gets used if the parameter is not assigned otherwise.

    Let us see an example before we go into details:

    simple App
    {
        parameters:
            int address;  // local node address
            string destAddresses;  // destination addresses
            volatile double sendIaTime @unit(s) = default(exponential(1s));
                                   // time between generating packets
            volatile int packetLength @unit(byte);  // length of one packet
        ...
    }
    

    Values

    Parameters may get their values from several places: from NED code, from the configuration (omnetpp.ini), or even, interactively from the user.

    The following example shows how parameters of an App module (from the previous example) may be assigned when App gets used as a submodule:

    module Node
    {
        submodules:
            app : App {
                sendIaTime = 3s;
                packetLength = 1024B; // B=byte
            }
            ...
    }
    

    After the above definition, the app submodule's parameters cannot be changed from omnetpp.ini any more.

    Provided that the value isn't set in the NED file, a parameter can be assigned in the configuration in the following way:

    **.sendIaTime = 100ms
    

    The above line applies to all parameters called sendIaTime, whichever module they belong to; it is possible to write more selective assignments by replacing ** with more specific patterns. Parameter assignments in the configuration are described in section [8.4].

    One can also write expressions, including stochastic expressions, in the ini file:

    **.sendIaTime = 2s + exponential(100ms)
    

    If there is no assignment in the ini file, the default value (given with =default(...) in NED) is applied implicitly. If there is no default value, the user will be asked, provided the simulation program is allowed to do that; otherwise there will be an error. (Interactive mode is typically disabled for batch executions where it would do more harm than good.)

    It is also possible to explicitly apply the default (this can sometimes be useful):

    **.sendIaTime = default
    

    Finally, one can explicitly ask the simulator to prompt the user interactively for the value (again, provided that interactivity is enabled, otherwise this will result in an error):

    **.sendIaTime = ask
    

    Expressions

    Parameter values may be given with expressions. NED language expressions have a C-like syntax, with some variations on operator names: binary and logical XOR are # and ##, while \^ has been reassigned to power-of instead. The + operator does string concatenation as well as numeric addition. Expressions can use various numeric, string, stochastic and other functions (fabs(), toUpper(), uniform(), erlang_k(), etc.).

    Expressions may refer to module parameters, gate vector and module vector sizes (using the sizeof operator) and the index of the current module in a submodule vector (index).

    A Expressions may refer to parameters of the compound module being defined, of the current module (with the this. prefix), and to parameters of already defined submodules, with the syntax submodule.parametername (or submodule[index].parametername).

    volatile

    The volatile modifier causes the parameter's value expression to be evaluated every time the parameter is read. This has significance if the expression is not constant, for example it involves numbers drawn from a random number generator. In contrast, non-volatile parameters are evaluated only once. (This practically means that they are evaluated and replaced with the resulting constant at the start of the simulation.)

    To better understand volatile, let's suppose we have an ActiveQueue simple module that has a volatile double parameter named serviceTime.

    The queue module's C++ implementation would re-read the serviceTime parameter at runtime for every job serviced; so if serviceTime is assigned an expression like uniform(0.5s, 1.5s), every job would have a different, random service time.

    In practice, a volatile parameter usually means that the underlying C++ code will re-read the parameter every time a value is needed at runtime, so the parameter can be used a source of random numbers.

    Units

    One can declare a parameter to have an associated unit of measurement, by adding the @unit property. An example:

    simple App
    {
        parameters:
            volatile double sendIaTime @unit(s) = default(exponential(350ms));
            volatile int packetLength @unit(byte) = default(4KB);
        ...
    }
    

    The @unit(s) and @unit(byte) bits declare the measurement unit for the parameter. Values assigned to parameters must have the same or compatible unit, i.e. @unit(s) accepts milliseconds, nanoseconds, minutes, hours, etc., and @unit(byte) accepts kilobytes, megabytes, etc. as well.

    The OMNeT++ runtime does a full and rigorous unit check on parameters to ensure ``unit safety'' of models. Constants should always include the measurement unit.

    The @unit property of a parameter cannot be added or overridden in subclasses or in submodule declarations.

    XML parameters

    Sometimes modules need more complex input than simple module parameters can describe. Then you'd put these parameters into an external config file, and let the modules read and process the file. You'd pass the file name to the modules in a string parameter.

    These days, XML is increasingly becoming a standard format for configuration files as well, so you might as well describe your configuration in XML. From the 3.0 version, OMNeT++ contains built-in support for XML config files.

    OMNeT++ wraps the XML parser (LibXML, Expat, etc.), reads and DTD-validates the file (if the XML document contains a DOCTYPE), caches the file (so that if you refer to it from several modules, it'll still be loaded only once), lets you pick parts of the document via an XPath-subset notation, and presents the contents to you in a DOM-like object tree.

    This machinery can be accessed via the NED parameter type xml, and the xmldoc() operator. You can point xml-type module parameters to a specific XML file (or to an element inside an XML file) via the xmldoc() operator. You can assign xml parameters both from NED and from omnetpp.ini.

    The following example declares an xml parameter, and assigns an XML file to it:

    simple TrafGen {
        parameters:
            xml profile;
        gates:
            output out;
    }
    
    module Node {
        submodules:
            trafGen1 : TrafGen {
                profile = xmldoc("data.xml");
            }
            ...
    }
    

    It is also possible to assign an XML element within a file to the parameter:

    module Node {
        submodules:
            trafGen1 : TrafGen {
                profile = xmldoc("all.xml", "profile[@id='gen1']");
            }
            trafGen2 : TrafGen {
                profile = xmldoc("all.xml", "profile[@id='gen2']");
            }
    }
    

    <?xml>
    XXX example
    

    3.7 Gates

    Gates are the connection points of modules. OMNeT++ has three types of gates: input, output and inout, the latter being essentially an input and an output gate glued together.

    A gate, whether input or output, cannot be connected to two or more other gates. (For compound module gates, this means one connection "outside" and one "inside".) It is possible, though generally not recommended, to connect the input and output sides of an inout gate separately.

    One can create single gates and gate vectors. The size of a gate vector can be given inside square brackets in the declaration, but it also possible to leave it open by just writing a pair of empty brackets ("[]").

    When the gate vector size is left open, one can still specify it later, when subclassing the module, or when using the module for a submodule in a compound module. However, it does not need to be specified, because one can create connections with the gate++ operator that automatically expands the gate vector.

    The gate size can be queried from various NED expressions with the sizeof() operator.

    NED normally requires that all gates be connected. To relax this requirement, you can annotate selected gates with the @loose property, which turns off connectivity check for that gate. Also, input gates that solely exist so that the module can receive messages via sendDirect() (see [4.6.6]) should be annotated with @directIn. It is also possible to turn off connectivity check for all gates within a compound module, by specifying the allowunconnected keyword in the module's connections section.

    Let us see some examples.

    In the following example, the Classifier module has one input for receiving jobs, which it will send to one of the outputs. The number of outputs is determined by a module parameter:

    simple Classifier {
        parameters:
            int numCategories;
        gates:
            input in;
            output out[numCategories];
    }
    

    The following Sink module also has its in[] gate defined as vector, so that it can be connected to several modules:

    simple Sink {
        gates:
            input in[];
    }
    

    A node for building a square grid. Gates around the edges of the grid are expected to remain unconnected, hence the @loose annotation:

    simple GridNode {
        gates:
            inout neighbour[4] @loose;
    }
    

    WirelessNode below is expected to receive messages (radio transmissions) via direct sending, so its radioIn gate is marked with @directIn.

    simple WirelessNode {
        gates:
            input radioIn @directIn;
    }
    

    In the following example, we define TreeNode as having gates to connect any number of children, then subclass it to get a BinaryTreeNode to set the gate size to two:

    simple TreeNode {
        gates:
            inout parent;
            inout children[];
    }
    
    simple BinaryTreeNode extends TreeNode {
        gates:
            children[2];
    }
    

    An example for setting the gate vector size in a submodule, using the same TreeNode module type as above:

    module BinaryTree {
        submodules:
            nodes[31]: TreeNode {
                gates:
                    children[2];
            }
        connections:
            ...
    }
    

    3.8 Submodules

    Modules that a compound module is composed of are called its submodules. A submodule has a name, and it is an instance of a compound or simple module type. In the NED definition of a submodule, this module type may be given explicitly, but, as we'll see later, it is also possible to specify the type with a string expression (see section [3.11].)

    NED supports submodule arrays (vectors) as well. Submodule vector size, unlike gate vector size, must always be specified and cannot be left open as with gates.

    The basic syntax of submodules is shown below:

    module Node
    {
        submodules:
            routing: Routing;   // a submodule
            queue[sizeof(port)]: Queue;  // submodule vector
            ...
    }
    

    A submodule vector may also be used to implement a conditional submodule, like in the example below:

    module Host
    {
        parameters:
            bool withTCP = default(true);
        submodules:
            tcp[withTCP ? 1 : 0]: TCP;  // conditional submodule
            ...
        connections:
            tcp[0].ipOut --> ip.tcpIn if withTCP;
            tcp[0].ipIn <-- ip.tcpOut if withTCP;
            ...
    }
    

    As already seen in previous code examples, a submodule may also have a curly brace block as body, where one can assign parameters, set the size of gate vectors, and add/modify properties like the display string (@display). It is not possible to add new parameters and gates.

    Display strings specified here will be merged with the display string from the type to get the effective display string. This is described in chapter [10].

    module Node
    {
        gates:
            inout port[];
        submodules:
            routing: Routing {
                parameters:   // this keyword is optional
                    routingTable = "routingtable.txt"; // assign parameter
                gates:
                    in[sizeof(port)];  // set gate vector size
                    out[sizeof(port)];
            }
            queue[sizeof(port)]: Queue {
                @display("t=queue id $id"); // modify display string
                id = 1000+index;  // different "id" parameter for each element
            }
        connections:
            ...
    }
    

    An empty body may be omitted, that is,

          queue: Queue;
    

    is the same as

          queue: Queue {
          }
    

    It is possible to add new submodules to an existing compound module via subclassing; this has been described in the section [3.4].

    3.9 Connections

    Connections are defined in the connections section of compound modules. Connections cannot span across hierarchy levels: one can connect two submodule gates, a submodule gate and the "inside" of the parent (compound) module's gates, or two gates of the parent module (though this is rarely useful). It is not possible to connect to any gate outside the parent module, or inside compound submodules.

    Input and output gates are connected with a normal arrow, and inout gates with a double-headed arrow ``<-->''. To connect the two gates with a channel, use two arrows and put the channel specification in between. The same syntax is used to add properties such as @display to the connection.

    Some examples have already been shown in the Warmup section ([3.2]); let's see some more.

    It has been mentioned that an inout gate is basically an input and an output gate glued together. These sub-gates can also be addressed (and connected) individually if needed, as port$i and port$o (or for vector gates, as port$i[$k$] and port$o[k]).

    Gates are specified as modulespec.gatespec (to connect a submodule), or as gatespec (to connect the compound module). modulespec is either a submodule name (for scalar submodules), or a submodule name plus an index in square brackets (for submodule vectors). For scalar gates, gatespec is the gate name; for gate vectors it is either the gate name plus an index in square brackets, or gatename++.

    The gatename++ notation causes the first unconnected gate index to be used. If all gates of the given gate vector are connected, the behavior is different for submodules and for the enclosing compound module. For submodules, the gate vector expands by one. For a compound module, after the last gate is connected, ++ will stop with on error.

    When the ++ operator is used with $i or $o (e.g. g$i++ or g$o++, see later), it will actually add a gate pair (input+output) to maintain equal gate size for the two directions.

    Channel specification

    A channel specification (--> channelspec --> inside a connection) are similar to submodules in many respect.

    Let's see some examples:

    <--> {delay=10ms;} <-->
    <--> {delay=10ms; datarate=1e-8;} <-->
    <--> C <-->
    <--> BBone {cost=100; length=52km; datarate=1e-8;} <-->
    <--> {@display("XXX");} <-->
    <--> BBone {@display("XXX");} <-->
    

    When a channel type is missing, one of the built-in channel types will be used, based on the parameters assigned in the connection. If datarate, ber or per is assigned, ned.DatarateChannel will be chosen. Otherwise, if delay or disabled is present, it will be ned.DelayChannel; otherwise it is ned.IdealChannel. Naturally, if other parameter names are assigned in an connection without an explicit channel type, it will be an error (with ``ned.DelayChannel has no such parameter'' or similar message).

    3.10 Multiple connections

    Simple programming constructs (loop, conditional) allow creating multiple connections easily.

    This will be shown in the following examples.

    Chain

    One can create a chain of modules like this:

    module Chain
        parameters:
            int count;
        submodules:
            node[count] : Node {
                gates:
                    port[2];
            }
        connections allowunconnected:
            for i = 0..count-2 {
                node[i].port[1] <--> node[i+1].port[0];
            }
    }
    

    Binary Tree

    One can build a binary tree in the following way:

    simple BinaryTreeNode {
        gates:
            inout left;
            inout right;
            inout parent;
    }
    
    module BinaryTree {
        parameters:
            int height;
        submodules:
            node[2^height-1]: BinaryTreeNode;
        connections allowunconnected:
            for i=0..2^(height-1)-2 {
                node[i].left <--> node[2*i+1].parent;
                node[i].right <--> node[2*i+2].parent;
            }
    }
    

    Note that not every gate of the modules will be connected. By default, an unconnected gate produces a run-time error message when the simulation is started, but this error message is turned off here with the allowunconnected modifier. Consequently, it is the simple modules' responsibility not to send on a gate which is not leading anywhere.

    Random graph

    Conditional connections can be used to generate random topologies, for example. The following code generates a random subgraph of a full graph:

    module RandomGraph {
        parameters:
            int count;
            double connectedness; // 0.0<x<1.0
        submodules:
            node[count]: Node {
                gates:
                    in[count];
                    out[count];
            }
        connections allowunconnected:
            for i=0..count-1, j=0..count-1 {
                node[i].out[j] --> node[j].in[i]
                    if i!=j && uniform(0,1)<connectedness;
            }
    }
    

    Note the use of the allowunconnected modifier here too, to turn off error messages given by the network setup code for unconnected gates.

    3.10.1 Connection patterns

    Several approaches can be used when you want to create complex topologies which have a regular structure; three of them are described below.

    ``Subgraph of a Full Graph''

    This pattern takes a subset of the connections of a full graph. A condition is used to ``carve out'' the necessary interconnection from the full graph:

    for i=0..N-1, j=0..N-1 {
        node[i].out[...] --> node[j].in[...] if condition(i,j);
    }
    

    The RandomGraph compound module (presented earlier) is an example of this pattern, but the pattern can generate any graph where an appropriate condition(i,j) can be formulated. For example, when generating a tree structure, the condition would return whether node j is a child of node i or vica versa.

    Though this pattern is very general, its usage can be prohibitive if the N number of nodes is high and the graph is sparse (it has much fewer connections that N2). The following two patterns do not suffer from this drawback.

    ``Connections of Each Node''

    The pattern loops through all nodes and creates the necessary connections for each one. It can be generalized like this:

    for i=0..Nnodes, j=0..Nconns(i)-1 {
        node[i].out[j] --> node[rightNodeIndex(i,j)].in[j];
    }
    

    The Hypercube compound module (to be presented later) is a clear example of this approach. BinaryTree can also be regarded as an example of this pattern where the inner j loop is unrolled.

    The applicability of this pattern depends on how easily the rightNodeIndex(i,j) function can be formulated.

    ``Enumerate All Connections''

    A third pattern is to list all connections within a loop:

    for i=0..Nconnections-1 {
        node[leftNodeIndex(i)].out[...] --> node[rightNodeIndex(i)].in[...];
    }
    

    The pattern can be used if leftNodeIndex(i) and rightNodeIndex(i) mapping functions can be sufficiently formulated.

    The Chain module is an example of this approach where the mapping functions are extremely simple: leftNodeIndex(i)=i and rightNodeIndex(i) = i+1. The pattern can also be used to create a random subset of a full graph with a fixed number of connections.

    In the case of irregular structures where none of the above patterns can be employed, you can resort to listing all connections, like you would do it in most existing simulators.

    3.11 Submodule type as parameter

    A submodule type may be specified with a module parameter of the type string, or in general, with any string-typed expression. The syntax uses the like keyword.

    Let us begin with an example:

    network Net6
    {
        parameters:
            string nodeType;
        submodules:
            node[6]: <nodeType> like INode {
                address = index;
            }
        connections:
            ...
    }
    

    It creates a submodule vector whose module type will come from the nodeType parameter. For example, if nodeType="SensorNode", then the module vector will consist of sensor nodes (provided such module type exists and it qualifies -- the latter will be explained right now).

    The missing piece is the like INode bit. INode must be an existing module interface, which the SensorNode module type must implement (more about this later).

    The corresponding NED declarations:

    moduleinterface INode
    {
        parameters:
            int address;
        gates:
            inout port[];
    }
    

    module SensorNode like INode
    {
        parameters:
            int address;
            ...
        gates:
            inout port[];
            ...
    }
    

    3.12 Properties (metadata annotations)

    Properties allow adding metadata annotations to modules, parameters, gates, connections, NED files, packages, and virtually anything in NED. @display, @class, @namespace, @unit, @prompt, @loose, @directIn are all properties that have been mentioned in previous sections, but those examples only scratch the surface of what can be done with properties.

    Using properties, one can attach extra information to NED elements. Some properties are interpreted by NED, by the simulation kernel; other properties may be read and used from within the simulation model, or provide hints for NED editing tools.

    Properties are attached to the type, so you cannot have properties per-instance different properties. All instances of modules, connections, parameters, etc. created from any particular location in the NED files have identical properties.

    The following example shows the syntax for annotating various NED elements:

    @prop;  // file property
    
    module Example
    {
        parameters:
           @prop;   // module property
           int a @prop = default(1); // parameter property
        gates:
           output out @prop;
        submodules:
           src: Source {
               parameters:
                  @prop;  // submodule property
                  count @prop;  // adding a property to a parameter
               gates:
                  out[] @prop;  // adding a property to a gate
           }
           ...
        connections:
           src.out++ --> { @prop; } --> sink1.in;
           src.out++ --> Channel { @prop; } --> sink2.in;
    }
    

    Data model

    Properties may contain data, given in parentheses; the data model is quite flexible. Properties may contain lists:

    @enum(Sneezy,Sleepy,Dopey,Doc,Happy,Bashful,Grumpy);
    

    They may contain key-value pairs, separated by semicolons:

    @coords(x=10.31; y=30.2; unit=km);
    

    In key-value pairs, each value can be a (comma-separated) list:

    @nodeinfo(id=742;labels=swregion,routers,critical);
    

    The above examples are special cases of the general data model. According to the data model, properties contain key-valuelist pairs, separated by semicolons. Items in valuelist are separated by commas. Wherever key is missing, values go on the valuelist of the default key, the empty string. @prop is the same as @prop().

    The syntax for value items is a bit restrictive: they may contain words, numbers, string constants and some more, but not arbitrary strings. Whenever the syntax does not permit some value, it should be enclosed in quotes. This quoting does not make any difference in the value, because the parser automatically drops one layer of quotes; thus, @class(TCP) and @class("TCP") are exactly the same.

    There are also some conventions. One can use properties to tag some NED element with a label; for example, a @host property could be used to mark all module types that represent various hosts. This property could be used e.g. by editing tools, by topology discovery code inside the simulation model, etc.

    The convention for such a "label" property is that any extra data in it (i.e. within parens) is ignored, except a single word false, which is reserved to "remove" the property. Thus, simulation model or tool source code that interprets properties should handle all the following forms as equivalent to @host: @host(), @host(true), @host(anything-but-false), @host(a=1;b=2); and @host(false) should be interpreted as the lack of the @host tag.

    Modifying properties

    When you subclass a NED type, use a module type as submodule or use a channel type for a connection, you may add new properties to the module or channel, or to its parameters and gates, and you can also modify existing properties.

    When modifying a property, the new property gets merged with the old one, with a few simple rules. New keys simply get added. If a key already exists in the old property, items in its valuelist overwrite items on the same position in the old property. A single hyphen ($-$) as valuelist item serves as ``antivalue'', it removes the item at the corresponding position.

    Some examples:

    base @prop
    new @prop(a)
    result @prop(a)

    base @prop(a,b,c)
    new @prop(,-)
    result @prop(a,,c)

    base @prop(foo=a,b)
    new @prop(foo=A,,c;bar=1,2)
    result @prop(foo=A,b,c;bar=1,2)

    Indices

    Properties are identified by names, so if the same @name occurs in the same context, then it names the exact same property object. If you want to have multiple properties with the same name, then you need to distinguish them with an index. An index is a name or number, written in square brackets after the property name. The index may be chosen to carry a meaning, or it may be a dummy whose only purpose is to tell multiple properties with the same name apart. (The code that interprets properties may be written to observe or to ignore indices, as needed).

    The following example, the simple module declares in properties the statistics it collects. These declarations might be used by model editing tools, by simulation code, or by analysis tools.

    The statistic names are used as indices:
    simple App {
        // declare two statistics collected by the C++ code
        @statistic[packetsReceived](type=integer;label="Number of packets received");
        @statistic[responseTime](type=double;unit=s;label="Application response time");
        ...
    }
    
    simple HttpApp extends App {
        // tailor the label of the "packetsReceived" statistic:
        @statistic[packetsReceived](label="Number of HTTP requests received");
    }
    

    3.13 Inheritance

    Inheritance support in the NED language is only described briefly here, because several details and examples have been already presented in previous sections.

    In NED, a type may only extend (extends keyword) an element of the same component type: a simple module may only extend a simple module, compound module may only extend a compound module, and so on. Single inheritance is supported for modules and channels, and multiple inheritance is supported for module interfaces and channel interfaces. A network is a shorthand for a compound module with the @isNetwork property set, so the same rules apply to it as to compound modules.

    However, a simple or compound module type may implement (like keyword) several module interfaces; likewise, a channel type may implement several channel interfaces.

    Inheritance may:

    For details and examples, see the corresponding sections of this chapter (simple modules [3.3], compound modules [3.4], channels [3.5], parameters [3.6], gates [3.7], submodules [3.8], connections [3.9], module interfaces and channel interfaces [3.11]).

    3.14 Packages

    Small simulation projects are fine to have all NED files in a single directory. When a project grows, however, it sooner or later becomes inevitable to introduce a directory structure, and sort the NED files into them. NED natively supports directory trees with NED files, and calls directories packages. Packages are also useful for reducing name clashes, because names can be qualified with the package name.

    Overview

    When a simulation is run, you must tell the simulation kernel the directory which is the root of your package tree; let's call it NED source folder. The simulation kernel will traverse the whole directory tree, and load all NED files from every directory. You can have several NED directory trees, and their roots (the NED source folders) should be given to the simulation kernel in the NEDPATH variable. NEDPATH can be specified in several ways: as an environment variable (NEDPATH), as a configuration option (ned-path), or as a command-line option to the simulation runtime. NEDPATH is described in details in chapter [9].

    Directories in a NED source tree correspond to packages. If you have NED files in a <root>/a/b/c directory (where <root> gets listed in NEDPATH), then the package name is a.b.c. The package name has to be explicitly declared at the top of the NED files as well, like this:

    package a.b.c;
    

    The package name that follows from the directory name and the declared package must match; it is an error if they don't. (The only exception is the root package.ned file, as described below.)

    By convention, package names are all lowercase, and begin with either the project name (myproject), or the reversed domain name plus the project name (org.example.myproject). The latter convention would cause the directory tree to begin with a few levels of empty directories, but this can be eliminated with a toplevel package.ned.

    NED files called package.ned have a special role, as they are meant to represent the whole package. For example, comments in package.ned are treated as documentation of the package. Also, a @namespace property in a package.ned file affects all NED files in that directory and all directories below.

    The toplevel package.ned file can be used to designate the root package, which is useful for eliminating a few levels of empty directories resulting from the package naming convention. That is, if you have a package.ned file in your <root> directory whose package declaration says org.example.myproject, then the <root>/a/b/c directory will be package org.example.myproject.a.b.c -- and NED files in them must contain that as package declaration. Only the root package.ned has this property, other package.ned's cannot change the package.

    Let's look at the INET Framework as example, which contains hundreds of NED files in several dozen packages. The directory structure looks like this:

    INET/
        src/
            base/
            transport/
                tcp/
                udp/
                ...
            networklayer/
            linklayer/
            ...
        examples/
            adhoc/
            ethernet/
            ...
    

    The src and examples subdirectories are denoted as NED source folders, so NEDPATH is the following (provided INET was unpacked in /home/joe):

    /home/joe/INET/src;/home/joe/INET/examples
    

    Both src and examples contain package.ned files to define the root package:

    // INET/src/package.ned:
    package inet;
    

    // INET/examples/package.ned:
    package inet.examples;
    

    And other NED files follow the package defined in package.ned:

    // INET/src/transport/tcp/TCP.ned:
    package inet.transport.tcp;
    

    Name resolution, imports

    We already mentioned that packages can be used to distinguish similarly named NED types. The name that includes the package name (a.b.c.Queue for a Queue module in the a.b.c package) is called fully qualified name; without the package name (Queue) it is called simple name.

    Simple names alone are not enough to unambiguously identify a type. Here is how you can refer to an existing type:

    1. By fully qualified name. This is often cumbersome though, as names tend to be too long;
    2. Import the type, then the simple name will be enough;
    3. If the type is in the same package, then it doesn't need to be imported; it can be referred to by simple name

    Types can be imported with the import keyword by either fully qualified name, or by a wildcard pattern. In wildcard patterns, one asterisk ("*") stands for "any character sequence not containing period", and two asterisks ("**") mean "any character sequence which may contain period".

    So, any of the following lines can be used to import a type called inet.protocols.networklayer.ip.RoutingTable:

    import inet.protocols.networklayer.ip.RoutingTable;
    import inet.protocols.networklayer.ip.*;
    import inet.protocols.networklayer.ip.Ro*Ta*;
    import inet.protocols.*.ip.*;
    import inet.**.RoutingTable;
    

    If an import explicitly names a type with its exact fully qualified name, then that type must exist, otherwise it's an error. Imports containing wildcards are more permissive, it is allowed for them not to match any existing NED type (although that might generate a warning.)

    Inner types may not be referred to outside their enclosing types, so they cannot be imported either.

    Name resolution with "like"

    The situation is a little different for submodule and connection channel specifications using the like keyword, when the type name comes from a string-valued expression (see section [3.11] about submodule and channel types as parameters). Imports are not much use here: at the time of writing the NED file it is not yet known what NED types will be suitable for being "plugged in" there, so they cannot be imported in advance.

    There is no problem with fully qualified names, but simple names need to be resolved differently. What NED does is this: it determines which interface the module or channel type must implement (i.e. ... like INode), and then collects the types that have the given simple name AND implement the given interface. There must be exactly one such type, which is then used. If there's none or there's more than one, it will be reported as an error.

    Let us see the following example:

    module MobileHost
    {
        parameters:
            string mobilityType;
        submodules:
            mobility: <mobilityType> like IMobility;
            ...
    }
    

    and suppose that the following modules implement the IMobility module interface: inet.mobility.RandomWalk, inet.adhoc.RandomWalk, inet.mobility.MassMobility; and suppose that there's also a type called inet.examples.adhoc.MassMobility but it does not implement the interface.

    So if mobilityType="MassMobility", then inet.mobility.MassMobility will be selected; the other MassMobility doesn't interfere. However, if mobilityType="RandomWalk", then it's an error because there're two matching RandomWalk types. Both RandomWalk's can still be used, but one must explicitly choose one of them by providing a package name: mobilityType="inet.adhoc.RandomWalk".

    The default package

    It is not mandatory to make use of packages: if all NED files are in a single directory listed on the NEDPATH, then package declarations (and imports) can be omitted. Those files are said to be in the default package.



    4 Simple Modules

    Simple modules are the active components in the model. Simple modules are programmed in C++, using the OMNeT++ class library. The following sections contain a short introduction to discrete event simulation in general, explain how its concepts are implemented in OMNeT++, and give an overview and practical advice on how to design and code simple modules.

    4.1 Simulation concepts

    This section contains a very brief introduction into how Discrete Event Simulation (DES) works, in order to introduce terms we'll use when explaining OMNeT++ concepts and implementation.

    4.1.1 Discrete Event Simulation

    A Discrete Event System is a system where state changes (events) happen at discrete instances in time, and events take zero time to happen. It is assumed that nothing (i.e. nothing interesting) happens between two consecutive events, that is, no state change takes place in the system between the events (in contrast to continuous systems where state changes are continuous). Those systems that can be viewed as Discrete Event Systems can be modeled using Discrete Event Simulation. (Other systems can be modelled e.g. with continuous simulation models.)

    For example, computer networks are usually viewed as discrete event systems. Some of the events are:

    This implies that between two events such as start of a packet transmission and end of a packet transmission, nothing interesting happens. That is, the packet's state remains being transmitted. Note that the definition of ``interesting'' events and states always depends on the intent and purposes of the person doing the modeling. If we were interested in the transmission of individual bits, we would have included something like start of bit transmission and end of bit transmission among our events.

    The time when events occur is often called event timestamp ; with OMNeT++ we'll say arrival time (because in the class library, the word ``timestamp'' is reserved for a user-settable attribute in the event class). Time within the model is often called simulation time, model time or virtual time as opposed to real time or CPU time which refer to how long the simulation program has been running and how much CPU time it has consumed.

    4.1.2 The event loop

    Discrete event simulation maintains the set of future events in a data structure often called FES (Future Event Set) or FEL (Future Event List). Such simulators usually work according to the following pseudocode:

    initialize -- this includes building the model and
                  inserting initial events to FES
    
    

    while (FES not empty and simulation not yet complete) { retrieve first event from FES t:= timestamp of this event process event (processing may insert new events in FES or delete existing ones) } finish simulation (write statistical results, etc.)

    The first, initialization step usually builds the data structures representing the simulation model, calls any user-defined initialization code, and inserts initial events into the FES to ensure that the simulation can start. Initialization strategy can differ considerably from one simulator to another.

    The subsequent loop consumes events from the FES and processes them. Events are processed in strict timestamp order in order to maintain causality, that is, to ensure that no event may have an effect on earlier events.

    Processing an event involves calls to user-supplied code. For example, using the computer network simulation example, processing a ``timeout expired'' event may consist of re-sending a copy of the network packet, updating the retry count, scheduling another ``timeout'' event, and so on. The user code may also remove events from the FES, for example when canceling timeouts.

    The simulation stops when there are no events left (this happens rarely in practice), or when it isn't necessary for the simulation to run further because the model time or the CPU time has reached a given limit, or because the statistics have reached the desired accuracy. At this time, before the program exits, the user will typically want to record statistics into output files.

    4.1.3 Simple modules in OMNeT++

    In OMNeT++, events occur inside simple modules. Simple modules encapsulate C++ code that generates events and reacts to events, in other words, implements the behaviour of the model.

    The user creates simple module types by subclassing the cSimpleModule class, which is part of the OMNeT++ class library. cSimpleModule, just as cCompoundModule, is derived from a common base class, cModule.

    cSimpleModule, although packed with simulation-related functionality, doesn't do anything useful by itself -- you have to redefine some virtual member functions to make it do useful work.

    These member functions are the following:

    In the initialization step, OMNeT++ builds the network: it creates the necessary simple and compound modules and connects them according to the NED definitions. OMNeT++ also calls the initialize() functions of all modules.

    The handleMessage() and activity() functions are called during event processing. This means that the user will implement the model's behavior in these functions. handleMessage() and activity() implement different event processing strategies: for each simple module, the user has to redefine exactly one of these functions.

    handleMessage() is a method that is called by the simulation kernel when the module receives a message. activity() is a coroutine-based solution which implements the process interaction approach (coroutines are non-preemptive (i.e. cooperative) threads). Generally, it is recommended that you prefer handleMessage() to activity() -- mainly because activity() doesn't scale well. Later in this chapter we'll discuss both methods including their advantages and disadvantages.

    Modules written with activity() and handleMessage() can be freely mixed within a simulation model.

    The finish() functions are called when the simulation terminates successfully. The most typical use of finish() is the recording of statistics collected during simulation.

    4.1.4 Events in OMNeT++

    OMNeT++ uses messages to represent events. Each event is represented by an instance of the cMessage class or one its subclasses; there is no separate event class. Messages are sent from one module to another -- this means that the place where the ``event will occur'' is the message's destination module, and the model time when the event occurs is the arrival time of the message. Events like ``timeout expired'' are implemented by the module sending a message to itself.

    Events are consumed from the FES in arrival time order, to maintain causality. More precisely, given two messages, the following rules apply:

    1. the message with earlier arrival time is executed first. If arrival times are equal,
    2. the one with smaller priority value is executed first. If priorities are the same,
    3. the one scheduled or sent earlier is executed first.

    Priority is a user-assigned integer attribute of messages.

    4.1.5 Simulation time

    The current simulation time can be obtained with the simTime() function.

    Simulation time in OMNeT++ is represented by the C++ type simtime_t, which is by default a typedef to the SimTime class. SimTime class stores simulation time in a 64-bit integer, using decimal fixed-point representation. The resolution is controlled by the scale exponent global configuration variable, that is, SimTime instances have the same resolution. The exponent can be between chosen between -18 (attosecond resolution) and 0 (seconds). Some exponents with the ranges they provide are shown in the following table.

    Exponent Resolution Approx. Range
    -18 10-18s (1as) +/- 9.22s
    -15 10-15s (1fs) +/- 153.72 minutes
    -12 10-12s (1ps) +/- 106.75 days
    -9 10-9s (1ns) +/- 292.27 years
    -6 10-6s (1us) +/- 292271 years
    -3 10-3s (1ms) +/- 2.9227e8 years
    0 1s +/- 2.9227e11 years

    Note that although simulation time cannot be negative, it is still useful to be able to represent negative numbers, because they often arise during the evaluation of arithmetic expressions.

    The SimTime class performs additions and substractions as 64-bit integer operations. Integer overflows are checked, and will cause the simulation to stop with an error message. Other operations (multiplication, division, etc) are performed in double, then converted back to integer.

    There is no implicit conversion from SimTime to double, mostly because it would conflict with overloaded arithmetic operations of SimTime; use the dbl() method of Simtime to convert. To reduce the need for dbl(), several functions and methods have overloaded variants that directly accept SimTime, for example fabs(), fmod(), ceil(), floor(), uniform(), exponential(), and normal().

    Other useful methods of SimTime include str() which returns the value as a string; parse() which converts a string to SimTime; raw() which returns the underlying int64 value; getScaleExp() which returns the global scale exponent; and getMaxTime which returns the maximum simulation time that can be represented at the current scale exponent.

    Compatibility

    Earlier versions of OMNeT++ used double for simulation time. To facilitate porting existing models to OMNeT++ 4.0 or later, OMNeT++ can be compiled to use double for simtime_t. To enable this mode, define the USE_DOUBLE_SIMTIME preprocessor macro during compiling OMNeT++ and the simulation models.

    There are several macros that can be used in simulation models to make them compile with both double and SimTime simulation time: SIMTIME_STR() converts simulation time to a const char * (can be used in printf argument lists); SIMTIME_DBL(t) converts simulation time to double; SIMTIME_RAW(t) returns the underlying int64 or double; STR_SIMTIME(s) converts string to simulation time; and SIMTIME_TTOA(buf,t) converts simulation time to string, and places the result into the given buffer. MAXTIME is also defined correctly for both simtime_t types.

    4.1.6 FES implementation

    The implementation of the FES is a crucial factor in the performance of a discrete event simulator. In OMNeT++, the FES is implemented with binary heap, the most widely used data structure for this purpose. Heap is also the best algorithm we know, although exotic data structures like skiplist may perform better than heap in some cases. In case you're interested, the FES implementation is in the cMessageHeap class, but as a simulation programmer you won't ever need to care about that.

    4.2 Defining simple module types

    4.2.1 Overview

    As mentioned before [4.1.3], a simple module is nothing more than a C++ class which has to be subclassed from cSimpleModule, with one or more virtual member functions redefined to define its behavior.

    The class has to be registered with OMNeT++ via the Define_Module() macro. The Define_Module() line should always be put into .cc or .cpp files and not header file (.h), because the compiler generates code from it.

    The following HelloModule is about the simplest simple module one could write. (We could have left out the initialize() method as well to make it even smaller, but how would it say Hello then?) Note cSimpleModule as base class, and the Define_Module() line.

    // file: HelloModule.cc
    #include <omnetpp.h>
    
    class HelloModule : public cSimpleModule
    {
      protected:
        virtual void initialize();
        virtual void handleMessage(cMessage *msg);
    };
    
    // register module class with `\opp`
    Define_Module(HelloModule);
    
    void HelloModule::initialize()
    {
        ev << "Hello World!\n";
    }
    
    void HelloModule::handleMessage(cMessage *msg)
    {
        delete msg; // just discard everything we receive
    }
    

    In order to be able to refer to this simple module type in NED files, we also need an associated NED declaration which might look like this:

    // file: HelloModule.ned
    simple HelloModule
    {
        gates:
            input in;
    }
    

    4.2.2 Constructor

    Simple modules are never instantiated by the user directly, but rather by the simulation kernel. This implies that one cannot write arbitrary constructors: the signature must be what is expected by the simulation kernel. Luckily, this contract is very simple: the constructor must be public, and must take no arguments:

      public:
        HelloModule();  // constructor takes no arguments
    

    cSimpleModule itself has two constructors:

    1. cSimpleModule() -- one without arguments
    2. cSimpleModule(size_t stacksize) -- one that accepts the coroutine stack size

    The first version should be used with handleMessage() simple modules, and the second one with activity() modules. (With the latter, the activity() method of the module class runs as a coroutine which needs a separate CPU stack, usually of 16..32K. This will be discussed in detail later.) Passing zero stack size to the latter constructor also selects handleMessage().

    Thus, the following constructor definitions are all OK, and select handleMessage() to be used with the module:

    HelloModule::HelloModule() {...}
    HelloModule::HelloModule() : cSimpleModule() {...}
    

    It is also OK to omit the constructor altogether, because the compiler-generated one is suitable too.

    The following constructor definition selects activity() to be used with the module, with 16K of coroutine stack:

    HelloModule::HelloModule() : cSimpleModule(16384) {...}
    

    4.2.3 Constructor and destructor vs initialize() and finish()

    The initialize() and finish() methods will be discussed in a later section in detail, but because their apparent similarity to the constructor and the destructor is prone to cause some confusion, we'll briefly cover them here.

    The constructor gets called when the module is created, as part of the model setup process. At that time, everything is just being built, so there isn't a lot things one can do from the constructor. In contrast, initialize() gets called just before the simulation starts executing, when everything else has been set up already.

    finish() is for recording statistics, and it only gets called when the simulation has terminated normally. It does not get called when the simulations stops with an error message. The destructor always gets called at the end, no matter how the simulation stopped, but at that time it is fair to assume that the simulation model has been halfway demolished already.

    Based on the above, the following conventions exist for these four methods:

  • Constructor:

    Set pointer members of the module class to NULL; postpone all other initialization tasks to initialize().

  • initialize():

    Perform all initialization tasks: read module parameters, initialize class variables, allocate dynamic data structures with new; also allocate and initialize self-messages (timers) if needed.

  • finish():

    Record statistics. Do not delete anything or cancel timers -- all cleanup must be done in the destructor.

  • Destructor:

    Delete everything which was allocated by new and is still held by the module class. With self-messages (timers), use the cancelAndDelete(msg) function! It is almost always wrong to just delete a self-message from the destructor, because it might be in the scheduled events list. The cancelAndDelete(msg) function checks for that first, and cancels the message before deletion if necessary.

  • OMNeT++ prints the list of unreleased objects at the end of the simulation. Simulation models that dump "undisposed object ..." messages need to get their module destructors fixed. As a temporary measure, these messages may be hidden by setting print-undisposed=false in the configuration.

    4.2.4 An example

    The following code is a bit longer but actually useful simple module implementation. It demonstrates several of the above concepts, plus some others which will be explained in later sections:

    1. constructor, initialize and destructor conventions
    2. using messages for timers
    3. accessing module parameters
    4. recording statistics at the end of the simulation
    5. documenting the programmer's assumptions using ASSERT()
    // file: FFGenerator.h
    
    #include <omnetpp.h>
    
    /**
     * Generates messages or jobs; see NED file for more info.
     */
    class FFGenerator : public cSimpleModule
    {
      private:
        cMessage *sendMessageEvent;
        long numSent;
    
      public:
        FFGenerator();
        virtual ~FFGenerator();
    
      protected:
        virtual void initialize();
        virtual void handleMessage(cMessage *msg);
        virtual void finish();
    };
    

    // file: FFGenerator.cc
    
    #include "FFGenerator.cc"
    
    // register module class with `\opp`
    Define_Module(FFGenerator);
    
    FFGenerator::FFGenerator()
    {
        sendMessageEvent = NULL;
    }
    
    void FFGenerator::initialize()
    {
        numSent = 0;
        sendMessageEvent = new cMessage("sendMessageEvent");
        scheduleAt(0.0, sendMessageEvent);
    }
    
    void FFGenerator::handleMessage(cMessage *msg)
    {
        ASSERT(msg==sendMessageEvent);
    
        cMessage *m = new cMessage("packet");
        m->setBitLength(par("msgLength"));
        send(m, "out");
        numSent++;
    
        double deltaT = (double)par("sendIaTime");
        scheduleAt(simTime()+deltaT, sendMessageEvent);
    }
    
    void FFGenerator::finish()
    {
        recordScalar("packets sent", numSent);
    }
    
    FFGenerator::~FFGenerator()
    {
        cancelAndDelete(sendMessageEvent);
    }
    

    The corresponding NED declaration:

    // file: FFGenerator.ned
    simple FFGenerator
    {
        parameters:
            volatile double sendIaTime;
        gates:
            output out;
    }
    

    4.2.5 Using global variables

    If possible, avoid using global variables, including static class members. They are prone to cause several problems. First, they are not reset to their initial values (to zero) when you rebuild the simulation in Tkenv, or start another run in Cmdenv. This may produce surprising results. Second, they prevent you from running your simulation in parallel. When using parallel simulation, each partition of your model (may) run in a separate process, having its own copy of the global variables. This is usually not what you want.

    The solution is to encapsulate the variables into simple modules as private or protected data members, and expose them via public methods. Other modules can then call these public methods to get or set the values. Calling methods of other modules will be discussed in section . Examples of such modules are the Blackboard in the Mobility Framework, and InterfaceTable and RoutingTable in the INET Framework.

    4.3 Adding functionality to cSimpleModule

    This section discusses cSimpleModule's four previously mentioned member functions, intended to be redefined by the user: initialize(), handleMessage(), activity() and finish(), plus a fifth, less frequently used one, handleParameterChange.

    4.3.1 handleMessage()

    Function called for each event

    The idea is that at each event (message arrival) we simply call a user-defined function. This function, handleMessage(cMessage *msg) is a virtual member function of cSimpleModule which does nothing by default -- the user has to redefine it in subclasses and add the message processing code.

    The handleMessage() function will be called for every message that arrives at the module. The function should process the message and return immediately after that. The simulation time is potentially different in each call. No simulation time elapses within a call to handleMessage().

    The event loop inside the simulator handles both activity() and handleMessage() simple modules, and it corresponds to the following pseudocode:

    while (FES not empty and simulation not yet complete)
    {
        retrieve first event from FES
        t:= timestamp of this event
        m:= module containing this event
        if (m works with handleMessage())
            m->handleMessage( event )
        else // m works with activity()
            transferTo( m )
    }
    

    Modules with handleMessage() are NOT started automatically: the simulation kernel creates starter messages only for modules with activity(). This means that you have to schedule self-messages from the initialize() function if you want a handleMessage() simple module to start working ``by itself'', without first receiving a message from other modules.

    Programming with handleMessage()

    To use the handleMessage() mechanism in a simple module, you must specify zero stack size for the module. This is important, because this tells OMNeT++ that you want to use handleMessage() and not activity().

    Message/event related functions you can use in handleMessage():

    You cannot use the receive() family and wait() functions in handleMessage(), because they are coroutine-based by nature, as explained in the section about activity().

    You have to add data members to the module class for every piece of information you want to preserve. This information cannot be stored in local variables of handleMessage() because they are destroyed when the function returns. Also, they cannot be stored in static variables in the function (or the class), because they would be shared between all instances of the class.

    Data members to be added to the module class will typically include things like:

    You can initialize these variables from the initialize() function. The constructor is not a very good place for this purpose, because it is called in the network setup phase when the model is still under construction, so a lot of information you may want to use is not yet available.

    Another task you have to do in initialize() is to schedule initial event(s) which trigger the first call(s) to handleMessage(). After the first call, handleMessage() must take care to schedule further events for itself so that the ``chain'' is not broken. Scheduling events is not necessary if your module only has to react to messages coming from other modules.

    finish() is normally used to record statistics information accumulated in data members of the class at the end of the simulation.

    Application area

    handleMessage() is in most cases a better choice than activity():

    1. When you expect the module to be used in large simulations, involving several thousand modules. In such cases, the module stacks required by activity() would simply consume too much memory.
    2. For modules which maintain little or no state information, such as packet sinks, handleMessage() is more convenient to program.
    3. Other good candidates are modules with a large state space and many arbitrary state transition possibilities (i.e. where there are many possible subsequent states for any state). Such algorithms are difficult to program with activity(), or the result is code which is better suited for handleMessage() (see rule of thumb below). Most communication protocols are like this.

    Example 1: Protocol models

    Models of protocol layers in a communication network tend to have a common structure on a high level because fundamentally they all have to react to three types of events: to messages arriving from higher layer protocols (or apps), to messages arriving from lower layer protocols (from the network), and to various timers and timeouts (that is, self-messages).

    This usually results in the following source code pattern:

    class FooProtocol : public cSimpleModule
    {
      protected:
        // state variables
        // ...
    
        virtual void processMsgFromHigherLayer(cMessage *packet);
        virtual void processMsgFromLowerLayer(FooPacket *packet);
        virtual void processTimer(cMessage *timer);
    
        virtual void initialize();
        virtual void handleMessage(cMessage *msg);
    };
    
    // ...
    
    void FooProtocol::handleMessage(cMessage *msg)
    {
        if (msg->isSelfMessage())
            processTimer(msg);
        else if (msg->arrivedOn("fromNetw"))
            processMsgFromLowerLayer(check_and_cast<FooPacket *>(msg));
        else
            processMsgFromHigherLayer(msg);
    }
    

    The functions processMsgFromHigherLayer(), processMsgFromLowerLayer() and processTimer() are then usually split further: there are separate methods to process separate packet types and separate timers.

    Example 2: Simple traffic generators and sinks

    The code for simple packet generators and sinks programmed with handleMessage() might be as simple as the following pseoudocode:

    PacketGenerator::handleMessage(msg)
    {
        create and send out a new packet;
        schedule msg again to trigger next call to handleMessage;
    }
    
    PacketSink::handleMessage(msg)
    {
        delete msg;
    }
    

    Note that PacketGenerator will need to redefine initialize() to create m and schedule the first event.

    The following simple module generates packets with exponential inter-arrival time. (Some details in the source haven't been discussed yet, but the code is probably understandable nevertheless.)

    class Generator : public cSimpleModule
    {
      public:
        Generator() : cSimpleModule() {}
      protected:
        virtual void initialize();
        virtual void handleMessage(cMessage *msg);
    };
    
    Define_Module(Generator);
    
    void Generator::initialize()
    {
        // schedule first sending
        scheduleAt(simTime(), new cMessage);
    }
    
    void Generator::handleMessage(cMessage *msg)
    {
        // generate & send packet
        cMessage *pkt = new cMessage;
        send(pkt, "out");
        // schedule next call
        scheduleAt(simTime()+exponential(1.0), msg);
    }
    

    Example 3: Bursty traffic generator

    A bit more realistic example is to rewrite our Generator to create packet bursts, each consisting of burstLength packets.

    We add some data members to the class:

    The code:

    class BurstyGenerator : public cSimpleModule
    {
      protected:
        int burstLength;
        int burstCounter;
    
        virtual void initialize();
        virtual void handleMessage(cMessage *msg);
    };
    
    Define_Module(BurstyGenerator);
    
    void BurstyGenerator::initialize()
    {
        // init parameters and state variables
        burstLength = par("burstLength");
        burstCounter = burstLength;
        // schedule first packet of first burst
        scheduleAt(simTime(), new cMessage);
    }
    
    void BurstyGenerator::handleMessage(cMessage *msg)
    {
        // generate & send packet
        cMessage *pkt = new cMessage;
        send(pkt, "out");
        // if this was the last packet of the burst
        if (--burstCounter == 0)
        {
            // schedule next burst
            burstCounter = burstLength;
            scheduleAt(simTime()+exponential(5.0), msg);
        }
        else
        {
            // schedule next sending within burst
            scheduleAt(simTime()+exponential(1.0), msg);
        }
    }
    

    Pros and Cons of using handleMessage()

    Pros:

    Cons:

    Usually, handleMessage() should be preferred to activity().

    Other simulators

    Many simulation packages use a similar approach, often topped with something like a state machine (FSM) which hides the underlying function calls. Such systems are:

    OMNeT++'s FSM support is described in the next section.

    4.3.2 activity()

    Process-style description

    With activity(), you can code the simple module much like you would code an operating system process or a thread. You can wait for an incoming message (event) at any point of the code, you can suspend the execution for some time (model time!), etc. When the activity() function exits, the module is terminated. (The simulation can continue if there are other modules which can run.)

    The most important functions you can use in activity() are (they will be discussed in detail later):

    The activity() function normally contains an infinite loop, with at least a wait() or receive() call in its body.

    Application area

    Generally you should prefer handleMessage() to activity(). The main problem with activity() is that it doesn't scale because every module needs a separate coroutine stack. It has also been observed that activity() does not encourage a good programming style.

    There is one scenario where activity()'s process-style description is convenient: when the process has many states but transitions are very limited, ie. from any state the process can only go to one or two other states. For example, this is the case when programming a network application, which uses a single network connection. The pseudocode of the application which talks to a transport layer protocol might look like this:

    activity()
    {
        while(true)
        {
            open connection by sending OPEN command to transport layer
            receive reply from transport layer
            if (open not successful)
            {
                wait(some time)
                continue // loop back to while()
            }
    
    

    while(there's more to do) { send data on network connection if (connection broken) { continue outer loop // loop back to outer while() } wait(some time) receive data on network connection if (connection broken) { continue outer loop // loop back to outer while() } wait(some time) } close connection by sending CLOSE command to transport layer if (close not successful) { // handle error } wait(some time) } }

    If you have to handle several connections simultaneously, you may dynamically create them as instances of the simple module above. Dynamic module creation will be discussed later.

    There are situations when you certainly do not want to use activity(). If your activity() function contains no wait() and it has only one receive() call at the top of an infinite loop, there's no point in using activity() and the code should be written with handleMessage(). The body of the infinite loop would then become the body to handleMessage(), state variables inside activity() would become data members in the module class, and you'd initialize them in initialize().

    Example:

    void Sink::activity()
    {
        while(true)
        {
            msg = receive();
            delete msg;
        }
    }
    

    should rather be programmed as:

    void Sink::handleMessage(cMessage *msg)
    {
        delete msg;
    }
    

    Activity() is run as a coroutine

    activity() is run in a coroutine. Coroutines are a sort of threads which are scheduled non-preemptively (this is also called cooperative multitasking). From one coroutine you can switch to another coroutine by a transferTo(otherCoroutine) call. Then this coroutine is suspended and otherCoroutine will run. Later, when otherCoroutine does a transferTo(firstCoroutine) call, execution of the first coroutine will resume from the point of the transferTo(otherCoroutine) call. The full state of the coroutine, including local variables are preserved while the thread of execution is in other coroutines. This implies that each coroutine must have its own processor stack, and transferTo() involves a switch from one processor stack to another.

    Coroutines are at the heart of OMNeT++, and the simulation programmer doesn't ever need to call transferTo() or other functions in the coroutine library, nor does he need to care about the coroutine library implementation. It is important to understand, however, how the event loop found in discrete event simulators works with coroutines.

    When using coroutines, the event loop looks like this (simplified):

    while (FES not empty and simulation not yet complete)
    {
        retrieve first event from FES
        t:= timestamp of this event
        transferTo(module containing the event)
    }
    

    That is, when the module has an event, the simulation kernel transfers the control to the module's coroutine. It is expected that when the module ``decides it has finished the processing of the event'', it will transfer the control back to the simulation kernel by a transferTo(main) call. Initially, simple modules using activity() are ``booted'' by events (''starter messages'') inserted into the FES by the simulation kernel before the start of the simulation.

    How does the coroutine know it has ``finished processing the event''? The answer: when it requests another event. The functions which request events from the simulation kernel are the receive() and wait(), so their implementations contain a transferTo(main) call somewhere.

    Their pseudocode, as implemented in OMNeT++:

    receive()
    {
        transferTo(main)
        retrieve current event
        return the event // remember: events = messages
    }
    
    

    wait() { create event e schedule it at (current sim. time + wait interval) transferTo(main) retrieve current event if (current event is not e) { error } delete e // note: actual impl. reuses events return }

    Thus, the receive() and wait() calls are special points in the activity() function, because they are where

    Starter messages

    Modules written with activity() need starter messages to ``boot''. These starter messages are inserted into the FES automatically by OMNeT++ at the beginning of the simulation, even before the initialize() functions are called.

    Coroutine stack size

    The simulation programmer needs to define the processor stack size for coroutines. This cannot be automated.

    16 or 32 kbytes is usually a good choice, but you may need more if the module uses recursive functions or has local variables, which occupy a lot of stack space. OMNeT++ has a built-in mechanism that will usually detect if the module stack is too small and overflows. OMNeT++ can also tell you how much stack space a module actually uses, so you can find out if you overestimated the stack needs.

    initialize() and finish() with activity()

    Because local variables of activity() are preserved across events, you can store everything (state information, packet buffers, etc.) in them. Local variables can be initialized at the top of the activity() function, so there isn't much need to use initialize().

    You do need finish(), however, if you want to write statistics at the end of the simulation. Because finish() cannot access the local variables of activity(), you have to put the variables and objects containing the statistics into the module class. You still don't need initialize() because class members can also be initialized at the top of activity().

    Thus, a typical setup looks like this in pseudocode:

    class MySimpleModule...
    {
        ...
        variables for statistics collection
        activity();
        finish();
    };
    
    

    MySimpleModule::activity() { declare local vars and initialize them initialize statistics collection variables

    while(true) { ... } }

    MySimpleModule::finish() { record statistics into file }

    Pros and Cons of using activity()

    Pros:

    Cons:

    In most cases, cons outweigh pros and it is a better idea to use handleMessage() instead.

    Other simulators

    Coroutines are used by a number of other simulation packages:

    4.3.3 initialize() and finish()

    Purpose

    initialize() -- to provide place for any user setup code

    finish() -- to provide place where the user can record statistics after the simulation has completed

    When and how they are called

    The initialize() functions of the modules are invoked before the first event is processed, but after the initial events (starter messages) have been placed into the FES by the simulation kernel.

    Both simple and compound modules have initialize() functions. A compound module's initialize() function runs before that of its submodules.

    The finish() functions are called when the event loop has terminated, and only if it terminated normally (i.e. not with a runtime error). The calling order is the reverse of the order of initialize(): first submodules, then the encompassing compound module. (The bottom line is that at the moment there is no ``official'' possibility to redefine initialize() and finish() for compound modules; the unofficial way is to write into the nedtool-generated C++ code. Future versions of OMNeT++ will support adding these functions to compound modules.)

    This is summarized in the following pseudocode:

    perform simulation run:
        build network
          (i.e. the system module and its submodules recursively)
        insert starter messages for all submodules using activity()
        do callInitialize() on system module
            enter event loop // (described earlier)
        if (event loop terminated normally) // i.e. no errors
            do callFinish() on system module
        clean up
    
    

    callInitialize() { call to user-defined initialize() function if (module is compound) for (each submodule) do callInitialize() on submodule }

    callFinish() { if (module is compound) for (each submodule) do callFinish() on submodule call to user-defined finish() function }

    initialize() vs. constructor

    Usually you should not put simulation-related code into the simple module constructor. This is because modules often need to investigate their surroundings (maybe the whole network) at the beginning of the simulation and save the collected info into internal tables. Code like that cannot be placed into the constructor since the network is still being set up when the constructor is called.

    finish() vs. destructor

    Keep in mind that finish() is not always called, so it isn't a good place for cleanup code which should run every time the module is deleted. finish() is only a good place for writing statistics, result post-processing and other operations which are supposed to run only on successful completion. Cleanup code should go into the destructor.

    Multi-stage initialization

    In simulation models, when one-stage initialization provided by initialize() is not sufficient, one can use multi-stage initialization. Modules have two functions which can be redefined by the user:

    void initialize(int stage);
    int numInitStages() const;
    

    At the beginning of the simulation, initialize(0) is called for all modules, then initialize(1), initialize(2), etc. You can think of it like initialization takes place in several ``waves''. For each module, numInitStages() must be redefined to return the number of init stages required, e.g. for a two-stage init, numInitStages() should return 2, and initialize(int stage) must be implemented to handle the stage=0 and stage=1 cases.

    The callInitialize() function performs the full multi-stage initialization for that module and all its submodules.

    If you do not redefine the multi-stage initialization functions, the default behavior is single-stage initialization: the default numInitStages() returns 1, and the default initialize(int stage) simply calls initialize().

    ``End-of-Simulation'' event

    The task of finish() is solved in several simulators by introducing a special end-of-simulation event. This is not a very good practice because the simulation programmer has to code the models (often represented as FSMs) so that they can always properly respond to end-of-simulation events, in whichever state they are. This often makes program code unnecessarily complicated.

    This can also be witnessed in the design of the PARSEC simulation language (UCLA). Its predecessor Maisie used end-of-simulation events, but -- as documented in the PARSEC manual -- this has led to awkward programming in many cases, so for PARSEC end-of-simulation events were dropped in favour of finish() (called finalize() in PARSEC).

    4.3.4 handleParameterChange()

    The handleParameterChange() method was added in OMNeT++ 3.2, and it gets called by the simulation kernel when a module parameter changes. The method signature is the following:

    void handleParameterChange(const char *parname);
    

    The user can redefine this method to let the module react to runtime parameter changes. A typical use is to re-read the changed parameter, and update the module state if needed. For example, if a timeout value changes, one can restart or modify running timers.

    The primary motivation for this functionality was to facilitate the implementation of scenario manager modules which can be programmed to change parameters at certain simulation times. Such modules can be very convenient in studies involving transient behaviour.

    The following example shows a queue module, which supports runtime change of its serviceTime parameter:

    void Queue::handleParameterChange(const char *parname)
    {
        if (strcmp(parname, "serviceTime")==0)
        {
            // queue service time parameter changed, re-read it
            serviceTime = par("serviceTime");
    
            // if there any job being serviced, modify its service time
            if (endServiceMsg->isScheduled())
            {
                cancelEvent(endServiceMsg);
                scheduleAt(simTime()+serviceTime, endServiceMsg);
            }
        }
    }
    

    4.3.5 Reusing module code via subclassing

    It is often needed to have several variants of a simple module. A good design strategy is to create a simple module class with the common functionality, then subclass from it to create the specific simple module types.

    An example:

    class ModifiedTransportProtocol : public TransportProtocol
    {
      protected:
        virtual void recalculateTimeout();
    };
    
    Define_Module(ModifiedTransportProtocol);
    
    void ModifiedTransportProtocol::recalculateTimeout()
    {
        //...
    }
    

    4.4 Accessing module parameters

    Module parameters declared in NED files are represented with the cPar class at runtime, and be accessed by calling the par() member function of cModule:

    cPar& delayPar = par("delay");
    

    cPar's value can be read with methods that correspond to the parameter's NED type: boolValue(), longValue(), doubleValue(), stringValue(), stdstringValue(), xmlValue(). There are also overloaded type cast operators for the corresponding types (bool; integer types including int, long, etc; double; const char *; cXMLElement *).

    long numJobs = par("numJobs").longValue();
    double processingDelay = par("processingDelay"); // using operator double()
    

    Note that cPar has two methods for returning string value: stringValue() which returns const char *, and stdstringValue() which returns std::string. For volatile parameters, only stdstringValue() may be used, but otherwise the two are interchangeable.

    If you use the par("foo") parameter in expressions (such as 4*par("foo")+2), the C++ compiler may be unable to decide between overloaded operators and report ambiguity. In that case you have to clarify by adding either an explicit cast ((double)par("foo") or (long)par("foo")) or use the doubleValue() or longValue() methods.

    4.4.1 Volatile and non-volatile parameters

    A parameter can be declared volatile in the NED file. The volatile modifier indicates that a parameter is re-read every time a value is needed during simulation. Volatile parameters typically are used for things like random packet generation interval, and get values like exponential(1.0) (numbers drawn from the exponential distribution with mean 1.0).

    In contrast, non-volatile NED parameters are constants, and reading their values multiple times is guaranteed to yield the same value. When a non-volatile parameter is assigned a random value like exponential(1.0), it gets evaluated once at the beginning of the simulation and replaced with the result, so all reads will get same (randomly generated) value.

    The typical usage for non-volatile parameters is to read them in the initialize() method of the module class, and store the values in class variables for easy access later:

    class Source : public cSimpleModule
    {
      protected:
        long numJobs;
        virtual void initialize();
        ...
    };
    
    void Source::initialize()
    {
        numJobs = par("numJobs");
        ...
    }
    

    volatile parameters need to be re-read every time the value is needed. For example, a parameter that represents a random packet generation interval may be used like this:

    void Source::handleMessage(cMessage *msg)
    {
        ...
        scheduleAt(simTime() + par("interval").doubleValue(), timerMsg);
        ...
    }
    

    This code looks up the the parameter by name every time. This lookup can be spared by storing the parameter object's pointer in a class variable, resulting in the following code:

    class Source : public cSimpleModule
    {
      protected:
        cPar *intervalp;
        virtual void initialize();
        virtual void handleMessage(cMessage *msg);
        ...
    };
    
    void Source::initialize()
    {
        intervalp = &par("interval");
        ...
    }
    
    void Source::handleMessage(cMessage *msg)
    {
        ...
        scheduleAt(simTime() + intervalp->doubleValue(), timerMsg);
        ...
    }
    

    4.4.2 Changing a parameter's value

    Parameter values can be changed from the program, during execution. This is rarely needed, but may be useful for some scenarios.

    The methods to set the parameter value are setBoolValue(), setLongValue(), setStringValue(), setDoubleValue(), setXMLValue(). There are also overloaded assignment operators for various types including bool, int, long, double, const char *, and cXMLElement *.

    To let a module get notified about parameter changes, override its handleParameterChange() method, see .

    4.4.3 Further cPar methods

    The parameter's name and type are returned by the getName() and getType() methods. The latter returns a value from an enum, which can be converted to a readable string with the getTypeName() static method. The enum values are BOOL, DOUBLE, LONG, STRING and XML; and since the enum is an inner type, they usually have to be qualified with cPar::.

    isVolatile() returns whether the parameter was declared volatile in the NED file. isNumeric() returns true if the parameter type is double or long.

    The str() method returns the parameter's value in a string form. If the parameter contains an expression, then the string representation of the expression gets returned.

    An example usage of the above methods:

    int n = getNumParams();
    for (int i=0; i<n; i++)
    {
        cPar& p = par(i);
        ev << "parameter: " << p.getName() << "\n";
        ev << "  type:" << cPar::getTypeName(p.getType()) << "\n";
        ev << "  contains:" << p.str() << "\n";
    }
    

    The NED properties of a parameter can be accessed with the getProperties() method that returns a pointer to the cProperties object that stores the properties of this parameter. Specifically, getUnit() returns the unit of measurement associated with the parameter (@unit property in NED).

    Further cPar methods and related classes like cExpression and cDynamicExpression are used by the NED infrastructure to set up and assign parameters. They are documented in the API Reference, but they are normally of little interest for users.

    4.4.4 Emulating parameter arrays

    As of version 4.0, OMNeT++ does not support parameter arrays, but in practice they can be emulated using string parameters. One can assign the parameter a string which contains all values in a textual form (for example, "0 1.234 3.95 5.467"), then parse this string in the simple module.

    The cStringTokenizer class can be quite useful for this purpose. The constructor accepts a string, which it regards as a sequence of tokens (words) separated by delimiter characters (by default, spaces). Then you can either enumerate the tokens and process them one by one (hasMoreTokens(), nextToken()), or use one of the cStringTokenizer convenience methods to convert them into a vector of strings (asVector()), integers (asIntVector()), or doubles (asDoubleVector()).

    The latter methods can be used like this:

    const char *vstr = par("v").stringValue(); // e.g. "aa bb cc";
    std::vector<std::string> v = cStringTokenizer(vstr).asVector();
    

    and

    const char *str = "34 42 13 46 72 41";
    std::vector<int> v = cStringTokenizer().asIntVector();
    
    const char *str = "0.4311 0.7402 0.7134";
    std::vector<double> v = cStringTokenizer().asDoubleVector();
    

    The following example processes the string by enumerating the tokens:

    const char *str = "3.25 1.83 34 X 19.8"; // input
    
    std::vector<double> result;
    cStringTokenizer tokenizer(str);
    while (tokenizer.hasMoreTokens())
    {
        const char *token = tokenizer.nextToken();
        if (strcmp(token, "X")==0)
            result.push_back(DEFAULT_VALUE);
        else
            result.push_back(atof(token));
    }
    

    4.5 Accessing gates and connections

    4.5.1 Gate objects

    Module gates are represented by cGate objects. Gate objects know to which other gates they are connected, and what are the channel objects associated with the links.

    Accessing gates by name

    The cModule class has a number of member functions that deal with gates. You can look up a gate by name using the gate() method:

    cGate *outGate = gate("out");
    

    This works for input and output gates. However, when a gate was declared inout in NED, it is actually represented by the simulation kernel with two gates, so the above call would result in a gate not found error. The gate() method needs to be told whether the input or the output half of the gate you need. This can be done by appending the "$i" or "$o" to the gate name. The following example retrieves the two gates for the inout gate "g":

    cGate *gIn = gate("g$i");
    cGate *gOut = gate("g$o");
    

    Another way is to use the gateHalf() function, which takes the inout gate's name plus either cGate::INPUT or cGate::OUTPUT:

    cGate *gIn = gateHalf("g", cGate::INPUT);
    cGate *gOut = gateHalf("g", cGate::OUTPUT);
    

    These methods throw an error if the gate does not exist, so they cannot be used to determine whether the module has a particular gate. The hasGate() method can be used then. An example:

    if (hasGate("optOut"))
       send(new cMessage(), "optOut");
    

    A gate can also be identified and looked up by a numeric gate ID. You can get the ID from the gate itself (getId() method), or from the module by gate name (findGate() method). The gate() method also has an overloaded variant which returns the gate from the gate ID.

    int gateId = gate("in")->getId();  // or:
    int gateId = findGate("in");
    

    As gate IDs are more useful with gate vectors, we'll cover them in detail in a later section.

    Gate vectors

    Gate vectors possess one cGate object per element. To access individual gates in the vector, you need to call the gate() function with an additional index parameter. The index should be between zero and size-1. The size of the gate vector can be read with the gateSize() method. The following example iterates through all elements in the gate vector:

    for (int i=0; i<gateSize("out"); i++) {
        cGate *gate = gate("out", i);
        //...
    }
    

    A gate vector cannot have ``holes'' in it, that is, gate() never returns NULL or throws an error if the gate vector exists and the index is within bounds.

    For inout gates, gateSize() may be called with or without the "$i"/"$o" suffix, and returns the same number.

    The hasGate() method may be used both with and without an index, and they mean two different things: without an index it tells the existence of a gate vector with the given name, regardless of its size (it returns true for an existing vector even if its size is currently zero!); with an index it also examines whether the index is within the bounds.

    Gate IDs

    A gate can also be accessed by its ID. A very important property of gate IDs is that the ID of gate k in a gate vector equals the ID of gate 0 plus the index. This allows you to efficiently access any gate in a gate vector, because retrieving a gate by ID is more efficient than by name and index. The index of the first gate can be obtained with gate("out",0)->getId(), but it is better to use a dedicated method, gateBaseId(), which also works if the gate size is zero.

    Two further important properties of gate IDs: they are stable and unique (within the module). By stable we mean that the ID of a gate never changes; and by unique we not only mean that at any given time no two gates have the same IDs, but also that IDs of deleted gates do not get reused later, so gate IDs are unique in the lifetime of a simulation run.

    The following example iterates through a gate vector, using IDs:

    int baseId = getBaseId("out");
    int size = gateSize("out");
    for (int i=0; i<size; i++) {
        cGate *gate = gate(baseId + i);
        //...
    }
    

    Enumerating all gates

    If you need to go through all gates of a module, there are two possibilities. One is invoking the getGateNames() method that returns the names of all gates and gate vectors the module has; then you can call isGateVector(name) to determine whether individual names identify a scalar gate or a gate vector; then gate vectors can be enumerated by index. Also, for inout gates getGateNames() returns the base name without the "$i"/"$o" suffix, so the two directions need to be handled separately. The gateType(name) method can be used to test whether a gate is inout, output or inout (it returns cGate::INOUT, cGate::INPUT, or cGate::OUTPUT).

    Clearly, the above solution can be quite hairy. An alternative is to use the GateIterator class provided by cModule. It goes like this:

    for (cModule::GateIterator i(this); !i.end(); i++) {
        cGate *gate = i();
        ...
    }
    

    Where this denotes the module whose gates are being enumerated (it can be replaced by any cModule * variable).

    Adding and deleting gates

    Although rarely needed, it is possible to add and remove gates during simulation. You can add scalar gates and gate vectors, change the size of gate vectors, and remove scalar gates and whole gate vectors. It is not possible to remove individual random gates from a gate vector, to remove one half of an inout gate (e.g. "gate$o"), or to set different gate vector sizes on the two halves of an inout gate vector.

    The cModule methods for adding and removing gates are addGate(name,type,isvector=false) and deleteGate(name). Gate vector size can be changed by using setGateSize(name,size). None of these methods accept "$i" / "$o" suffix in gate names.

    cGate methods

    The getName() method of cGate returns the name of the gate or gate vector. If you need a string that contains the gate index as well, getFullName() is what you want. If you also want to include the hierarchical name of the owner module, call getFullPath().

    The getType() method of cGate returns the gate type, either cGate::INPUT or cGate::OUTPUT. (It cannot return cGate::INOUT, because an inout gate is represented by a pair of cGates.) The isVector(), getIndex(), getVectorSize(), getId() method names speak for themselves. size() is an alias to getVectorSize().

    The getOwnerModule() method returns the module the gate object belongs to.

    To illustrate these methods, we expand the gate iterator example to print some information about each gate:

    for (cModule::GateIterator i(this); !i.end(); i++) {
        cGate *gate = i();
        ev << gate->getFullName() << ": ";
        ev << "id=" << gate->getId() << ", ";
        if (!gate->isVector())
            ev << "scalar gate, ";
        else
            ev << "gate " << gate->getIndex()
               << " in vector " << gate->getName()
               << " of size " << gate->getVectorSize() << ", ";
        ev << "type:" << cGate::getTypeName(gate->getType());
        ev << "\n";
    }
    

    There are further cGate methods to access and manipulate the connection(s) attached to the gate; they will be covered in the following sections.

    4.5.2 Connections

    Simple module gates have normally one connection attached. Compound module gates, however, need to be connected both inside and outside of the module to be useful. A series of connections (joined with compound module gates) is called a path. A path is directed, and it normally starts at an output gate of a simple module, ends at an input gate of a simple module, and passes through several compound module gates.

    Every cGate object contains pointers to the previous gate and the next gate in the path (returned by the getPreviousGate() and getNextGate() methods), so a path can be thought of as a double-linked list.

    The use of the previous gate / next gate pointers with various gate types is illustrated on figure below.

    Figure: (a) simple module output gate, (b) compound module output gate, (c) simple module input gate, (d) compound module input gate

    The start and end gates of the path can be found with the getPathStartGate() and getPathEndGate() methods, which simply follow the previous gate / next gate pointers until they return NULL.

    The isConnectedOutside() and isConnectedInside() methods return whether a gate is connected on the outside or on the inside. They examine either the from or the to pointer, depending on the gate type (input or output). Again, see figure below for an illustration.

    The isConnected() method is a bit different: it returns true if the gate is fully connected, that is, for a compound module gate both inside and outside, and for a simple module gate, outside.

    The following code prints the name of the gate a simple module gate is connected to:

    cGate *gate = gate("somegate");
    cGate *otherGate = gate->getType()==cGate::OUTPUT ? gate->getNextGate() :
                                                        gate->getPreviousGate();
    if (otherGate)
      ev << "gate is connected to: " << otherGate->getFullPath() << endl;
    else
      ev << "gate not connected" << endl;
    

    4.5.3 The connection's channel

    TODO

    4.6 Sending and receiving messages

    On an abstract level, an OMNeT++ simulation model is a set of simple modules that communicate with each other via message passing. The essence of simple modules is that they create, send, receive, store, modify, schedule and destroy messages -- everything else is supposed to facilitate this task, and collect statistics about what was going on.

    Messages in OMNeT++ are instances of the cMessage class or one of its subclasses. Message objects are created using the C++ new operator and destroyed using the delete operator when they are no longer needed. During their lifetimes, messages travel between modules via gates and connections (or are sent directly, bypassing the connections), or they are scheduled by and delivered to modules, representing internal events of that module.

    Messages are described in detail in chapter [5]. At this point, all we need to know about them is that they are referred to as cMessage * pointers. Message objects can be given descriptive names (a const char * string) that often helps in debugging the simulation. The message name string can be specified in the constructor, so it should not surprise you if you see something like new cMessage("token") in the examples below.

    4.6.1 Sending messages

    Once created, a message object can be sent through an output gate using one of the following functions:

    send(cMessage *msg, const char *gateName, int index=0);
    send(cMessage *msg, int gateId);
    send(cMessage *msg, cGate *gate);
    

    In the first function, the argument gateName is the name of the gate the message has to be sent through. If this gate is a vector gate, index determines though which particular output gate this has to be done; otherwise, the index argument is not needed.

    The second and third functions use the gate Id and the pointer to the gate object. They are faster than the first one because they don't have to search through the gate array.

    Examples:

    send(msg, "outGate");
    send(msg, "outGates", i); // send via outGates[i]
    

    The following code example creates and sends messages every 5 simulated seconds:

    int outGateId = findGate("outGate");
    while(true)
    {
      send(new cMessage("job"), outGateId);
      wait(5);
    }
    

    4.6.2 Packet transmissions

    When a message is sent out on a gate, it usually travels through a series of connections until it arrives at the destination module. We call this series of connections a connection path or simply path.

    Several connections in the path may have an associated channel, but there can be only one channel per path that models nonzero transmission duration. This channel is called the transmission channel.

    Transmitting a packet

    The first packet can be simply send out on the output gate. However, subsequent packets may only be sent when the transmission channel is free, that is, it has finished transmitting earlier packets.

    You can get a pointer to the transmission channel by calling the getDatarateChannel() method on the output gate. The channel's isBusy() and getTransmissionFinishTime() methods can tell you whether a channel is currently transmitting, and when the transmission is going to finish. (When the latter is less or equal the current simulation time, the channel is free.) If the channel is currently busy, a timer (self-message) needs to be scheduled, and the packet should be stored until then, for example in a queue.

    The output gate also has isBusy() and getTransmissionFinishTime() methods, which are basically shortcuts to getDatarateChannel()->isBusy() and getDatarateChannel()->getTransmissionFinishTime(). When performance is important, it is recommended to obtain a pointer to the transmission channel once, and then call isBusy() and getTransmissionFinishTime() on the cached channel pointer.

    An incomplete code example to illustrate the above process:

    simtime_t txfinishTime = gate("out")->getTransmissionFinishTime();
    if (txfinishTime <= simTime())
        send(pkt, "out");
    else
        scheduleAt(txFinishTime, timerMsg); // also: remember pkt,
        // and ensure that when timerMsg expires, it will get sent out
    

    Receiving a packet

    Normally the packet object gets delivered to the destination module at the simulation time that corresponds to finishing the reception of the message (ie. the arrival of its last bit). However, the receiver module may change this, by "reprogramming" the receiver gate with the setDeliverOnReceptionStart() method:

    gate("in")->setDeliverOnReceptionStart(true);
    

    This method may only be called on simple module input gates, and it instructs the simulation kernel to give arriving packets to the receiver module when reception begins not ends, that is, on arrival of the first bit of the message. getDeliverOnReceptionStart() only needs to be called once, so it is usually done in the initialize() method of the module.

    When a packet gets delivered to the module, the packet's isReceptionStart() method can be called to determine whether it corresponds to the start or end of the reception process (it should be the same as the getDeliverOnReceptionStart() flag of the input gate), and getDuration() returns the transmission duration.

    4.6.3 Delay, data rate, bit error rate, packet error rate

    Connections can be assigned three parameters, which facilitate the modeling of communication networks, but can be useful for other models too:

    Each of these parameters is optional. One can specify link parameters individually for each connection, or define link types (also called channel types) once and use them throughout the whole model.

    The propagation delay is the amount of time the arrival of the message is delayed by when it travels through the channel. Propagation delay is specified in seconds.

    The bit error rate has influence on the transmission of messages through the channel. The bit error rate (ber) is the probability that a bit is incorrectly transmitted. Thus, the probability that a message of n bits length is transferred without bit errors is:

    Pno bit error = (1 - ber)length

    The message has an error flag which is set in case of transmission errors.

    The data rate is specified in bits/second, and it is used for transmission delay calculation. The sending time of the message normally corresponds to the transmission of the first bit, and the arrival time of the message corresponds to the reception of the last bit (Fig. below).

    Figure: Message transmission

    The above model may not be suitable to model all protocols. In Token Ring and FDDI, stations start to repeat bits before the whole frame arrives; in other words, frames ``flow through'' the stations, being delayed only a few bits. In such cases, the data rate modeling feature of OMNeT++ cannot be used.

    If a message travels along a path, passing through successive links and compound modules, the model behaves as if each module waited until the last bit of the message arrives and only started its transmission afterwards. (Fig. below).

    Figure: Message sending over multiple channels

    Since the above effect is usually not the desired one, typically you will want to assign data rate to only one connection in the path.

    Multiple transmissions on links

    If a data rate is specified for a connection, a message will have a certain nonzero transmission time, depending on the length of the connection. This implies that a message that is passsing through an output gate, ``reserves'' the gate for a given period (``it is being transmitted'').

    Figure: Connection with a data rate

    While a message is under transmission, other messages have to wait until the transmission is completed. The module sends another message while the gate is busy, a runtime error will be thrown.

    The OMNeT++ class library provides functions to check whether a certain output gate is transmitting and find out when when it finishes transmission.

    If the connection with a data rate is not directly connected to the simple module's output gate but is the second one in the path, you have to check the second gate's busy condition.

    Implementation of message sending

    Message sending is implemented like this: the arrival time and the bit error flag of a message are calculated right inside the send() call, then the message gets inserted into the FES with the calculated arrival time. The message does not get scheduled individually for each link. This implementation was chosen because of its run-time efficiency.

    The approach of some other simulators

    Note that some simulators (e.g. OPNET) assign packet queues to input gates (ports), and messages sent are buffered at the destination module (or the remote end of the link) until they are received by the destination module. With that approach, events and messages are separate entities, that is, a send operation includes placing the message in the packet queue and scheduling an event, which signals the arrival of the packet. In some implementations, output gates also have packet queues where packets will be buffered until the channel is ready (available for transmission).

    OMNeT++ gates don't have associated queues. The place where sent but not yet received messages are buffered in the FES. OMNeT++'s approach is potentially faster than the solution mentioned above because it doesn't have the enqueue/dequeue overhead and also spares an event creation. The drawback is, that changes to channel parameters do not take effect immediately.

    In OMNeT++ one can implement point-to-point transmitter modules with packet queues if needed. For example, the INET Framework follows this approach.

    Connection attributes (propagation delay, transmission data rate, bit error rate) are represented by the channel object, which is available via the source gate of the connection.

    cChannel *chan = outgate->getChannel();
    

    cChannel is a small base class. All interesting attributes are part of its subclass cDatarateChannel, so you have to cast the pointer before getting to the delay, error and data rate values.

    cDatarateChannel *chan = check_and_cast<cDatarateChannel *>(outgate->getChannel());
    double d = chan->getDelay();
    double e = chan->getBitErrorRate();
    double r = chan->getDatarate();
    

    You can also change the channel attributes with the corresponding setXXX() functions. Note, however, that (as it was explained in section ) changes will not affect messages already sent, even if they have not begun transmission yet.

    Channel transmission state

    The isBusy() member function returns whether the gate is currently transmitting, and if so, the getTransmissionFinishTime() member function returns the simulation time when the gate is going to finish transmitting. (If the gate in not currently transmitting, getTransmissionFinishTime() returns the simulation time when it finished its last transmission.)

    An example:

    cMessage *packet = new cMessage("DATA");
    packet->setByteLength(1024);  // 1K
    
    if (gate("TxGate")->isBusy()) // if gate is busy, wait until it
    {                             // becomes free
      wait( gate("TxGate")->getTransmissionFinishTime() - simTime());
    }
    send( packet, "TxGate");
    

    If the connection with a data rate is not directly connected to the simple module's output gate but is the second one in the path, you have to check the second gate's busy condition. You could use the following code:

    if (gate("out")->getNextGate()->isBusy())
      //...
    

    Note that if data rates change during the simulation, the changes will affect only the messages that are sent after the change.

    4.6.4 Broadcasts and retransmissions

    When you implement broadcasts or retransmissions, two frequently occurring tasks in protocol simulation, you might feel tempted to use the same message in multiple send() operations. Do not do it -- you cannot send the same message object multiple times. The solution in such cases is duplicating the message.

    Broadcasting messages

    In your model, you may need to broadcast a message to several destinations. Broadcast can be implemented in a simple module by sending out copies of the same message, for example on every gate of a gate vector. As described above, you cannot use the same message pointer for in all send() calls -- what you have to do instead is create copies (duplicates) of the message object and send them.

    Example:

    for (int i=0; i<n; i++)
    {
        cMessage *copy = msg->dup();
        send(copy, "out", i);
    }
    delete msg;
    

    You might have noticed that copying the message for the last gate is redundant: we can just send out the original message there. Also, we can utilize gate IDs to spare looking up the gate by name for each send operation. The optimized version of the code looks like this:

    int outGateBaseId = gateBaseId("out");
    for (int i=0; i<n; i++)
        send(i==n-1 ? msg : msg->dup(), outGateBaseId+i);
    

    Retransmissions

    Many communication protocols involve retransmissions of packets (frames). When implementing retransmissions, you cannot just hold a pointer to the same message object and send it again and again -- you'd get the not owner of message error on the first resend.

    Instead, whenever it comes to (re)transmission, you should create and send copies of the message, and retain the original. When you are sure there will not be any more retransmission, you can delete the original message.

    Creating and sending a copy:

    // (re)transmit packet:
    cMessage *copy = packet->dup();
    send(copy, "out");
    

    and finally (when no more retransmissions will occur):

    delete packet;
    

    Why?

    A message is like any real world object -- it cannot be at two places at the same time. Once you've sent it, the message object no longer belongs to the module: it is taken over by the simulation kernel, and will eventually be delivered to the destination module. The sender module should not even refer to its pointer any more. Once the message arrived in the destination module, that module will have full authority over it -- it can send it on, destroy it immediately, or store it for further handling. The same applies to messages that have been scheduled -- they belong to the simulation kernel until they are delivered back to the module.

    To enforce the rules above, all message sending functions check that you actually own the message you are about to send. If the message is with another module, it is currently scheduled or in a queue etc., you'll get a runtime error: not owner of message.

    4.6.5 Delayed sending

    It is often needed to model a delay (processing time, etc.) immediately followed by message sending. In OMNeT++, it is possible to implement it like this:

    wait(someDelay);
    send(msg, "outgate");
    

    If the module needs to react to messages that arrive during the delay, wait() cannot be used and the timer mechanism described in Section [4.6.9], ``Self-messages'', would need to be employed.

    There is also a more straightforward method than those mentioned above: delayed sending. Delayed sending can be achieved by using one of these functions:

    sendDelayed(cMessage *msg, double delay, const char *gateName, int index);
    sendDelayed(cMessage *msg, double delay, int gateId);
    sendDelayed(cMessage *msg, double delay, cGate *gate);
    

    The arguments are the same as for send(), except for the extra delay parameter. The effect of the function is the same as if the module had kept the message for the delay interval and sent it afterwards. That is, the sending time of the message will be the current simulation time (time at the sendDelayed() call) plus the delay. The delay value must be non-negative.

    Example:

    sendDelayed(msg, 0.005, "outGate");
    

    4.6.6 Direct message sending

    Sometimes it is necessary or convenient to ignore gates/connections and send a message directly to a remote destination module. The sendDirect() function does that:

    sendDirect(cMessage *msg, double delay, cModule *mod, int gateId)
    sendDirect(cMessage *msg, double delay, cModule *mod, const char *gateName, int index=-1)
    sendDirect(cMessage *msg, double delay, cGate *gate)
    

    In addition to the message and a delay, it also takes the destination module and gate. The gate should be an input gate and should not be connected. In other words, the module needs dedicated gates for receiving via sendDirect(). (Note: For leaving a gate unconnected in a compound module, you'll need to specify connections nocheck: instead of plain connections: in the NED file.)

    An example:

    cModule *destinationModule = getParentModule()->getSubmodule("node2");
    double delay = truncnormal(0.005, 0.0001);
    sendDirect(new cMessage("packet"), delay, destinationModule, "inputGate");
    

    At the destination module, there is no difference between messages received directly and those received over connections.

    4.6.7 Receiving messages

    With activity() only! The message receiving functions can only be used in the activity() function, handleMessage() gets received messages in its argument list.

    Messages are received using the receive() function. receive() is a member of cSimpleModule.

    cMessage *msg = receive();
    

    The receive() function accepts an optional timeout parameter. (This is a delta, not an absolute simulation time.) If an appropriate message doesn't arrive within the timeout period, the function returns a NULL pointer.

    simtime_t timeout = 3.0;
    cMessage *msg = receive( timeout );
    
    if (msg==NULL)
    {
        ...   // handle timeout
    }
    else
    {
        ...  // process message
    }
    

    4.6.8 The wait() function

    With activity() only! The wait() function's implementation contains a receive() call which cannot be used in handleMessage().

    The wait() function suspends the execution of the module for a given amount of simulation time (a delta).

    wait(delay);
    

    In other simulation software, wait() is often called hold. Internally, the wait() function is implemented by a scheduleAt() followed by a receive(). The wait() function is very convenient in modules that do not need to be prepared for arriving messages, for example message generators. An example:

    for (;;)
    {
      // wait for a (potentially random amount of) time, specified
      // in the interArrivalTime volatile module parameter
      wait(par("interArrivalTime").doubleValue());
    
      // generate and send message
      ...
    }
    

    It is a runtime error if a message arrives during the wait interval. If you expect messages to arrive during the wait period, you can use the waitAndEnqueue() function. It takes a pointer to a queue object (of class cQueue, described in chapter [6]) in addition to the wait interval. Messages that arrive during the wait interval will be accumulated in the queue, so you can process them after the waitAndEnqueue() call returned.

    cQueue queue("queue");
    ...
    waitAndEnqueue(waitTime, &queue);
    if (!queue.empty())
    {
      // process messages arrived during wait interval
      ...
    }
    

    4.6.9 Modeling events using self-messages

    In most simulation models it is necessary to implement timers, or schedule events that occur at some point in the future. For example, when a packet is sent by a communications protocol model, it has to schedule an event that would occur when a timeout expires, because it will have to resent the packet then. As another example, suppose you want to write a model of a server which processes jobs from a queue. Whenever it begins processing a job, the server model will want to schedule an event to occur when the job finishes processing, so that it can begin processing the next job.

    In OMNeT++ you solve such tasks by letting the simple module send a message to itself; the message would be delivered to the simple module at a later point of time. Messages used this way are called self-messages. Self-messages are used to model events which occur within the module.

    Scheduling an event

    The module can send a message to itself using the scheduleAt() function. scheduleAt() accepts an absolute simulation time, usually calculated as simTime()+delta:

    scheduleAt(absoluteTime, msg);
    scheduleAt(simtime()+delta, msg);
    

    Self-messages are delivered to the module in the same way as other messages (via the usual receive calls or handleMessage()); the module may call the isSelfMessage() member of any received message to determine if it is a self-message.

    As an example, here's how you could implement your own wait() function in an activity() simple module, if the simulation kernel didn't provide it already:

    cMessage *msg = new cMessage();
    scheduleAt(simtime()+waitTime, msg);
    cMessage *recvd = receive();
    if (recvd!=msg)
       // hmm, some other event occurred meanwhile: error!
    ...
    

    You can determine if a message is currently in the FES by calling its isScheduled() member:

    if (msg->isScheduled())
      // currently scheduled
    else
      // not scheduled
    

    Re-scheduling an event

    If you want to reschedule an event which is currently scheduled to a different simulation time, first you have to cancel it using cancelEvent().

    Cancelling an event

    Scheduled self-messages can be cancelled (removed from the FES). This is particularly useful because self-messages are often used to model timers.

    cancelEvent( msg );
    

    The cancelEvent() function takes a pointer to the message to be cancelled, and also returns the same pointer. After having it cancelled, you may delete the message or reuse it in the next scheduleAt() calls. cancelEvent() gives an error if the message is not in the FES.

    Implementing timers

    The following example shows how to implement timers:

    cMessage *timeoutEvent = new cMessage("timeout");
    
    scheduleAt(simTime()+10.0, timeoutEvent);
    //...
    
    cMessage *msg = receive();
    if (msg == timeoutEvent)
    {
      // timeout expired
    }
    else
    {
      // other message has arrived, timer can be cancelled now:
      delete cancelEvent(timeoutEvent);
    }
    

    4.7 Stopping the simulation

    4.7.1 Normal termination

    You can finish the simulation with the endSimulation() function:

    endSimulation();
    

    endSimulation() is rarely needed in practice because you can specify simulation time and CPU time limits in the ini file (see later).

    4.7.2 Raising errors

    If your simulation encounters an error condition, you can throw a cRuntimeError exception to terminate the simulation with an error message (and in case of Cmdenv, a nonzero exit code). The cRuntimeError class has a constructor whose argument list is similar to printf():

    if (windowSize<1)
        throw cRuntimeError("Invalid window size %d; must be >=1", windowSize);
    

    Do not include a newline (``\n'') or punctuation (period or exclamation mark) in the error text; it will be added by OMNeT++.

    You can achieve the same effect by calling the error() method of cModule.

    if (windowSize<1)
        error("Invalid window size %d; must be >=1", windowSize);
    

    Of course, the error() method can only be used when a module pointer is available.

    4.8 Finite State Machines in OMNeT++

    Overview

    Finite State Machines (FSMs) can make life with handleMessage() easier. OMNeT++ provides a class and a set of macros to build FSMs. OMNeT++'s FSMs work very much like OPNET's or SDL's.

    The key points are:

    OMNeT++'s FSMs can be nested. This means that any state (or rather, its entry or exit code) may contain a further full-fledged FSM_Switch() (see below). This allows you to introduce sub-states and thereby bring some structure into the state space if it would become too large.

    The FSM API

    FSM state is stored in an object of type cFSM. The possible states are defined by an enum; the enum is also a place to define, which state is transient and which is steady. In the following example, SLEEP and ACTIVE are steady states and SEND is transient (the numbers in parentheses must be unique within the state type and they are used for constructing the numeric IDs for the states):

    enum {
      INIT = 0,
      SLEEP = FSM_Steady(1),
      ACTIVE = FSM_Steady(2),
      SEND = FSM_Transient(1),
    };
    

    The actual FSM is embedded in a switch-like statement, FSM_Switch(), where you have cases for entering and leaving each state:

    FSM_Switch(fsm)
    {
      case FSM_Exit(state1):
        //...
        break;
      case FSM_Enter(state1):
        //...
        break;
      case FSM_Exit(state2):
        //...
        break;
      case FSM_Enter(state2):
        //...
        break;
      //...
    };
    

    State transitions are done via calls to FSM_Goto(), which simply stores the new state in the cFSM object:

    FSM_Goto(fsm,\textit{newState});
    

    The FSM starts from the state with the numeric code 0; this state is conventionally named INIT.

    Debugging FSMs

    FSMs can log their state transitions ev, with the output looking like this:

    ...
    FSM GenState: leaving state SLEEP
    FSM GenState: entering state ACTIVE
    ...
    FSM GenState: leaving state ACTIVE
    FSM GenState: entering state SEND
    FSM GenState: leaving state SEND
    FSM GenState: entering state ACTIVE
    ...
    FSM GenState: leaving state ACTIVE
    FSM GenState: entering state SLEEP
    ...
    

    To enable the above output, you have to #define FSM_DEBUG before including omnetpp.h.

    #define FSM_DEBUG    // enables debug output from FSMs
    #include <omnetpp.h>
    

    The actual logging is done via the FSM_Print() macro. It is currently defined as follows, but you can change the output format by undefining FSM_Print() after including omnetpp.ini and providing a new definition instead.

    #define FSM_Print(fsm,exiting)
      (ev << "FSM " << (fsm).getName()
          << ((exiting) ? ": leaving state " : ": entering state ")
          << (fsm).getStateName() << endl)
    

    Implementation

    The FSM_Switch() is a macro. It expands to a switch() statement embedded in a for() loop which repeats until the FSM reaches a steady state. (The actual code is rather scary, but if you're dying to see it, it is in cfsm.h.)

    Infinite loops are avoided by counting state transitions: if an FSM goes through 64 transitions without reaching a steady state, the simulation will terminate with an error message.

    An example

    Let us write another bursty generator. It will have two states, SLEEP and ACTIVE. In the SLEEP state, the module does nothing. In the ACTIVE state, it sends messages with a given inter-arrival time. The code was taken from the Fifo2 sample simulation.

    #define FSM_DEBUG
    #include <omnetpp.h>
    
    class BurstyGenerator : public cSimpleModule
    {
      protected:
        // parameters
        double sleepTimeMean;
        double burstTimeMean;
        double sendIATime;
        cPar *msgLength;
    
        // FSM and its states
        cFSM fsm;
        enum {
          INIT = 0,
          SLEEP = FSM_Steady(1),
          ACTIVE = FSM_Steady(2),
          SEND = FSM_Transient(1),
        };
    
        // variables used
        int i;
        cMessage *startStopBurst;
        cMessage *sendMessage;
    
        // the virtual functions
        virtual void initialize();
        virtual void handleMessage(cMessage *msg);
    };
    
    Define_Module(BurstyGenerator);
    
    void BurstyGenerator::initialize()
    {
        fsm.setName("fsm");
        sleepTimeMean = par("sleepTimeMean");
        burstTimeMean = par("burstTimeMean");
        sendIATime = par("sendIATime");
        msgLength = &par("msgLength");
        i = 0;
        WATCH(i); // always put watches in initialize()
        startStopBurst = new cMessage("startStopBurst");
        sendMessage = new cMessage("sendMessage");
        scheduleAt(0.0,startStopBurst);
    }
    
    void BurstyGenerator::handleMessage(cMessage *msg)
    {
       FSM_Switch(fsm)
       {
         case FSM_Exit(INIT):
           // transition to SLEEP state
           FSM_Goto(fsm,SLEEP);
           break;
         case FSM_Enter(SLEEP):
           // schedule end of sleep period (start of next burst)
           scheduleAt(simTime()+exponential(sleepTimeMean),
                      startStopBurst);
         break;
         case FSM_Exit(SLEEP):
           // schedule end of this burst
           scheduleAt(simTime()+exponential(burstTimeMean),
                      startStopBurst);
           // transition to ACTIVE state:
           if (msg!=startStopBurst) {
             error("invalid event in state ACTIVE");
           }
           FSM_Goto(fsm,ACTIVE);
           break;
         case FSM_Enter(ACTIVE):
           // schedule next sending
           scheduleAt(simTime()+exponential(sendIATime), sendMessage);
         break;
         case FSM_Exit(ACTIVE):
           // transition to either SEND or SLEEP
           if (msg==sendMessage) {
             FSM_Goto(fsm,SEND);
           } else if (msg==startStopBurst) {
             cancelEvent(sendMessage);
             FSM_Goto(fsm,SLEEP);
           } else {
             error("invalid event in state ACTIVE");
           }
           break;
         case FSM_Exit(SEND):
         {
           // generate and send out job
           char msgname[32];
           sprintf( msgname, "job-%d", ++i);
           ev << "Generating " << msgname << endl;
           cMessage *job = new cMessage(msgname);
           job->setBitLength( (long) *msgLength );
           job->setTimestamp();
           send( job, "out" );
           // return to ACTIVE
           FSM_Goto(fsm,ACTIVE);
           break;
         }
       }
    }
    

    4.9 Walking the module hierarchy

    Module vectors

    If a module is part of a module vector, the getIndex() and size() member functions can be used to query its index and the vector size:

    ev << "This is module [" << module->getIndex() <<
          "] in a vector of size [" << module->size() << "].\n";
    

    Module IDs

    Each module in the network has a unique ID that is returned by the getId() member function. The module ID is used internally by the simulation kernel to identify modules.

    int myModuleId = getId();
    

    If you know the module ID, you can ask the simulation object (a global variable) to get back the module pointer:

    int id = 100;
    cModule *mod = simulation.getModule( id );
    

    Module IDs are guaranteed to be unique, even when modules are created and destroyed dynamically. That is, an ID which once belonged to a module which was deleted is never issued to another module later.

    Walking up and down the module hierarchy

    The surrounding compound module can be accessed by the getParentModule() member function:

    cModule *parent = getParentModule();
    

    For example, the parameters of the parent module are accessed like this:

    double timeout = getParentModule()->par( "timeout" );
    

    cModule's findSubmodule() and getSubmodule() member functions make it possible to look up the module's submodules by name (or name+index if the submodule is in a module vector). The first one returns the numeric module ID of the submodule, and the latter returns the module pointer. If the submodule is not found, they return -1 or NULL, respectively.

    int submodID = compoundmod->findSubmodule("child",5);
    cModule *submod = compoundmod->getSubmodule("child",5);
    

    The getModuleByRelativePath() member function can be used to find a submodule nested deeper than one level below. For example,

    compoundmod->getModuleByRelativePath("child[5].grandchild");
    

    would give the same results as

    compoundmod->getSubmodule("child",5)->getSubmodule("grandchild");
    

    (Provided that child[5] does exist, because otherwise the second version would crash with an access violation because of the NULL pointer dereference.)

    The cSimulation::getModuleByPath() function is similar to cModule's moduleByRelativePath() function, and it starts the search at the top-level module.

    Iterating over submodules

    To access all modules within a compound module, use cSubModIterator.

    For example:

    for (cSubModIterator iter(*getParentModule()); !iter.end(); iter++)
    {
      ev << iter()->getFullName();
    }
    

    (iter() is pointer to the current module the iterator is at.)

    The above method can also be used to iterate along a module vector, since the getName() function returns the same for all modules:

    for (cSubModIterator iter(*getParentModule()); !iter.end(); iter++)
    {
      if (iter()->isName(getName())) // if iter() is in the same
                                  // vector as this module
      {
        int itsIndex = iter()->getIndex();
        // do something to it
      }
    }
    

    Walking along links

    To determine the module at the other end of a connection, use cGate's getPreviousGate(), getNextGate() and getOwnerModule() functions. For example:

    cModule *neighbour = gate("out")->getNextGate()->getOwnerModule();
    

    For input gates, you would use getPreviousGate() instead of getNextGate().

    4.10 Direct method calls between modules

    In some simulation models, there might be modules which are too tightly coupled for message-based communication to be efficient. In such cases, the solution might be calling one simple module's public C++ methods from another module.

    Simple modules are C++ classes, so normal C++ method calls will work. Two issues need to be mentioned, however:

    Typically, the called module is in the same compound module as the caller, so the getParentModule() and getSubmodule() methods of cModule can be used to get a cModule* pointer to the called module. (Further ways to obtain the pointer are described in the section [4.9].) The cModule* pointer then has to be cast to the actual C++ class of the module, so that its methods become visible.

    This makes the following code:

    cModule *calleeModule = getParentModule()->getSubmodule("callee");
    Callee *callee = check_and_cast<Callee *>(calleeModule);
    callee->doSomething();
    

    The check_and_cast<>() template function on the second line is part of OMNeT++. It does a standard C++ dynamic_cast, and checks the result: if it is NULL, check_and_cast raises an OMNeT++ error. Using check_and_cast saves you from writing error checking code: if calleeModule from the first line is NULL because the submodule named "callee" was not found, or if that module is actually not of type Callee, an error gets thrown from check_and_cast.

    The second issue is how to let the simulation kernel know that a method call across modules is taking place. Why is this necessary in the first place? First, the simulation kernel always has to know which module's code is currently executing, in order to several internal mechanisms to work correctly. (One such mechanism is ownership handling.) Second, the Tkenv simulation GUI can animate method calls, but to be able to do that, it has to know about them.

    The solution is to add the Enter_Method() or Enter_Method_Silent() macro at the top of the methods that may be invoked from other modules. These calls perform context switching, and, in case of Enter_Method(), notify the simulation GUI so that animation of the method call can take place. Enter_Method_Silent() does not animate the call. Enter_Method() expects a printf()-like argument list -- the resulting string will be displayed during animation.

    void Callee::doSomething()
    {
        Enter_Method("doSomething()");
        ...
    }
    

    4.11 Dynamic module creation

    4.11.1 When do you need dynamic module creation

    In some situations you need to dynamically create and maybe destroy modules. For example, when simulating a mobile network, you may create a new module whenever a new user enters the simulated area, and dispose of them when they leave the area.

    As another example, when implementing a server or a transport protocol, it might be convenient to dynamically create modules to serve new connections, and dispose of them when the connection is closed. (You would write a manager module that receives connection requests and creates a module for each connection. The Dyna example simulation does something like this.)

    Both simple and compound modules can be created dynamically. If you create a compound module, all its submodules will be created recursively.

    It is often convenient to use direct message sending with dynamically created modules.

    Once created and started, dynamic modules aren't any different from ``static'' modules; for example, one could also delete static modules during simulation (though it is rarely useful.)

    4.11.2 Overview

    To understand how dynamic module creation works, you have to know a bit about how normally OMNeT++ instantiates modules. Each module type (class) has a corresponding factory object of the class cModuleType. This object is created under the hood by the Define_Module() macro, and it has a factory function which can instantiate the module class (this function basically only consists of a return new module-class(...) statement).

    The cModuleType object can be looked up by its name string (which is the same as the module class name). Once you have its pointer, it is possible to call its factory method and create an instance of the corresponding module class -- without having to include the C++ header file containing module's class declaration into your source file.

    The cModuleType object also knows what gates and parameters the given module type has to have. (This info comes from compiled NED code.)

    Simple modules can be created in one step. For a compound module, the situation is more complicated, because its internal structure (submodules, connections) may depend on parameter values and gate vector sizes. Thus, for compound modules it is generally required to first create the module itself, second, set parameter values and gate vector sizes, and then call the method that creates its submodules and internal connections.

    As you know already, simple modules with activity() need a starter message. For statically created modules, this message is created automatically by OMNeT++, but for dynamically created modules, you have to do this explicitly by calling the appropriate functions.

    Calling initialize() has to take place after insertion of the starter messages, because the initializing code may insert new messages into the FES, and these messages should be processed after the starter message.

    4.11.3 Creating modules

    The first step, finding the factory object:

    cModuleType *moduleType = cModuleType::get("WirelessNode");
    

    Simplified form

    cModuleType has a createScheduleInit(const char *name, cModule *parentmod) convenience function to get a module up and running in one step.

    mod = modtype->createScheduleInit("node",this);
    

    It does create() + buildInside() + scheduleStart(now) + callInitialize().

    This method can be used for both simple and compound modules. Its applicability is somewhat limited, however: because it does everything in one step, you do not have the chance to set parameters or gate sizes, and to connect gates before initialize() is called. (initialize() expects all parameters and gates to be in place and the network fully built when it is called.) Because of the above limitation, this function is mainly useful for creating basic simple modules.

    Expanded form

    If the previous simple form cannot be used. There are 5 steps:

    1. find factory object
    2. create module
    3. set up parameters and gate sizes (if needed)
    4. call function that builds out submodules and finalizes the module
    5. call function that creates activation message(s) for the new simple getModule(s)
    Each step (except for Step 3.) can be done with one line of code.

    See the following example, where Step 3 is omitted:

    // find factory object
    cModuleType *moduleType = cModuleType::get("WirelessNode");
    
    // create (possibly compound) module and build its submodules (if any)
    cModule *module = moduleType->create("node", this);
    module->buildInside();
    
    // create activation message
    module->scheduleStart( simTime() );
    

    If you want to set up parameter values or gate vector sizes (Step 3.), the code goes between the create() and buildInside() calls:

    // create
    cModuleType *moduleType = cModuleType::get("WirelessNode");
    cModule *module = moduleType->create("node", this);
    
    // set up parameters and gate sizes before we set up its submodules
    module->par("address") = ++lastAddress;
    module->setGateSize("in", 3);
    module->setGateSize("out", 3);
    
    // create internals, and schedule it
    module->buildInside();
    module->scheduleStart(simTime());
    

    4.11.4 Deleting modules

    To delete a module dynamically:

    module->deleteModule();
    

    If the module was a compound module, this involves recursively destroying all its submodules. A simple module can also delete itself; in this case, the deleteModule() call does not return to the caller.

    Currently, you cannot safely delete a compound module from a simple module in it; you must delegate the job to a module outside the compound module.

    4.11.5 Module deletion and finish()

    When you delete a module during simulation, its finish() function is not called automatically (deleteModule() doesn't do it.) How the module was created doesn't play any role here: finish() gets called for all modules -- at the end of the simulation. If a module doesn't live that long, finish() is not invoked, but you can still manually invoke it.

    You can use the callFinish() function to arrange finish() to be called. It is usually not a good idea to invoke finish() directly. If you're deleting a compound module, callFinish() will recursively invoke finish() for all submodules, and if you're deleting a simple module from another module, callFinish() will do the context switch for the duration of the call.

    Example:

    mod->callFinish();
    mod->deleteModule();
    

    4.11.6 Creating connections

    Connections can be created using cGate's connectTo() method.

    connectTo() should be invoked on the source gate of the connection, and expects the destination gate pointer as an argument:
    srcGate->connectTo(destGate);
    

    The source and destination words correspond to the direction of the arrow in NED files.

    As an example, we create two modules and connect them in both directions:

    cModuleType *moduleType = cModuleType::get("TicToc");
    cModule *a = modtype->createScheduleInit("a",this);
    cModule *b = modtype->createScheduleInit("b",this);
    
    a->gate("out")->connectTo(b->gate("in"));
    b->gate("out")->connectTo(a->gate("in"));
    

    connectTo() also accepts a channel object as an additional, optional argument. Channels are subclassed from cChannel. Almost always you'll want use an instance of cDatarateChannel as channel -- this is the one that supports delay, bit error rate and data rate. The channel object will be owned by the source gate of the connection, and you cannot reuse the same channel object with several connections.

    cDatarateChannel has setDelay(), setBitErrorRate() and setDatarate() methods to set up the channel attributes.

    An example that sets up a channel with a delay:

    cDatarateChannel *channel = new cDatarateChannel("channel");
    channel->setDelay(0.001);
    
    a->gate("out")->connectTo(b->gate("in"), channel); // a,b are modules
    

    4.11.7 Removing connections

    The disconnect() method of cGate can be used to remove connections. This method has to be invoked on the source side of the connection. It also destroys the channel object associated with the connection, if one has been set.

    srcGate->disconnect();
    



    5 Messages

    5.1 Messages and packets

    5.1.1 The cMessage class

    cMessage is a central class in OMNeT++. Objects of cMessage and subclasses may model a number of things: events; messages; packets, frames, cells, bits or signals travelling in a network; entities travelling in a system and so on.

    Attributes

    A cMessage object has number of attributes. Some are used by the simulation kernel, others are provided just for the convenience of the simulation programmer. A more-or-less complete list:

    Basic usage

    The cMessage constructor accepts several arguments. Most commonly, you would create a message using an object name (a const char * string) and a message kind (int):

    cMessage *msg = new cMessage("MessageName", msgKind);
    

    Both arguments are optional and initialize to the null string ("") and 0, so the following statements are also valid:

    cMessage *msg = new cMessage();
    cMessage *msg = new cMessage("MessageName");
    

    It is a good idea to always use message names -- they can be extremely useful when debugging or demonstrating your simulation.

    Message kind is usually initialized with a symbolic constant (e.g. an enum value) which signals what the message object represents in the simulation (i.e. a data packet, a jam signal, a job, etc.) Please use positive values or zero only as message kind -- negative values are reserved for use by the simulation kernel.

    The cMessage constructor accepts further arguments too (length, priority, bit error flag), but for readability of the code it is best to set them explicitly via the set...() methods described below. Length and priority are integers, and the bit error flag is boolean.

    Once a message has been created, its data members can be changed by the following functions:

    msg->setKind( kind );
    msg->setBitLength( length );
    msg->setByteLength( lengthInBytes );
    msg->setPriority( priority );
    msg->setBitError( err );
    msg->setTimestamp();
    msg->setTimestamp( simtime );
    

    With these functions the user can set the message kind, the message length, the priority, the error flag and the time stamp. The setTimeStamp() function without any argument sets the time stamp to the current simulation time. setByteLength() sets the same length field as setBitLength(), only the parameters gets internally multiplied by 8.

    The values can be obtained by the following functions:

    int k       = msg->getKind();
    int p       = msg->getPriority();
    int l       = msg->getBitLength();
    int lb      = msg->getByteLength();
    bool b      = msg->hasBitError();
    simtime_t t = msg->getTimestamp();
    

    getByteLength() also reads the length field as length(), but the result gets divided by 8 and rounded up.

    Duplicating messages

    It is often necessary to duplicate a message (for example, sending one and keeping a copy). This can be done in the same way as for any other OMNeT++ object:

    cMessage *copy = msg->dup();
    

    or

    cMessage *copy = new cMessage( *msg );
    

    The two are equivalent. The resulting message is an exact copy of the original, including message parameters (cMsgPar or other object types) and encapsulated messages.

    5.1.2 Self-messages

    Using a message as self-message

    Messages are often used to represent events internal to a module, such as a periodically firing timer on expiry of a timeout. A message is termed self-message when it is used in such a scenario -- otherwise self-messages are normal messages, of class cMessage or a class derived from it.

    When a message is delivered to a module by the simulation kernel, you can call the isSelfMessage() method to determine if it is a self-message; it other words, if it was scheduled with scheduleAt() or was sent with one of the send...() methods. The isScheduled() method returns true if the message is currently scheduled. A scheduled message can also be cancelled (cancelEvent()).

    bool isSelfMessage();
    bool isScheduled();
    

    The following methods return the time of creating and scheduling the message as well as its arrival time. While the message is scheduled, arrival time is the time it will be delivered to the module.

    simtime_t getCreationTime();
    simtime_t getSendingTime();
    simtime_t getArrivalTime();
    

    Context pointer

    cMessage contains a void* pointer which is set/returned by the setContextPointer() and getContextPointer() functions:

    void *context =...;
    msg->setContextPointer(context);
    void *context2 = msg->getContextPointer();
    

    It can be used for any purpose by the simulation programmer. It is not used by the simulation kernel, and it is treated as a mere pointer (no memory management is done on it).

    Intended purpose: a module which schedules several self-messages (timers) will need to identify a self-message when it arrives back to the module, ie. the module will have to determine which timer went off and what to do then. The context pointer can be made to point at a data structure kept by the module which can carry enough ``context'' information about the event.

    5.1.3 Modelling packets

    Arrival gate and time

    The following methods can tell where the message came from and where it arrived (or will arrive if it is currently scheduled or under way.)

    int getSenderModuleId();
    int getSenderGateId();
    int getArrivalModuleId();
    int getArrivalGateId();
    

    The following methods are just convenience functions which build on the ones above.

    cModule *getSenderModule();
    cGate *getSenderGate();
    cGate *getArrivalGate();
    

    And there are further convenience functions to tell whether the message arrived on a specific gate given with id or name+index.

    bool arrivedOn(int id);
    bool arrivedOn(const char *gname, int gindex=0);
    

    The following methods return message creation time and the last sending and arrival times.

    simtime_t getCreationTime();
    simtime_t getSendingTime();
    simtime_t getArrivalTime();
    

    Control info

    One of the main application areas of OMNeT++ is the simulation of telecommunication networks. Here, protocol layers are usually implemented as modules which exchange packets. Packets themselves are represented by messages subclassed from cMessage.

    However, communication between protocol layers requires sending additional information to be attached to packets. For example, a TCP implementation sending down a TCP packet to IP will want to specify the destination IP address and possibly other parameters. When IP passes up a packet to TCP after decapsulation from the IP header, it'll want to let TCP know at least the source IP address.

    This additional information is represented by control info objects in OMNeT++. Control info objects have to be subclassed from cObject (a small footprint base class with no data members), and attached to the messages representing packets. cMessage has the following methods for this purpose:

    void setControlInfo(cObject *controlInfo);
    cObject *getControlInfo();
    cObject *removeControlInfo();
    

    When a "command" is associated with the message sending (such as TCP OPEN, SEND, CLOSE, etc), the message kind field (getKind(), setKind() methods of cMessage) should carry the command code. When the command doesn't involve a data packet (e.g. TCP CLOSE command), a dummy packet (empty cMessage) can be sent.

    Identifying the protocol

    In OMNeT++ protocol models, the protocol type is usually represented in the message subclass. For example, instances of class IPv6Datagram represent IPv6 datagrams and EthernetFrame represents Ethernet frames) and/or in the message kind value. The PDU type is usually represented as a field inside the message class.

    The C++ dynamic_cast operator can be used to determine if a message object is of a specific protocol.

    cMessage *msg = receive();
    if (dynamic_cast<IPv6Datagram *>(msg) != NULL)
    {
        IPv6Datagram *datagram = (IPv6Datagram *)msg;
        ...
    }
    

    5.1.4 Encapsulation

    Encapsulating packets

    It is often necessary to encapsulate a message into another when you're modeling layered protocols of computer networks. Although you can encapsulate messages by adding them to the parameter list, there's a better way.

    The encapsulate() function encapsulates a message into another one. The length of the message will grow by the length of the encapsulated message. An exception: when the encapsulating (outer) message has zero length, OMNeT++ assumes it is not a real packet but some out-of-band signal, so its length is left at zero.

    cMessage *userdata = new cMessage("userdata");
    
    userdata->setByteLength(2048);  // 2K
    cMessage *tcpseg = new cMessage("tcp");
    tcpseg->setByteLength(24);
    tcpseg->encapsulate(userdata);
    ev << tcpseg->getByteLength() << endl; // --> 2048+24 = 2072
    

    A message can only hold one encapsulated message at a time. The second encapsulate() call will result in an error. It is also an error if the message to be encapsulated isn't owned by the module.

    You can get back the encapsulated message by decapsulate():

    cMessage *userdata = tcpseg->decapsulate();
    

    decapsulate() will decrease the length of the message accordingly, except if it was zero. If the length would become negative, an error occurs.

    The getEncapsulatedMsg() function returns a pointer to the encapsulated message, or NULL if no message was encapsulated.

    Reference counting

    Since the 3.2 release, OMNeT++ implements reference counting of encapsulated messages, meaning that if you dup() a message that contains an encapsulated message, then the encapsulated message will not be duplicated, only a reference count incremented. Duplication of the encapsulated message is deferred until decapsulate() actually gets called. If the outer message gets deleted without its decapsulate() method ever being called, then the reference count of the encapsulated message simply gets decremented. The encapsulated message is deleted when its reference count reaches zero.

    Reference counting can significantly improve performance, especially in LAN and wireless scenarios. For example, in the simulation of a broadcast LAN or WLAN, the IP, TCP and higher layer packets won't get duplicated (and then discarded without being used) if the MAC address doesn't match in the first place.

    The reference counting mechanism works transparently. However, there is one implication: one must not change anything in a message that is encapsulated into another! That is, getEncapsulatedMsg() should be viewed as if it returned a pointer to a read-only object (it returns a const pointer indeed), for quite obvious reasons: the encapsulated message may be shared between several messages, and any change would affect those other messages as well.

    Encapsulating several messages

    The cMessage class doesn't directly support adding more than one messages to a message object, but you can subclass cMessage and add the necessary functionality. (It is recommended that you use the message definition syntax [5.2] and customized messages [5.2.6] to be described later on in this chapter -- it can spare you some work.)

    You can store the messages in a fixed-size or a dynamically allocated array, or you can use STL classes like std::vector or std::list. There is one additional ``trick'' that you might not expect: your message class has to take ownership of the inserted messages, and release them when they are removed from the message. These are done via the take() and drop() methods. Let us see an example which assumes you have added to the class an std::list member called messages that stores message pointers:

    void MessageBundleMessage::insertMessage(cMessage *msg)
    {
        take(msg);  // take ownership
        messages.push_back(msg);  // store pointer
    }
    
    void MessageBundleMessage::removeMessage(cMessage *msg)
    {
        messages.remove(msg);  // remove pointer
        drop(msg);  // release ownership
    }
    

    You will also have to provide an operator=() method to make sure your message objects can be copied and duplicated properly -- this is something often needed in simulations (think of broadcasts and retransmissions!). Section [6.10] contains more info about the things you need to take care of when deriving new classes.

    5.1.5 Attaching parameters and objects

    If you want to add parameters or objects to a message, the preferred way to do that is via message definitions, described in chapter [5.2].

    Attaching objects

    The cMessage class has an internal cArray object which can carry objects. Only objects that are derived from cOwnedObject (most OMNeT++ classes are so) can be attached. The addObject(), getObject(), hasObject(), removeObject() methods use the object name as the key to the array. An example:

    cLongHistogram *pklenDistr = new cLongHistogram("pklenDistr");
    msg->addObject(pklenDistr);
    ...
    if (msg->hasObject("pklenDistr"))
    {
       cLongHistogram *pklenDistr =
           (cLongHistogram *) msg->getObject("pklenDistr");
       ...
    }
    

    You should take care that names of the attached objects do not clash with each other or with cMsgPar parameter names (see next section). If you do not attach anything to the message and do not call the getParList() function, the internal cArray object will not be created. This saves both storage and execution time.

    You can attach non-object types (or non-cOwnedObject objects) to the message by using cMsgPar's void* pointer 'P') type (see later in the description of cMsgPar). An example:

    struct conn_t *conn = new conn_t; // conn_t is a C struct
    msg->addPar("conn") = (void *) conn;
    msg->par("conn").configPointer(NULL,NULL,sizeof(struct conn_t));
    

    Attaching parameters

    The preferred way of extending messages with new data fields is to use message definitions (see section [5.2]).

    The old, deprecated way of adding new fields to messages is via attaching cMsgPar objects. There are several downsides of this approach, the worst being large memory and execution time overhead. cMsgPar's are heavy-weight and fairly complex objects themselves. It has been reported that using cMsgPar message parameters might account for a large part of execution time, sometimes as much as 80%. Using cMsgPars is also error-prone because cMsgPar objects have to be added dynamically and individually to each message object. In contrast, subclassing benefits from static type checking: if you mistype the name of a field in the C++ code, already the compiler can detect the mistake.

    If you still need cMsgPars for some reason, here's a short summary. At the sender side you can add a new named parameter to the message with the addPar() member function, then set its value with one of the methods setBoolValue(), setLongValue(), setStringValue(), setDoubleValue(), setPointerValue(), setObjectValue(), and setXMLValue(). There are also overloaded assignment operators for the corresponding C/C++ types.

    At the receiver side, you can look up the parameter object on the message by name and obtain a reference to it with the par() member function. hasPar() can be used to check first whether the message object has a parameter object with the given name. Then the value can be read with the methods boolValue(), longValue(), stringValue(), doubleValue(), pointerValue(), objectValue(), xmlValue(), or by using the provided overloaded type cast operators.

    Example usage:

    msg->addPar("destAddr");
    msg->par("destAddr").setLongValue(168);
    ...
    long destAddr = msg->par("destAddr").longValue();
    

    Or, using overloaded operators:

    msg->addPar("destAddr");
    msg->par("destAddr") = 168;
    ...
    long destAddr = msg->par("destAddr");
    

    5.2 Message definitions

    5.2.1 Introduction

    In practice, you'll need to add various fields to cMessage to make it useful. For example, if you're modelling packets in communication networks, you need to have a way to store protocol header fields in message objects. Since the simulation library is written in C++, the natural way of extending cMessage is via subclassing it. However, because for each field you need to write at least three things (a private data member, a getter and a setter method), and the resulting class has to integrate with the simulation framework, writing the necessary C++ code can be a tedious and time-consuming task.

    OMNeT++ offers a more convenient way called message definitions. Message definitions provide a very compact syntax to describe message contents. C++ code is automatically generated from message definitions, saving you a lot of typing.

    A common source of complaint about code generators in general is lost flexibility: if you have a different idea how the generated code should look like, there's little you can do about it. In OMNeT++, however, there's nothing to worry about: you can customize the generated class to any extent you like. Even if you decide to heavily customize the generated class, message definitions still save you a great deal of manual work.

    The subclassing approach for adding message parameters was originally suggested by Nimrod Mesika.

    The first message class

    Let us begin with a simple example. Suppose that you need message objects to carry source and destination addresses as well as a hop count. You could write a mypacket.msg file with the following contents:

    message MyPacket
    {
         int srcAddress;
         int destAddress;
         int hops = 32;
    };
    

    The task of the message subclassing compiler is to generate C++ classes you can use from your models as well as ``reflection'' classes that allow Tkenv to inspect these data structures.

    If you process mypacket.msg with the message subclassing compiler, it will create the following files for you: mypacket_m.h and mypacket_m.cc. mypacket_m.h contains the declaration of the MyPacket C++ class, and it should be included into your C++ sources where you need to handle MyPacket objects.

    The generated mypacket_m.h will contain the following class declaration:

    class MyPacket : public cMessage {
        ...
        virtual int getSrcAddress() const;
        virtual void setSrcAddress(int srcAddress);
        ...
    };
    

    So in your C++ file, you could use the MyPacket class like this:

    #include "mypacket_m.h"
    
    ...
    MyPacket *pkt = new MyPacket("pkt");
    pkt->setSrcAddress( localAddr );
    ...
    

    The mypacket_m.cc file contains implementation of the generated MyPacket class, as well as ``reflection'' code that allows you to inspect these data structures in the Tkenv GUI. The mypacket_m.cc file should be compiled and linked into your simulation. (If you use the opp_makemake tool to generate your makefiles, the latter will be automatically taken care of.)

    What is message subclassing not?

    There might be some confusion around the purpose and concept of message definitions, so it seems to be a good idea to deal with them right here.

    It is not:

    The goal is to define the interface (getter/setter methods) of messages rather than their implementations in C++. A simple and straightforward implementation of fields is provided -- if you'd like a different internal representation for some field, you can have it by customizing the class.

    There are questions you might ask:

    The following sections describe the message syntax and features in detail.

    5.2.2 Declaring enums

    An enum (declared with the enum keyword) generates a normal C++ enum, plus creates an object which stores text representations of the constants. The latter makes it possible to display symbolic names in Tkenv. An example:

    enum ProtocolTypes
    {
       IP = 1;
       TCP = 2;
    };
    

    Enum values need to be unique.

    5.2.3 Message declarations

    Basic use

    You can describe messages with the following syntax:

    message FooPacket
    {
        int sourceAddress;
        int destAddress;
        bool hasPayload;
    };
    

    Processing this description with the message compiler will produce a C++ header file with a generated class, FooPacket. FooPacket will be a subclass of cMessage.

    For each field in the above description, the generated class will have a protected data member, a getter and a setter method. The names of the methods will begin with get and set, followed by the field name with its first letter converted to uppercase. Thus, FooPacket will contain the following methods:

    virtual int getSourceAddress() const;
    virtual void setSourceAddress(int sourceAddress);
    
    virtual int getDestAddress() const;
    virtual void setDestAddress(int destAddress);
    
    virtual bool getHasPayload() const;
    virtual void setHasPayload(bool hasPayload);
    

    Note that the methods are all declared virtual to give you the possibility of overriding them in subclasses.

    Two constructors will be generated: one that optionally accepts object name and (for cMessage subclasses) message kind, and a copy constructor:

    FooPacket(const char *name=NULL, int kind=0);
    FooPacket(const FooPacket& other);
    

    Appropriate assignment operator (operator=()) and dup() methods will also be generated.

    Data types for fields are not limited to int and bool. You can use the following primitive types (i.e. primitive types as defined in the C++ language):

    Field values are initialized to zero.

    Initial values

    You can initialize field values with the following syntax:

    message FooPacket
    {
        int sourceAddress = 0;
        int destAddress = 0;
        bool hasPayload = false;
    };
    

    Initialization code will be placed in the constructor of the generated class.

    Enum declarations

    You can declare that an int (or other integral type) field takes values from an enum. The message compiler can than generate code that allows Tkenv display the symbolic value of the field.

    Example:

    message FooPacket
    {
        int payloadType enum(PayloadTypes);
    };
    

    The enum has to be declared separately in the message file.

    Fixed-size arrays

    You can specify fixed size arrays:

    message FooPacket
    {
        long route[4];
    };
    

    The generated getter and setter methods will have an extra k argument, the array index:

    virtual long getRoute(unsigned k) const;
    virtual void setRoute(unsigned k, long route);
    

    If you call the methods with an index that is out of bounds, an exception will be thrown.

    Dynamic arrays

    If the array size is not known in advance, you can declare the field to be a dynamic array:

    message FooPacket
    {
        long route[];
    };
    

    In this case, the generated class will have two extra methods in addition to the getter and setter methods: one for setting the array size, and another one for returning the current array size.

    virtual long getRoute(unsigned k) const;
    virtual void setRoute(unsigned k, long route);
    virtual unsigned getRouteArraySize() const;
    virtual void setRouteArraySize(unsigned n);
    

    The set...ArraySize() method internally allocates a new array. Existing values in the array will be preserved (copied over to the new array.)

    The default array size is zero. This means that you need to call the set...ArraySize() before you can start filling array elements.

    String members

    You can declare string-valued fields with the following syntax:

    message FooPacket
    {
        string hostName;
    };
    

    The generated getter and setter methods will return and accept const char* pointers:

    virtual const char *getHostName() const;
    virtual void setHostName(const char *hostName);
    

    The generated object will have its own copy of the string.

    Note that a string member is different from a character array, which is treated as an array of any other type. For example,

    message FooPacket
    {
        char chars[10];
    };
    

    will generate the following methods:

    virtual char getChars(unsigned k);
    virtual void setChars(unsigned k, char a);
    

    5.2.4 Inheritance, composition

    So far we have discussed how to add fields of primitive types (int, double, char, ...) to cMessage. This might be sufficient for simple models, but if you have more complex models, you'll probably need to:

    The following section describes how to do this.

    Inheritance among message classes

    By default, messages are subclassed from cMessage. However, you can explicitly specify the base class using the extends keyword:

    message FooPacket extends FooBase
    {
        ...
    };
    

    For the example above, the generated C++ code will look like:

    class FooPacket : public FooBase { ... };
    

    Inheritance also works for structs and classes (see next sections for details).

    Defining classes

    Until now we have used the message keyword to define classes, which implies that the base class is cMessage, either directly or indirectly.

    But as part of complex messages, you'll need structs and other classes (rooted or not rooted in cOwnedObject) as building blocks. Classes can be created with the class class keyword; structs we'll cover in the next section.

    The syntax for defining classes is almost the same as defining messages, only the class keyword is used instead of message.

    Slightly different code is generated for classes that are rooted in cOwnedObject than for those which are not. If there is no extends, the generated class will not be derived from cOwnedObject, thus it will not have getName(), getClassName(), etc. methods. To create a class with those methods, you have to explicitly write extends cOwnedObject.

    class MyClass extends cOwnedObject
    {
        ...
    };
    

    Defining plain C structs

    You can define C-style structs to be used as fields in message classes, ``C-style'' meaning ``containing only data and no methods''. (Actually, in the C++ a struct can have methods, and in general it can do anything a class can.)

    The syntax is similar to that of defining messages:

    struct MyStruct
    {
        char array[10];
        short version;
    };
    

    However, the generated code is different. The generated struct has no getter or setter methods, instead the fields are represented by public data members. For the definition above, the following code is generated:

    // generated C++
    struct MyStruct
    {
        char array[10];
        short version;
    };
    

    A struct can have primitive types or other structs as fields. It cannot have string or class as field.

    Inheritance is supported for structs:

    struct Base
    {
        ...
    };
    
    struct MyStruct extends Base
    {
        ...
    };
    

    But because a struct has no member functions, there are limitations:

    Using structs and classes as fields

    In addition to primitive types, you can also use other structs or objects as a field. For example, if you have a struct named IPAddress, you can write the following:

    message FooPacket
    {
        IPAddress src;
    };
    

    The IPAddress structure must be known in advance to the message compiler; that is, it must either be a struct or class defined earlier in the message description file, or it must be a C++ type with its header file included via cplusplus {{...}} and its type announced (see Announcing C++ types).

    The generated class will contain an IPAddress data member (that is, not a pointer to an IPAddress). The following getter and setter methods will be generated:

    virtual const IPAddress& getSrc() const;
    virtual void setSrc(const IPAddress& src);
    

    Pointers

    Not supported yet.

    5.2.5 Using existing C++ types

    Announcing C++ types

    If you want to use one of your own types (a class, struct or typedef, declared in a C++ header) in a message definition, you have to announce those types to the message compiler. You also have to make sure that your header file gets included into the generated _m.h file so that the C++ compiler can compile it.

    Suppose you have an IPAddress structure, defined in an ipaddress.h file:

    // ipaddress.h
    struct IPAddress {
        int byte0, byte1, byte2, byte3;
    };
    

    To be able to use IPAddress in a message definition, the message file (say foopacket.msg) should contain the following lines:

    cplusplus {{
    #include "ipaddress.h"
    }};
    
    struct IPAddress;
    

    The effect of the first three lines is simply that the #include statement will be copied into the generated foopacket_m.h file to let the C++ compiler know about the IPAddress class. The message compiler itself will not try to make sense of the text in the body of the cplusplus {{ ... }} directive.

    The next line, struct IPAddress, tells the message compiler that IPAddress is a C++ struct. This information will (among others) affect the generated code.

    Classes can be announced using the class keyword:

    class cSubQueue;
    

    The above syntax assumes that the class is derived from cOwnedObject either directly or indirectly. If it is not, the noncobject keyword should be used:

    class noncobject IPAddress;
    

    The distinction between classes derived and not derived from cOwnedObject is important because the generated code differs at places. The generated code is set up so that if you incidentally forget the noncobject keyword (and thereby mislead the message compiler into thinking that your class is rooted in cOwnedObject when in fact it is not), you'll get a C++ compiler error in the generated header file.

    5.2.6 Customizing the generated class

    The Generation Gap pattern

    Sometimes you need the generated code to do something more or do something differently than the version generated by the message compiler. For example, when setting a integer field named payloadLength, you might also need to adjust the packet length. That is, the following default (generated) version of the setPayloadLength() method is not suitable:

    void FooPacket::setPayloadLength(int payloadLength)
    {
        this->payloadLength = payloadLength;
    }
    

    Instead, it should look something like this:

    void FooPacket::setPayloadLength(int payloadLength)
    {
        int diff = payloadLength - this->payloadLength;
        this->payloadLength = payloadLength;
        setBitLength(length() + diff);
    }
    

    According to common belief, the largest drawback of generated code is that it is difficult or impossible to fulfill such wishes. Hand-editing of the generated files is worthless, because they will be overwritten and changes will be lost in the code generation cycle.

    However, object oriented programming offers a solution. A generated class can simply be customized by subclassing from it and redefining whichever methods need to be different from their generated versions. This practice is known as the Generation Gap design pattern. It is enabled with the @customize property set on the message:

    message FooPacket
    {
       @customize(true);
       int payloadLength;
    };
    

    If you process the above code with the message compiler, the generated code will contain a FooPacket_Base class instead of FooPacket. Then you would subclass FooPacket_Base to produce FooPacket, while doing your customizations by redefining the necessary methods.

    class FooPacket_Base : public cMessage
    {
      protected:
        int src;
        // make constructors protected to avoid instantiation
        FooPacket_Base(const char *name=NULL);
        FooPacket_Base(const FooPacket_Base& other);
      public:
        ...
        virtual int getSrc() const;
        virtual void setSrc(int src);
    };
    

    There is a minimum amount of code you have to write for FooPacket, because not everything can be pre-generated as part of FooPacket_Base, e.g. constructors cannot be inherited. This minimum code is the following (you'll find it the generated C++ header too, as a comment):

    class FooPacket : public FooPacket_Base
    {
      public:
        FooPacket(const char *name=NULL) : FooPacket_Base(name) {}
        FooPacket(const FooPacket& other) : FooPacket_Base(other) {}
        FooPacket& operator=(const FooPacket& other)
            {FooPacket_Base::operator=(other); return *this;}
        virtual FooPacket *dup() {return new FooPacket(*this);}
    };
    
    Register_Class(FooPacket);
    

    Note that it is important that you redefine dup() and provide an assignment operator (operator=()).

    So, returning to our original example about payload length affecting packet length, the code you'd write is the following:

    class FooPacket : public FooPacket_Base
    {
        // here come the mandatory methods: constructor,
        // copy constructor, operator=(), dup()
        // ...
    
        virtual void setPayloadLength(int newlength);
    }
    
    void FooPacket::setPayloadLength(int newlength)
    {
        // adjust message length
        setBitLength(length()-getPayloadLength()+newlength);
    
        // set the new length
        FooPacket_Base::setPayloadLength(newlength);
    }
    

    Abstract fields

    The purpose of abstract fields is to let you to override the way the value is stored inside the class, and still benefit from inspectability in Tkenv.

    For example, this is the situation when you want to store a bitfield in a single int or short, and still you want to present bits as individual packet fields. It is also useful for implementing computed fields.

    You can declare any field to be abstract with the following syntax:

    message FooPacket
    {
       @customize(true);
       abstract bool urgentBit;
    };
    

    For an abstract field, the message compiler generates no data member, and generated getter/setter methods will be pure virtual:

    virtual bool getUrgentBit() const = 0;
    virtual void setUrgentBit(bool urgentBit) = 0;
    

    Usually you'll want to use abstract fields together with the Generation Gap pattern, so that you can immediately redefine the abstract (pure virtual) methods and supply your implementation.

    5.2.7 Using STL in message classes

    You may want to use STL vector or stack classes in your message classes. This is possible using abstract fields. After all, vector and stack are representations of a sequence -- same abstraction as dynamic-size vectors. That is, you can declare the field as abstract T fld[], and provide an underlying implementation using vector<T>. You can also add methods to the message class that invoke push_back(), push(), pop(), etc. on the underlying STL object.

    See the following message declaration:

    struct Item
    {
        int a;
        double b;
    }
    
    message STLMessage
    {
       @customize(true);
       abstract Item foo[]; // will use vector<Item>
       abstract Item bar[]; // will use stack<Item>
    }
    

    If you compile the above, in the generated code you'll only find a couple of abstract methods for foo and bar, no data members or anything concrete. You can implement everything as you like. You can write the following C++ file then to implement foo and bar with std::vector and std::stack:

    #include <vector>
    #include <stack>
    #include "stlmessage_m.h"
    
    
    class STLMessage : public STLMessage_Base
    {
      protected:
        std::vector<Item> foo;
        std::stack<Item> bar;
    
      public:
        STLMessage(const char *name=NULL, int kind=0) : STLMessage_Base(name,kind) {}
        STLMessage(const STLMessage& other) : STLMessage_Base(other.getName()) {operator=(other);}
        STLMessage& operator=(const STLMessage& other) {
            if (&other==this) return *this;
            STLMessage_Base::operator=(other);
            foo = other.foo;
            bar = other.bar;
            return *this;
        }
        virtual STLMessage *dup() {return new STLMessage(*this);}
    
        // foo methods
        virtual void setFooArraySize(unsigned int size) {}
        virtual unsigned int getFooArraySize() const {return foo.size();}
        virtual Item& getFoo(unsigned int k) {return foo[k];}
        virtual void setFoo(unsigned int k, const Item& afoo) {foo[k]=afoo;}
        virtual void addToFoo(const Item& afoo) {foo.push_back(afoo);}
    
        // bar methods
        virtual void setBarArraySize(unsigned int size) {}
        virtual unsigned int getBarArraySize() const {return bar.size();}
        virtual Item& getBar(unsigned int k) {throw cRuntimeException("sorry");}
        virtual void setBar(unsigned int k, const Item& bar) {throw cRuntimeException("sorry");}
        virtual void barPush(const Item& abar) {bar.push(abar);}
        virtual void barPop() {bar.pop();}
        virtual Item& barTop() {return bar.top();}
    };
    
    Register_Class(STLMessage);
    

    Some additional notes:

    1. setFooArraySize(), setBarArraySize() are redundant.
    2. getBar(int k) cannot be implemented in a straightforward way (std::stack does not support accessing elements by index). It could still be implemented in a less efficient way using STL iterators, and efficiency does not seem to be major problem because only Tkenv is going to invoke this function.
    3. setBar(int k, const Item&) could not be implemented, but this is not particularly a problem. The exception will materialize in a Tkenv error dialog when you try to change the field value.

    You may regret that the STL vector/stack are not directly exposed. Well you could expose them (by adding a vector<Item>& getFoo() {return foo;} method to the class) but this is probably not a good idea. STL itself was purposefully designed with a low-level approach, to provide ``nuts and bolts'' for C++ programming, and STL is better used in other classes for internal representation of data.

    5.2.8 Summary

    This section attempts to summarize the possibilities.

    You can generate:

    The following data types are supported for fields:

    Further features:

    Generated code (all generated methods are virtual, although this is not written out in the following table):

    Field declaration

    Generated code
    primitive types
    double field;
    
    double getField();
    void setField(double d);
    
    string type
    string field;
    
    const char *getField();
    void setField(const char *);
    
    fixed-size arrays
    double field[4];
    
    double getField(unsigned k);
    void setField(unsigned k, double d);
    unsigned getFieldArraySize();
    

    dynamic arrays
    double field[];
    
    void setFieldArraySize(unsigned n);
    unsigned getFieldArraySize();
    double getField(unsigned k);
    void setField(unsigned k, double d);
    
    customized class
    class Foo {
      @customize(true);
    
    class Foo_Base { ... };
    
    and you have to write:
    class Foo : public Foo_Base {
       ...
    };
    
    abstract fields
    abstract double field;
    
    double getField() = 0;
    void setField(double d) = 0;
    

    Example simulations

    Several of the example simulations contain message definitions, for example Tictoc, Dyna, Routing and Hypercube. For example, in Dyna you'll find this:

    5.2.9 What else is there in the generated code?

    In addition to the message class and its implementation, the message compiler also generates reflection code which makes it possible to inspect message contents in Tkenv. To illustrate why this is necessary, suppose you manually subclass cMessage to get a new message class. You could write the following:

    class RadioMsg : public cMessage
    {
      public:
        int freq;
        double power;
        ...
    };
    

    Now it is possible to use RadioMsg in your simple modules:

    RadioMsg *msg = new RadioMsg();
    msg->freq = 1;
    msg->power = 10.0;
    ...
    

    You'll notice one drawback of this solution when you try to use Tkenv for debugging. While cMsgPar-based message parameters can be viewed in message inspector windows, fields added via subclassing do not appear there. The reason is that Tkenv, being just another C++ library in your simulation program, doesn't know about your C++ instance variables. The problem cannot be solved entirely within Tkenv, because C++ does not support ``reflection'' (extracting class information at runtime) like for example Java does.

    There is a solution however: one can supply Tkenv with missing ``reflection'' information about the new class. Reflection info might take the form of a separate C++ class whose methods return information about the RadioMsg fields. This descriptor class might look like this:

    class RadioMsgDescriptor : public Descriptor
    {
      public:
        virtual int getFieldCount() {return 2;}
    
        virtual const char *getFieldName(int k) {
            const char *fieldname[] = {"freq", "power";}
            if (k<0 || k>=2) return NULL;
            return fieldname[k];
        }
    
        virtual double getFieldAsDouble(RadioMsg *msg, int k) {
            if (k==0) return msg->freq;
            if (k==1) return msg->power;
            return 0.0; // not found
        }
        //...
    };
    

    Then you have to inform Tkenv that a RadioMsgDescriptor exists and that it should be used whenever Tkenv finds messages of type RadioMsg (as it is currently implemented, whenever the object's getClassName() method returns "RadioMsg"). So when you inspect a RadioMsg in your simulation, Tkenv can use RadioMsgDescriptor to extract and display the values of the freq and power variables.

    The actual implementation is somewhat more complicated than this, but not much.



    6 The Simulation Library

    OMNeT++ has an extensive C++ class library which you can use when implementing simple modules. Parts of the class library have already been covered in the previous chapters:

    This chapter discusses the rest of the simulation library:

    6.1 Class library conventions

    6.1.1 Base class

    Classes in the OMNeT++ simulation library are derived from cOwnedObject. Functionality and conventions that come from cOwnedObject:

    Classes inherit and redefine several cOwnedObject member functions; in the following we'll discuss some of the practically important ones.

    6.1.2 Setting and getting attributes

    Member functions that set and query object attributes follow a naming convention: the setter member function begins with set, and the getter begins with get (or in the case of boolean attributes, with is or has, whichever is more appropriate). For example, the length attribute of the cPacket class can be set and read like this:

    pk->setBitLength(1024);
    length = pk->getBitLength();
    

    6.1.3 getClassName()

    For each class, the getClassName() member function returns the class name as a string:

    const char *classname = msg->getClassName(); // returns "cMessage"
    

    6.1.4 Name attribute

    An object can be assigned a name (a character string). The name string is the first argument to the constructor of every class, and it defaults to NULL (no name string). An example:

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

    You can also set the name after the object has been created:

    timeoutMsg->setName("timeout");
    

    You can get a pointer to the internally stored copy of the name string like this:

    const char *name = timeoutMsg->getName(); // --> "timeout"
    

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

    cMessage *msg = new cMessage(NULL, <additional args>);
    const char *str = msg->getName(); // --> returns ""
    

    6.1.5 getFullName() and getFullPath()

    Objects have two more member functions which return strings based on object names: getFullName() and getFullPath(). For gates and modules which are part of gate or module vectors, getFullName() returns the name with the index in brackets. That is, for a module node[3] in the submodule vector node[10] getName() returns "node", and getFullName() returns "node[3]". For other objects, getFullName() is the same as getName().

    getFullPath() returns getFullName(), prepended with the parent or owner object's getFullPath() and separated by a dot. That is, if the node[3] module above is in the compound module "net.subnet1", its getFullPath() method will return "net.subnet1.node[3]".

    ev << this->getName();     // --> "node"
    ev << this->getFullName(); // --> "node[3]"
    ev << this->getFullPath(); // --> "net.subnet1.node[3]"
    

    getClassName(), getFullName() and getFullPath() are extensively used on the graphical runtime environment Tkenv, and also appear in error messages.

    getName() and getFullName() return const char * pointers, and getFullPath() returns std::string. This makes no difference with ev<<, but when getFullPath() is used as a "%s" argument to sprintf() you have to write getFullPath().c_str().

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

    6.1.6 Copying and duplicating 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, which in turn relies on the assignment operator between objects. operator=() can be used to copy contents of an object into another object of the same type. This is a deep copy: object contained in the object will also be duplicated if necessary. operator=() does not copy the name string -- this task is done by the copy constructor.

    6.1.7 Iterators

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

    For example:

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

    6.1.8 Error handling

    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 ini file entry lets you do that -- check it in section .

    If you detect an error condition in your code, you can stop the simulation with an error message using the opp_error() function. opp_error()'s argument list works like printf(): the first argument is a format string which can contain "%s", "%d" etc, filled in using subsequent arguments.

    An example:

    if (msg->getControlInfo()==NULL)
        opp_error("message (%s)%s has no control info attached",
                  msg->getClassName(), msg->getName());
    

    6.2 Logging from modules

    The logging feature will be used extensively in the code examples, we introduce it here.

    The ev object represents the user interface of the simulation. You can send debugging output to ev with the C++-style output operators:

    ev << "packet received, sequence number is " << seqNum << endl;
    ev << "queue full, discarding packet\n";
    

    An alternative solution is ev.printf():

    ev.printf("packet received, sequence number is %d\n", seqNum);
    

    The exact way messages are displayed to the user depends on the user interface. In the command-line user interface (Cmdenv), it is simply dumped to the standard output. (This output can also be disabled from omnetpp.ini so that it doesn't slow down simulation when it is not needed.) In Tkenv, the runtime GUI, you can open a text output window for every module. It is not recommended that you use printf() or cout to print messages -- ev output can be controlled much better from omnetpp.ini and it is more convenient to view, using Tkenv.

    One can save CPU cycles by making logging statements conditional on whether the output actually gets displayed or recorded anywhere. The ev.isDisabled() call returns true when ev<< output is disabled, such as in Tkenv or Cmdenv ``express'' mode. Thus, one can write code like this:

    if (!ev.isDisabled())
        ev << "Packet " << msg->getName() << " received\n";
    

    A more sophisticated implementation of the same idea is to the EV macro which can be used in logging statements instead of ev. One would simply write EV<< instead of ev<<:

    EV << "Packet " << msg->getName() << " received\n";
    

    EV's implementation makes use of the fact that the << operator binds looser than the conditional operator (?:).

    6.3 Simulation time conversion

    Simulation time is represented by the type simtime_t which is a typedef to double. OMNeT++ provides utility functions, which convert simtime_t to a printable string ("3s 130ms 230us") and vica versa.

    The simtimeToStr() function converts a simtime_t (passed in the first argument) to textual form. The result is placed into the char array pointed to by the second argument. If the second argument is omitted or it is NULL, simtimeToStr() will place the result into a static buffer which is overwritten with each call. An example:

    char buf[32];
    ev.printf("t1=%s, t2=%s\n", simtimeToStr(t1), simTimeToStr(t2,buf));
    

    The simtimeToStrShort() is similar to simtimeToStr(), but its output is more concise.

    The strToSimtime() function parses a time specification passed in a string, and returns a simtime_t. If the string cannot be entirely interpreted, -1 is returned.

    simtime_t t = strToSimtime("30s 152ms");
    

    Another variant, strToSimtime0() can be used if the time string is a substring in a larger string. Instead of taking a char*, it takes a reference to char* (char*&) as the first argument. The function sets the pointer to the first character that could not be interpreted as part of the time string, and returns the value. It never returns -1; if nothing at the beginning of the string looked like simulation time, it returns 0.

    const char *s = "30s 152ms and something extra";
    
    simtime_t t = strToSimtime0(s); // now s points to "and something extra"
    

    6.4 Generating random numbers

    Random numbers in simulation are never random. Rather, they are produced using deterministic algorithms. Algorithms take a seed value and perform some deterministic calculations on them to produce a ``random'' number and the next seed. 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.

    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 produce uniformly distributed integers in some range, usually between 0 or 1 and 232 or so. Mathematical transformations are used to produce random variates from them that correspond to specific distributions.

    6.4.1 Random number generators

    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().

    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 [9]). 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.

    The Akaroa RNG

    When you execute simulations under Akaroa control (see section [9.5]), you can also select Akaroa's RNG as the RNG underlying for the OMNeT++ random number functions. The Akaroa RNG also has to be selected from omnetpp.ini (section [8.7]).

    Other RNGs

    OMNeT++ allows plugging in your own RNGs as well. This mechanism, based on the cRNG interface, is described in section . 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.

    6.4.2 Random number streams, RNG mapping

    Simulation programs may consume random numbers from several streams, that is, from several independent RNG instances. For example, if a network simulation uses random numbers for generating packets and for simulating bit errors in the transmission, it might be a good idea to use different random streams for both. Since the seeds for each stream can be configured independently, this arrangement would allow you 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.

    It is also important that different streams and also different simulation runs use non-overlapping series of random numbers. Overlap in the generated random number sequences can introduce unwanted correlation in your results.

    The number of random number streams as well as seeds for the individual streams can be configured in omnetpp.ini (section [8.7]). For the "minimal standard RNG", the seedtool program can be used for selecting good seeds (section ).

    In OMNeT++, streams are identified with RNG numbers. The RNG numbers used in simple modules may be arbitrarily mapped to the actual random number streams (actual RNG instances) from omnetpp.ini (section [8.7]). The mapping allows for great flexibility in RNG usage and random number streams configuration -- even for simulation models which were not written with RNG awareness.

    6.4.3 Accessing the RNGs

    The intrand(n) function generates random integers in the range [0, n-1], and dblrand() generates a random double on [0,1). These functions simply wrap the underlying RNG objects. Examples:

    int dice = 1 + intrand(6); // result of intrand(6) is in the range 0..5
    double p = dblrand();      // dblrand() produces numbers in [0,1)
    

    They also have a counterparts that use generator k:

    int dice = 1 + genk_intrand(k,6); // uses generator k
    double prob = genk_dblrand(k);    // ""
    

    The underlying RNG objects are subclassed from cRNG, and they can be accessed via cModule's getRNG() method. The argument to getRNG() is a local RNG number which will undergo RNG mapping.

    cRNG *rng1 = getRNG(1);
    

    cRNG contains the methods implementing the above intrand() and dblrand() functions. The cRNG interface also allows you to access the ``raw'' 32-bit random numbers generated by the RNG and to learn their ranges (intRand(), intRandMax()) as well as to query the number of random numbers generated (getNumbersDrawn()).

    6.4.4 Random variates

    The following functions are based on dblrand() and return random variables of different distributions:

    Random variate functions use one of the random number generators (RNGs) provided by OMNeT++. By default this is generator 0, but you can specify which one to be used.

    OMNeT++ has the following predefined distributions:

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

    They are the same functions that can be used in NED files. intuniform() generates integers including both the lower and upper limit, so for example the outcome of tossing a coin could be written as intuniform(1,2). truncnormal() is the normal distribution truncated to nonnegative values; its implementation generates a number with normal distribution and if the result is negative, it keeps generating other numbers until the outcome is nonnegative.

    If the above distributions do not suffice, you can write your own functions. If you register your functions with the Register_Function() macro, you can use them in NED files and ini files too.

    6.4.5 Random numbers from histograms

    You can also specify your distribution as a histogram. The cLongHistogram, cDoubleHistogram, cVarHistogram, cKSplit or cPSquare classes are there to generate random numbers from equidistant-cell or equiprobable-cell histograms. This feature is documented later, with the statistical classes.

    6.5 Container classes

    6.5.1 Queue class: cQueue

    Basic usage

    cQueue is a container class that acts as a queue. cQueue can hold objects of type derived from cOwnedObject (almost all classes from the OMNeT++ library), such as cMessage, cPar, etc. Internally, cQueue uses a double-linked list to store the elements.

    A queue object has a head and a tail. Normally, new elements are inserted at its head and elements are removed at its tail.

    Figure: cQueue: insertion and removal

    The basic cQueue member functions dealing with insertion and removal are insert() and pop(). They are used like this:

    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.empty())
    {
      msg = (cMessage *)queue.pop();
      delete msg;
    }
    

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

    There are other functions dealing with insertion and removal. The insertBefore() and insertAfter() functions insert a new item exactly before and 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);
    

    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. If you want to use this feature, you have to provide a function that takes two cOwnedObject pointers, compares the two objects and returns -1, 0 or 1 as the result (see the reference for details). An example of setting up an ordered cQueue:

    cQueue sortedqueue("sortedqueue", cOwnedObject::cmpbyname, true );
                            // sorted by object name, ascending
    

    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.

    Iterators

    Normally, you can only access the objects at the head or tail of the queue. However, if you use an iterator class, cQueue::Iterator, you can examine each object in the queue.

    The cQueue::Iterator constructor takes two arguments, the first is the queue object and the second one specifies the initial position of the iterator: 0=tail, 1=head. Otherwise it acts as any other OMNeT++ iterator class: you 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 if you're at the end (or the beginning) of the queue.

    An example:

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

    6.5.2 Expandable array: cArray

    Basic usage

    cArray is a container class that holds objects derived from cOwnedObject. cArray stores the pointers of the objects inserted instead of making copies. cArray works as an array, but it grows automatically when it gets full. Internally, cArray is implemented with an array of pointers; when the array fills up, it is reallocated.

    cArray objects are used in OMNeT++ to store parameters attached to messages, and internally, for storing module parameters and gates.

    Creating an array:

    cArray array("array");
    

    Adding an object at the first free index:

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

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

    cPar *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 );
    

    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?
      {
        cOwnedObject *obj = array[i];
        ev << obj->getName() << endl;
      }
    }
    

    6.6 Routing support: cTopology

    6.6.1 Overview

    The cTopology class was designed primarily to support routing in telecommunication or multiprocessor networks.

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

    You can specify which modules (either simple or compound) you want to include in the graph. The graph will include all connections among the selected modules. In the graph, all nodes are at the same level, there's 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're 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: you can easily find the corresponding module for a cTopology node and vica versa.

    6.6.2 Basic usage

    You can extract the network topology into a cTopology object by a single function call. You have several ways to select which modules you want to include 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", NULL);
    

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

    A dynamically assembled list of module types can be passed as a NULL-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] = NULL;
    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() == simulation.getSystemModule();
    }
    
    topo.extractFromNetwork( selectFunction, NULL );
    

    A cTopology object uses two types: cTopology::Node for nodes and cTopology::Link for edges. (sTopoLinkIn 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 (1st line) returns the number of nodes in the graph, and getNode(i) returns a pointer to the ith node, an cTopology::Node structure.

    The correspondence between a graph node and a module can be obtained by:

    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 NULL). getNodeFor() uses binary search within the cTopology object so it is fast enough.

    cTopology::Node's other member functions let you determine the connections of this node: getNumInLinks(), getNumOutLinks() return the number of connections, in(i) and out(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.)

    6.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 when we have the target module pointer, finding the shortest path 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 == NULL)
    {
      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 calculate this, you calculate the shortest paths to the target from the neighbor node, but you must disable 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);
    

    6.7 Statistics and distribution estimation

    6.7.1 cStatistic and descendants

    There are several statistic and result collection classes: cStdDev, cWeightedStdDev, LongHistogram, cDoubleHistogram, cVarHistogram, cPSquare and cKSplit. They are all derived from the abstract base class cStatistic.

    Basic usage

    One can insert an observation into a statistic object with the collect() function or the += operator (they are equivalent). cStdDev has the following methods for getting statistics out of the object: getCount(), getMin(), getMax(), getMean(), getStddev(), getVariance(), getSum(), getSqrSum() with the obvious meanings. An example usage for cStdDev:

    cStdDev stat("stat");
    
    for (int i=0; i<10; i++)
      stat.collect( normal(0,1) );
    
    long numSamples = stat.getCount();
    double smallest = stat.getMin(),
           largest = stat.getMax();
    double mean = stat.getMean(),
           standardDeviation = stat.getStddev(),
           variance = stat.getVariance();
    

    6.7.2 Distribution estimation

    Initialization and usage

    The distribution estimation classes (cLongHistogram, cDoubleHistogram, cVarHistogram, cPSquare and cKSplit) are derived from cDensityEstBase. Distribution estimation classes (except for cPSquare) assume that the observations are within a range. You may specify the range explicitly (based on some a-priori info about the distribution) or you may let the object collect the first few observations and determine the range from them. Methods which let you specify range settings are part of cDensityEstBase.

    The following member functions exist for setting up the range and to specify how many observations should be used for automatically determining the range.

    setRange(lower,upper);
    setRangeAuto(numFirstvals, rangeExtFactor);
    setRangeAutoLower(upper, numFirstvals, rangeExtFactor);
    setRangeAutoUpper(lower, numFirstvals, rangeExtFactor);
    

    setNumFirstVals(numFirstvals);
    

    The following example creates a histogram with 20 cells and automatic range estimation:

    cDoubleHistogram histogram("histogram", 20);
    histogram.setRangeAuto(100,1.5);
    

    Here, 20 is the number of cells (not including the underflow/overflow cells, see later), and 100 is the number of observations to be collected before setting up the cells. 1.5 is the range extension factor. It means that the actual range of the initial observations will be expanded 1.5 times and this expanded range will be used to lay out the cells. This method increases the chance that further observations fall in one of the cells and not outside the histogram range.

    Figure: Setting up a histogram's range

    After the cells have been set up, collection can go on.

    The isTransformed() function returns true when the cells have already been set up. You can force range estimation and setting up the cells by calling the transform() function.

    The observations that fall outside the histogram range will be counted as underflows and overflows. The number of underflows and overflows are returned by the getUnderflowCell() and getOverflowCell() member functions.

    Figure: Histogram structure after setting up the cells

    You create a P2 object by specifying the number of cells:

    cPSquare psquare("interarrival-times", 20);
    

    Afterwards, a cPSquare can be used with the same member functions as a histogram.

    Getting histogram data

    There are three member functions to explicitly return cell boundaries and the number of observations is each cell. getNumCells() returns the number of cells, getBasepoint(int k) returns the kth base point, getCellValue(int k) returns the number of observations in cell k, and getCellPDF(int k) returns the PDF value in the cell (i.e. between getBasepoint(k) and getBasepoint(k+1)). The getCellInfo(k) method returns multiple data (cell bounds, counter, relatile frequency) packed together in a struct. These functions work for all histogram types, plus cPSquare and cKSplit.

    Figure: base points and cells

    An example:

    long n = histogram.getCount();
    for (int i=0; i<histogram.getNumCells(); i++)
    {
      double cellWidth = histogram.getBasepoint(i+1)-histogram.getBasepoint(i);
      int count = histogram.getCellValue(i);
      double pdf = histogram.getCellPDF(i);
      //...
    }
    

    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.

    Random number generation from distributions

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

    double rnd = histogram.random();
    

    cStdDev assumes normal distribution.

    You can also wrap the distribution object in a cPar:

    cMsgPar rndPar("rndPar");
    rndPar.setDoubleValue(&histogram);
    

    The cPar object stores the pointer to the histogram (or P2 object), and whenever it is asked for the value, calls the histogram object's random() function:

    double rnd = (double)rndPar; // random number from the cPSquare
    

    Storing/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);
    
    cDoubleHistogram hist2("Hist-from-file");
    FILE *f2 = fopen("histogram.dat","r");
    hist2.loadFromFile(f2); // load stored distribution
    fclose(f2);
    

    Histogram with custom cells

    The cVarHistogram class can be used to create histograms with arbitrary (non-equidistant) cells. It can operate in two modes:

    Modes are selected with a transform-type parameter:

    Creating an object:

    cVarHistogram(const char *s=NULL,
                  int numcells=11,
                  int transformtype=HIST_TR_AUTO_EPC_DBL);
    

    Manually adding a cell boundary:

    void addBinBound(double x);
    

    Rangemin and rangemax is chosen after collecting the numFirstVals initial observations. One cannot add cell boundaries when the histogram has already been transformed.

    6.7.3 The k-split algorithm

    Purpose

    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.

    The 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 shows that k=2 worked 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 experimental implementation of the k-split algorithm, the cKSplit class. Research on k-split is still under way.

    The cKSplit class

    The cKSplit class is an implementation of the k-split method. Member functions:

    void setCritFunc(KSplitCritFunc _critfunc, double *_critdata);
    void setDivFunc(KSplitDivFunc \_divfunc, double *\_divdata);
    void rangeExtension( bool enabled );
    

    int getTreeDepth();
    int getTreeDepth(cKSplit::Grid& grid);
    

    double getRealCellValue(cKSplit::Grid& grid, int cell);
    void printGrids();
    
    cKSplit::Grid& getGrid(int k);
    cKSplit::Grid& getRootGrid();
    

    struct cKSplit::Grid
    {
      int parent;   // index of parent grid
      int reldepth; // depth = (reldepth - rootgrid's reldepth)
      long total;   // sum of cells & all subgrids (includes "mother")
      int mother;   // observations "inherited" from mother cell
      int cells[K]; // cell values
    };
    

    6.7.4 Transient detection and result accuracy

    In many simulations, only the steady state performance (i.e. the performance after the system has reached a stable state) is of interest. The initial part of the simulation is called the transient period. After the model has entered steady state, simulation must proceed until enough statistical data has been collected to compute result with the required accuracy.

    Detection of the end of the transient period and a certain result accuracy is supported by OMNeT++. The user can attach transient detection and result accuracy objects to a result object (cStatistic's descendants). The transient detection and result accuracy objects will do the specific algorithms on the data fed into the result object and tell if the transient period is over or the result accuracy has been reached.

    The base classes for classes implementing specific transient detection and result accuracy detection algorithms are:

    Basic usage

    Attaching detection objects to a cStatistic and getting pointers to the attached objects:
    addTransientDetection(cTransientDetection *object);
    addAccuracyDetection(cAccuracyDetection *object);
    cTransientDetection *getTransientDetectionObject();
    cAccuracyDetection *getAccuracyDetectionObject();
    

    Detecting the end of the period:

    Transient detection

    Currently one transient detection algorithm is implemented, i.e. there's one class derived from cTransientDetection. The cTDExpandingWindows class uses the sliding window approach with two windows, and checks the difference of the two averages to see if the transient period is over.

    void setParameters(int reps=3,
                       int minw=4,
                       double wind=1.3,
                       double acc=0.3);
    

    Accuracy detection

    Currently one accuracy detection algorithm is implemented, i.e. there's one class derived from cAccuracyDetection. The algorithm implemented in the cADByStddev class is: divide the standard deviation by the square of the number of values and check if this is small enough.

    void setParameters(double acc=0.1, int reps=3);
    

    6.8 Recording simulation results

    6.8.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 named omnetpp.vec by default. You can configure output vectors from omnetpp.ini: you can disable writing to the file, or limit it to a certain simulation time interval for recording (section [8.6.2]).

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

    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 inspector window open for the output vector object, the values will be displayed there, regardless of the state of the output vector object.

    6.8.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'll 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 statistics 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 write into the output scalar file which is named omnetpp.sca by default. The output scalar file is preserved across simulation runs (unlike the output vector file which gets deleted at the beginning of every simulation run). Data are always appended at the end of the file, and output from different simulation runs are separated by special lines. The format and processing of output vector files is described in section .

    6.8.3 Precision

    Output scalar and output vector files are text files, and floating point values (doubles) are recorded into it using fprintf()'s "%g" format. The number of significant digits can be configured using the output-scalar-precision= and output-vector-precision= configuration entries (see ). The default precision is 12 digits. The following has to be considered when changing the default value:

    IEEE-754 doubles are 64-bit numbers. The mantissa is 52 bits, which is roughly equivalent to 16 decimal places (52*log(2)/log(10)). However, due to rounding errors, usually only 12..14 digits are correct, and the rest is pretty much random garbage which should be ignored. However, when you convert the decimal representation back into an IEEE-754 double (as in Plove and Scalars), an additional small error will occurs because 0.1, 0.01, etc cannot be accurately represented in binary. This conversion error is usually smaller than the one that the double variable already had before recording into the file, however if it is important you can eliminate it by setting >16 digits precision for the file (but again, be aware that the last digits are garbage). The practical upper limit is 17 digits, setting it higher doesn't make any difference in fprintf()'s output.

    Errors coming from converting to/from decimal representation can be eliminated by choosing an output vector/output scalar manager class which stores doubles in their native binary form. The appropriate configuration entries are outputvectormanager-class= and outputvectormanager-class=; see . For example, cMySQLOutputScalarManager and cMySQLOutputScalarManager provided in samples/database fulfill this requirement.

    However, before worrying too much about rounding and conversion errors, it is worth considering what is the real accuracy of your results. Some things to consider:

    6.9 Watches and snapshots

    6.9.1 Basic watches

    It would be nice, but variables of type int, long, double do not show up by default in Tkenv; 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 set of other macros to come to your rescue, and make variable to be inspectable in Tkenv 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);
    

    When you open an inspector for the simple module in Tkenv and click the Objects/Watches tab in it, you'll see your watched variables and their values there. Tkenv also lets you change the value of a watched variable.

    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);
    

    6.9.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);
    

    6.9.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 one can do better. 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 NULL at all times, otherwise the GUI may crash while trying to display the object. This practically means that the pointer should be initialized to NULL even if not used, and should be set to NULL when the object to which it points gets deleted.

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

    6.9.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's 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);
    

    6.9.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(cOwnedObject *obj = &simulation, const char *label = NULL);
    

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

    snapshot();     // dump the whole network
    snapshot(this); // dump this simple module and all its objects
    snapshot(&simulation.msgQueue); // dump future events
    

    This will append snapshot information to the end of the snapshot file. (The snapshot file name has an extension of .sna, default is omnetpp.sna. Actual file name can be set in the config file.)

    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, you can also create a snapshot from the menu.

    An example of a snapshot file:

    [...]
    
    (cSimulation) 'simulation' begin
      Modules in the network:
        'token' #1 (TokenRing)
          'comp[0]' #2 (Computer)
            'mac' #3 (TokenRingMAC)
            'gen' #4 (Generator)
            'sink' #5 (Sink)
          'comp[1]' #6 (Computer)
            'mac' #7 (TokenRingMAC)
            'gen' #8 (Generator)
            'sink' #9 (Sink)
          'comp[2]' #10 (Computer)
            'mac' #11 (TokenRingMAC)
            'gen' #12 (Generator)
            'sink' #13 (Sink)
    end
    
    (TokenRing) 'token' begin
      #1 params     (cArray) (n=6)
      #1 gates      (cArray) (empty)
      comp[0]          (cCompoundModule,#2)
      comp[1]          (cCompoundModule,#6)
      comp[2]          (cCompoundModule,#10)
    end
    
    (cArray) 'token.parameters' begin
      num_stations (cModulePar) 3 (L)
      num_messages (cModulePar) 10000 (L)
      ia_time      (cModulePar) truncnormal(0.005,0.003) (F)
      THT          (cModulePar) 0.01 (D)
      data_rate    (cModulePar) 4000000 (L)
      cable_delay  (cModulePar) 1e-06 (D)
    end
    
    [...]
    
    (cQueue) 'token.comp[0].mac.local-objects.send-queue' begin
      0-->1         (cMessage) Tarr=0.0158105774 ( 15ms) Src=#4 Dest=#3
      0-->2         (cMessage) Tarr=0.0163553310 ( 16ms) Src=#4 Dest=#3
      0-->1         (cMessage) Tarr=0.0205628236 ( 20ms) Src=#4 Dest=#3
      0-->2         (cMessage) Tarr=0.0242203591 ( 24ms) Src=#4 Dest=#3
      0-->2         (cMessage) Tarr=0.0300994268 ( 30ms) Src=#4 Dest=#3
      0-->1         (cMessage) Tarr=0.0364005251 ( 36ms) Src=#4 Dest=#3
      0-->1         (cMessage) Tarr=0.0370745702 ( 37ms) Src=#4 Dest=#3
      0-->2         (cMessage) Tarr=0.0387984129 ( 38ms) Src=#4 Dest=#3
      0-->1         (cMessage) Tarr=0.0457462493 ( 45ms) Src=#4 Dest=#3
      0-->2         (cMessage) Tarr=0.0487308918 ( 48ms) Src=#4 Dest=#3
      0-->2         (cMessage) Tarr=0.0514466766 ( 51ms) Src=#4 Dest=#3
    end
    
    (cMessage) 'token.comp[0].mac.local-objects.send-queue.0-->1' begin
      #4 --> #3
      sent:         0.0158105774 ( 15ms)
      arrived:      0.0158105774 ( 15ms)
      length:       33536
      kind:         0
      priority:     0
      error:        FALSE
      time stamp:   0.0000000 ( 0.00s)
      parameter list:
        dest        (cPar) 1 (L)
        source      (cPar) 0 (L)
        gentime     (cPar) 0.0158106 (D)
    end
    
    [...]
    

    It is possible that the format of the snapshot file will change to XML in future OMNeT++ releases.

    6.9.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.

    From the Feb99 release, 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.

    6.10 Deriving new classes

    6.10.1 cOwnedObject 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 cOwnedObject or not. Note that we are not saying you should always subclass from cOwnedObject. Both solutions have advantages and disadvantages, which you have to consider individually for each class.

    cOwnedObject already carries (or provides a framework for) significant functionality that is either relevant to your particular purpose or not. Subclassing cOwnedObject 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'll want to store OMNeT++ classes in your object, then you must subclass from cOwnedObject.

    The most significant features cOwnedObject has is the name string (which has to be stored somewhere, so it has its overhead) and ownership management (see section [6.11]) which also has the advantages but also some costs.

    As a general rule, small struct-like classes like IPAddress, MACAddress, RoutingTableEntry, TCPConnectionDescriptor, etc. are better not sublassed from cOwnedObject. 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.

    6.10.2 cOwnedObject virtual methods

    Most classes in the simulation class library are descendants of cOwnedObject. If you want to derive a new class from cOwnedObject or a cOwnedObject descendant, you must redefine some member functions so that objects of the new type can fully co-operate with other parts of the simulation system. A more or less complete list of these functions is presented here. You do not need to worry about the length of the list: most functions are not absolutely necessary to implement. For example, you do not need to redefine forEachChild() unless your class is a container class.

    The following methods must be implemented:

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

    The following methods are recommended to implement:

    6.10.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 [15])

    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 [14]) to create blank objects to unmarshal into on the receiving side.

    6.10.4 Details

    We'll go through the details using an example. We create a new class NewClass, redefine all above mentioned cOwnedObject 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-cOwnedObject 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 cOwnedObject
    {
      protected:
        int data;
        double *array;
        cQueue queue;
        cMessage *msg;
        ...
      public:
        NewClass(const char *name=NULL, 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's 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 d) : cOwnedObject(name)
    {
        data = d;
        array = new double[10];
        take(&queue);
        msg = NULL;
    }
    

    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) : cOwnedObject(other.getName())
    {
        array = new double[10];
        msg = NULL;
        take(&queue);
        operator=(other);
    }
    

    The copy constructor relies on the assignment operator. Because by convention the assignment operator does not copy the name member, it is passed here to the base class constructor. (Alternatively, we could have written setName(other.getName()) into the function body.)

    Note that pointer members have to be initialized (to NULL or to an allocated object/memory) before calling the assignment operator, to avoid crashes.

    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 [6.11].

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

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

    NewClass& NewClass::operator=(const NewClass& other)
    {
        if (&other==this)
            return *this;
        cOwnedObject::operator=(other);
    
        data = other.data;
    
        for (int i=0; i<10; 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;
        return *this;
    }
    

    Complexity associated with copying and duplicating the object is concentrated in the assignment operator, so it is usually the one that requires the most work from you of all methods required by cOwnedObject.

    If you do not want to implement object copying and duplication, you should implement the assignment operator to call copyNotSupported() -- it'll throw an exception that stops the simulation with an error message if this function is called.

    The assignment operator copies contents of the other object to this one, except the name string. It should always return *this.

    First, we should make sure we're not trying to copy the object to itself, because it might be disastrous. If so (that is, &other==this), we return immediately without doing anything.

    The base class part is copied via invoking the assignment operator of the base class.

    New data members are copied in the normal C++ way. If the class contains pointers, you'll 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 [6.11].

    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 of 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.

    6.11 Object ownership management

    6.11.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 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 cOwnedObject, 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.

    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 must 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: which object has the ownership of the message, why's that, and then probably you'll need to fix the logic somewhere in your program.

    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 currently kept by) another module.

    6.11.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.

    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's 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);
    
    }
    

    Removal

    Here's 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 returning cOwnedObject* 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;
    }
    

    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.

    Object copying

    The ownership mechanism also has to be taken into consideration when a cArray or cQueue object is duplicated. 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.

    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.

    In fact, the same question arises in three places: the assignment operator operator=(), the copy constructor and the dup() method. In OMNeT++, the convention is that copying is implemented in the assignment operator, and the other two just rely on it. (The copy constructor just constructs an empty object and invokes assignment, while dup() is implemented as new cArray(*this)).



    7 Building Simulation Programs

    7.1 Overview

    As it was already mentioned, an OMNeT++ model physically consists of the following parts:

    To build an executable simulation program, you first need to translate the MSG files into C++, using the message compiler (opp_msgc). After this step, the process is the same as building any C/C++ program from source: all C++ sources need to be compiled into object files (.o files (using gcc on Mac, Linux) or mingw on Windows) and all object files need to be linked with the necessary libraries to get an executable or shared library.

    File names for libraries differ for Unix/Linux and for Windows, and also different for static and shared libraries. Let us suppose you have a library called Tkenv. If you are compiling with gcc or mingw, the file name for the static library would be something like libopptkenv[d].a, and the shared library would be called libopptkenv[d].so. (libopptkenvd.so would be used for the debug version while libopptkenv.so is for the release build.)

    In OMNeT++ 4.0 we recommend to use shared libraries whenever it is possible. You'll need to link with the following libraries:

    Luckily, you do not have to worry about the above details, because automatic tools like opp_makemake will take care of the hard part for you.

    The following figure gives an overview of the process of building and running simulation programs.

    Figure: Building and running simulation

    This section discusses how to use the simulation system on the following platforms:

    7.2 Using gcc

    The following section applies to using OMNeT++ on Linux, Solaris, Mac OS X, FreeBSD and other Unix derivatives, and also to MinGW on Windows.

    7.2.1 The opp_makemake tool

    The opp_makemake tool can automatically generate a Makefile for your simulation program, based on the source files in the current directory or directory tree. opp_makemake has several options; opp_makemake -h displays help.

    The most important options are:

    7.2.2 Basic use

    Once you have the source files (*.ned, *.msg, *.cc, *.h) in a directory, change there and type:

    $ opp_makemake
    

    This will create a file named Makefile. If you type make, your simulation program should build.

    If you already had a Makefile in that directory, opp_makemake will refuse to overwrite it. You can force overwriting the old Makefile with the -f option:

    $ opp_makemake -f
    

    The name of the output file will be derived from the name of the project directory (see later). You can override it with the -o option:

    $ opp_makemake -f -o aloha
    

    In addition to the default target that builds the simulation executable, the Makefile also contains the following targets:

    Target

    Action
    all The default target is to build the simulation executable
    depend Adds (or refreshes) dependencies in the Makefile
    clean Deletes all files that were produced by the make process

    7.2.3 Debug and release builds

    opp_makemake generates a makefile that can create both release and debug builds. By default it creates debug version, but it is easy to override this behaviour. Just define the MODE variable on the make command line.

    $ make MODE=release
    

    If you want to create release builds by default you should use the --mode mode option for opp_makemake when generating your makefiles.

    $ opp_makemake --mode release ...
    

    7.2.4 Using external C/C++ libraries

    If you are using external libraries you should specify the include path for the header files with the -I includedir option. You should specify this option if you are using anything outside from the source directory tree (except the system and OMNeT++ headers which are always included automatically)

    To define an external library to be linked with, use -Ldir to specify the directory of the external library and -llibrary to specify the name of the external dependency.

    7.2.5 Building directory trees

    It is possible to build a whole source directory tree with a single makefile. A source tree will generate a single output file (executable or library). A source directory tree will always have a Makefile in its root, and source files may be placed anywhere in the tree.

    To turns on this option, use the opp_makemake --deep option. opp_makemake will collect all .cc and .msg files from the whole subdirectory tree, and generate a makefile that covers all. If you need to exclude a specific directory, use the -X exclude/dir/path option. (Multiple -X options are accepted.)

    An example:

    $ opp_makemake -f --deep -X experimental -X obsolete
    

    7.2.6 Automatic include dirs

    If your source tree contains several subdirectories (maybe several levels deep), it can be annoying that you should specify relative paths for your header files in your .cc files or you should specify the include path explicitly by the -I includepath option. opp_makemake has a command line option, which adds all directories in the current source tree to the compiler command line. This option is turned on by default.

    The only requirement is that your #include statements must unambigously specify the name of the header file. (i.e. if you have two common.h files, one in subdir1 and the other in subdir2 specify #include "subdir1/common.h" instead of #include "common.h". If you want to include a directory which is outside of your source directory tree you always must specify it with the -I external/include/dir option.

    7.2.7 Dependency handling

    Dependency information is used by the makefile to minimize the time required to compile and link your project. If your makefile contains up-to date dependency info -- only files changed since you last compiled your project will be re-compiled or linked.

    opp_makemake automatically adds dependencies to the makefile. You can regenerate the dependencies by typing make depend any time. The warnings during the dependency generation process can be safely ignored.

    You may generate and add dependencies to the makefile manually using the opp_makedep tool. Use opp_makedep --help to display the supported command line options.

    7.2.8 Out-of-directory build

    The build system creates object and executable files in a separate directory, called output directory. The structure of the output folder will be the same as your sourcefolder structure except that it will be placed in the out/configname directory. The configname part will mirror your compiler toolchain and build mode settings. (i.e. The result of a debug build with gcc will be placed in out/gcc-debug)

    The location of the generated output file is determined by the -O option. (The default value is 'out', relative to the project root directory):

    $ opp_makemake -O ../tmp/obj
    

    7.2.9 Building shared and static libraries

    By default the makefile will create an executable file, but it is also possible to build shared or static libraries. Shared libraries are usually a better choice.

    Use --make-so to create shared libraries, and --make-lib to build static libraries. The --nolink option completely avoids the linking step, which is useful for top-level makefiles that only invoke other makefiles, or if you want to do the linking manually.

    7.2.10 Recursive builds

    The --recurse option enables recursive make: when you build the simulation, make will descend into the subdirectories and runs make in them too. By default, --recurse decends into all subdirectories; the -X directory option can be used to make it ignore certain subdirectories. This option is especially useful for top level makefiles.

    The --recurse option automatically discovers subdirectories, but this is sometimes inconvenient. Your source directory tree may contain parts which need their own hand written Makefile. This can happen if you include source files from an other non OMNeT++ project.With the -d dir or --subdir dir option, you can explicitly specify which directories to recurse into, and also, the directories need not be direct children of the current directory.

    The recursive make options (--recurse, -d, --subdir) imply -X, that is, the directories recursed into will be automatically excluded from deep makefiles.

    You can control the order of traversal by adding dependencies into the makefrag file (see [7.2.11])

    Motivation for recursive builds:

    7.2.11 Customizing the Makefile

    It is possible to add rules or otherwise customize the generated makefile by providing a makefrag file. When you run opp_makemake, it will automatically insert makefrag into the resulting Makefile. With the -i option, you can also name other files to be included into the Makefile.

    makefrag will be inserted after the definitions but before the first rule, so it is possible to override existing definitions and add new ones, and also to override the default target.

    makefrag can be useful if some of your source files are generated from other files (for example, you use generated NED files), or you need additional targets in your makefile or just simply wants to override the default target in the makefile.

    7.2.12 Projects with multiple source trees

    In the case of a large project, your source files may be spread across several directories and your project may generate more than one executable file (i.e. several shared libraries, examples etc.).

    Once you have created your makefiles with opp_makemake in every source directory tree, you will need a toplevel makefile. The toplevel makefile usually calls only the makefiles recursively in the source directory trees.

    7.2.13 A multi-directory example

    For a complex example of using opp_makemake, we will check how to create the makefiles for the mobility-framework. First take a look at the project's directory structure and find the directories that should be used as source trees:

    mobility-framework
        bitmaps
        contrib <-- source tree (build libmfcontrib.so from this dir)
        core <-- source tree (build libmfcore.so from this dir)
        docs
        network
        template
        testSuite <-- source tree (build testSuite executable from this dir)
    

    Additionally there are dependencies between these output files: mfcontrib requires mfcore and testSuite requires mfcontrib (and indirectly mfcore of course).

    First create the makefile for the core directory (build a shared lib from all .cc files found in the core subtree and name it 'mfcore'):

    $ cd core && opp_makemake -f --deep --make-so -o mfcore -O out
    

    The contrib directory is depending on mfcore so we use the -L and -l options to specify the library we should link with. Note that we must also add the include directories manually from the core source tree, because autodiscovery works only in the same source tree:

    $ cd contrib && opp_makemake -f --deep --make-so -o mfcontrib -O out \\
      -I../core/basicModules -I../core/utils -L../out/$(CONFIGNAME)/core -lmfcore
    

    The testSuite will be created as an executable file which depends on both mfcontrib and mfcore.

    $ cd testSuite && opp_makemake -f --deep -o testSuite -O out
        -I../core/utils -I../core/basicModules -I../contrib/utils \\
        -I../contrib/applLayer -L../out/$(CONFIGNAME)/contrib -lmfcontrib
    

    Now the last step is to create a top-level makefile in the root of the project that calls the previously created makefiles in the correct order. We will use the --nolink option, exclude every subdirectory from the build (-X.) and explicitly call the above makefiles (-d dirname).

    $ opp_makemake -f --nolink -O out -d testSuite -d core -d contrib -X.
    

    Finally we have to specify the dependencies between the above directories. Add the lines below to the makefrag file in the project directory root.

    contrib_dir: core_dir
    testSuite_dir: contrib_dir
    



    8 Configuring Simulations

    8.1 Configuring simulations

    Configuration and input data for the simulation are in a configuration file usually called omnetpp.ini.

    The following sections explain omnetpp.ini.

    8.2 The configuration file: omnetpp.ini

    8.2.1 An example

    For a start, let us see a simple omnetpp.ini file which can be used to run the Fifo example simulation.

    [General]
    network = FifoNet
    sim-time-limit = 100h
    cpu-time-limit = 300s
    #debug-on-errors = true
    #record-eventlog = true
    
    [Config Fifo1]
    description = "low job arrival rate"
    **.gen.sendIaTime = exponential(0.2s)
    **.gen.msgLength = 100b
    **.fifo.bitsPerSec = 1000bps
    
    [Config Fifo2]
    description = "high job arrival rate"
    **.gen.sendIaTime = exponential(0.01s)
    **.gen.msgLength = 10b
    **.fifo.bitsPerSec = 1000bps
    

    The file is grouped into sections named [General], [Config Fifo1] and [Config Fifo2], each one containing several entries.

    Lines that start with ``#'' are comments.

    8.2.2 File syntax

    The ini file is a text file consisting of entries grouped into different sections. The order of the sections doesn't matter.

    Lines that start with "#" are comments, and will be ignored during processing.

    Long lines can be broken up using the backslash notation: if the last character of a line is "\", it will be merged with the next line.

    There is no limit on the file size or the maximum line length.

    Example:

    [General]
    # this is a comment
    **.foo = "this is a single value \
    for the foo parameter"
    

    8.2.3 File inclusion

    OMNeT++ supports including an ini file in another, via the include keyword. This feature allows you to partition large ini files into logical units, fixed and varying part etc.

    An example:

    # omnetpp.ini
    ...
    include parameters.ini
    include per-run-pars.ini
    ...
    

    You can also include files from other directories. If the included ini file further includes others, their path names will be understood as relative to the location of the file which contains the reference, rather than relative to the current working directory of the simulation.

    This rule also applies to other file names occurring in ini files (such as the load-libs=, output-vector-file=, output-scalar-file= etc. options, and xmldoc() module parameter values.)

    8.3 Sections

    8.3.1 The [General] section

    The most commonly used options of the [General] section are the following.

    It is important to note, that the loaded NED files may contain any number of modules, channel and any number of networks as well. It does not matter whether you use all or just some of them in the simulations. You will be able to select any of the networks that occur in the loaded NED files using the network= omnetpp.ini entry, and as long as every module, channel etc for it has been loaded, network setup will be successful.

    8.3.2 Named configurations

    Named configurations are sections of the form [Config <configname>], where <configname> is by convention a camel-case string that starts with a capital letter: Config1, WirelessPing, OverloadedFifo, etc. For example, omnetpp.ini for an Aloha simulation might have the following skeleton:

    [General]
    ...
    [Config PureAloha]
    ...
    [Config SlottedAloha1]
    ...
    [Config SlottedAloha2]
    ...
    

    Some configuration keys (such as user interface selection) are only accepted in the [General] section, but most of them can go into Config sections as well.

    When you run a simulation, you need to select one of the configurations to be activated. In Cmdenv, this is done with the -c command-line option:

    $ aloha -c PureAloha
    

    The simulation will then use the contents of the [Config PureAloha] section to set up the simulation. (Tkenv, of course, lets you select the configuration from a dialog.)

    8.3.3 Section inheritance

    Actually, when you activate the PureAloha configuration, the contents of the [General] section will also be taken into account: if some configuration key or parameter value is not found in [Config PureAloha], then the search will continue in the [General] section. In other words, lookups in [Config PureAloha] will fall back to [General]. The [General] section itself is optional; when it is absent, it is treated like an empty [General] section.

    All named configurations fall back to [General] by default. However, for each configuration it is possible to specify a fall-back section explicitly, using the extends= key. Consider the following ini file skeleton:

    [General]
    ...
    [Config SlottedAlohaBase]
    ...
    [Config SlottedAloha1]
    extends = SlottedAlohaBase
    ...
    [Config SlottedAloha2]
    extends = SlottedAlohaBase
    ...
    [Config SlottedAloha2a]
    extends = SlottedAloha2
    ...
    [Config SlottedAloha2b]
    extends = SlottedAloha2
    ...
    

    If you activate the SlottedAloha2b configuration, lookups will consider sections in the following order (this is also called the section fallback chain): SlottedAloha2b, SlottedAloha2, SlottedAlohaBase, General.

    The effect is the same as if the contents of the sections SlottedAloha2b, SlottedAloha2, SlottedAlohaBase and General were copied together into one section, one after another, [Config SlottedAloha2b] being at the top, and [General] at the bottom. Lookups always start at the top, and stop at the first matching entry.

    The concept is similar to inheritance in object-oriented languages, and benefits are similar too: you can factor out the common parts of several configurations into a "base" configuration, and the other way round, you can reuse existing configurations (as opposed to copying them) by using them as a base. In practice you will often have "abstract" configurations too (in the C++/Java sense), which assign only a subset of parameters and leave the others open, to be assigned in derived configurations.

    If you are experimenting a lot with different parameter settings of a simulation model, these techniques will make it a lot easier to manage ini files.

    8.4 Setting module parameters

    Simulations get input via module parameters, which can be assigned a value in NED files or in omnetpp.ini -- in this order. Since parameters assigned in NED files cannot be overridden in omnetpp.ini, one can think about them as being ``hardcoded''. In contrast, it is easier and more flexible to maintain module parameter settings in omnetpp.ini.

    In omnetpp.ini, module parameters are referred to by their full paths or hierarchical names. This name consists of the dot-separated list of the module names (from the top-level module down to the module containing the parameter), plus the parameter name (see section [6.1.5]).

    An example omnetpp.ini which sets the numHosts parameter of the toplevel module and the transactionsPerSecond parameter of the server module:

    [General]
    net.numHosts = 15
    net.server.transactionsPerSecond = 100
    

    8.4.1 Using wildcard patterns

    Models can have a large number of parameters to be configured, and it would be tedious to set them one-by-one in omnetpp.ini. OMNeT++ supports wildcards patterns which allow for setting several model parameters at once.

    The notation is a variation on the usual glob-style patterns. The most apparent differences to the usual 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 {a-zA-Z} not [a-zA-Z], because square brackets are already reserved for the notation of module vector indices).

    Pattern syntax:

    Precedence

    If you use wildcards, the order of entries is important: if a parameter name matches several wildcards-patterns, the first matching occurrence is used. This means that you need to list specific settings first, and more general ones later. Catch-all settings should come last.

    An example ini file:

    [General]
    *.host[0].waitTime = 5ms   # specifics come first
    *.host[3].waitTime = 6ms
    *.host[*].waitTime = 10ms  # catch-all comes last
    

    Asterisk vs double asterisk

    The * wildcard is for matching a single module or parameter name in the path name, while ** can be used to match several components in the path. For example, **.queue*.bufSize matches the bufSize parameter of any module whose name begins with queue in the model, while *.queue*.bufSize or net.queue*.bufSize selects only queues immediately on network level. Also note that **.queue**.bufSize would match net.queue1.foo.bar.bufSize as well!

    Sets, negated sets

    Sets and negated sets can contain several character ranges and also enumeration of characters. For example, {_a-zA-Z0-9} matches any letter or digit, plus the underscore; {xyzc-f} matches any of the characters x, y, z, c, d, e, f. To include '-' in the set, put it at a position where it cannot be interpreted as character range, for example: {a-z-} or {-a-z}. If you want to include '}' in the set, it must be the first character: {}a-z}, or as a negated set: {^}a-z}. A backslash is always taken as literal backslash (and NOT as escape character) within set definitions.

    Numeric ranges and index ranges

    Only nonnegative integers can be matched. The start or the end of the range (or both) can be omitted: {10..}, {..99} or {..} are valid numeric ranges (the last one matches any number). The specification must use exactly two dots. Caveat: *{17..19} will match a17, 117 and 963217 as well, because the * can also match digits!

    An example for numeric ranges:

    [General]
    *.*.queue[3..5].bufSize = 10
    *.*.queue[12..].bufSize = 18
    *.*.queue[*].bufSize = 6  # this will only affect queues 0,1,2 and 6..11
    

    8.4.2 Using the default values

    It is also possible to utilize the default values specified in the NED files. The <parameter-fullpath>=default setting assigns the default value to the parameter if it has one.

    The <parameter-fullpath>=ask setting will try to get the parameter value interactively from the user.

    If a parameter was not set but has a default value, that value will be assigned. This is like having a **=default line at the bottom of the [General] section.

    If a parameter was not set and has no default value, that will either cause an error or will be interactively prompted for, depending on the particular user interface.

    More precisely, parameter resolution takes place as follows:

    1. If the parameter is assigned in NED, it cannot be overridden in the configuration. The value is applied and the process finishes.
    2. If the first match is a value line (matches <parameter-fullpath>=somevalue), the value is applied and the process finishes.
    3. If the first match is a <parameter-fullpath>=default line, the default value is applied and the process finishes.
    4. If the first match is a <parameter-fullpath>=ask line, the parameter will be asked from the user interactively (UI dependent).
    5. If there was no match and the parameter has a default value, it is applied and the process finishes.
    6. Otherwise the parameter is declared unassigned, and handled accordingly by the user interface. It may be reported as an error, or may be asked from the user interactively.

    8.5 Parameter studies

    8.5.1 Basic use

    It is quite common in simulation studies that the simulation model is run several times with different parameter settings, and the results are analyzed in relation to the input parameters. OMNeT++ 3.x had no direct support for batch runs, and users had to resort to writing shell (or Python, Ruby, etc.) scripts that iterated over the required parameter space, and generated a (partial) ini file and run the simulation program in each iteration.

    OMNeT++ 4.0 largely automates this process, and eliminates the need for writing batch execution scripts. It is the ini file where the user can specify iterations over various parameter settings. Here's an example:

    [Config AlohaStudy]
    *.numHosts = ${1, 2, 5, 10..50 step 10}
    **.host[*].generationInterval = exponential(${0.2, 0.4, 0.6}s)
    

    This parameter study expands to 8*3 = 24 simulation runs, where the number of hosts iterates over the numbers 1, 2, 5, 10, 20, 30, 40, 50, and for each host count three simulation runs will be done, with the generation interval being exponential(0.2), exponential(0.4), and exponential(0.6).

    How does it get run? First of all, Cmdenv with the '-x' option will tell you how many simulation runs a given section expands to. (You'll of course use Cmdenv for batch runs, not Tkenv.)

    $ aloha -u Cmdenv -x AlohaStudy
    
    `\opp` Discrete Event Simulation
    ...
    Config: AlohaStudy
    Number of runs: 24
    

    If you add the '-g' option, the program will also print out the values of the iteration variables for each run. (Use '-G' for even more info) Note that the parameter study actually maps to nested loops, with the last "${..}" becoming the innermost loop. The iteration variables are just named $0 and $1 -- we'll see that it is possible to give meaningful names to them. Please ignore the '$repetition=0' part in the printout for now.

    $ aloha -u Cmdenv -x AlohaStudy -g
    `\opp` Discrete Event Simulation
    ...
    Config: AlohaStudy
    Number of runs: 24
    Run 0: $0=1, $1=0.2, $repetition=0
    Run 1: $0=1, $1=0.4, $repetition=0
    Run 2: $0=1, $1=0.6, $repetition=0
    Run 3: $0=2, $1=0.2, $repetition=0
    Run 4: $0=2, $1=0.4, $repetition=0
    Run 5: $0=2, $1=0.6, $repetition=0
    Run 6: $0=5, $1=0.2, $repetition=0
    Run 7: $0=5, $1=0.4, $repetition=0
    ...
    Run 19: $0=40, $1=0.4, $repetition=0
    Run 20: $0=40, $1=0.6, $repetition=0
    Run 21: $0=50, $1=0.2, $repetition=0
    Run 22: $0=50, $1=0.4, $repetition=0
    Run 23: $0=50, $1=0.6, $repetition=0
    

    Any of these runs can be executed by passing the '-r <runnumber>' option to Cmdenv. So, the task is now to run the simulation program 24 times, with '-r' running from 0 through 23:

    $ aloha -u Cmdenv -c AlohaStudy -r 0
    $ aloha -u Cmdenv -c AlohaStudy -r 1
    $ aloha -u Cmdenv -c AlohaStudy -r 2
    ...
    $ aloha -u Cmdenv -c AlohaStudy -r 23
    

    This batch can be executed either from the OMNeT++ IDE (where you are prompted to pick an executable and an ini file, choose the configuration from a list, and just click Run), or using a little command-line batch execution tool (opp_runall) supplied with OMNeT++.

    Actually, it is also possible to get Cmdenv execute all runs in one go, by simply omitting the '-r' option.

    $ aloha -u Cmdenv -c AlohaStudy
    
    `\opp` Discrete Event Simulation
    Preparing for running configuration AlohaStudy, run #0...
    ...
    Preparing for running configuration AlohaStudy, run #1...
    ...
    ...
    Preparing for running configuration AlohaStudy, run #23...
    

    However, this approach is not recommended, because it is more susceptible to C++ programming errors in the model. (For example, if any of the runs crashes, the whole batch is terminated -- which may not be what the user wants).

    Let us get back to the ini file. We had:

    [Config AlohaStudy]
    *.numHosts = ${1, 2, 5, 10..50 step 10}
    **.host[*].generationInterval = exponential( ${0.2, 0.4, 0.6}s )
    

    The ${...} syntax specifies an iteration. It is sort of a macro: at each run, the whole ${...} string gets textually replaced with the current iteration value. The values to iterate over do not need to be numbers (unless you want to use the "a..b" or "a..b step c" syntax), and the substitution takes place even inside string constants. So, the following examples are all valid (note that textual substitution is used):

    *.param = 1 + ${1e-6, 1/3, sin(0.5)}
        ==> *.param = 1 + 1e-6
            *.param = 1 + 1/3
            *.param = 1 + sin(0.5)
    *.greeting = "We will simulate ${1,2,5} host(s)."
        ==> *.greeting = "We will simulate 1 host(s)."
            *.greeting = "We will simulate 2 host(s)."
            *.greeting = "We will simulate 5 host(s)."
    

    To write a literal ${..} inside a string constant, quote "{" with a backslash, and write "$\{..}".

    8.5.2 Named iteration variables

    You can assign names to iteration variables, which has the advantage that you'll see meaningful names instead of $0 and $1 in the Cmdenv output, and also lets you refer to the variables at more than one place in the ini file. The syntax is ${<varname>=<iteration>}, and variables can be referred to simply as ${<varname>}:

    [Config Aloha]
    *.numHosts = ${N=1, 2, 5, 10..50 step 10}
    **.host[*].generationInterval = exponential( ${mean=0.2, 0.4, 0.6}s )
    **.greeting = "There are ${N} hosts"
    

    The scope of the variable name is the section that defines it, plus sections based on that section (via extends=).

    There are also a number of predefined variables: ${configname} and ${runnumber} with the obvious meanings; ${network} is the name of the network that is simulated; ${processid} and ${datetime} expand to the OS process id of the simulation and the time it was started; and there are some more: ${runid}, ${iterationvars} and ${repetition}.

    ${runid} holds the Run ID. When a simulation is run, it gets assigned a Run ID, which uniquely identifies that instance of running the simulation: if you run the same thing again, it will get a different Run ID. Run ID is a concatenation of several variables like ${configname}, ${runnumber}, ${datetime} and ${processid}. This yields an identifier that is unique "enough" for all practical purposes, yet it is meaningful for humans. The Run ID is recorded into result files written during the simulation, and can be used to match vectors and scalars written by the same simulation run.

    In cases when not all combinations of the iteration variables make sense or need to be simulated, it is possible to specify an additional constraint expression. This expression is interpreted as a conditional (an "if" statement) within the innermost loop, and it must evaluate to "true" for the variable combination to generate a run. The expression should be given with the constraint= configuration key. An example:

    *.numNodes = ${n=10..100 step 10}
    **.numNeighbors = ${m=2..10 step 2}
    constraint = $m <= sqrt($n)
    

    The expression syntax supports most C language operators (including boolean, conditional and binary shift operations) and most <math.h> functions; data types are boolean, double and string. The expression must evaluate to a boolean.

    8.5.3 Repeating runs with different seeds

    It is directly supported to perform several runs with the same parameters but different random number seeds. There are two configuration keys related to this: repeat= and seed-set=. The first one simple specifies how many times a run needs to be repeated. For example,

    repeat = 10
    

    causes every combination of iteration variables to be repeated 10 times, and the ${repetition} predefined variable holds the loop counter. Indeed, repeat=10 is equivalent of adding ${repetition=0..9} to the ini file. The ${repetition} loop always becomes the innermost loop.

    The seed-set= configuration key affects seed selection. Every simulation uses one or more random number generators (as configured by the num-rngs= key), for which the simulation kernel can automatically generate seeds. The first simulation run may use one set of seeds (seed set 0), the second run may use a second set (seed set 1), and so on. Each set contains as many seeds as there are RNGs configured. All automatic seeds generate random number sequences that are far apart in the RNG's cycle, so they will never overlap during simulations.

    The seed-set= key tells the simulation kernel which seed set to use. It can be set to a concrete number (such as seed-set=0), but it usually does not make sense as it would cause every simulation to run with exactly the same seeds. It is more practical to set it to either ${runnumber} or to ${repetition}. The default setting is ${runnumber}:

    seed-set = ${runnumber}   # this is the default
    

    This makes every simulation run to execute with a unique seed set. The second option is:

    seed-set = ${repetition}
    

    where all $repetition=0 runs will use the same seeds (seed set 0), all $repetition=1 runs use another seed set, $repetition=2 a third seed set, etc.

    To perform runs with manually selected seed sets, you can just define an iteration for the seed-set= key:

    seed-set = ${5,6,8..11}
    

    In this case, the repeat= key should be left out, as seed-set= already defines an iteration and there's no need for an extra loop.

    It is of course also possible to manually specify individual seeds for simulations. This is rarely necessary, but we can use it here to demonstrate another feature, parallel iterators:

    repeat = 4
    seed-1-mt = ${53542, 45732, 47853, 33434 ! repetition}
    seed-2-mt = ${75335, 35463, 24674, 56673 ! repetition}
    seed-3-mt = ${34542, 67563, 96433, 23567 ! repetition}
    

    The meaning of the above is this: in the first repetition, the first column of seeds is chosen, for the second repetition, the second column, etc. The "!" syntax chooses the kth value from the iteration, where k is the position (iteration count) of the iteration variable after the "!". Thus, the above example is equivalent to the following:

    # no repeat= line!
    seed-1-mt = ${seed1 = 53542, 45732, 47853, 33434}
    seed-2-mt = ${        75335, 35463, 24674, 56673 ! seed1}
    seed-3-mt = ${        34542, 67563, 96433, 23567 ! seed1}
    

    That is, the iterators of seed-2-mt and seed-3-mt are advanced in lockstep with the seed1 iteration.

    8.6 Parameter Studies and Result Analysis

    8.6.1 Output vectors and scalars

    In OMNeT++ 3.x, the default result file names were ``omnetpp.vec'' and ``omnetpp.sca''. This is not very convenient for batch execution, where an output vector file created in one run would be overwritten in the next run. Thus, we have changed the default file names to make them differ for every run. The new defaults are:

    output-vector-file = "${resultdir}/${configname}-${runnumber}.vec"
    output-scalar-file = "${resultdir}/${configname}-${runnumber}.sca"
    

    Where ${resultdir} is the value of the result-dir configuration option, and defaults to results/. So the above defaults generate file names like "results/PureAloha-0.vec", "results/PureAloha-1.vec", and so on.

    Also, in OMNeT++ 3.x output scalar files were always appended to by the simulation program, rather than being overwritten. This behavior was changed in 4.0 to make it consistent with vector files, that is, output scalar files are also overwritten by the simulator, and not appended to.

    Although it has nothing to do with our main topic (ini files), this is a good place to mention that the format of result files have been extended to include meta info such as the run ID, network name, all configuration settings, etc. These data make the files more self-documenting, which can be valuable during the result analysis phase, and increase reproducibility of the results. Another change is that vector data are now recorded into the file clustered by the output vectors, which (combined with index files) allows much more efficient processing.

    8.6.2 Configuring output vectors

    As a simulation program is evolving, it is becoming capable of collecting more and more statistics. The size of output vector files can easily reach a magnitude of several ten or hundred megabytes, but very often, only some of the recorded statistics are interesting to the analyst.

    In OMNeT++, you can control how cOutVector objects record data to disk. You can turn output vectors on/off or you can assign a result collection interval. By default, all output vectors are turned on.

    Output vectors can be configured with the following syntax:

    module-pathname.objectname.vector-recording = true/false
    module-pathname.objectname.vector-recording-interval = start1..stop1,
                                                           start2..stop2, ...
    

    Either start or stop can be omitted, to mean the beginning and the end of the simulation, respectively.

    The object name is the string passed to cOutVector in its constructor or with the setName() member function.

    cOutVector eed("End-to-End Delay");
    

    Start and stop values can be any time specification accepted in NED and config files (e.g. 10h 30m 45.2s).

    As with parameter names, wildcards are allowed in the object names and module path names.

    An example:

    [General]
    **.vector-recording-interval = 1s..60s
    **.End-to-End Delay.vector-recording = true
    **.Router2.**.vector-recording = true
    **.vector-recording = false
    

    The above configuration limits collection of all output vectors to the 1s..60s interval, and disables collection of output vectors except all end-to-end delays and the ones in any module called Router2.

    8.6.3 Saving parameters as scalars

    When you are running several simulations with different parameter settings, you'll usually want to refer to selected input parameters in the result analysis as well -- for example when drawing a throughput (or response time) versus load (or network background traffic) plot. Average throughput or response time numbers are saved into the output scalar files, and it is useful for the input parameters to get saved into the same file as well.

    For convenience, OMNeT++ automatically saves the iteration variables into the output scalar file if they have numeric value, so they can be referred to during result analysis.

    Module parameters can also be saved, but this has to be requested by the user, by configuring save-as-scalar=true for the parameters in question. The configuration key is a pattern that identifies the parameter, plus ".save-as-scalar". An example:

    **.host[*].networkLoad.save-as-scalar = true
    

    This looks simple enough. However, there are three pitfalls: non-numeric parameters, too many matching parameters, and random-valued volatile parameters.

    First, the scalar file only holds numeric results, so non-numeric parameters cannot be recorded -- that will result in a runtime error.

    Second, if wildcards in the pattern match too many parameters, that might unnecessarily increase the size of the scalar file. For example, if the host[] module vector size is 1000 in the example below, then the same value (3) will be saved 1000 times into the scalar file, once for each host.

    **.host[*].startTime = 3
    **.host[*].startTime.save-as-scalar = true  # saves "3" once for each host
    

    Third, recording a random-valued volatile parameter will just save a random number from that distribution. This is rarely what you need, and the simulation kernel will also issue a warning if this happens.

    **.interarrivalTime = exponential(1s)
    **.interarrivalTime.save-as-scalar = true  # wrong: saves random values!
    

    These pitfalls are not rare in practice, so it is usually more convenient to rely on the iteration variables in the result analysis. That is, one can rewrite the above example as

    **.interarrivalTime = exponential( ${mean=1}s )
    

    and refer to the $mean iteration variable instead of the interarrivalTime module parameter(s) during result analysis. save-as-scalar=true is not needed because iteration variables are automatically saved into the result files.

    8.6.4 Experiment-Measurement-Replication

    We have introduced three concepts that are useful for organizing simulation results generated by batch executions or several batches of executions.

    During a simulation study, a person prepares several experiments. The purpose of an experiment is to find out the answer to questions like "how does the number of nodes affect response times in the network?" For an experiment, several measurements are performed on the simulation model, and each measurement runs the simulation model with a different parameter settings. To eliminate the bias introduced by the particular random number stream used for the simulation, several replications of every measurement are run with different random number seeds, and the results are averaged.

    OMNeT++ result analysis tools can take advantage of experiment/measurement/replication labels recorded into result files, and organize simulation runs and recorded output scalars and vectors accordingly on the user interface.

    These labels can be explicitly specified in the ini file using the experiment=, measurement= and replication= config keys. If they are missing, the default is the following:

    experiment = "${configname}"
    measurement = "${iterationvars}"
    replication = "#${repetition},seed-set=<seedset>"
    

    That is, the default experiment label is the configuration name; the measurement label is concatenated from the iteration variables; and the replication label contains the repeat loop variable and for the seed-set. Thus, for our first example the experiment-measurement-replication tree would look like this:

    "PureAloha" Unknown LaTeX command  \textrm --experiment    
      $N=1,$mean=0.2 Unknown LaTeX command  \textrm  -- measurement    
        #0, seed-set=0 Unknown LaTeX command  \textrm  -- replication    
        #1, seed-set=1
        #2, seed-set=2
        #3, seed-set=3
        #4, seed-set=4
      $N=1,$mean=0.4
        #0, seed-set=5
        #1, seed-set=6
        ...
        #4, seed-set=9
      $N=1,$mean=0.6
        #0, seed-set=10
        #1, seed-set=11
        ...
        #4, seed-set=14
      $N=2,$mean=0.2
        ...
      $N=2,$mean=0.4
        ...
        ...
    

    The experiment-measurement-replication labels should be enough to reproduce the same simulation results, given of course that the ini files and the model (NED files and C++ code) haven't changed.

    Every instance of running the simulation gets a unique run ID. We can illustrate this by listing the corresponding run IDs under each repetition in the tree. For example:

    "PureAloha"
      $N=1,$mean=0.2
        #0, seed-set=0
          PureAloha-0-20070704-11:38:21-3241
          PureAloha-0-20070704-11:53:47-3884
          PureAloha-0-20070704-16:50:44-4612
        #1, seed-set=1
          PureAloha-1-20070704-16:50:55-4613
        #2, seed-set=2
          PureAloha-2-20070704-11:55:23-3892
          PureAloha-2-20070704-16:51:17-4615
          ...
    

    The tree shows that ("PureAloha", "$N=1,$mean=0.2", "#0, seed-set=0") was run three times. The results produced by these three executions should be identical, unless, for example, some parameter was modified in the ini file, or a bug got fixed in the C++ code.

    We believe that the default way of generating experiment-measurement-replication labels will be useful and sufficient in the majority of the simulation studies. However, you can customize it if needed. For example, here is a way to join two configurations into one experiment:

    [Config PureAloha_Part1]
    experiment = "PureAloha"
    ...
    [Config PureAloha_Part2]
    experiment = "PureAloha"
    ...
    

    Measurement and replication labels can be customized in a similar way, making use of named iteration variables, ${repetition}, ${runnumber} and other predefined variables. One possible benefit is to customize the generated measurement and replication labels. For example:

    [Config PureAloha_Part1]
    measurement = "${N} hosts, exponential(${mean}) packet generation interval"
    

    One should be careful with the above technique though, because if some iteration variables are left out of the measurement labels, runs with all values of those variables will be grouped together to the same replications.

    8.7 Configuring the random number generators

    The random number architecture of OMNeT++ was already outlined in section [6.4]. Here we'll cover the configuration of RNGs in omnetpp.ini.

    8.7.1 Number of RNGs

    The num-rngs= configuration option sets the number of random number generator instances (i.e. random number streams) available for the simulation model (see [6.4]). Referencing an RNG number greater or equal to this number (from a simple module or NED file) will cause a runtime error.

    8.7.2 RNG choice

    The rng-class= configuration option sets the random number generator class to be used. It defaults to "cMersenneTwister", the Mersenne Twister RNG. Other available classes are "cLCG32" (the "legacy" RNG of OMNeT++ 2.3 and earlier versions, with a cycle length of 231-2), and "cAkaroaRNG" (Akaroa's random number generator, see section [9.5]).

    8.7.3 RNG mapping

    The RNG numbers used in simple modules may be arbitrarily mapped to the actual random number streams (actual RNG instances) from omnetpp.ini. The mapping allows for great flexibility in RNG usage and random number streams configuration -- even for simulation models which were not written with RNG awareness.

    RNG mapping may be specified in omnetpp.ini. The syntax of configuration entries is the following.

    [General]
    <modulepath>.rng-N = M  # where N,M are numeric, M < num-rngs
    

    This maps module-local RNG N to physical RNG M. The following example maps all gen module's default (N=0) RNG to physical RNG 1, and all noisychannel module's default (N=0) RNG to physical RNG 2.

    [General]
    num-rngs = 3
    **.gen[*].rng-0 = 1
    **.noisychannel[*].rng-0 = 2
    

    This mapping allows variance reduction techniques to be applied to OMNeT++ models, without any model change or recompilation.

    8.7.4 Automatic seed selection

    Automatic seed selection gets used for an RNG if you don't explicitly specify seeds in omnetpp.ini. Automatic and manual seed selection can co-exist: for a particular simulation, some RNGs can be configured manually, and some automatically.

    The automatic seed selection mechanism uses two inputs: the run number and the RNG number. For the same run number and RNG number, OMNeT++ always selects the same seed value for any simulation model. If the run number or the RNG number is different, OMNeT++ does its best to choose different seeds which are also sufficiently apart in the RNG's sequence so that the generated sequences don't overlap.

    The run number can be specified either in in omnetpp.ini (e.g. via the cmdenv-runs-to-execute= entry) or on the command line:

    ./mysim -r 1
    ./mysim -r 2
    ./mysim -r 3
    

    For the cMersenneTwister random number generator, selecting seeds so that the generated sequences don't overlap is easy, due to the extremely long sequence of the RNG. The RNG is initialized from the 32-bit seed value seed = runNumber*numRngs + rngNumber. (This implies that simulation runs participating in the study should have the same number of RNGs set).

    For the cLCG32 random number generator, the situation is more difficult, because the range of this RNG is rather short (231-1, about 2 billion). For this RNG, OMNeT++ uses a table of 256 pre-generated seeds, equally spaced in the RNG's sequence. Index into the table is calculated with the runNumber*numRngs + rngNumber formula. Care should be taken that one doesn't exceed 256 with the index, or it will wrap and the same seeds will be used again. It is best not to use the cLCG32 at all -- cMersenneTwister is superior in every respect.

    8.7.5 Manual seed configuration

    In some cases you may want manually configure seed values. Reasons for doing that may be that you want to use variance reduction techniques, or you may want to use the same seeds for several simulation runs.

    To manually set seeds for the Mersenne Twister RNG, use the seed-k-mt option, where k is the RNG index. An example:

    [General]
    num-rngs = 3
    seed-0-mt = 12
    seed-1-mt = 9
    seed-2-mt = 7
    

    For the now obsolete cLCG32 RNG, the name of the corresponding option is seed-k-lcg32, and OMNeT++ provides a standalone program called opp_lcg32_seedtool to generate good seed values that are sufficiently apart in the RNG's sequence.



    9 Running Simulations

    9.1 Introduction

    This chapter describes how to run simulations. It covers basic usage, user interfaces, batch runs, how to use Akaroa, and also explains how to solve the most common errors.

    9.1.1 Running a simulation executable

    By default, the output of an opp_makemake-generated makefile is a simulation executable that can be run directly. In simple cases, this executable can be run without command-line arguments, but usually one will need to specify options to specify what ini file to use, which user interface to activate, where to load NED files from, and so on.

    Getting help

    The following sections describe the most frequently used command-line options. To get a complete list of supported command line options, run a simulation executable (or opp_run) with the -h option.

    $ ./fifo -h
    

    Specifying ini files

    The default ini file is omnetpp.ini, and is loaded if no other ini file is given on the command line.

    Ini files can be specified both as plain arguments and with the -f option, so the following two commands are equivalent:

    $ ./fifo experiment.ini common.ini
    $ ./fifo -f experiment.ini -f common.ini
    

    Multiple ini files can be given, and their contents will get merged. This allows for partitioning the configuration into separate files, for example to simulation options, module parameters and result recording options.

    Specifying the NED path

    NED files are loaded from directories listed on the NED path. More precisely, they are loaded from the listed directories and their whole subdirectory trees. Directories are separated with a semicolon (;).

    The NED path can be specified in several ways:

    NED path resolution rules are as follows:

    Selecting a user interface

    OMNeT++ simulations can be run under different user interfaces. Currently the following user interfaces are supported:

    You would typically test and debug your simulation under Tkenv, then run actual simulation experiments from the command line or shell script, using Cmdenv. Tkenv is also better suited for educational or demonstration purposes.

    Both Tkenv and Cmdenv are provided in the form of a library, and you may choose between them by linking one or both into your simulation executable. (Creating the executable was described in chapter [7]). Both user interfaces are supported on Unix and Windows platforms.

    You can choose which runtime environment is included in your simulation executable when you generate your makefile. By default both Tkenv and Cmdenv is linked in so you can choose between them during runtime, but it is possible to specify only a single user interface with the -u Cmdenv or -u Tkenv option on the opp_makemake command line. This can be useful if you intend to run your simulations on a machine where Tcl/Tk is not installed.

    By default, Tkenv will be used if both runtime environment is present in your executable, but you can override this with the user-interface=Cmdenv in your ini file or by specifying -u Cmdenv on the command line. If both the config option and the command line option are present, the command line option takes precedence.

    Selecting a configuration and run number

    Configurations can be selected with the -c <configname> command line option. If you do not specify the configuration and you are running under:

    User interfaces may support the -r runnumber option to select runs, either one or more, depending on the type of the user interface.

    There are several command line options to get information about the iteration variables and the number of runs in the configurations:

    Loading extra libraries

    OMNeT++ allows you to load shared libraries at runtime. This means that you can create simulation models as a shared library and load the model later into a different executable without the need to explicitly link against that library. This approach has several advantages.