Introducing absolute quantities¶
This post introduces a new abstraction called an absolute quantity. It complements affine space abstractions (point and delta) and will most probably be a new default in the library when we release V3.
Affine space in a nutshell¶
So far mp-units and other quantities and units libraries have been modeling two kinds of abstractions:
-
Points (modeled with
quantity_point
class template)- can be interpolated,
- can be subtracted,
- can't be added,
- can't be multiplied or divided by another quantity or scalar,
- some of them could be specified as non-negative,
- specified relative to some absolute or relative point origin,
- conversion to offset units (e.g., degree Celsius) accounts for offset.
-
Deltas (modeled with
quantity
class template)- can be interpolated,
- can be subtracted,
- can be added,
- can be multiplied and divided by another quantity and scalar,
- may be negative as there is always a chance that we will subtract a larger value from a smaller one,
- don't use point origins in its definition,
- conversion to offset units (e.g., degree Celsius) ignores the offset (just the conversion factor is used).
Info
More information on this subject can be found in the Affine Space chapter.
Issues¶
A current affine space implementation works well for many essential use cases. However, it also produces some issues.
- We can't print quantity points as, at least for today, we do not have the means to properly describe the user-provided origin in the text output.
- Quantity points are hard to use in physical equations to denote not-delta values.
To make both of the above work, a user needs to convert the quantity point to quantity with
either qp.quantity_from_zero()
or qp.quantity_from(some_origin)
member functions.
quantity_point m1(2 * kg);
quantity_point m2(3 * kg);
quantity_point m = m1 + m2.quantity_from_zero();
quantity d_v = 30 * km / h;
quantity E_k = m.quantity_from_zero() * pow<2>(d_v) / 2;
std::cout << "Mass: " << m.quantity_from_zero() << "\n";
std::cout << "Velocity: " << d_v << "\n";
std::cout << "Kinetic energy: " << E_k.in<double>(J) << "\n";
This is quite inconvenient and is a common reason for avoiding quantity point abstraction in many equations in source code where it would be a good fit otherwise.
Absolute quantities¶
Despite the above drawbacks, affine space points are necessary to model some abstractions (e.g., temperatures in degrees Celsius, tared mass measurements, altitudes above some reference point, etc.) and do it really well. Constrained affine space arithmetic (e.g., preventing accidental addition of points) also improves the safety of our programs. This is why it is a valuable abstraction and should be used more even often than now.
To improve the user experience and open the doors for new features in the future, we are considering adding a third abstraction for absolute quantities. In terms of properties, an absolute quantity will lie between points and deltas.
Feature | Point | Absolute | Delta |
---|---|---|---|
Interpolation | |||
Subtraction | |||
Addition | |||
Multiply/Divide | |||
May be non-negative | |||
Relative to origin | Absolute & relative | Measured against nothing/void | |
Can use offset units | |||
Conversion to offset units | With offset | No offset | |
Text output |
As we can see above, absolute quantities have only two limitations, and both are connected to the offset units' usage. They can't use those because they must remain absolute instead of being measured relative to some custom origin.
Absolute quantities could be considered delta quantities that represent a whole -- the entire entity being measured. This is why we can represent a system mass by adding absolute masses of all system elements or a system energy by adding absolute temperatures of all the system elements.
As those are more related to deltas than points, it is impossible to specify their origin points. This also allows us to print them, as we do not need any special text to describe their origin as they are always measured against nothing/void.
Interfaces refactoring¶
As I mentioned in my previous post,
we are seriously considering removing quantity_point
class template and replacing it with
a quantity_spec
point wrapper. For example, quantity_point<isq::altitude[m]>
will become
quantity<point<isq::altitude[m]>>
.
I initially planned quantity<isq::mass>
to be the same as quantity<delta<isq::mass>>
,
but it turns out that deltas probably should not be the default. It is consistent with how
we write physical expressions on paper, right? Delta symbol (∆) is always "verbose"
in physical equations, it would be nice for the C++ code to do the same. So, deltas will always
need to be explicit.
And this brings us to absolute quantities. They should actually be the default we are looking for. This is what we write as quantities in most of the physical equations. This is why we will not need any specifier to denote them.
For example:
inline constexpr struct tare final : relative_point_origin<quantity_point{2 * kg}> {} tare;
quantity<point<isq::mass>> m1(10 * kg); // point quantity with an implicit point origin
quantity<point<isq::mass, tare>> m2 = tare + 8 * kg; // point quantity with an explicit relative point origin
quantity<isq::mass> m3 = 15 * kg; // absolute quantity (e.g., non-negative)
quantity<delta<isq::mass>> m13 = m1 - m3; // delta quantity (e.g., may be negative)
quantity<delta<isq::mass>> m23 = m2 - m3; // delta quantity (e.g., may be negative)
With the above, the initial example may be refactored to:
quantity m1 = 2 * kg;
quantity m2 = 3 * kg;
quantity m = m1 + m2;
quantity d_v = delta<km / h>(30);
quantity E_k = m * pow<2>(d_v) / 2;
std::cout << "Mass: " << m << "\n";
std::cout << "Velocity: " << d_v << "\n";
std::cout << "Kinetic energy: " << E_k.in<double>(J) << "\n";
Arithmetic¶
Affine space arithmetic is well-defined. However, we are adding a new type to the library that lands between points and deltas. This is why we must agree on the arithmetic for all possible combinations.
Addition¶
Absolute quantities are deltas against nothing so adding them to a point yields another point.
Adding a delta to them yields a delta, as a delta may represent only a part of something and a whole, and a part is not a whole. The delta may also be negative and greater than the absolute quantity, which may yield a negative value. This is why delta is a good result here.
Only adding whole non-negative entities of the system yields a system being expressed as an entire non-negative entity. This is why adding absolute quantities results in an absolute quantity.
Lhs \ Rhs | Point | Absolute | Delta |
---|---|---|---|
Point | Point | Point | |
Absolute | Point | Absolute | Delta |
Delta | Point | Delta | Delta |
Subtraction¶
Similarly, during subtraction, regular affine space arithmetics for deltas apply. Subtracting an absolute quantity from a point yields a point, and trying to do the opposite does not make physical sense.
Subtracting two non-negative absolute quantities may yield a negative value if we subtract a larger one from the smaller one, so the result should be a delta. A similar result may be obtained by subtracting a delta from absolute quantity or absolute quantity from a delta.
Lhs \ Rhs | Point | Absolute | Delta |
---|---|---|---|
Point | Delta | Point | Point |
Absolute | Delta | Delta | |
Delta | Delta | Delta |
Info
Based on the above assumptions, one of the lines of the below code can't compile:
Motivating example¶
Let's consider a room with a table and two glasses filled with fluid on top of it. Let's also assume that we want to stack one on top of the other and treat them as a system we observe.
// absolute quantities
quantity<isq::height[cm]> glass1_height = 20 * cm;
quantity<isq::height[cm]> glass2_height = 15 * cm;
// delta quantities
quantity<delta<isq::height>[cm]> fluid_level = 16 * cm;
// point quantities
inline constexpr struct floor_level final : absolute_point_origin<isq::height> floor_level;
quantity<point<isq::height[cm], floor_level>> table_top = floor_level + 1 * m;
// absolute results
quantity system_height = glass1_height + glass2_height;
// delta results
quantity empty_height_res = glass1_height - fluid_level;
quantity glass2_height_res = system_height - glass1_height; // could result in an absolute quantity
assert(glass2_height_res == glass2_height);
quantity height_diff_res = glass2_height - glass1_height; // but this one should definitely return delta
// point results
quantity<point<isq::height[cm], floor_level>> glass1_top = table_top + glass1_height;
quantity point1 = glass1_top - glass1_height;
assert(point1 == table_top);
// quantity point2 = glass1_height - glass1_top; // no sense - does not compile
What about time?¶
Everything looks promising and nice for now. But let's look closer into the quantity of time. There is no way to measure its absolute value as we don't even know where (when?) the time axis starts... Only time points and time deltas (durations) make sense.
The above raises a few questions:
- Does it mean that
quantity<si::seconds>
orquantity<isq::time[s]>
should not compile? - Should we require the user to always state
quantity<delta<si::seconds>>
orquantity<delta<isq::time[s]>>
? This would be consistent with physical equations but more verbose in the source code. - Should the syntax
40 * s
be disallowed or should it implicitly createquantity<delta<si::seconds>>
instead ofquantity<si::seconds>
? - A somehow similar case might be the length quantity as there is no one well-established
zero origin that serves for all length measurements. However, asking the users always
to provide a
delta
specifier for length would probably be an overkill.
As you can see, I do not yet have good answers to the above problems yet. Please feel welcome to share some feedback on this.
New opportunities¶
The new syntax simplifies API as one quantity
class template will now serve all quantity
variations (possibly even more in the future). It also allows us to model quantities that
were impossible to express before without some workarounds.
For example, we can now correctly calculate Carnot engine efficiency with any of the following:
quantity temp_cold = 300. * K;
quantity temp_hot = 500. * K;
quantity carnot_eff_1 = 1. - temp_cold / temp_hot;
quantity carnot_eff_2 = (temp_hot - temp_cold) / temp_hot;
In the above code, we can easily create absolute or delta values of temperatures and do arithmetics on them. Previously, we had to create deltas from both points artificially with:
quantity temp_cold = point<K>(300.);
quantity temp_hot = point<K>(500.);
quantity carnot_eff_1 = 1. - temp_cold.quantity_from_zero() / temp_hot.quantity_from_zero();
quantity carnot_eff_2 = (temp_hot - temp_cold) / temp_hot.quantity_from_zero();
It worked but was far from being physically pure and pretty.
Summary¶
We believe that adding absolute quantities will be a significant improvement in the library that will allow us to model physical equations in a terser, more correct, easier to write, understand, and maintain way.
We plan to deliver the features mentioned in this post as a part of mp-units V3.
Please share your feedback.