Draft: Use of nholthaus/units to provide strongly typed units
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"));