Configuration and Attributes¶
In ns-3 simulations, there are two main aspects to configuration:
The simulation topology and how objects are connected.
The values used by the models instantiated in the topology.
This chapter focuses on the second item above: how the many values in use in ns-3 are organized, documented, and modifiable by ns-3 users. The ns-3 attribute system is also the underpinning of how traces and statistics are gathered in the simulator.
In the course of this chapter we will discuss the various ways to set or modify the values used by ns-3 model objects. In increasing order of specificity, these are:
Method |
Scope |
---|---|
Default Attribute values set when
Attributes are defined in
|
Affect all instances of the class. |
|
Affect all future instances. |
|
Affects all instances created with the factory. |
Helper methods with (string/ AttributeValue) parameter pairs |
Affects all instances created by the helper. |
|
Alters this particular instance. Generally this is the only form which can be scheduled to alter an instance once the simulation is running. |
By “specificity” we mean that methods in later rows in the table override the values set by, and typically affect fewer instances than, earlier methods.
Before delving into details of the attribute value system, it will help to
review some basic properties of class Object
.
Object Overview¶
ns-3 is fundamentally a C++ object-based system. By this we mean that new C++ classes (types) can be declared, defined, and subclassed as usual.
Many ns-3 objects inherit from the Object
base class. These
objects have some additional properties that we exploit for organizing the
system and improving the memory management of our objects:
“Metadata” system that links the class name to a lot of meta-information about the object, including:
The base class of the subclass,
The set of accessible constructors in the subclass,
The set of “attributes” of the subclass,
Whether each attribute can be set, or is read-only,
The allowed range of values for each attribute.
Reference counting smart pointer implementation, for memory management.
ns-3 objects that use the attribute system derive from either
Object
or ObjectBase
. Most ns-3 objects we
will discuss derive from Object
, but a few that are outside
the smart pointer memory management framework derive from
ObjectBase
.
Let’s review a couple of properties of these objects.
Smart Pointers¶
As introduced in the ns-3 tutorial, ns-3 objects are memory managed by a
reference counting smart pointer implementation, class Ptr
.
Smart pointers are used extensively in the ns-3 APIs, to avoid passing references to heap-allocated objects that may cause memory leaks. For most basic usage (syntax), treat a smart pointer like a regular pointer:
Ptr<WifiNetDevice> nd = ...;
nd->CallSomeFunction ();
// etc.
So how do you get a smart pointer to an object, as in the first line of this example?
CreateObject¶
As we discussed above in Memory management and class Ptr, at the
lowest-level API, objects of type Object
are not instantiated
using operator new
as usual but instead by a templated function called
CreateObject ()
.
A typical way to create such an object is as follows:
Ptr<WifiNetDevice> nd = CreateObject<WifiNetDevice> ();
You can think of this as being functionally equivalent to:
WifiNetDevice* nd = new WifiNetDevice ();
Objects that derive from Object
must be allocated on the heap
using CreateObject ()
. Those deriving from ObjectBase
,
such as ns-3 helper functions and packet headers and trailers,
can be allocated on the stack.
In some scripts, you may not see a lot of CreateObject ()
calls
in the code; this is because there are some helper objects in effect
that are doing the CreateObject ()
calls for you.
TypeId¶
ns-3 classes that derive from class Object
can include
a metadata class called TypeId
that records meta-information
about the class, for use in the object aggregation and component manager
systems:
A unique string identifying the class.
The base class of the subclass, within the metadata system.
The set of accessible constructors in the subclass.
A list of publicly accessible properties (“attributes”) of the class.
Object Summary¶
Putting all of these concepts together, let’s look at a specific
example: class Node
.
The public header file node.h
has a declaration that includes
a static GetTypeId ()
function call:
class Node : public Object
{
public:
static TypeId GetTypeId (void);
...
This is defined in the node.cc
file as follows:
TypeId
Node::GetTypeId (void)
{
static TypeId tid = TypeId ("ns3::Node")
.SetParent<Object> ()
.SetGroupName ("Network")
.AddConstructor<Node> ()
.AddAttribute ("DeviceList",
"The list of devices associated to this Node.",
ObjectVectorValue (),
MakeObjectVectorAccessor (&Node::m_devices),
MakeObjectVectorChecker<NetDevice> ())
.AddAttribute ("ApplicationList",
"The list of applications associated to this Node.",
ObjectVectorValue (),
MakeObjectVectorAccessor (&Node::m_applications),
MakeObjectVectorChecker<Application> ())
.AddAttribute ("Id",
"The id (unique integer) of this Node.",
TypeId::ATTR_GET, // allow only getting it.
UintegerValue (0),
MakeUintegerAccessor (&Node::m_id),
MakeUintegerChecker<uint32_t> ())
;
return tid;
}
Consider the TypeId
of the ns-3 Object
class
as an extended form of run time type information (RTTI). The C++ language
includes a simple kind of RTTI in order to support dynamic_cast
and
typeid
operators.
The SetParent<Object> ()
call in the definition above is used in
conjunction with our object aggregation mechanisms to allow safe up- and
down-casting in inheritance trees during GetObject ()
.
It also enables subclasses to inherit the Attributes of their parent class.
The AddConstructor<Node> ()
call is used in conjunction
with our abstract object factory mechanisms to allow us to construct
C++ objects without forcing a user to know the concrete class of
the object she is building.
The three calls to AddAttribute ()
associate a given string
with a strongly typed value in the class. Notice that you must provide
a help string which may be displayed, for example, via command line
processors. Each Attribute
is associated with mechanisms
for accessing the underlying member variable in the object (for example,
MakeUintegerAccessor ()
tells the generic Attribute
code how to get to the node ID above). There are also “Checker” methods which
are used to validate values against range limitations, such as maximum
and minimum allowed values.
When users want to create Nodes, they will usually call some form of
CreateObject ()
,:
Ptr<Node> n = CreateObject<Node> ();
or more abstractly, using an object factory, you can create a
Node
object without even knowing the concrete C++ type:
ObjectFactory factory;
const std::string typeId = "ns3::Node'';
factory.SetTypeId (typeId);
Ptr<Object> node = factory.Create <Object> ();
Both of these methods result in fully initialized attributes being available
in the resulting Object
instances.
We next discuss how attributes (values associated with member variables or
functions of the class) are plumbed into the above TypeId
.
Attributes¶
The goal of the attribute system is to organize the access of internal member objects of a simulation. This goal arises because, typically in simulation, users will cut and paste/modify existing simulation scripts, or will use higher-level simulation constructs, but often will be interested in studying or tracing particular internal variables. For instance, use cases such as:
“I want to trace the packets on the wireless interface only on the first access point.”
“I want to trace the value of the TCP congestion window (every time it changes) on a particular TCP socket.”
“I want a dump of all values that were used in my simulation.”
Similarly, users may want fine-grained access to internal variables in the simulation, or may want to broadly change the initial value used for a particular parameter in all subsequently created objects. Finally, users may wish to know what variables are settable and retrievable in a simulation configuration. This is not just for direct simulation interaction on the command line; consider also a (future) graphical user interface that would like to be able to provide a feature whereby a user might right-click on an node on the canvas and see a hierarchical, organized list of parameters that are settable on the node and its constituent member objects, and help text and default values for each parameter.
Available AttributeValue Types¶
AddressValue
AttributeContainerValue
BooleanValue
BoxValue
CallbackValue
DataRateValue
DoubleValue
EmptyAttributeValue
EnumValue
IntegerValue
Ipv4AddressValue
Ipv4MaskValue
Ipv6AddressValue
Ipv6PrefixValue
LengthValue
Mac16AddressValue
Mac48AddressValue
Mac64AddressValue
ObjectFactoryValue
ObjectPtrContainerValue
PairValue<A, B>
PointerValue
PriomapValue
QueueSizeValue
RectangleValue
SsidValue
TimeValue
TupleValue<Args…>
A TupleValue is capable of storing values of different types, hence it is suitable for structured data. A prominent example is the ChannelSettings attribute of WifiPhy, which consists of channel number, channel width, PHY band and primary 20 MHz channel index. In this case the values have to be mutually consistent, which makes it difficult to set them as individual Attributes. Capturing them in a TupleValue simplifies this problem, see
src/wifi/model/wifi-phy.cc
.Values stored in a TupleValue object can be set/get through a std::tuple object or can be serialized to/deserialized from a string containing a comma-separated sequence of the values enclosed in a pair of curly braces (e.g., “{36, 20, BAND_5GHZ, 0}”).
The usage of the TupleValue attribute is illustrated in
src/core/test/tuple-value-test-suite.cc
.TypeIdValue
UanModesListValue
UintegerValue
Vector2DValue
Vector3DValue
WaypointValue
WifiModeValue
Defining Attributes¶
We provide a way for users to access values deep in the system, without having
to plumb accessors (pointers) through the system and walk pointer chains to get
to them. Consider a class QueueBase
that has a member variable
m_maxSize
controlling the depth of the queue.
If we look at the declaration of QueueBase
, we see
the following:
class QueueBase : public Object {
public:
static TypeId GetTypeId (void);
...
private:
...
QueueSize m_maxSize; //!< max queue size
...
};
QueueSize
is a special type in ns-3 that allows size
to be represented in different units:
enum QueueSizeUnit
{
PACKETS, /**< Use number of packets for queue size */
BYTES, /**< Use number of bytes for queue size */
};
class QueueSize
{
...
private:
...
QueueSizeUnit m_unit; //!< unit
uint32_t m_value; //!< queue size [bytes or packets]
};
Finally, the class DropTailQueue
inherits from this base
class and provides the semantics that packets that are submitted to
a full queue will be dropped from the back of the queue (“drop tail”).
/**
* \ingroup queue
*
* \brief A FIFO packet queue that drops tail-end packets on overflow
*/
template <typename Item>
class DropTailQueue : public Queue<Item>
Let’s consider things that a user may want to do with the value of
m_maxSize
:
Set a default value for the system, such that whenever a new
DropTailQueue
is created, this member is initialized to that default.Set or get the value on an already instantiated queue.
The above things typically require providing Set ()
and Get ()
functions, and some type of global default value.
In the ns-3 attribute system, these value definitions and accessor function
registrations are moved into the TypeId
class; e.g.:
NS_OBJECT_ENSURE_REGISTERED (QueueBase);
TypeId
QueueBase::GetTypeId (void)
{
static TypeId tid = TypeId ("ns3::DropTailQueue")
.SetParent<Queue> ()
.SetGroupName ("Network")
...
.AddAttribute ("MaxSize",
"The max queue size",
QueueSizeValue (QueueSize ("100p")),
MakeQueueSizeAccessor (&QueueBase::SetMaxSize,
&QueueBase::GetMaxSize),
MakeQueueSizeChecker ())
...
;
return tid;
}
The AddAttribute ()
method is performing a number of things for the
m_maxSize
value:
Binding the (usually private) member variable
m_maxSize
to a public string"MaxSize"
.Providing a default value (0 packets).
Providing some help text defining the meaning of the value.
Providing a “Checker” (not used in this example) that can be used to set bounds on the allowable range of values.
The key point is that now the value of this variable and its default value are
accessible in the attribute namespace, which is based on strings such as
"MaxSize"
and TypeId
name strings. In the next section,
we will provide an example script that shows how users may manipulate
these values.
Note that initialization of the attribute relies on the macro
NS_OBJECT_ENSURE_REGISTERED (QueueBase)
being called; if you leave this
out of your new class implementation, your attributes will not be initialized
correctly.
While we have described how to create attributes, we still haven’t described how
to access and manage these values. For instance, there is no globals.h
header file where these are stored; attributes are stored with their classes.
Questions that naturally arise are how do users easily learn about all of the
attributes of their models, and how does a user access these attributes, or
document their values as part of the record of their simulation?
Detailed documentation of the actual attributes defined for a type, and a global list of all defined attributes, are available in the API documentation. For the rest of this document we are going to demonstrate the various ways of getting and setting attribute values.
Setting Default Values¶
Config::SetDefault and CommandLine¶
Let’s look at how a user script might access a specific attribute value.
We’re going to use the
src/point-to-point/examples/main-attribute-value.cc
script for illustration, with some details stripped out. The main
function begins:
// This is a basic example of how to use the attribute system to
// set and get a value in the underlying system; namely, the maximum
// size of the FIFO queue in the PointToPointNetDevice
//
int
main (int argc, char *argv[])
{
// Queues in ns-3 are objects that hold items (other objects) in
// a queue structure. The C++ implementation uses templates to
// allow queues to hold various types of items, but the most
// common is a pointer to a packet (Ptr<Packet>).
//
// The maximum queue size can either be enforced in bytes ('b') or
// packets ('p'). A special type called the ns3::QueueSize can
// hold queue size values in either unit (bytes or packets). The
// queue base class ns3::QueueBase has a MaxSize attribute that can
// be set to a QueueSize.
// By default, the MaxSize attribute has a value of 100 packets ('100p')
// (this default can be observed in the function QueueBase::GetTypeId)
//
// Here, we set it to 80 packets. We could use one of two value types:
// a string-based value or a QueueSizeValue value
Config::SetDefault ("ns3::QueueBase::MaxSize", StringValue ("80p"));
// The below function call is redundant
Config::SetDefault ("ns3::QueueBase::MaxSize", QueueSizeValue (QueueSize (QueueSizeUnit::PACKETS, 80)));
The main thing to notice in the above are the two equivalent calls to
Config::SetDefault ()
. This is how we set the default value
for all subsequently instantiated DropTailQueue
s. We illustrate
that two types of Value
classes, a StringValue
and
a QueueSizeValue
class, can be used to assign the value
to the attribute named by “ns3::QueueBase::MaxSize”.
It is also possible to manipulate Attributes using the CommandLine
;
we saw some examples early in the ns-3 Tutorial. In particular, it is
straightforward to add a shorthand argument name, such as --maxSize
,
for an Attribute that is particular relevant for your model, in this case
"ns3::QueueBase::MaxSize"
. This has the additional feature that
the help string for the Attribute will be printed as part of the usage
message for the script. For more information see
the CommandLine
API documentation.
// Allow the user to override any of the defaults and the above
// SetDefaults() at run-time, via command-line arguments
// For example, via "--ns3::QueueBase::MaxSize=80p"
CommandLine cmd;
// This provides yet another way to set the value from the command line:
cmd.AddValue ("maxSize", "ns3::QueueBase::MaxSize");
cmd.Parse (argc, argv);
Now, we will create a few objects using the low-level API. Our
newly created queues will not have m_maxSize
initialized to
0 packets, as defined in the QueueBase::GetTypeId ()
function, but to 80 packets, because of what we did above with
default values.:
Ptr<Node> n0 = CreateObject<Node> ();
Ptr<PointToPointNetDevice> net0 = CreateObject<PointToPointNetDevice> ();
n0->AddDevice (net0);
Ptr<Queue<Packet> > q = CreateObject<DropTailQueue<Packet> > ();
net0->AddQueue(q);
At this point, we have created a single Node
(n0
)
and a single PointToPointNetDevice
(net0
),
added a DropTailQueue
(q
) to net0
,
which will be configured with a queue size limit of 80 packets.
As a final note, the Config::Set…() functions will throw an error if the targeted Attribute does not exist at the path given. There are also “fail-safe” versions, Config::Set…FailSafe(), if you can’t be sure the Attribute exists. The fail-safe versions return true if at least one instance could be set.
Constructors, Helpers and ObjectFactory¶
Arbitrary combinations of attributes can be set and fetched from the helper and low-level APIs; either from the constructors themselves:
Ptr<GridPositionAllocator> p =
CreateObjectWithAttributes<GridPositionAllocator>
("MinX", DoubleValue (-100.0),
"MinY", DoubleValue (-100.0),
"DeltaX", DoubleValue (5.0),
"DeltaY", DoubleValue (20.0),
"GridWidth", UintegerValue (20),
"LayoutType", StringValue ("RowFirst"));
or from the higher-level helper APIs, such as:
mobility.SetPositionAllocator
("ns3::GridPositionAllocator",
"MinX", DoubleValue (-100.0),
"MinY", DoubleValue (-100.0),
"DeltaX", DoubleValue (5.0),
"DeltaY", DoubleValue (20.0),
"GridWidth", UintegerValue (20),
"LayoutType", StringValue ("RowFirst"));
We don’t illustrate it here, but you can also configure an
ObjectFactory
with new values for specific attributes.
Instances created by the ObjectFactory
will have those
attributes set during construction. This is very similar to using
one of the helper APIs for the class.
To review, there are several ways to set values for attributes for class instances to be created in the future:
Config::SetDefault ()
CommandLine::AddValue ()
CreateObjectWithAttributes<> ()
Various helper APIs
But what if you’ve already created an instance, and you want
to change the value of the attribute? In this example, how can we
manipulate the m_maxSize
value of the already instantiated
DropTailQueue
? Here are various ways to do that.
Changing Values¶
SmartPointer¶
Assume that a smart pointer (Ptr
) to a relevant network device
is in hand; in the current example, it is the net0
pointer.
One way to change the value is to access a pointer to the underlying queue and modify its attribute.
First, we observe that we can get a pointer to the (base class)
Queue
via the
PointToPointNetDevice
attributes, where it is called
"TxQueue"
:
PointerValue ptr;
net0->GetAttribute ("TxQueue", ptr);
Ptr<Queue<Packet> > txQueue = ptr.Get<Queue<Packet> > ();
Using the GetObject ()
function, we can perform a safe downcast
to a DropTailQueue
. The NS_ASSERT checks that the pointer is
valid.
Ptr<DropTailQueue<Packet> > dtq = txQueue->GetObject <DropTailQueue<Packet> > ();
NS_ASSERT (dtq != 0);
Next, we can get the value of an attribute on this queue. We have introduced
wrapper Value
classes for the underlying data types, similar
to Java wrappers around these types, since the attribute system stores values
serialized to strings, and not disparate types. Here, the attribute value
is assigned to a QueueSizeValue
, and the Get ()
method on this value produces the (unwrapped) QueueSize
. That is,
the variable limit is written into by the GetAttribute method.:
QueueSizeValue limit;
dtq->GetAttribute ("MaxSize", limit);
NS_LOG_INFO ("1. dtq limit: " << limit.Get ());
Note that the above downcast is not really needed; we could have gotten
the attribute value directly from txQueue
:
txQueue->GetAttribute ("MaxSize", limit);
NS_LOG_INFO ("2. txQueue limit: " << limit.Get ());
Now, let’s set it to another value (60 packets). Let’s also make use of the StringValue shorthand notation to set the size by passing in a string (the string must be a positive integer suffixed by either the p or b character).
txQueue->SetAttribute ("MaxSize", StringValue ("60p"));
txQueue->GetAttribute ("MaxSize", limit);
NS_LOG_INFO ("3. txQueue limit changed: " << limit.Get ());
Config Namespace Path¶
An alternative way to get at the attribute is to use the configuration namespace. Here, this attribute resides on a known path in this namespace; this approach is useful if one doesn’t have access to the underlying pointers and would like to configure a specific attribute with a single statement.
Config::Set ("/NodeList/0/DeviceList/0/TxQueue/MaxSize",
StringValue ("25p"));
txQueue->GetAttribute ("MaxSize", limit);
NS_LOG_INFO ("4. txQueue limit changed through namespace: "
<< limit.Get ());
The configuration path often has the form of
".../<container name>/<index>/.../<attribute>/<attribute>"
to refer to a specific instance by index of an object in the container.
In this case the first container is the list of all Node
s;
the second container is the list of all NetDevice
s on
the chosen Node
. Finally, the configuration path usually
ends with a succession of member attributes, in this case the "MaxSize"
attribute of the "TxQueue"
of the chosen NetDevice
.
We could have also used wildcards to set this value for all nodes and all net
devices (which in this simple example has the same effect as the previous
Config::Set ()
):
Config::Set ("/NodeList/*/DeviceList/*/TxQueue/MaxSize",
StringValue ("15p"));
txQueue->GetAttribute ("MaxSize", limit);
NS_LOG_INFO ("5. txQueue limit changed through wildcarded namespace: "
<< limit.Get ());
If you run this program from the command line, you should see the following output corresponding to the steps we took above:
$ ./ns3 run main-attribute-value
1. dtq limit: 80p
2. txQueue limit: 80p
3. txQueue limit changed: 60p
4. txQueue limit changed through namespace: 25p
5. txQueue limit changed through wildcarded namespace: 15p
Object Name Service¶
Another way to get at the attribute is to use the object name service facility.
The object name service allows us to add items to the configuration
namespace under the "/Names/"
path with a user-defined name string.
This approach is useful if one doesn’t have access to the underlying
pointers and it is difficult to determine the required concrete configuration
namespace path.
Names::Add ("server", n0);
Names::Add ("server/eth0", net0);
...
Config::Set ("/Names/server/eth0/TxQueue/MaxPackets", UintegerValue (25));
Here we’ve added the path elements "server"
and "eth0"
under
the "/Names/"
namespace, then used the resulting configuration path
to set the attribute.
See Object names for a fuller treatment of the ns-3 configuration namespace.
Implementation Details¶
Value Classes¶
Readers will note the TypeValue
classes which are subclasses of the
AttributeValue
base class. These can be thought of as
intermediate classes which are used to convert from raw types to the
AttributeValue
s that are used by the attribute system.
Recall that this database is holding objects of many types serialized
to strings. Conversions to this type can either be done using
an intermediate class (such as IntegerValue
,
or DoubleValue
for floating point numbers)
or via strings. Direct implicit conversion of types to
AttributeValue
is not really practical.
So in the above, users have a choice of using strings or values:
p->Set ("cwnd", StringValue ("100")); // string-based setter
p->Set ("cwnd", IntegerValue (100)); // integer-based setter
The system provides some macros that help users declare and define new AttributeValue subclasses for new types that they want to introduce into the attribute system:
ATTRIBUTE_HELPER_HEADER
ATTRIBUTE_HELPER_CPP
See the API documentation for these constructs for more information.
Initialization Order¶
Attributes in the system must not depend on the state of any other Attribute in
this system. This is because an ordering of Attribute initialization is not
specified, nor enforced, by the system. A specific example of this can be seen
in automated configuration programs such as ConfigStore
.
Although a given model may arrange it so that Attributes are initialized in a
particular order, another automatic configurator may decide independently to
change Attributes in, for example, alphabetic order.
Because of this non-specific ordering, no Attribute in the system may have any dependence on any other Attribute. As a corollary, Attribute setters must never fail due to the state of another Attribute. No Attribute setter may change (set) any other Attribute value as a result of changing its value.
This is a very strong restriction and there are cases where Attributes must set
consistently to allow correct operation. To this end we do allow for consistency
checking when the attribute is used (cf. NS_ASSERT_MSG
or NS_ABORT_MSG
).
In general, the attribute code to assign values to the underlying class member
variables is executed after an object is constructed. But what if you need the
values assigned before the constructor body executes, because you need them in
the logic of the constructor? There is a way to do this, used for example in the
class ConfigStore
: call ObjectBase::ConstructSelf ()
as
follows:
ConfigStore::ConfigStore ()
{
ObjectBase::ConstructSelf (AttributeConstructionList ());
// continue on with constructor.
}
Beware that the object and all its derived classes must also implement
a GetInstanceTypeId ()
method. Otherwise
the ObjectBase::ConstructSelf ()
will not be able to read
the attributes.
Adding Attributes¶
The ns-3 system will place a number of internal values under the attribute system, but undoubtedly users will want to extend this to pick up ones we have missed, or to add their own classes to the system.
There are three typical use cases:
Making an existing class data member accessible as an Attribute, when it isn’t already.
Making a new class able to expose some data members as Attributes by giving it a TypeId.
Creating an
AttributeValue
subclass for a new class so that it can be accessed as an Attribute.
Existing Member Variable¶
Consider this variable in TcpSocket
:
uint32_t m_cWnd; // Congestion window
Suppose that someone working with TCP wanted to get or set the value of that
variable using the metadata system. If it were not already provided by ns-3,
the user could declare the following addition in the runtime metadata system (to
the GetTypeId()
definition for TcpSocket
):
.AddAttribute ("Congestion window",
"Tcp congestion window (bytes)",
UintegerValue (1),
MakeUintegerAccessor (&TcpSocket::m_cWnd),
MakeUintegerChecker<uint16_t> ())
Now, the user with a pointer to a TcpSocket
instance
can perform operations such as
setting and getting the value, without having to add these functions explicitly.
Furthermore, access controls can be applied, such as allowing the parameter to
be read and not written, or bounds checking on the permissible values can be
applied.
New Class TypeId¶
Here, we discuss the impact on a user who wants to add a new class to ns-3. What additional things must be done to enable it to hold attributes?
Let’s assume our new class, called ns3::MyMobility
,
is a type of mobility model. First, the class should inherit from
its parent class, ns3::MobilityModel
.
In the my-mobility.h
header file:
namespace ns3 {
class MyMobility : public MobilityModel
{
This requires we declare the GetTypeId ()
function.
This is a one-line public function declaration:
public:
/**
* Register this type.
* \return The object TypeId.
*/
static TypeId GetTypeId (void);
We’ve already introduced what a TypeId
definition will look like
in the my-mobility.cc
implementation file:
NS_OBJECT_ENSURE_REGISTERED (MyMobility);
TypeId
MyMobility::GetTypeId (void)
{
static TypeId tid = TypeId ("ns3::MyMobility")
.SetParent<MobilityModel> ()
.SetGroupName ("Mobility")
.AddConstructor<MyMobility> ()
.AddAttribute ("Bounds",
"Bounds of the area to cruise.",
RectangleValue (Rectangle (0.0, 0.0, 100.0, 100.0)),
MakeRectangleAccessor (&MyMobility::m_bounds),
MakeRectangleChecker ())
.AddAttribute ("Time",
"Change current direction and speed after moving for this delay.",
TimeValue (Seconds (1.0)),
MakeTimeAccessor (&MyMobility::m_modeTime),
MakeTimeChecker ())
// etc (more parameters).
;
return tid;
}
If we don’t want to subclass from an existing class, in the header file
we just inherit from ns3::Object
, and in the object file
we set the parent class to ns3::Object
with
.SetParent<Object> ()
.
Typical mistakes here involve:
Not calling
NS_OBJECT_ENSURE_REGISTERED ()
Not calling the
SetParent ()
method, or calling it with the wrong type.Not calling the
AddConstructor ()
method, or calling it with the wrong type.Introducing a typographical error in the name of the
TypeId
in its constructor.Not using the fully-qualified C++ typename of the enclosing C++ class as the name of the
TypeId
. Note that"ns3::"
is required.
None of these mistakes can be detected by the ns-3 codebase, so users are advised to check carefully multiple times that they got these right.
New AttributeValue Type¶
From the perspective of the user who writes a new class in the system and wants
it to be accessible as an attribute, there is mainly the matter of writing the
conversions to/from strings and attribute values. Most of this can be
copy/pasted with macro-ized code. For instance, consider a class declaration
for Rectangle
in the src/mobility/model
directory:
Header File¶
/**
* \brief a 2d rectangle
*/
class Rectangle
{
...
double xMin;
double xMax;
double yMin;
double yMax;
};
One macro call and two operators, must be added below the class declaration in
order to turn a Rectangle into a value usable by the Attribute
system:
std::ostream &operator << (std::ostream &os, const Rectangle &rectangle);
std::istream &operator >> (std::istream &is, Rectangle &rectangle);
ATTRIBUTE_HELPER_HEADER (Rectangle);
Implementation File¶
In the class definition (.cc
file), the code looks like this:
ATTRIBUTE_HELPER_CPP (Rectangle);
std::ostream &
operator << (std::ostream &os, const Rectangle &rectangle)
{
os << rectangle.xMin << "|" << rectangle.xMax << "|" << rectangle.yMin << "|"
<< rectangle.yMax;
return os;
}
std::istream &
operator >> (std::istream &is, Rectangle &rectangle)
{
char c1, c2, c3;
is >> rectangle.xMin >> c1 >> rectangle.xMax >> c2 >> rectangle.yMin >> c3
>> rectangle.yMax;
if (c1 != '|' ||
c2 != '|' ||
c3 != '|')
{
is.setstate (std::ios_base::failbit);
}
return is;
}
These stream operators simply convert from a string representation of the
Rectangle ("xMin|xMax|yMin|yMax"
) to the underlying Rectangle. The modeler
must specify these operators and the string syntactical representation of an
instance of the new class.
ConfigStore¶
Values for ns-3 attributes can be stored in an ASCII or XML text file
and loaded into a future simulation run. This feature is known as the
ns-3 ConfigStore. The ConfigStore
is a specialized database for attribute values and default values.
Although it is a separately maintained module in the
src/config-store/
directory, we document it here because of its
sole dependency on ns-3 core module and attributes.
We can explore this system by using an example from
src/config-store/examples/config-store-save.cc
.
First, all users of the ConfigStore
must include
the following statement:
#include "ns3/config-store-module.h"
Next, this program adds a sample object ConfigExample
to show how the system is extended:
class ConfigExample : public Object
{
public:
static TypeId GetTypeId (void) {
static TypeId tid = TypeId ("ns3::A")
.SetParent<Object> ()
.AddAttribute ("TestInt16", "help text",
IntegerValue (-2),
MakeIntegerAccessor (&A::m_int16),
MakeIntegerChecker<int16_t> ())
;
return tid;
}
int16_t m_int16;
};
NS_OBJECT_ENSURE_REGISTERED (ConfigExample);
Next, we use the Config subsystem to override the defaults in a couple of ways:
Config::SetDefault ("ns3::ConfigExample::TestInt16", IntegerValue (-5));
Ptr<ConfigExample> a_obj = CreateObject<ConfigExample> ();
NS_ABORT_MSG_UNLESS (a_obj->m_int16 == -5,
"Cannot set ConfigExample's integer attribute via Config::SetDefault");
Ptr<ConfigExample> a2_obj = CreateObject<ConfigExample> ();
a2_obj->SetAttribute ("TestInt16", IntegerValue (-3));
IntegerValue iv;
a2_obj->GetAttribute ("TestInt16", iv);
NS_ABORT_MSG_UNLESS (iv.Get () == -3,
"Cannot set ConfigExample's integer attribute via SetAttribute");
The next statement is necessary to make sure that (one of) the objects
created is rooted in the configuration namespace as an object instance.
This normally happens when you aggregate objects to a ns3::Node
or ns3::Channel
instance,
but here, since we are working at the core level, we need to create a
new root namespace object:
Config::RegisterRootNamespaceObject (a2_obj);
Writing¶
Next, we want to output the configuration store. The examples show how
to do it in two formats, XML and raw text. In practice, one should perform
this step just before calling Simulator::Run ()
to save the
final configuration just before running the simulation.
There are three Attributes that govern the behavior of the ConfigStore:
"Mode"
, "Filename"
, and "FileFormat"
. The Mode (default "None"
)
configures whether ns-3 should load configuration from a previously saved file
(specify "Mode=Load"
) or save it to a file (specify "Mode=Save"
).
The Filename (default ""
) is where the ConfigStore should read or write
its data. The FileFormat (default "RawText"
) governs whether
the ConfigStore format is plain text or Xml ("FileFormat=Xml"
)
The example shows:
Config::SetDefault ("ns3::ConfigStore::Filename", StringValue ("output-attributes.xml"));
Config::SetDefault ("ns3::ConfigStore::FileFormat", StringValue ("Xml"));
Config::SetDefault ("ns3::ConfigStore::Mode", StringValue ("Save"));
ConfigStore outputConfig;
outputConfig.ConfigureDefaults ();
outputConfig.ConfigureAttributes ();
// Output config store to txt format
Config::SetDefault ("ns3::ConfigStore::Filename", StringValue ("output-attributes.txt"));
Config::SetDefault ("ns3::ConfigStore::FileFormat", StringValue ("RawText"));
Config::SetDefault ("ns3::ConfigStore::Mode", StringValue ("Save"));
ConfigStore outputConfig2;
outputConfig2.ConfigureDefaults ();
outputConfig2.ConfigureAttributes ();
Simulator::Run ();
Simulator::Destroy ();
Note the placement of these statements just prior to the
Simulator::Run ()
statement. This output logs all of the
values in place just prior to starting the simulation (i.e. after
all of the configuration has taken place).
After running, you can open the output-attributes.txt
file and see:
...
default ns3::ErrorModel::IsEnabled "true"
default ns3::RateErrorModel::ErrorUnit "ERROR_UNIT_BYTE"
default ns3::RateErrorModel::ErrorRate "0"
default ns3::RateErrorModel::RanVar "ns3::UniformRandomVariable[Min=0.0|Max=1.0]"
default ns3::BurstErrorModel::ErrorRate "0"
default ns3::BurstErrorModel::BurstStart "ns3::UniformRandomVariable[Min=0.0|Max=1.0]"
default ns3::BurstErrorModel::BurstSize "ns3::UniformRandomVariable[Min=1|Max=4]"
default ns3::PacketSocket::RcvBufSize "131072"
default ns3::PcapFileWrapper::CaptureSize "65535"
default ns3::PcapFileWrapper::NanosecMode "false"
default ns3::SimpleNetDevice::PointToPointMode "false"
default ns3::SimpleNetDevice::TxQueue "ns3::DropTailQueue<Packet>"
default ns3::SimpleNetDevice::DataRate "0bps"
default ns3::PacketSocketClient::MaxPackets "100"
default ns3::PacketSocketClient::Interval "+1000000000.0ns"
default ns3::PacketSocketClient::PacketSize "1024"
default ns3::PacketSocketClient::Priority "0"
default ns3::ConfigStore::Mode "Save"
default ns3::ConfigStore::Filename "output-attributes.txt"
default ns3::ConfigStore::FileFormat "RawText"
default ns3::ConfigExample::TestInt16 "-5"
global SimulatorImplementationType "ns3::DefaultSimulatorImpl"
global SchedulerType "ns3::MapScheduler"
global RngSeed "1"
global RngRun "1"
global ChecksumEnabled "false"
value /$ns3::ConfigExample/TestInt16 "-3"
In the above, several of the default values for attributes for the core
and network modules are shown. Then, all the values for the ns-3 global values
are recorded. Finally, the value of the instance of ConfigExample
that was rooted in the configuration namespace is shown. In a real
ns-3 program, many more models, attributes, and defaults would be shown.
An XML version also exists in output-attributes.xml
:
<?xml version="1.0" encoding="UTF-8"?>
<ns3>
<default name="ns3::ErrorModel::IsEnabled" value="true"/>
<default name="ns3::RateErrorModel::ErrorUnit" value="ERROR_UNIT_BYTE"/>
<default name="ns3::RateErrorModel::ErrorRate" value="0"/>
<default name="ns3::RateErrorModel::RanVar" value="ns3::UniformRandomVariable[Min=0.0|Max=1.0]"/>
<default name="ns3::BurstErrorModel::ErrorRate" value="0"/>
<default name="ns3::BurstErrorModel::BurstStart" value="ns3::UniformRandomVariable[Min=0.0|Max=1.0]"/>
<default name="ns3::BurstErrorModel::BurstSize" value="ns3::UniformRandomVariable[Min=1|Max=4]"/>
<default name="ns3::PacketSocket::RcvBufSize" value="131072"/>
<default name="ns3::PcapFileWrapper::CaptureSize" value="65535"/>
<default name="ns3::PcapFileWrapper::NanosecMode" value="false"/>
<default name="ns3::SimpleNetDevice::PointToPointMode" value="false"/>
<default name="ns3::SimpleNetDevice::TxQueue" value="ns3::DropTailQueue<Packet>"/>
<default name="ns3::SimpleNetDevice::DataRate" value="0bps"/>
<default name="ns3::PacketSocketClient::MaxPackets" value="100"/>
<default name="ns3::PacketSocketClient::Interval" value="+1000000000.0ns"/>
<default name="ns3::PacketSocketClient::PacketSize" value="1024"/>
<default name="ns3::PacketSocketClient::Priority" value="0"/>
<default name="ns3::ConfigStore::Mode" value="Save"/>
<default name="ns3::ConfigStore::Filename" value="output-attributes.xml"/>
<default name="ns3::ConfigStore::FileFormat" value="Xml"/>
<default name="ns3::ConfigExample::TestInt16" value="-5"/>
<global name="SimulatorImplementationType" value="ns3::DefaultSimulatorImpl"/>
<global name="SchedulerType" value="ns3::MapScheduler"/>
<global name="RngSeed" value="1"/>
<global name="RngRun" value="1"/>
<global name="ChecksumEnabled" value="false"/>
<value path="/$ns3::ConfigExample/TestInt16" value="-3"/>
</ns3>
This file can be archived with your simulation script and output data.
Reading¶
Next, we discuss configuring simulations via a stored input configuration file. There are a couple of key differences compared to writing the final simulation configuration. First, we need to place statements such as these at the beginning of the program, before simulation configuration statements are written (so the values are registered before being used in object construction).
Config::SetDefault ("ns3::ConfigStore::Filename", StringValue ("input-defaults.xml"));
Config::SetDefault ("ns3::ConfigStore::Mode", StringValue ("Load"));
Config::SetDefault ("ns3::ConfigStore::FileFormat", StringValue ("Xml"));
ConfigStore inputConfig;
inputConfig.ConfigureDefaults ();
Next, note that loading of input configuration data is limited to Attribute default (i.e. not instance) values, and global values. Attribute instance values are not supported because at this stage of the simulation, before any objects are constructed, there are no such object instances around. (Note, future enhancements to the config store may change this behavior).
Second, while the output of ConfigStore
state
will list everything in the database, the input file need only contain
the specific values to be overridden. So, one way to use this class
for input file configuration is to generate an initial configuration
using the output ("Save"
) "Mode"
described above, extract from
that configuration file only the elements one wishes to change,
and move these minimal elements to a new configuration file
which can then safely be edited and loaded in a subsequent simulation run.
When the ConfigStore
object is instantiated, its attributes
"Filename"
, "Mode"
, and "FileFormat"
must be set,
either via command-line or via program statements.
Reading/Writing Example¶
As a more complicated example, let’s assume that we want to read in a
configuration of defaults from an input file named input-defaults.xml
, and
write out the resulting attributes to a separate file called
output-attributes.xml
.:
#include "ns3/config-store-module.h"
...
int main (...)
{
Config::SetDefault ("ns3::ConfigStore::Filename", StringValue ("input-defaults.xml"));
Config::SetDefault ("ns3::ConfigStore::Mode", StringValue ("Load"));
Config::SetDefault ("ns3::ConfigStore::FileFormat", StringValue ("Xml"));
ConfigStore inputConfig;
inputConfig.ConfigureDefaults ();
//
// Allow the user to override any of the defaults and the above Bind () at
// run-time, viacommand-line arguments
//
CommandLine cmd;
cmd.Parse (argc, argv);
// setup topology
...
// Invoke just before entering Simulator::Run ()
Config::SetDefault ("ns3::ConfigStore::Filename", StringValue ("output-attributes.xml"));
Config::SetDefault ("ns3::ConfigStore::Mode", StringValue ("Save"));
ConfigStore outputConfig;
outputConfig.ConfigureAttributes ();
Simulator::Run ();
}
ConfigStore use cases (pre- and post-simulation)¶
It is worth stressing that ConfigStore can be used for different purposes, and this is reflected in where in the script ConfigStore is invoked.
The typical use-cases are:
Change an Object default attributes
Inspect/change a specific Object attributes
Inspect the simulation Objects and their attributes
As a matter of fact, some Objects might be created when the simulation starts. Hence, ConfigStore will not “report” their attributes if invoked earlier in the code.
A typical workflow might involve running the simulation, calling ConfigStore
at the end of the simulation (after Simulator::Run ()
and before Simulator::Destroy ()
)
This will show all the attributes in the Objects, both those with default values, and those
with values changed during the simulation execution.
To change these values, you’ll need to either change the default (class-wide) attribute values
(in this case call ConfigStore before the Object creation), or specific object attribute
(in this case call ConfigStore after the Object creation, typically just before Simulator::Run ()
.
ConfigStore GUI¶
There is a GTK-based front end for the ConfigStore. This allows users to use a GUI to access and change variables.
Some screenshots are presented here. They are the result of using GtkConfig on
src/lte/examples/lena-dual-stripe.cc
after Simulator::Run ()
.
To use this feature, one must install libgtk-3-dev
; an example
Ubuntu installation command is:
$ sudo apt-get install libgtk-3-dev
On a MacOS it is possible to install GTK-3 using Homebrew. The installation command is:
$ brew install gtk+3 adwaita-icon-theme
To check whether it is configured or not, check the output of the step:
$ ./ns3 configure --enable-examples --enable-tests
---- Summary of optional NS-3 features:
Python Bindings : enabled
Python API Scanning Support : enabled
NS-3 Click Integration : enabled
GtkConfigStore : not enabled (library 'gtk+-3.0 >= 3.0' not found)
In the above example, it was not enabled, so it cannot be used until a suitable version is installed and:
$ ./ns3 configure --enable-examples --enable-tests
$ ./ns3
is rerun.
Usage is almost the same as the non-GTK-based version, but there
are no ConfigStore
attributes involved:
// Invoke just before entering Simulator::Run ()
GtkConfigStore config;
config.ConfigureDefaults ();
config.ConfigureAttributes ();
Now, when you run the script, a GUI should pop up, allowing you to open menus of attributes on different nodes/objects, and then launch the simulation execution when you are done.
Note that “launch the simulation” means to proceed with the simulation script.
If GtkConfigStore has been called after Simulator::Run ()
the simulation will
not be started again - it will just end.