Skip to content

Draft: Use of nholthaus/units to provide strongly typed units

Update (October 13, 2025):

  • Rebased to August 19 version of ns-3-dev
  • Converted src/wifi power units (dBm_u -> dBm_t, dBW_u -> dBW_t, dB_u -> dB_t, dBr_u -> dBr_t, Watt_u -> Watt_t)
  • Added support for dBm_per_Hz_t and dBm_per_MHz_t to units.h (but haven't converted the wifi code yet; see units-example.cc)

This code now compiles and runs tests for wifi module only:

./ns3 configure --enable-modules=wifi --enable-examples --enable-tests && ./ns3 build

Next steps:

  • Fix failing tests (all remaining test failures are unrelated to units conversion (i.e., they are test problems that can be reproduced outside of this branch))
  • Rebase to latest ns-3-dev (and port new code since August 19 snapshot)
  • Convert frequency related units
  • Convert remaining units for Wi-Fi energy model
  • Enable all modules (mesh module needs conversion, and some examples and tests are not covered by --enable-modules=wifi)

Following the above, if successful, we could consider to start modifying other modules (e.g, propagation, spectrum, and any users of those modules).

Summary:

This is proposed for people to consider as a solution to #1042 (alternative to !1887). I wrote an example that generates the following tutorial to aid in review:

Certain quantities in ns-3 could benefit from strongly-typed units:

  • Time (already covered)
  • Frequency
  • Power
  • ...

In this proposal, most units are provided by an imported header-only library See src/core/model/units.h (or search for "nholthaus/units" on GitHub). Class "ns3::Time" would be excepted but nearly everything else could be covered, including replacement of the Boost-like length in src/core. This example provides an overview of what porting to this might be like. To run this example yourself, type:

./ns3 run units-example

Units are defined in the "units" namespace.They can be brought into the current scope via one or more "using" directives, or can be referred to by a fully qualified name:

using namespace units;
using namespace units::literals;
using namespace units::length;
using namespace units::power;

Unit types begin with a lowercase letter and end with an underscore-- different from usual ns-3 naming conventions. Examples:

meter_t distance{8};
units::length::meter_t distance2{10.5};
watt_t transmitPower{1};
units::power::watt_t transmitPower2{2.5};

If you import the "units::literals" namespace, you can use literal syntax:

auto distance3 = 8_m;
NS_ASSERT_MSG(distance3 == distance, "Distance values are not equal");

The underlying type of all of these units is the C++ double. You can extract this value using the to() method:

auto converted = distance3.to<double>();
std::cout << Converted distance is " << converted << " m" << std::endl;

Converted distance is 8 m

One of the key features is that expressions with incompatible types will not compile. For example:

// will fail with: error: Units are not compatible.
auto sum = distance2 + transmitPower2;

and:

double doubleValue{4};
// will fail with: error: Cannot add units with different linear/non-linear scales.
auto sumDouble = distance3 + doubleValue;

Another feature is that arithmetic operations on different units with the same underlying conceptual type (e.g., length) will work as expected, even if the units differ. Below, we add one variable initialized to 8 m with one initialized to 8 km:

auto distance4{8_km};
std::cout << "Sum of distances is " << distance3 + distance4 << std::endl

Sum of distances is 8008 m

In ns-3, handling of power values with linear and log scale is important. The units library supports power quantities like watts (_w) and milliwatts (_milliwatt) as well as the logarithmic variants (_dBw, _dBm).

milliwatt_t txPwr{100}; // 100 mW
std::cout << "  txPwr = " << txPwr << std::endl; // should print 100 mW
dBm_t txPwrDbm(txPwr);  // 20 dBm
std::cout << "  txPwrDbm = " << txPwrDbm << std::endl; // should print 20 dBm
dBW_t txPwrDbW(txPwrDbm);  // -10 dBW
std::cout << "  txPwrDbW = " << txPwrDbW << std::endl; // should print -10 dBW

Below are the printouts from the running code:

txPwr = 100 mW
txPwrDbm = 20 dBm
txPwrDbW = -10 dBW

We can add linear power values:

txPwr + txPwr = 200 mW

We can scale linear power values:

txPwr * 2 = 200 mW

We can add logarithmic power values, but the resulting unit is strange:

txPwrDbm + txPwrDbm = 4e-05 m^4 kg^2 s^-6

Note: this is an operation (adding dBm) that we should disallow if we adopt this library.

Adding linear and non-linear values will cause a compile-time error:

dBW_t loss{-20}; // equivalent to 10 mW
std::cout << "loss = " << loss << std::endl; // -20 dBW = 10 mW
#ifdef WONT_COMPILE
std::cout << txPwr - loss  << std::endl; // Won't compile; mixing linear and non-linear
#endif

loss = -20 dBW

We can solve this by converting the logarithmic quantity back to linear:

std::cout << "txPwr - milliwatt_t(loss) = " << txPwr - milliwatt_t(loss)  << std::endl; // OK, should print 90 mW

Yields:

txPwr - milliwatt_t(loss) = 90 mW

Decibel (dB) is available in namespace units::dimensionless. We want to be able to add it to logarithmic power (but not linear power):

dB_t gain{10}
std::cout << "loss (-20 dBW) + gain (10 dB) = " << loss + gain << std::endl
#ifdef WONT_COMPILE
std::cout << "txPwr (100 mW) + gain (10 dB) = " << txPwr + gain << std::endl
#endif

Yields:

loss (-20 dBW) + gain (10 dB) = -10 dBW

We want these types to be available to the ns-3 CommandLine system and as Attribute values. This is possible in the usual way, as demonstrated by the Decibel value (src/core/model/decibel.h).

The things needed to wrap these types are to define "operator>>", and to use the ATTRIBUTE_* macros.

This example program demonstrates the use of a decibel value as a CommandLine argument (--cmdLineDecibel). Passing a plain double value will raise an error about invalid values. Instead, try this:

./ns3 run units-example  -- --cmdLineDecibel=5_dB

The value that you input will be printed below:

cmdLineDecibel = 3 dB

Attribute values will look like the following (see wifi-phy.cc):

Old code:

    .AddAttribute("TxGain",
                  "Transmission gain (dB).",
                  DoubleValue(0.0),
                  MakeDoubleAccessor(&WifiPhy::SetTxGain, &WifiPhy::GetTxGain),
                  MakeDoubleChecker<double>())

New code:

    .AddAttribute("TxGain",
                  "Transmission gain.",
                  DecibelValue(0.0),
                  MakeDecibelAccessor(&WifiPhy::SetTxGain, &WifiPhy::GetTxGain),
                  MakeDecibelChecker())

Client code will look like this (see wifi-phy-ofda-test.cc)

Old code:

  phy->SetAttribute("TxGain", DoubleValue(1.0));

New code:

  phy->SetAttribute("TxGain", DecibelValue(units::dimensionless::dB_t(1)));

Alternative new code (if "using units::dimensionless;" is added):

  phy->SetAttribute("TxGain", DecibelValue(dB_t(1)));

Alternative new code (using the StringValue alternative):

  phy->SetAttribute("TxGain", StringValue("1_dB"));

The WifiPhy attribute RxGain is declared to be a 'DbValue()' This can be set in several equivalent ways:

  Ptr<WifiPhy> phy = ...
  phy->SetAttribute("RxGain", DbValue{dB_t{2}});
  phy->SetAttribute("RxGain", DbValue{units::dimensionless::dB_t{2}});
  phy->SetAttribute("RxGain", DbValue{2});
  phy->SetAttribute("RxGain", StringValue{"2"});
  phy->SetAttribute("RxGain", StringValue{"2_dB"});
  phy->SetAttribute("RxGain", DoubleValue{2});

Power spectral density units are available as dBm_per_Hz_t and dBm_per_MHz_t. These can be multiplied by frequency quantities to yield total power in dBm:

  // Create power spectral density values
  auto psd1 = dBm_per_Hz_t{-50.0};  // -50 dBm/Hz
  auto psd2 = dBm_per_MHz_t{-20.0}; // -20 dBm/MHz

  // Create frequency values
  auto bandwidth1 = Hz_t{1000.0};     // 1 kHz
  auto bandwidth2 = MHz_t{10.0};   // 10 MHz

  // Multiply to get total power
  auto total_power1 = psd1 * bandwidth1;  // Results in dBm_t
  auto total_power2 = psd2 * bandwidth2;  // Results in dBm_t
  auto total_power3 = bandwidth1 * psd1;  // Also works (commutative)

Running the above code produces:

  psd1 = -50 dBm_per_Hz
  psd2 = -20 dBm_per_MHz
  bandwidth1 = 1000 Hz
  bandwidth2 = 10 MHz
  total_power1 (psd1 * bandwidth1) = -20 dBm
  total_power2 (psd2 * bandwidth2) = -10 dBm
  total_power3 (bandwidth1 * psd1) = -20 dBm
Edited by Peter Barnes

Merge request reports

Loading