Closed StefanoD closed 5 months ago
Thanks for the great questions!
Whenever I encounter complexity, I try to make a habit of asking whether it's essential or accidental. Essential complexity is intrinsic to the problem space, and any possible solution is going to have to deal with it, one way or another. Solutions that try to hide it in some use cases will end up making other use cases much worse. Accidental complexity is an extra cost that you have to pay in order to use one particular solution, but which doesn't help you handle the problem space.
Having two core data types instead of one represents extra complexity. So, is it essential or accidental?
IMO, it's an open question right now! In broader units library discussions, including with both this library and with mp-units, I've often encountered people who are convinced it's accidental and we can get rid of it. It would be really exciting if they turn out to be right --- I remember seeing mp-units go from 4 core types to 2, and it did wonders for that library's usability! But for now, I think it's more likely that this is "essential" complexity, and that attempts to circumvent it would do more harm than good.
The core reason is that there are at least two different concepts that you might have in mind when you talk about a "temperature", and they behave very differently. Consider this example, which provides the following two simple use cases:
A user has a sensor which measures the outside temperature as 5 degrees celsius. Write the C++ source code that converts this to a temperature in Fahrenheit using your proposed units library interfaces.
A user is modeling the climate effects of a catastrophic global temperature rise of 5 degrees celsius. Write the C++ source code that converts this temperature rise to Fahrenheit using your proposed units library interfaces.
The first refers to what Au calls "quantity points" (affine space types), and the second to simple "quantities". If we try to conflate them for "simplicity" --- say, by taking the linear offset into account when converting between Celsius and Fahrenheit --- then we can't simultaneously satisfy both of these simple use cases.
Incidentally, I was recently (2024) in a multi-units-library zoom meeting with others including @nholthaus, and he mentioned that he would have used two separate types if he could have gone back and changed things. (Nic, please correct me if I've mis-remembered or misunderstood what you said!)
With that background out of the way, I'll respond more directly to a few of your questions and comments.
You have an interesting project here and because the nholthaus-units library is stalled in development, we want to port our software from nholthaus-units to your Au library. We have grately simplified the usage of the library for better acceptance in our company and to make also the code simpler. That is,
- we are always using aliases which is shorter
- the underlying type of the aliases is always
double
as this simplifies the usage
This is a great option for many use cases! Au was designed to support this mode of operation from the beginning. Our unit-safe interfaces --- where users always name the unit explicitly at the callsite, on entering or exiting the library --- make this usage pattern safe.
Now, we want to port this to the Au library and my straight-forward solution is this:
using Celsius = au::QuantityD<decltype(au::Celsius{})>; using Kelvin = au::QuantityD<decltype(au::Kelvins{})>; using Fahrenheit = au::QuantityD<decltype(au::Fahrenheit{})>; // Quantity Makers constexpr auto celsius = au::celsius_qty; constexpr auto kelvin = au::kelvins; constexpr auto fahrenheit = au::fahrenheit_qty;
Quick comment: I'd suggest:
using Celsius = au::QuantityD<au::Celsius>;
using Kelvin = au::QuantityD<au::Kelvins>;
using Fahrenheit = au::QuantityD<au::Fahrenheit>;
i.e., Use <au::Celsius>
instead of <decltype(au::Celsius{})>
.
I'd also suggest using aliases that distinguish quantities and points, but that's a separate question. :slightly_smiling_face:
- Can I port the nholthaus code to Au like I have shown above?
Sadly, no. Porting temperatures involves more friction than other units. This is for fundamental reasons rather than incompleteness --- i.e., I can't think of any library feature that I could write that would make this meaningfully easier.
The core thing that's going on here is that the nholthaus temperature types conflate two distinct ideas, which Au distinguishes. We've worked really hard to make the nholthaus -> Au path as frictionless as possible, even to the point where (for most cases) you can pass one library's types to APIs that take the other library's types! But if you're going from a domain that conflates, to one where the ideas are distinct, then there's no way around it: you'll need to clarify which concept each one is. Essential complexity.
If you did do the above as originally written, with quantity instead of quantity point, then it would "work"... but then, you'd always be using quantities, whereas in reality you sometimes want quantities and usually want quantity points. This means, for instance, that any temperature (point) conversions between Celsius and Fahrenheit would give incorrect results.
So, how about the obvious next question --- what would it take to convert nholthaus temperatures to Au? Here are some things we've learned from our experience.
TemperatureDiffD
and TemperatureDiffF
for quantities, and TemperaturePointD
and TemperaturePointF
for points. (Our aliases happen to be dimension-based rather than unit-based. I might make a different decision today, but that's what we did in 2021.) In your case, I'd suggest CelsiusDiff
and CelsiusPoint
, KelvinsDiff
and KelvinsPoint
, etc.
- Are there any limitations regarding calculation with
QuantityD
beyond the limitations which are shown in the Au documentation?
Not that I know of! The main limitation is that if you use quantities, but your use case wants quantity points, you'll get the wrong answer --- so, whatever you do, don't do that!
I have seen one instance where a team tried using Quantity
instead of QuantityPoint
for temperatures in a package, simply because they wanted a particular feature that we hadn't yet implemented for QuantityPoint
. Fortunately, we caught this at an early stage of testing. The fix was just to implement that feature for QuantityPoint
, so they could use what they really needed.
- Does it behave like nholthaus-units?
No, but as discussed, this brings benefits in terms of increased expressivity. We can easily and correctly handle simple calculations that the current nholthaus-units temperature approach cannot represent.
Thanks in advance for your answer! 😊
Always happy to help, and thanks for your interest in Au!
@chiphogg Thanks for your fast and thorough reply!
I think, I get slowly the point. But what shall I do when QuantityPoints
get propagated through compound units?
I. e., shall I use QuantityPoints
for the compound unit MetersPerSecond
?
using MetersPerSecond = au::QuantityPointD<decltype(au::Meters{} / au::Seconds{})>;
// Quantity Makers
constexpr auto metersPerSecond = (au::meters / au::second);
// This does not compile!
MetersPerSecond mps1 = metersPerSecond(1);
It's a good question, with a surprising answer. Turns out, this doesn't happen in practice, because affine space types (such as QuantityPoint
) don't support multiplication or division! These operations wouldn't make much sense for point types. But you can subtract two quantity points, obtaining a quantity, and that is what would participate in multiplication and division.
So, the way to fix your concrete issue would be to change the first line like so (simply using Quantity
):
using MetersPerSecond = au::QuantityD<decltype(au::Meters{} / au::Seconds{})>;
Some people have proposed experimenting with using "quantity point for everything". I can see the motivation, but I suspect that wouldn't be a great user experience in practice. Concretely, I've never made a quantity point type for speed, even though in some sense it might sound appealing (because speed is relative).
Overall, I think quantity point is:
Using QuantityD
for MetersPerSecond
was actually my old solution. It indeed surprises me that the problem which we have with meters, does not appear in compound units with meters.
I will take as it is and try to play around.
Thanks again for your very quick reply and thorough answer! We are happy to found a promising candidate to replace nholthaus.
I will continue with the port on monday, because I have holidays. 🙂
@chiphogg I'm continuing porting and stumbling into the next problem.
As you said, I declared MetersPerSecond
as QuantityD
.
using Meters = au::QuantityPointD<au::Meters>;
using MetersDiff = au::QuantityD<au::Meters>;
using MetersPerSecond = au::QuantityD<decltype(au::Meters{} / au::Seconds{})>;
// Quantity Makers
constexpr auto meters = au::meters_pt;
constexpr auto metersDiff = au::meters;
constexpr auto metersPerSecond = (au::meters / au::second);
MetersPerSecond mps1 = metersPerSecond(1);
Meters m = mps1 * seconds(1); // Does not compile as expected
MetersPerSecond mps2 = meters(50.0) / seconds(10.0); // Does not compile as expected
I know what to do in order to make it compilable, but from a user perspective, I can't naturally calculate like I could calculate with nholthaus or on paper.
I guess there is no good workaround available?
I can tell you how we approach the problem. Basically, we use "quantity" rather than "quantity point" for everything by default, except those use cases where experience has shown it to be likely to cause serious errors. In practice, temperatures are the overwhelmingly most common and important example of the latter.
So, in your case, I'd suggest these replacements.
Meters
-> MetersPt
MetersDiff
-> Meters
meters
-> metersPt
metersDiff
-> meters
Note that this brings your naming schema into greater harmony with Au's, which should reduce friction and confusion!
Wait, so: why did I mention "length" as a canonical "good use" of quantity point in the first place? :sweat_smile:
The reason is that I had a very specific use case in mind: labels for along-path positions. Basically, "mile markers".
I wrote some internal "path" APIs that underpin many of our testing libraries. Here, it's very useful to distinguish along-path positions (essentially, labels) from physical displacements and movements along those paths.
This also helps us deal with neighbouring curved paths, such as lanes on a curved road. Suppose we take one path in the scene as the "mile marker defining" path: we pick a point to call "0", and we label every other point by its true along-path physical distance. Now, for labeling adjacent paths, we can simply take the same label as whatever point on the mile marker defining path is laterally adjacent --- just like mile markers in the real world.
Of course, we can add a physical distance (a quantity) to a mile marker (a quantity point) and get a result, but in this case, we would take this result as a good approximation, rather than exact --- just like driving 2 miles from the 10 mile marker won't put you exactly at the 12 mile marker in the real world. And we can also give our paths APIs that can produce the exact answer when needed --- for example, you can ask the path how far exactly it is from the 10 to 12 mile markers, or where exactly you will end up if you drive 2 miles from the 10 mile marker.
Now (at last) the point. The reason quantity points help here is because they prevent confusion. They force the user to notice whether they are dealing with labeled points, or with physical displacements. It does mean that some use cases won't compile --- but in my experience, the code you have to write to make it compile will tend to be more obviously correct. (For example, instead of passing a displacement as a position, you would have to pass a named along-path point plus that displacement.)
(This approach isn't perfect. For example, I'd love to have a nice way to distinguish quantity points ("labels") from different paths. But it's worked really well for us overall for the last 3 years or so.)
Back to your example: I think you are right to point out that these compiler failures are not helpful. They don't guide the user to write more correct code, they just force them to jump through more hoops. I think the takeaway is that we should use quantity by default for lengths, and use quantity points for lengths only in specialized use cases where they do add that value.
@chiphogg Thanks for your quick reply!
Regarding temperatures your old suggestion stays to use QuantityPoint
per default?
- Distinguish points and quantities in your aliases. For example, those who use aliases at Aurora use TemperatureDiffD and TemperatureDiffF for quantities, and TemperaturePointD and TemperaturePointF for points. (Our aliases happen to be dimension-based rather than unit-based. I might make a different decision today, but that's what we did in 2021.) In your case, I'd suggest CelsiusDiff and CelsiusPoint, KelvinsDiff and KelvinsPoint, etc.
Basically, yep!
I will say that I think there's value in retaining the explicit "diff" and "point" vocabulary words for both. I worry that it would be surprising for users when most "unit named" things give them something that acts-like-a-quantity, but these other "unit named" things give them something that acts-like-a-point.
The way I think of it is that, in principle, every quantity maker should say "diff" or "qty" in its name, and every quantity point maker should say "point" in its name... but, in practice, since most things are quantities, we can just drop that part of the name without risking confusion. And when we come to something like temperatures where the defaults are reversed, then it's time to be explicit.
For extra credit, you could consider doing what we do for the "unadorned" version of our quantity makers for temperatures. See this example for celsius
. By making it explicitly deprecated
, you can ensure that when users try to do the "simple" thing they've come to expect, then they'll get an annotation telling them why it's dangerous, and what to do instead.
I'll close this for now, as I don't think there's any work left to do here, but if I've erred in doing so then please feel free to re-open. :slightly_smiling_face:
Hello guys! 😊
You have an interesting project here and because the nholthaus-units library is stalled in development, we want to port our software from nholthaus-units to your Au library. We have grately simplified the usage of the library for better acceptance in our company and to make also the code simpler. That is,
double
as this simplifies the usageHere one example how we use nholthaus-units:
That is, we are working exclusively with the aliases!
Now, we want to port this to the Au library and my straight-forward solution is this:
After reading your documentation more thoroughly, I've found
QuantityPoint
and temperature units and length units are making use of this. After reading the reason to introduceQuantityPoint
, I came to the conclusion that we actually not only don't need this, it would also be a dealbreaker, because it introduces complexity we actually don't want. That is, we value simplicity more than 100% correctness which has in practice (in our company) not really any value.My questions are:
QuantityD
beyond the limitations which are shown in the Au documentation?Thanks in advance for your answer! 😊