mpusz / mp-units

The quantities and units library for C++
https://mpusz.github.io/mp-units/
MIT License
1.1k stars 90 forks source link

V2 `quantity_point` #414

Closed mpusz closed 1 year ago

mpusz commented 1 year ago

I don't think the "offset" should be part of the unit, but rather the quantity (this is the main beef I have with my favourite units library in python, pint, see my comment here). The main argument is that, as soon as units appear in a compound expression, the offset immediately loses it's meaning (think for example the temperature coefficient of a resistor in kOhm/°C; obviously, that can only refer to temperature differences measured in °C, and there is simply no reasonable meaning to the "offset" of that quantity).

That said, it may make sense to define an implicit "offset" implied by the unit when describing a quantity point (e.g. a thermodynamic temperature) without explicitly specifying a reference point.

I also think we should really consider that information the "point of reference", not an "offset" - that one only appears between related quantities. But then, I have never really worked with quantity equations (and I don't have access to the ISO-80000 standard).

In the end, it would be great if the library would not only work for ISO thermodynamic temperatures, but also custom quantity points. Specifically, while the standard only endorses using °C to specify temperatures with respect to the "Celsisus temperature reference point", C++ programs may have a need to work with "unendorsed" combinations for efficiency reasons. As a specific example, we use this library on an embedded device employing a temperature sensor whose output is an 16 bit integer where zero corresponds to the temperature point -45 °C, and an increment of one corresponds to a temperature change of 0.01 °C. With #232, I can fully embed this information in the quantity_point and work with the original representation. However, I can also do computation with a floating point representation where the point of reference is still - 45 °C, but now I use SI °C for the scaling. Or, I can convert to the standard Celsius temperature reference point and continue to work with 0.01 °C increments, all in integer arithmetic.

Another use-case to consider is the glide-computer example, with different ways to express height/elevation, all using units of m and/or km.

_Originally posted by @burnpanck in https://github.com/mpusz/units/pull/391#discussion_r1059739263_

mpusz commented 1 year ago

I also think we should really consider that information the "point of reference", not an "offset"

That is more accurate.

where zero corresponds to the temperature point -45 °C, and an increment of one corresponds to a temperature change of 0.01 °C.

So the formula of this quantity Q should be something like Q = 𝘵 × 0.01 - 45 °C, where 𝘵 is Celsius temperature. By specifying the factor of 0.01 to 𝘵, and the point of reference -45 °C, you should be able to do the same with this V2 framework.

_Originally posted by @JohelEGP in https://github.com/mpusz/units/pull/391#discussion_r1059748011_

mpusz commented 1 year ago

That said, it may make sense to define an implicit "offset" implied by the unit when describing a quantity point (e.g. a thermodynamic temperature) without explicitly specifying a reference point.

In the current design, the quantity_point has access point_origin, quantity_spec, and a unit. In theory, any or all of them can introduce an offset for quantity_point arithmetic needs. It is up to us to specify what has sense and what does not. The problem with handling Celsius via quantity_spec is that with the current design, we cannot easily restrict a specific unit for a specific quantity. So both thermodynamic_temperature and Celsius_temperature can take K and deg_C as a unit. Even if we introduce some way to enforce such restrictions for those specific quantities similar problems will appear with all the derived quantities, like the mentioned temperature coefficient of a resistor in kOhm/°C.

mpusz commented 1 year ago

In the end, it would be great if the library would not only work for ISO thermodynamic temperatures, but also custom quantity points.

Sure, I totally agree. This is what I used for testing but still didn't have enough time to make it an official example or unit test:

  constexpr auto room_reference_temperature = quantity_point{21 * isq::Celsius_temperature[deg_C]};
  constexpr auto temperature_controller_delta = 3 * isq::Celsius_temperature[deg_C];

  constexpr quantity_point<isq::Celsius_temperature[deg_C], room_reference_temperature> room_default{0};
  constexpr quantity_point<isq::Celsius_temperature[deg_C], room_reference_temperature> room_low = room_default - temperature_controller_delta;
  constexpr quantity_point<isq::Celsius_temperature[deg_C], room_reference_temperature> room_high = room_default + temperature_controller_delta;

  static_assert(room_low.relative() == -3 * isq::Celsius_temperature[deg_C]);
  static_assert(room_low.relative() == -3 * isq::thermodynamic_temperature[K]);
  static_assert(room_default.relative() == 0 * isq::Celsius_temperature[deg_C]);
  static_assert(room_default.relative() == 0 * isq::thermodynamic_temperature[K]);
  static_assert(room_high.relative() == 3 * isq::Celsius_temperature[deg_C]);
  static_assert(room_high.relative() == 3 * isq::thermodynamic_temperature[K]);

  static_assert(room_low.absolute() == 18 * isq::Celsius_temperature[deg_C]);
  // static_assert(room_low.absolute() == (273.15 + 18) * isq::thermodynamic_temperature[K]);
  static_assert(room_default.absolute() == 21 * isq::Celsius_temperature[deg_C]);
  // static_assert(room_default.absolute() == (273.15 + 21) * isq::thermodynamic_temperature[K]);
  static_assert(room_high.absolute() == 24 * isq::Celsius_temperature[deg_C]);
  // static_assert(room_high.absolute() == (273.15 + 24) * isq::thermodynamic_temperature[K]);

  static_assert(room_high - room_low == 6 * isq::Celsius_temperature[deg_C]);
  static_assert(room_high - room_low == 6 * isq::thermodynamic_temperature[K]);
  static_assert((room_high - room_low)[K] == 6 * isq::Celsius_temperature[deg_C]);

The commented out code still does not work but I think it should.

mpusz commented 1 year ago

Another example I prepared while working on V2 quantity_point was:

  struct mean_sea_level : absolute_point_origin<isq::height> {};

  constexpr quantity_point<isq::height[m], mean_sea_level{}> ZRH_ground_level{432 * isq::height[m]};
  constexpr quantity_point<isq::height[m], mean_sea_level{}> GDN_ground_level{149 * isq::height[m]};
  constexpr quantity_point<isq::height[m], ZRH_ground_level> ZRH_afe{200 * isq::height[m]};
  constexpr quantity_point<isq::height[m], GDN_ground_level> GDN_afe{450 * isq::height[m]};
  static_assert(ZRH_afe.absolute() == 632 * isq::height[m]);
  static_assert(GDN_afe.absolute() == 599 * isq::height[m]);
  static_assert(GDN_afe - ZRH_afe == -33 * isq::height[m]);
  static_assert(GDN_ground_level - ZRH_ground_level == -283 * isq::height[m]);
  static_assert(ZRH_afe - GDN_ground_level == 483 * isq::height[m]);
mpusz commented 1 year ago

That looks all pretty cool to me, and I especially like directly specifying the reference point as quantity_point NTTP template argument, e.g. as in constexpr quantity_point<isq::height[m], ZRH_ground_level>.

The only place where I tend to disagree are the .absolute() accessors. To me, I lack a clear explanation what that would semantically do. q.relative() is easy: It is the same as q - q.origin_point. q.absolute() doesn't mean anything to me. A potential definition would be q.absolute() == q - q.ultimate_origin_point, and we could reasonably require that ultimate_origin_point is one arbitrary point that is the same for all quantity_points that are interconvertible. While this doesn't fully specify ultimate_origin_point, the only reasonable choice for thermodynamic temperatures would be (thermodynamic) absolute zero, so we would have your commented out variants correct instead, and the currently passing static_asserts would fail instead: room_low.absolute() == isq::thermodynamic_temperature(237.15+18, K) == isq::Celsius_temperature(237.15+18, deg_C). That said, we could also just leave .absolute out of the initial interface. (In fact, given the q.relative() == q - q.origin_point equivalence, we could also leave the .relative out of the interface).

Why cannot we have both X == isq::Celsius_temperature(18, deg_C) and X == isq::thermodynamic_temperature(273.15 + 18, K), for any hypothetical X? It is the same argument why we cannot have Y == isq::radius(2, m) and Y == isq::diameter(1, m) for any hypothetical Y: In both cases, we may interpret the ISO 80000 standard for the two quantities to be "equal". However, as we have seen, it turns out that this is at odds with the mathematical rules of standard operations when applied between quantities that aren't of equal (again; we lack a good name to to describe that equivalence class/concept): If we define isq::radius(2, m) == isq::diameter(1, m), then there will be no reasonable definition of isq::radius(2, m) + isq::length(1, m) that will preserve associativity and commutativity - that being unacceptable, the only option is to disallow it completely. The exact same happens for the temperatures: If we define isq::Celsius_temperature(0, deg_C) == isq::thermodynamic_temperature(273.15, K), then there is no reasonable definition of 2*(isq::Celsius_temperature(0, deg_C) + isq::thermodynamic_temperature(0, K)) (unless you consider 2*isq::Celsius_temperature(0, deg_C) == isq::Celsius_temperature(273.15, deg_C) reasonable). Furthermore, there is no reasonable definition to isq::thermodynamic_temperature(5, K) + imperial::Fahrenheit_temperature(9, deg_F).

For ISO 80000, mixed quantity operations aren't an issue, as it doesn't talk about mathematical operations between quantities at all as far as I know (though I don't know ISO 80000 well). For a library intent on modelling physical relations, it is however. This is why essentially all quantity libraries in all languages I know of discard those special equalities in their "normal mode" and sometimes provide special ways to opt-in into those equalities (e.g. astropy's equivalencies, mp-units' quantity_points, etc...).

_Originally posted by @burnpanck in https://github.com/mpusz/units/pull/391#discussion_r1060416845_

mpusz commented 1 year ago

Here is some more data to understand how it is currently defined:

https://github.com/mpusz/units/blob/2cf736a1e685457adf0e6e4b746f406c16a53c00/src/core/include/mp_units/quantity_point.h#L58-L70

https://github.com/mpusz/units/blob/2cf736a1e685457adf0e6e4b746f406c16a53c00/src/core/include/mp_units/quantity_point.h#L117-L128

mpusz commented 1 year ago

quantity_points have their own compatibility rules. Two quantity points are considered to be compatible if they are specified in terms of the same absolute_point_origin, which determines the beginning of the scale for all measurements. For example, you can do all the arithmetic and comparisons on all quantity_points specified in terms of mean_sea_level absolute point origin. It doesn't matter if mean_sea_level is a point origin of a current quantity_point or if it is the point origin of our point origin. In every case, those are considered compatible. I think that is exactly what you mean by the ultimate_origin_point, right?

absolute_point_origin specifies a value zero on our scale, and I do not care if it is an actual 0 or not. In fact, I do not know what the correct altitude for means_sea_level is, and I shouldn't care. But because of the fact I do not know this specific altitude I cannot subtract a quantity_point defined (potentially transitively) in terms of mean_sea_level with another one of the same quantity but a different absolute origin (i.e., the center of the Sun).

q.relative() is easy: It is the same as q - q.origin_point

I do not think that it should work this way. Every quantity_point stores its relative "distance" from the current origin, which does not have to be the absolute/ultimate one. This allows us to often store much smaller runtime values while a huge offset may be represented as a type. Like in the first example, I would prefer room_high to just store a value 3 rather than 24.

The only place where I tend to disagree are the .absolute() accessors. To me, I lack a clear explanation what that would semantically do

absolute() always provide a "distance" from the absolute_point_origin even if our current quantity_point is specified in terms of another point_origin (i.e. ZRH altitude).

mpusz commented 1 year ago

I believe that a unit should be orthogonal to a quantity_point. 18 * isq::Celsius_temperature[deg_C] and (273.15 + 18) * isq::thermodynamic_temperature[K] specify exactly the same temperature point expressed in different units. This is why I believe that the "offset" of 273.15 should be used only when converting a unit of a specific quantity_point and not be the source to form a totally different hierarchy of quantity_points.

As I stated above I would prefer this offset to be applied to a unit rather than quantity_spec as technically we can express both thermodynamic_temperature and Celsius_temperature in both K and deg_C.

mpusz commented 1 year ago

If we define isq::Celsius_temperature(0, deg_C) == isq::thermodynamic_temperature(273.15, K), then there is no reasonable definition of 2(isq::Celsius_temperature(0, deg_C) + isq::thermodynamic_temperature(0, K)) (unless you consider 2isq::Celsius_temperature(0, deg_C) == isq::Celsius_temperature(273.15, deg_C) reasonable). Furthermore, there is no reasonable definition to isq::thermodynamic_temperature(5, K) + imperial::Fahrenheit_temperature(9, deg_F).

I am not sure if that is true. isq::Celsius_temperature[deg_C](0) should not be the same as isq::thermodynamic_temperature[K](273.15) those are quantities and not quantity_points. For quantities, the following should be true:

static_assert(isq::Celsius_temperature[deg_C](42) == isq::thermodynamic_temperature[K](42));

When we are talking about quantity_points then the following should be true:

quantity_point{isq::Celsius_temperature[deg_C](42)} == quantity_point{isq::thermodynamic_temperature[K](273.15 + 42)}

The above should be implemented by applying an offset while converting a unit/quantity_spec from one to another.

mpusz commented 1 year ago

For ISO 80000, mixed quantity operations aren't an issue, as it doesn't talk about mathematical operations between quantities at all as far as I know (though I don't know ISO 80000 well).

https://www.iso.org/standard/64973.html 😉 Although, I still have to carefully study it...

mpusz commented 1 year ago

Maybe to reword this explicitly:

  1. I believe that the best solution is to specify an offset in the unit definition:

    inline constexpr struct degree_Celsius : named_unit<basic_symbol_text{"°C", "`C"}, kelvin, offset<-mag<273150> * milli<kelvin>>> {} degree_Celsius;
  2. Such offset should be applied only when converting units of quantity_point and not for casting a unit of a quantity.

  3. Trying to follow blindly what ISO 80000 provides and specify offset in the quantity_spec definition is really problematic as we do not mention units in quantity_spec definitions, so there is no way to tell what 273.15 means. Such an offset could be provided as a standalone customization point (i.e. non-member function). The next issue with this approach would be the need to limit K only for thermodynamic_temperature, and deg_C for Celsius_temperature:

    inline constexpr struct kelvin : named_unit<"K", isq::thermodynamic_temperature, not_for<isq::Celsius_temperature>> {} kelvin;
    inline constexpr struct degree_Celsius : named_unit<basic_symbol_text{"°C", "`C"}, kelvin, only_for<isq::Celsius_temperature>> {} degree_Celsius;

    And at this point, we will realize that we have to add the support for the Fahrenheit scale as well... That is a lot of strange hacks to make one quantity work.

Please let me know if I am missing something here or if my logic is incorrect.

burnpanck commented 1 year ago

Ok, many things to discuss here, so I'll start from the top.

burnpanck commented 1 year ago

quantity_points have their own compatibility rules. ... I think that is exactly what you mean by the ultimate_origin_point, right?

Agreed. It's up to us to define those compatibility rules, but I always ask for our C++ objects to model semantics of physical objects. For quantity_point I like to take it to model of a "point" in an abstract space. We may describe that point by agreeing on some reference point, and then use our usual physical quantity concept to describe the distance. There are an infinite number of options to chose a reference point, from which we derive infinite number of descriptions of the very same point; these are equivalent representations of the same semantic point. This is not much different from quantities: We can describe the average height of a human, which is an abstract physical length (a quantity), using either a reference length of "cm", "in" or "m". All those descriptions are different combinations of numbers and units, yet they describe the same physical length (in this case, more specifically a height). This is what we want to model with "compatibility" here, and the algorithm you have described involving a chain of point-origins ending in an ultimate_point_origin == absolute_point_origin does model that correctly.

q.relative() is easy: It is the same as q - q.origin_point

I do not think that it should work this way.

This wasn't intended as an instruction for how it should be implemented. It is just characterisation of the semantics of the q.relative() operation, in that the result should be "as-if". q.relative() is a C++ expression that we made up. We cannot discuss if it is "correct" or not, unless we have a specification of what it should do. By describing that specification using physically relevant concepts (both q and q.origin_point are again semantically "points in an abstract space"), I constrain q.relative() to physically consistent with whatever q.origin_point is specified to (assuming we have agree to that - shall mean "measure" distance projected onto the "positive direction" embodied in that abstract space).

absolute() always provide a "distance" from the absolute_point_origin

This is just q - q.absolute_point_origin in words, which is fine for me. I slightly prefer the name ultimate_point_origin though, because it is the ultimate end of the reference chain, but that point is usually still incidental and not an "absolute", objective origin. There is neither a green line passing from north to south through Greenwich, nor do the cliffs in portugal expose a red horizontal line when the sea is below MSL. It just so happens that we all agree to refer to those lines, but it is actually very difficult to describe the location of that line by phone to a martian.

So far I fully agree with the semantics you have described. In the original post, the disagreement came with the "values" you provided for room_low.absolute(). Let me discuss that further down.

JohelEGP commented 1 year ago

3. The next issue with this approach would be the need to limit K only for thermodynamic_temperature, and deg_C for Celsius_temperature:

Why do this? ISO 80000 permits mixing them.

burnpanck commented 1 year ago

18 * isq::Celsius_temperature[deg_C] and (273.15 + 18) * isq::thermodynamic_temperature[K] specify exactly the same temperature point expressed in different units.

I think you meant quantity_point{18 * isq::Celsius_temperature[deg_C]} and quantity_point{(273.15 + 18) * isq::thermodynamic_temperature[K]} in that statement above. Otherwise it would be a direct contradiction of you follow-up post:

isq::Celsius_temperaturedeg_Cshould not be the same asisq::thermodynamic_temperatureKthose arequantitiesand notquantity_points`.

...

isq::Celsius_temperature[deg_C](42) == isq::thermodynamic_temperature[K](42)

...

quantity_point{isq::Celsius_temperature[deg_C](42)} == quantity_point{isq::thermodynamic_temperature[K](273.15 + 42)}

I agree with all of those. But if room_low.absolute() returns a quantity (the distance between room_low and it's ultimate_origin_point, as we both agree), depending on how we define ultimate_origin_point, the result is either the quantity 18 * isq::Celsius_temperature[deg_C] (== 18 * isq::thermodynamic_temperature[K]) or the quantity (273.15 + 18) * isq::thermodynamic_temperature[K] (== (273.15 + 18) * isq::Celsius_temperature[deg_C]) but neither both - we just agree that they cannot as they are not equal.

If room_low.absolute() instead returned a quantity_point, then whatever representation it would choose, it would be equivalent to room_low itself (although potentially expressed with respect to a different direct reference point).

Either of the two choices is fine with me - just choose one, or leave .absolute() out completely.

burnpanck commented 1 year ago

Where I really disagree is that the "offset" should be implemented as a property of the unit. Semantically, the offset appears in the specification of the quantity "Celsius_temperature" in relation to the quantity "thermodynamic_temperature". This is very similar to the factor relation between the quantities "radius" and "diameter" - in fact, both of those relations can be expressed through the "quantity equation".

We have already established that neither of those relations should be applied implicitly / automatically, because that would be at odds with the rules of mathematical operations involving different quantity types (associativity etc...). Instead, we require the opt-in of the user. astropy provides the concept of explicit equivalencies, and is thereby able to cover a wide range of quantity equations. mp-units and many other units libraries instead provide the quantity_point concept, which only covers offset-type quantity equations, but which nicely provides a coordinate concept that is very useful, as seen in many of your glide computer examples.

About restricting deg_C to Celsius_temperature only, that also has nothing to do with the implementation of the offset. Again, it maps nicely to that quantity "type" concept for which we still lack a name: Similar to how we would like to restrict "Bq" to quantity of "type" (i.e. quantity_spec) describing radioactivity and the unit "Hz" to quantities of "type" ((i.e. quantity_spec) describing frequency, we could also restrict deg_C to Celsisus_temperature. That said, I'm not sure we should, even more so if ISO 80000 doesn't either.

burnpanck commented 1 year ago

If you want to restrict deg_C to Celsius_temperature,

inline constexpr struct degree_Celsius : named_unit<basic_symbol_text{"°C", "`C"}, kelvin, only_for<isq::Celsius_temperature>> {} degree_Celsius;

is actually not a hack at all, but generalises very nicely to

inline constexpr struct Herz : named_unit<"Hz", 1/second, only_for<isq::frequency>> {} Herz;

I like that API.

mpusz commented 1 year ago

Why do this? ISO 80000 permits mixing them.

Yes, I know, and I would like to preserve it. However, if we define an offset on a quantity_spec level rather than a unit (i.e. Celsius temperature to be t =T −T0) then this offset will be applied always no matter what the unit is. This is why I believe that in such a case, we will need to disallow "invalid" combinations here.

This is why I said that I would prefer to specify an offset on a unit level rather than a quantity level like ISO 80000 is doing.

mpusz commented 1 year ago

I think you meant quantity_point{18 isq::Celsius_temperature[deg_C]} and quantity_point{(273.15 + 18) isq::thermodynamic_temperature[K]} in that statement above.

Yes, you are right.

burnpanck commented 1 year ago

then this offset will be applied always no matter what the unit is.

Let us be precise in what that statement could or should mean, before we evaluate if it is correct or not. So far, we haven't looked at "mixed cases" before, so let us try to clarify what semantics we expect for such mixed expressions. Here is what I propose:

static_assert(quantity_point{isq::Celsius_temperature(10, K)} == quantity_point{isq::Celsius_temperature(10, deg_C)});
static_assert(quantity_point{isq::Celsius_temperature(10, K)} == quantity_point{isq::thermodynamic_temperature(283.15, K)});
static_assert(quantity_point{isq::Celsius_temperature(10, deg_C)} == quantity_point{isq::thermodynamic_temperature(283.15, K)}); // not mixed, but kind of the archetype we want to support

This matches the quantity equation interpretation, and this looks way more natural with the relation specified in the quantity_spec rather than the unit. Would you have expected different semantics than those here?

mpusz commented 1 year ago

I like that API.

Yes, only_for was actually invented for frequency (https://github.com/mpusz/units/pull/391#discussion_r1054740536).

mpusz commented 1 year ago

That said, I'm not sure we should, even more so if ISO 80000 doesn't either.

This is not that clear. ISO 80000 explicitly states that the unit of thermodynamic_temperature is K, and the unit of Celsius_temperature is deg_C. However, additionally, in the definition of thermodynamic temperature it says:

Differences of thermodynamic temperatures or changes may be expressed either in kelvin, symbol K, or in de grees Celsius, symbol °C

Also, in the definition of the Celsius temperature, we read:

The unit degree Celsius is a special name for the kelvin for use in stating values of Celsius temperature. The unit degree Celsius is by definition equal in magnitude to the kelvin. A difference or interval of temperature may be expressed in kelvin or in degrees Celsius.

Notice that in both cases it only allows for "differences" to be expressed in either unit, so our quantity type and not a quantity_point. I do not know yet how but we could disallow usage of deg_C for thermodynamic_temperature_point and K for Celsius_temperature_point but then it would be really frustrating to see: Celsius_temperature_point[deg_C] + Celsius_temperature[K].

mpusz commented 1 year ago

or leave .absolute() out completely.

I can agree with that as well. I provided this function only because you had something similar in your PR.

mpusz commented 1 year ago

So far, we haven't looked at "mixed cases" before, so let us try to clarify what semantics we expect for such mixed expressions. Here is what I propose:

static_assert(quantity_point{isq::Celsius_temperature(10, K)} == quantity_point{isq::Celsius_temperature(10, deg_C)});
static_assert(quantity_point{isq::Celsius_temperature(10, K)} == quantity_point{isq::thermodynamic_temperature(283.15, K)});
static_assert(quantity_point{isq::Celsius_temperature(10, deg_C)} == quantity_point{isq::thermodynamic_temperature(283.15, K)}); // not mixed, but kind of the archetype we want to support

Would you have expected different semantics than those here?

I believe that according to ISO 80000 quantity_point{isq::Celsius_temperature(10, K)} and quantity_point{isq::thermodynamic_temperature(283.15, K)} should not compile.

And there is one more case. What should quantity_point<isq::thermodynamic_temperature[K]>{isq::Celsius_temperature(10, deg_C)} or quantity_point<isq::thermodynamic_temperature[K]>{isq::Celsius_temperature(10, K)} do.

mpusz commented 1 year ago

My preferred solution would be to get rid of isq::Celsius_temperature, encode the offsets in the unit of deg_C and deg_F, and apply those wherever I am changing the unit of a quantity_point (but not for quantity).

mpusz commented 1 year ago

For me, isq::Celsius_temperature is somehow artificial and possibly wrongly defined by ISO 80000. We do not have metre_distance and kilometre_distance, right? We also do not have julian_time, gregorian_time, or gps_time as well as quantities. I think that introduction of a separate quantity type only to account for a different origin of its unit was a wrong decision made by ISO at some point. And yes, ISO standards have bugs as well 😉

JohelEGP commented 1 year ago

For me, isq::Celsius_temperature is somehow artificial and possibly wrongly defined by ISO 80000. We do not have metre_distance and kilometre_distance, right? We also do not have julian_time, gregorian_time, or gps_time as well as quantities. I think that introduction of a separate quantity type only to account for a different origin of its unit was a wrong decision.

Indeed. See also

And there's no need to. As you comment, it's even standard endorsed.

I think I know what the source of your worry is. Many quantity equations in ISO-80000 do not adhere to the definition of quantity equation. That includes the one for Celsius temperature, which is dependent on Kelvin.

Many of those quantities are correctly better represented in mp-units master and V2 as constants and unit constants, respectively. We can do a similar improvement of the representation. So while we could have a Celsius temperature quantity, perhaps it'd be better that the factor and offset are not part of the quantity, but the unit. Perhaps with a quantity_spec for units, unit_spec.

-- https://github.com/mpusz/units/pull/391#discussion_r1052330327

burnpanck commented 1 year ago

I think an edit may have corrupted your message. You probably meant

I believe that according to ISO 80000 quantity_point{isq::Celsius_temperature(10, K)} and quantity_point{isq::thermodynamic_temperature(287.15, deg_C)} should not compile.

... but simultaneously isq::thermodynamic_temperature(287.15, deg_C) and isq::Celsius_temperature(10, K) should. This is a possible interpretation of ISO 80000. Implementation may indeed be a bit awkward, but I don't believe this is solved by moving the offset into the unit. Instead, ISO 80000 is explicit in that deq_C may only appear in Celsius_temperature but not in thermodynamic_temperature when used to describe a point, but interchangeably when describing differences. So maybe we would just have to implement not_in<> as well as just_not_in_point<>.

However, I believe there is another interpretation of ISO 80000. I read the C++ expression differently: quantity_point{isq::Celsius_temperature(10, K)} isn't a specification of a quantity point of "type" Celsius_temperature and unit "kelvin". Instead, starts with a difference between two Celsius_temperatures (happened to be expressed in kelvin - but equivalent if it were expressed in deg_C according to ISO 80000), and then re-interpreted as a quantity point by considering that distance to be measured with respect to the origin of that quantity scale.

With this interpretation quantity_point{isq::Celsius_temperature(10, K)} == quantity_point{isq::thermodynamic_temperature(283.15, K)} is correct.

And there is one more case. What should quantity_point<isq::thermodynamic_temperature[K]>{isq::Celsius_temperature(10, deg_C)} or quantity_point<isq::thermodynamic_temperature[K]>{isq::Celsius_temperature(10, K)} do.

Here, I tend to disallow. This still doesn't need any support from the units at all, because the ambiguity arises through the specification of the offset point, as it is embedded in the quantity "type".

My preferred solution would be to get rid of isq::Celsius_temperature

That is essentially what my original implementation of #232 did. However, I started to like that exactly because of the way it clearly and unambiguously indicates the reference point, and I believe this is exactly the reason why ISO 80000 did introduce it in the first place. Without that, the C++ expression quantity_point{...} as a mathematical map doesn't preserve equivalency classes (and thus doesn't model physical semantics): The two thermodynamic temperature differences 10 °C and 10 K are equivalent ("should be able to stand in under all circumstances"), yet applying quantity_point{...} on them outputs two very different physical objects. For example, what should quantity_point{isq::thermodynamic_temperature(10, K) + isq::thermodynamic_temperature(10, deg_C)} return? Because of that, I was very reluctant to implement that constructor at all and preferred explicit origin_points everywhere.

encode the offsets in the unit of deg_C and deg_F, and apply those wherever I am changing the unit of a quantity_point (but not for quantity)

I believe that this is physically wrong. For a quantity_point you only ever need to apply that offset when you change the reference point (as an abstract point on the temperature scale): A quantity point represented as 5 deg C above room temperature converts to 5 kelvin above room temperature, not 278.15 kelvin above room temperature. It is just that when you convert from a "quantity point of 5 deg_C" to a quantity point of 278.15 kelvin", you are actually changing the reference point that you implicitly assume. So again, it has nothing to do with the units, but the reference point. So even though I hate implicit origin points (i.e. not clear - the python zen says "explicit is better than implicit" for a reason), I would still encode that offset in the origin point, not the unit.

As an example why implicit origins are bad, observe that it doesn't solve the quantity_point<isq::thermodynamic_temperature[K]>(isq::thermodynamic_temperature(10, deg_C)) case either.

For me, isq::Celsius_temperature is somehow artificial and possibly wrongly defined by ISO 80000. We do not have metre_distance and kilometre_distance, right?

But we do have radius and diameter. It allows us to declare the "how" we measure the thermodynamic temperature or "how" we measure the width of a cylinder separately and orthogonally from the unit we use to represent the result.

burnpanck commented 1 year ago

That said, I could probably live with getting rid of isq::Celsius_temperature:

This is essentially what #232 does today.

mpusz commented 1 year ago

A quantity point represented as 5 deg C above room temperature converts to 5 kelvin above room temperature, not 278.15 kelvin above room temperature.

"5 deg C above room temperature" is a difference type, so a quantity. It is by definition the same as the quantity of "5 kelvin above room temperature".

It is just that when you convert from a "quantity point of 5 deg_C" to a quantity point of 278.15 kelvin", you are actually changing the reference point that you implicitly assume.

I actually believe that at this stage, both have exactly the same reference physical point (namely absolute zero) and its value depends only on the unit you chose to represent it. I somehow do not agree that the reference point of some physical experiment depends on the unit I choose to measure the temperature.

mpusz commented 1 year ago

As an example why implicit origins are bad, observe that it doesn't solve the quantity_point<isq::thermodynamic_temperature[K]>(isq::thermodynamic_temperature(10, deg_C)) case either.

I actually think this is quite easy to resolve. A quantity of 10 °C is the same as 10 K as it is an offset/difference type. Stating that a new quantity_point is skewed by 10 units from its origin is clear to me as well. The implicit origin point for the temperature is an absolute zero (if a user will not provide anything else explicitly). With that, the above code sets the temperature point 10 units over the absolute zero so, depending on the unit you chose to represent it, either 10 K or -263.15 °C.

mpusz commented 1 year ago

But we do have radius and diameter.

Again, from a physical point of view, I do not think it is the same case as a radius and a diameter. The lengths of both of them are observable and different no matter what unit you chose to measure them. One is always 2x longer than the other.

Temperature difference (a quantity) has the same value for both K and °C. When I go outside without my gloves when there is a temperature point of either -10 °C or 263.15 K my hands will be freezing exactly the same because it is the same temperature point (the same relative temperature to absolute zero).

Another example could be the ice melting point. It is exactly the same temperature point when the ice "wants" to melt no matter if I will express it in K, °C, or °F, right? 😉 Having to specify distinct point origins to represent the same ice melting temperature point depending on a unit of measure feels really bad to me from the physics point of view.

mpusz commented 1 year ago

To express the above with code. I am opposed to having several origins for different temperature units in the code below. I do not think that temperatures are that much different from all other quantities (see the mean_sea_level example above).

constexpr quantity_point<isq::Celsius_temperature[deg_C]> ice_melting_point{0};

static_assert(melting(quantity_point<isq::Celsius_temperature[deg_C], ice_melting_point>{5}));
static_assert(melting(quantity_point<isq::thermodynamic_temperature[K], ice_melting_point>{5}));
static_assert(melting(quantity_point<isq::Celsius_temperature[deg_C]>{5}));
static_assert(melting(quantity_point<isq::thermodynamic_temperature[K]>{278.15}));
static_assert(!melting(quantity_point<isq::Celsius_temperature[deg_C], ice_melting_point>{-5}));
static_assert(!melting(quantity_point<isq::thermodynamic_temperature[K], ice_melting_point>{-5}));
static_assert(!melting(quantity_point<isq::Celsius_temperature[deg_C]>{-5}));
static_assert(!melting(quantity_point<isq::thermodynamic_temperature[K]>{268.15}));
burnpanck commented 1 year ago

We're still in the "no Celsius_temperature" world, so I'll use isq::temperature as short for isq::thermodynamic_temperature.

"5 deg C above room temperature" is a difference type, so a quantity. It is by definition the same as the quantity of "5 kelvin above room temperature".

Let's be precise again. I was talking about a C++ instance qp of type quantity_point<isq::temperature[deg_C],room_temperature,float> whose qp.relative() == isq::temperature(5, deg_C). What would quantity_cast<isq::temperature[K]>(q).relative() return? What would quantity_cast<quantity_point<isq::temperature[K],room_temperature,float>>(qp).relative() return? Both should most definitely return isq::temperature(5, K). Clearly, no offset has been applied.

I actually believe that at this stage, both have exactly the same reference physical point (namely absolute zero) and its value depends only on the unit you chose to represent it. I somehow do not agree that the reference point of some physical experiment depends on the unit I choose to measure the temperature.

Also here, I should have been more precise. We want to map the physical concept of a "quantity point of 5 deg_C" into C++. How would we do that? One proposal would be to use auto qp1 = quantity_point{isq::temperature(5, deg_C)}:. Similarly auto qp2 = quantity_point{isq::temperature(278.15, K)};. With "reference point", I specifically meant qp1.point_origin and qp2.point_origin. Those do depend on the unit we had chosen when we had written our isq::temperature, and they do so in a physically relevant way (static_assert(qp1.point_origin == qp2.point_origin); has to fail). Similarly, the two quantities returned by qp1.relative() and qp2.relative() are physically different. This is why you have to apply the offset when doing a quantity_cast<decltype(qp2)>(qp1); exactly the same as when you do quantity_cast<quantity_point<qp2.point_origin,qp1.units,qp1.representation>>(qp1), even though the unit doesn't change. Of couse qp1.absolute_point_origin == qp2.absolute_point_origin by requirement that the two are equivalent. So we just have to be very clear do we talk about .absolute_point_origin or .point_origin when we say "reference point".

[EDIT:] Strike the following two paragraphs, my mistake. Obviously, both your suggestions are equal.

Stating that a new quantity_point is skewed by 10 units from its origin is clear to me as well. The implicit origin point for the temperature is an absolute zero (if a user will not provide anything else explicitly). With that, the above code sets the temperature point 10 units over the absolute zero so, depending on the unit you chose to represent it, either 10 K or -263.15 °C.

So what is it now? Is quantity_point<isq::temperature[K]>{isq::temperature(10,deg_C)} == quantity_point{isq::temperature(10, K)} or is quantity_point<isq::temperature[K]>{isq::temperature(10,deg_C)} == quantity_point{isq::temperature(-263.15, °C)}? They are definitely not equal. To me it's exactly as ambiguous as before, and should neither be allowed.

It is exactly the same temperature point when the ice "wants" to melt no matter if I will express it in K, °C, or °F, right?

Right, I never said anything else.

Again: What are we discussing:

  1. There is a risk that quantity_point{isq::temperature(10, K)} and quantity_point{isq::temperature(10, deg_C)} lead to surprises, as they are somewhat ill-specified (implicit origin points). Definitely for quantity_point{isq::temperature(10, deg_C) + isq::temperature(10, K)} - if we want to respect symmetry of +, then both quantity points of "20 K" and "293.15 K" are equally valid answers though physically different very different. isq::Celsius_temperature may help here, because contrary to the unit of a quantity, it's "type" has physical meaning, to which we can specify behaviour.
  2. Either way if there is isq::Celsius_temperature or not: The offset has nothing to do with the units, and shouldn't be implemented with them.
burnpanck commented 1 year ago

Thanks for the C++ melting point examples. I agree with all of them. While quantity_point<isq::Celsius_temperature[deg_C]>{-5} could be considered an "implicit origin point" constructor, it is clear and never leads to surprises. So where I really see the problem is with deduced quantity_point{quantity_of<isq::temperature>}-style of constructors. Maybe we should just disallow those.

Then, there is only the issue on the implementation of that "offset" left.

mpusz commented 1 year ago

Both should most definitely return isq::temperature(5, K). Clearly, no offset has been applied.

This is right and correct in both cases because our quantity_point is 5 degrees above a specific point of temperature.

auto qp1 = quantity_point{isq::temperature(5, deg_C)}:

That might be unfortunate but I believe that the above should mean a temperature point being 5 degrees over the absolute zero. The reason for that is the fact that we use an implicit origin point being the absolute zero and we provide a difference type with the value 5.

mpusz commented 1 year ago

@burnpanck, thanks a lot for this discussion as it opened my eyes to some problems as well. For example, my initial example with room temperature was wrong here https://github.com/mpusz/units/issues/414#issuecomment-1369654841 as I used:

constexpr auto room_reference_temperature = quantity_point{21 * isq::Celsius_temperature[deg_C]};

so a temperature my body would definitely not survive 😉

If we want to specify a "quantity point of 5 deg_C" we need to do either:

auto qp1 = quantity_point<isq::temperature[deg_C]>{5};
auto qp2 = quantity_point<isq::temperature[deg_C], ice_melting_point>{isq::temperature[deg_C](5)};

The most interesting point here is that it is not how it is implemented now. For now, the value 5 is used to directly construct a quantity inside of the quantity_point, which I think is wrong and should be fixed.

So where I really see the problem is with deduced quantity_point{quantity_of}-style of constructors

Yes, those might be confusing. We should think if there is a way to improve them or if we should remove them indeed.

JohelEGP commented 1 year ago

[EDIT:] Strike the following two paragraphs, my mistake. Obviously, both your suggestions are equal. I indented them during the edit, because there doesn't seem to be a strike-through in markdown.

See https://github.github.com/gfm/#strikethrough-extension-.

mpusz commented 1 year ago

I think that the main issue is to clearly specify and document the interface of a quantity_point. After the above discussion, I think that so far we have agreed on the following:

  1. Constructor taking a value sets that absolute quantity point no matter what the origin is (i.e. quantity_point<isq::Celsius_temperature[deg_C], any_origin>{5} will always create a temperature point of 5 °C)
  2. Constructor taking a quantity sets the quantity_point at the provided offset from its origin (i.e. quantity_point<isq::Celsius_temperature[deg_C]>{isq::Celsius_temperature[deg_C](5)} will create a temperature point of -268,15 °C)
  3. We identified that the CTAD taking a quantity might be confusing as it is equivalent to (2) (i.e. quantity_point{isq::Celsius_temperature[deg_C](5)} will create a temperature point of -268,15 °C)
  4. qp.relative() always returns a quantity difference from its current origin (i.e. quantity_point<isq::thermodynamic_temperature[K], water_boiling_point>{5}.relative() returns -95 °C; quantity_point<isq::thermodynamic_temperature[K], water_boiling_point>{isq::thermodynamic_temperature[K](5)}.relative() returns 5 °C).
  5. qp.absolute() always returns a quantity difference from its absolute origin (i.e. assuming constexpr quantity_point<isq::Celsius_temperature[deg_C]> water_boiling_point{100}; that has an implicit absolute_point_origin<isq::Celsius_temperature> the following quantity_point<isq::thermodynamic_temperature[deg_C], water_boiling_point>{5}.absoute() returns 278,15 °C)
  6. Two quantity_point are considered equal if after converting them to a common unit they return the same absolute() value.

We can discuss if it wouldn't be better to remove 3, 5, or even 2.

mpusz commented 1 year ago

The next step is to provide conversions between K and °C. I still believe that the best way would be to provide an offset in the unit definition and apply it while converting a unit of a quantity_point.

Also, to answer the question what should be the unit of adding two quantities of 42 K + 42 °C I believe it should be K as it is the primary unit and °C is defined in terms of K.

burnpanck commented 1 year ago

I think that the main issue is to clearly specify and document the interface of a quantity_point.

Strong agreement! Just make sure to be extra precise in what you mean by "it's origin", which could be interpreted as anything between .point_origin, .absolute_point_origin (both of the resulting quantity_point), or the origin that may conceivably be implied by either of the units supplied in the quantity argument supplied to the constructor or the unit (deduced or explicitly specified) in the quantity_point template. The examples you provided clarify it enough in this case though. Especially your number 2: quantity_point<isq::Celsius_temperature[deg_C]>{isq::Celsius_temperature[deg_C](5)} is different than what I have always been assuming (in that the C++ expression really looks like "5 °C" to me, but doesn't produce a temperature point of 5 °C). I believe your definition of room-temperature above which is now wrong (as you indicated) under these semantics stemmed from the same ambiguity. But, we can define it this way, and it has the advantage that the "implicit origin" doesn't depend on any unit anymore, which I guess is favourable. Under these circumstances, number 3 (that is number 2 with suitably deduced template parameters) is also not ambiguous anymore and will give consistent results even for quantity_point("10 K + 10 °C") (excuse my sloppy syntax here).

For normal quantities, the exact unit to express a quantity is mostly inconsequential, so we are free to define what the unit of 42 K + 42 °C shall be. K is fine with me, though it seems to be slightly at odds with frequency + 1/time -> frequency, for which you would probably want to have 42 Hz + 42 / s = 84 Hz. Again, since the expressions are physically and dimensionally correct with either unit, I believe we are free to chose the behaviour here. Implementation-wise (as-in "finding a general algorithm that neither explicitly references temperatures nor frequencies"), we might be able to get both "K" (primary unit) and "Hz" (non-primary unit) in the respective case thanks to quantity type rules (which we have to formalise!), assuming we discard Celsius_temperature(if we don't, then likely Celsius_temperature is a more refined quantity type than thermodynamic_temperature and the two cases would be indistinguishable unless we invent one further property of quantity types to allow us to distinguish them).

For the implementation: Believe me, having the offset in the unit is a hack at best. Again, take quantity_point<isq::thermodynamic_temperature[K],absolute_thermodynamic_zero,float> qpK{170} and quantity_point<isq::temperature[deg_C],melting_point_of_water,float> qpC{-15} (I use isq::temperature to mean Celsius_temperature or thermodynamic_temperature, depending on if we have the former at all). We then have:

This should make it obvious that the offset is related to the point_origin, not the unit. Of course, implementation is an implementation detail, but I don't see how this is not going to be a hack if you somehow pull the difference between melting_point_of_water and absolute_thermodynamic_zero from the definition of deg_C. Remember, it also should work with room_temperature as a point_origin. If you really want to try an implementation with the offset in the unit, go ahead - when I did #232, it didn't feel like a good idea either though.

mpusz commented 1 year ago

I was rereading all the messages in this thread all over again a few times from yesterday. The more I read it, the more I try to prototype, the more I tend to agree with you 👍🏻

Just make sure to be extra precise in what you mean by "it's origin", which could be interpreted as anything between .point_origin, .absolute_point_origin (both of the resulting quantity_point)

Yes, I agree that my understanding of the "point origin" was the biggest issue here. I somehow blurred the "measurement origin" with just yet another quantity_point that I can subtract from the current one.

What turned out to be helpful for me to better understand the domain was trying to invent a way to print a quantity_point. Sometimes we may just print a quantity, and that is just fine, but often, we should be able to amend the quantity with some postfix (i.e. 632 m AMSL or 200 m ZRH_AFE). This postfix could be provided only with the origin and specify how we measure things.

For example, I can calibrate my barometric altimeter to AMSL and then measure both quantity_points ZRH_ground_level, and current_altitude_amsl with the same instrument. In such a case, those should be separate quantity_points with the same "measurement origin" being mean_sea_level. I should not put a ZRH_ground_level as an origin for current_altitude_amsl to get ZRH_afe. I should just measure current_altitude_amsl with this instrument and then subtract ZRH_ground_level from it, which will give me ZRH_afe as a quantity, and not a quantity_point. In this case, ZRH_ground_level would print as 432 m AMSL, current_altitude as 632 m AMSL, and ZRH_afe just as a quantity of 200 m.

However, if I intend to measure the altitude over the ZRH airport with a radar altimeter that measures the distance from the ground, then ZRH_afe will be a quantity_point. Its origin is a bit tricky as there are two options here. A good solution would be to use a new ZRH_afe_origin, which will create a new "domain" of altitude measurement. Such a domain would prevent comparing those measurements with anything else. An alternative, also a good solution, is to put ZRH_ground_level as an origin which will allow us to do comparisons and arithmetic on two domains of quantity_points measured with either a radar or barometric altimeter. In this case, a current_altitude_amsl will be a quantity_point created from ZRH_afe.point_origin + ZRH_afe.relative(), and we will get the following outputs on the screen: ZRH_ground_level = 432 m AMSL, ZRH_afe = 200 m ZRH_AFE, current_altitude_amsl = 632 m AMSL. Also, in the latter case, ZRH_afe quantity_point should compare equal to current_altitude, both from the previous and this example, as this is exactly the same point in space (just measured with different means). Also, after a few hours of rewriting things, I think it is fundamentally wrong to provide a value of 632 measured with a barometric altimeter to ZRH_afe to state we express the relative value 200 measured with the radar altimeter. A correct way to do that would be to pass a current_altitude_amsl to the constructor of ZRH_afe. So with that, I am backing out from point (1) in https://github.com/mpusz/units/issues/414#issuecomment-1370121078. I think that was a really bad idea of mine.

mpusz commented 1 year ago

So the question of how to construct a temperature point of 5 °C still remains. We have to somehow find a way to make the below:

auto temp = quantity_point<isq::Celsius_temperature[deg_C]>{5};  // implicit origin

mean:

constexpr auto ice_point = quantity_point<isq::thermodynamic_temperature[K]>{273.15};
auto temp = quantity_point<isq::Celsius_temperature[deg_C], ice_point>{5}; 

If we treat a Celsius_temperature with the implicit origin as temperature "distance" measured from the ice_point then I could even imagine what it could mean to express such a quantity_point in K or °F. So I am not sure if we should complicate the design to introduce things like just_not_in_point<> in the unit definition.

mpusz commented 1 year ago

Here is an updated "room temperature controller" example:

constexpr auto ice_point = quantity_point<isq::thermodynamic_temperature[K]>{273.15};
constexpr auto room_reference_temperature = quantity_point<isq::Celsius_temperature[deg_C], ice_point>{21};
constexpr auto temperature_controller_delta = 3 * isq::Celsius_temperature[deg_C];
using room_temperature = quantity_point<isq::Celsius_temperature[deg_C], room_reference_temperature>;

constexpr room_temperature room_default{0};
constexpr room_temperature room_low = room_default - temperature_controller_delta;
constexpr room_temperature room_high = room_default + temperature_controller_delta;

static_assert(room_low.relative() == -3 * isq::Celsius_temperature[deg_C]);
static_assert(room_low.relative() == -3 * isq::thermodynamic_temperature[K]);
static_assert(room_default.relative() == 0 * isq::Celsius_temperature[deg_C]);
static_assert(room_default.relative() == 0 * isq::thermodynamic_temperature[K]);
static_assert(room_high.relative() == 3 * isq::Celsius_temperature[deg_C]);
static_assert(room_high.relative() == 3 * isq::thermodynamic_temperature[K]);

static_assert(room_low.absolute() == (273.15 + 18) * isq::Celsius_temperature[deg_C]);
static_assert(room_low.absolute() == (273.15 + 18) * isq::thermodynamic_temperature[K]);
static_assert(room_default.absolute() == (273.15 + 21) * isq::Celsius_temperature[deg_C]);
static_assert(room_default.absolute() == (273.15 + 21) * isq::thermodynamic_temperature[K]);
static_assert(room_high.absolute() == (273.15 + 24) * isq::Celsius_temperature[deg_C]);
static_assert(room_high.absolute() == (273.15 + 24) * isq::thermodynamic_temperature[K]);

static_assert(room_high - room_low == 6 * isq::Celsius_temperature[deg_C]);
static_assert(room_high - room_low == 6 * isq::thermodynamic_temperature[K]);
static_assert((room_high - room_low)[K] == 6 * isq::Celsius_temperature[deg_C]);

static_assert((room_default.point_origin + room_default.relative()).relative() == 21 * isq::Celsius_temperature[deg_C]);
static_assert((room_default.point_origin + room_high.relative()).relative() == 24 * isq::Celsius_temperature[deg_C]);
static_assert((room_default.point_origin + room_low.relative()).relative() == 18 * isq::Celsius_temperature[deg_C]);

All of the above passes with the current implementation.

Additionally, I think that something like that should be possible:

static_assert(quantity_cast<ice_point>(room_default).relative() == 21 * isq::Celsius_temperature[deg_C]);
static_assert(quantity_cast<ice_point>(room_high).relative() == 24 * isq::Celsius_temperature[deg_C]);
static_assert(quantity_cast<ice_point>(room_low).relative() == 18 * isq::Celsius_temperature[deg_C]);

If we had such a cast I would love to remove absolute() from the public interface and leave it only as a private implementation for comparison needs.

The only problem above is that an ice_point may also be (and in this case is) a quantity_point, so the quantity_cast does not know if it should reinterpret the quantity_point type or just its origin. However, I think we can fix that with an extension to the library's engine. It would be nice to allow the framework to print quantity_points as well. With that, a point origin will have to provide not only a quantity_point but also, optionally, a symbol that would be appended to the text.

burnpanck commented 1 year ago

Cool, I think we agree! I definitely agree with all the static_asserts from the latest "room temperature controller" example.

If we treat a Celsius_temperature with the implicit origin as temperature "distance" measured from the ice_point then I could even imagine what it could mean to express such a quantity_point in K or °F. So I am not sure if we should complicate the design to introduce things like just_not_in_point<> in the unit definition.

Agree here too - that's what I meant that Celsius_temperature could somewhat help in identifying the "implicit origin point" (e.g. ice_point).

As for the quantity_cast<ice_point>, I refer back to my #239 😉: quantity_point has three more or less orthogonal aspects which all determine the representation of a physical point as a bit-pattern in RAM (point_origin, unit and rep), each of them can be changed without changing the physical meaning, if the bit-pattern is adjusted appropriately. With each of them, there is a certain risk that there will be numerical issues. These are representation casts (or even numerical cast - it could work on plain numbers too!). Then, there is changes to the kind or quantity_spec which does not require a change of the bit-pattern, but actually changes the physical interpretation to some degree, more so if that "implicit ice_point origin" is associated with Celsius_temperature. With at least four aspects that can be potentially targeted by the quantity_cast, I believe splitting off the last category from the previous ones will be helpful.

That said; I believe we can still distinguish all the different cases from the cast spec:

mpusz commented 1 year ago

Should the quantity_points be printable by default, or should a user provide dedicated support for it every single time like it is done in the glide_computer_example:

https://github.com/mpusz/units/blob/da64f1789583f7b2a32ce04e12731f6c1e123758/example/glide_computer/include/glide_computer.h#L84-L93

burnpanck commented 1 year ago

I think they should be printable by default, with a way for the user to customise. Let us see if there are many use-cases for having quantity_points simply print their .relative() plus a prefix and or suffix, then we could provide a dedicated API for that kind of customisation.

JohelEGP commented 1 year ago

Sometimes we may just print a quantity, and that is just fine, but often, we should be able to amend the quantity with some postfix (i.e. 632 m AMSL or 200 m ZRH_AFE). This postfix could be provided only with the origin and specify how we measure things.

Remember ISO 80000-1 7.2.1. 632 m AMSL should probably be something like h(ASML) = 632 m.

mpusz commented 1 year ago

632 m AMSL should probably be something like h(ASML) = 632 m.

I am not sure if that is the best idea. Our library is only responsible for the numeric part. Whatever user will before it is not up to us. We could just print .relative(), but I think it would be misleading.

If I am not wrong ISO 80000 does not mention quantity points, so it also does not provide a standard way to print its values.

BTW, we already have some interesting cases in the library. For example:

https://github.com/mpusz/units/blob/da64f1789583f7b2a32ce04e12731f6c1e123758/src/systems/usc/include/mp_units/systems/usc/usc.h#L58

mpusz commented 1 year ago

quantity_point is done in V2, but we still have to figure out where an offset for degree Celsius should be defined.