mpusz / mp-units

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

Determining the best way to create a quantity #413

Closed mpusz closed 1 year ago

mpusz commented 1 year ago

Today, after a code review comment, I added a second syntax that creates a quantity in V2 design:

static_assert(120 * isq::distance[km] / (2 * isq::duration[h]) == 60 * isq::speed[km / h]);  // #1
static_assert(isq::distance(120, km) / isq::duration(2, h) == isq::speed(60, km / h));       // #2

You can find diff with more examples here: https://github.com/mpusz/units/pull/391/commits/858cbb472fe320fdb648fb976f5f026145ba8009.

For now, we have two of those, but I would like to decide which one is better ASAP and remove the other one. Please review the above commit and share your opinion.

mpusz commented 1 year ago

If you prefer #1, please give a thumbs up here.

mpusz commented 1 year ago

If you prefer #2, please give a thumbs up here.

mpusz commented 1 year ago

Please share your comments below.

mhoemmen commented 1 year ago

Greetings! I haven't followed this discussion, but if I had to pick a syntax, it would definitely be (2). I wrote most of P2128's discussion about preferring to reserve operator[] for array access or things like it.

mpusz commented 1 year ago

@mhoemmen, whichever syntax we choose here the operator[] still will be used to specify a unit for quantity in many places because it is a common way to type that. For example:

quantity<isq::speed[m / s], int> speed{42};
auto distance = quantity_cast<isq::distance[km]>(isq::length(42, m));
quantity_of<isq::altitude[m]> auto alt = get_alt();

Additionally, it seems that even if we choose option 2, the following will be valid:

auto speed = isq::speed[km / h](42);

After the discussion in https://github.com/mpusz/units/pull/391#discussion_r1056327563 we consider the above to be the primary syntax for now and then possibly add a "comma syntax" as a shorthand wrapper to do the same in one step.

burnpanck commented 1 year ago

I guess I dislike both variants of creating quantities through the combination of a Reference with a Number (that is, both 42 * isq::speed[km / h] and isq::speed[km / h](42) look wrong to me). This is probably rooted in dimensional analysis - to me, units are mostly a representation "artefact" and do not matter until I need to specify a number for a quantity. Because of that, I want to always keep number and unit very close together, and (visually) parsing the construction of a Reference before I encounter the number is distracting to me.

mpusz commented 1 year ago

I see your point. We can discuss some other syntaxes as well here if someone has some ideas... At this point, we can still change everything, and we are still looking for the best way to define and construct a quantity and to specify other related properties (quantity_of<> concept, quantity_cast, etc). With the current proposal, I think it is good that we can clearly separate a representation type and the quantity value from its quantity-specific properties. As ISO 80000 specifies, "a quantity is a reference and a number" (https://mpusz.github.io/units/glossary.html#term-quantity).

burnpanck commented 1 year ago

As you write in the glossary, a unit can be a reference. So what if we restore the unit references that we have in the library now, such that 42 * km would be a perfectly valid way to specify a length? Then, we could make isq::speed(42 * (km/h)) and isq::velocity(42 * (km/h)) essentially an act of "down-casting"/narrowing of the reference, adding further properties to the quantity. The upshot would be that in the many cases where the unit is all we need, the comparatively verbose spelling of the quantity spec can be avoided (it is implied by the unit reference).

It would have the effect that the following two would be equivalent ways to specify a radioactivity: isq::radioactivity(1 * (1/s)) and 1 * Bq, but isq::radioactivity(1 * Hz) would be disallowed (radioactivity is incompatible with frequency). I would further tend to disallow up-casting (as in isq::time(1 * (1/Hz))) but leave that to quantity_cast. Cross-casting should probably be prohibited even for quantity_cast (between frequency and radioactivity), but can be achieved using double-cast.

mpusz commented 1 year ago

That is an interesting idea, but there are still some issues to be solved here. For example, what should 42 * J mean? ISO 80000 specifies at least the following quantities to be measured in joules:

Another issue is that isq::velocity(42 * (km/h)) needs to have a vector representation type. Of course, we can allow int to be a vector in a special case, but by default, it should not work. On the other hand, we cannot create a length with a vector representation type. So how to create a velocity and position_vector here? vector{1, 2, 3} * km will not compile because it is not a length. isq::velocity(42 * (km/h)) will not compile because 42 is not a vector representation type.

A lot of the derived quantities are not created in terms of base quantities but in terms of specific kinds, so it might look nice in the case of speed, but I am afraid that in most other cases, it will be really verbose and confusing.

Another case here is the natural units system. In such case 42 * second may mean both a length and a time if you assume c = 1. It turned out that one simply cannot reason about a quantity type having just a unit.

burnpanck commented 1 year ago

Given a hierarchy of quantity types as currently under discussion in #405, 42 * J would create a quantity of the base/root type covering all quantities of dimension $M L^2 T^{-2}$. That type may be named <energy_dim> ("aliased" - while not necessarily the same C++ type, it should be considered equivalent for all purposes by the machinery of the library). Here, <energy_dim> is whatever name the SI gave that dimension - if there is no SI name for that dimension, then that base/root type may just be a "derived"/"generated" type.

Then, by our tentative conclusion that narrowing_quantity_type_cast should be allowed implicitly, 42 * J would be acceptable by all those quantities you listed. Of course, the expression isq::heat(42 * J) for example would trigger that cast. Once down-casted, it would potentially require explicit casts to go up the hierarchy again, so attaining full "quantity-safety" through type-safety. How far that restriction shall go is of course another point of discussion (-> #405), but the machinery could work this way in any case.

As one of the more special cases, the expression 42 * Hz would not create a quantity of the base/root type of dimension $T^{-1}$, but rather a quantity of type frequency. isq::radioactivity(42 * Hz) would be definitely disallowed by all sane implementations (horizontal casting), but explicit up-casts to isq::inverse_time will remain allowed. However, we can still freely choose which syntaxes we allow for those up-casts, such that isq::inverse_time(42 * Hz) remains forbidden and leave (widening_)quantity_(type_)cast<isq::inverse_time>(42*Hz) as the only option.

As for the natural units system, the machinery works equally well. 42 * s could create a quantity of the base/root type of dimension $T$ (the only dimension there is), compatible with all kinds, including length and time. You could then also define a reference light_second = isq::length[s*c], which directly creates quantities of kind length (and dimension $T$). Again, if length and time should refer to incompatible kinds or compatible types is probably the point of another discussion.

What do you think?

mpusz commented 1 year ago

That is an interesting idea. However we have to think what it means from the interface point of view. I think that it makes quite a lot of mess here. But let's start from the beginning.

42 * J would create a quantity of the base/root type covering all quantities of dimension . That type may be named ("aliased" - while not necessarily the same C++ type, it should be considered equivalent for all purposes by the machinery of the library). Here, is whatever name the SI gave that dimension - if there is no SI name for that dimension, then that base/root type may just be a "derived"/"generated" type.

If I understand correctly you suggest 42 * J to directly create quantity<isq::energy[J], int>. However, J = kg⋅m2⋅s−2 = N⋅m = Pa⋅m3 = W⋅s = C⋅V. No matter which unit we will select to construct such a quantity we should end up in the same or at least 100% compatible type. When we follow that way of thinking Hz = 1/s and this should be the same case here so we should not do the following:

As one of the more special cases, the expression 42 * Hz would not create a quantity of the base/root type of dimension , but rather a quantity of type frequency

As for the natural units system, the machinery works equally well.

It depends how would you like to define a natural unit system. Even though the speed is dimensionless and expressed in unit 1, I prefer the option where length and time are both "strong" dimensions and speed can be constructed only with length / time and not length / length or time / time. This would not be possible here if I am not wrong.

Now let's try to think how we can define a quantity type. To be consistent with proposed behavior should quantity<J, int> be a valid syntax? What quantity_spec should be used in such a case? Moreover, we still have to support things like quantity<potential_energy[J], int> for specific kinds of quantity.

Next one is quantity_of. If I type quantity_of<J> should it just check a unit or also a quantity_spec? The same question should be answered for quanity_cast<J>(q). Does it cast only a unit or a quantity_spec as well.

mpusz commented 1 year ago

If we like the above idea, I have the following proposal. 42 * J is a shortcut for typing 42 * (isq::mass * pow<2>(isq::length) / pow<2>(isq::time))[J] and user can choose either of those. As a result, we get quantity<reference<derived_quantity_spec<isq::mass, power<isq::length, 2>, per<power<isq::time, 2>>>{}, si::joule>{}, int> (not a quantity<reference<isq::energy, si::joule>{}, int>). However, as we agree that quantities should implicitly downcast, this will be able to convert to a quantity of energy to improve compile-time errors and interfaces. If the user would like ot have isq::energy right away, the following will have to be used 42 * isq::energy[J].

This new syntax will deduce a quantity_spec only for a construction helper and will not work for quantity type, or quantity_of<> concept, or quantity_cast<>().

Additionally, number * reference will be the only helper to construct a quantity, and the other syntaxes of quantity_spec(number, unit) and reference(number) will be removed to not produce too many inconsistent interfaces.

Last but not least, this means that we will not be able to simplify units definitions from:

inline constexpr struct hour : named_unit<"h", mag<60> * minute> {} hour;

to

inline constexpr struct hour : named_unit<"h", 60 * minute> {} hour;

if the latter will start to be allowed by the C++ language.

How do you like such an approach?

mpusz commented 1 year ago

Some code examples:

quantity_of<isq::energy> auto q1 = 42 * isq::energy[J];
weak_quantity_of<isq::energy> auto q2 = 42 * J;
quantity_of<isq::energy> auto q3 = 42 * J + 42 * isq::energy[J];
quantity<isq::energy[J], int> q4 = 42 * J + 123 * (kg * m2 / s2);
quantity<isq::energy[J], int> q5 = 40 * N * (20 * m) + 123 * (kg * m2 / s2);
mpusz commented 1 year ago

Also, as @JohelEGP correctly noticed in https://github.com/mpusz/units/pull/391#discussion_r1055375095, the multiply syntax has some issues. So we have to choose the best compromise here.

burnpanck commented 1 year ago

If we like the above idea, I have the following proposal. 42 J is a shortcut for typing 42 (isq::mass pow<2>(isq::length) / pow<2>(isq::time))[J] and user can choose either of those. As a result, we get quantity<reference<derived_quantity_spec<isq::mass, power<isq::length, 2>, per<power<isq::time, 2>>>{}, si::joule>{}, int> (not a quantity<reference<isq::energy, si::joule>{}, int>). However, as we agree that quantities should implicitly downcast, this will be able to convert to a quantity of energy to improve compile-time errors and interfaces. If the user would like ot have isq::energy right away, the following will have to be used 42 isq::energy[J].

That is exactly what I am proposing. derived_quantity_spec<isq::mass, power<isq::length, 2>, per<power<isq::time, 2> is that root of the quantity type hierarchy of said dimension.

When we follow that way of thinking Hz = 1/s

I believe we have some freedom here. After all, we have already established that 1/s shall not be 100 % equivalent to Hz, in that "Hz shall only every be used for a frequency" (I think I'm quoting @JohelEGP here from somewhere). And that seems also what ISO 80000 says.

Additionally, number * reference will be the only helper to construct a quantity, and the other syntaxes of quantity_spec(number, unit) and reference(number) will be removed to not produce too many inconsistent interfaces.

Sounds reasonable. Could we still have the quantity_spec(quantity) as a syntax for that narrowing_quantity_type_cast? Personally, I prefer isq::energy(42 * J) over 42 * isq::energy[J].

Last but not least, this means that we will not be able to simplify units definitions

Why is that? By definition above, 60 * minute is a quantity, but with #411, it is acceptable as a NTTP. We should be able to create a template alias named_unit<Symbol,Quantity> that can construct whatever we want it to based on Quantity. I must be missing something, as I don't see how the latter could be disallowed even today if the former is allowed.

Also, as @JohelEGP correctly noticed in https://github.com/mpusz/units/pull/391#discussion_r1055375095, the multiply syntax has some issues.

I don't feel that these issues are all that relevant with this shorter form. If you still support isq::energy(42 * J), then this form is exactly equivalent to the form isq::energy(42, J) that was proposed to solve the perceived issues - with the exception of 5) "the amount of * required". Furthermore, the short form, while it does require those parentheses for associativity, all alternatives do also require parentheses as part of the syntax. The improvement of isq::energy(42, J) over (42 * isq::energy[J]) is that it removes that second set of parentheses. Parentheses however can indeed be omitted in some cases, which is a a slight disadvantage of the short form.

JohelEGP commented 1 year ago

The SI brochure says

(d) The hertz shall only be used for periodic phenomena and the becquerel shall only be used for stochastic processes in activity referred to a radionuclide.

ISO 80000-1, 3.9 unit of measurement, note 2 includes

However, in some cases special unit names are restricted to be used with quantities of specific kind only. For example, the unit second to the power minus one (1/s) is called hertz (Hz) when used for frequencies and becquerel (Bq) when used for activities of radionuclides. Another example is joule (J), used for energy, but never for moment of force, the unit of which is newton metre (N•m).

mpusz commented 1 year ago

I believe we have some freedom here. After all, we have already established that 1/s shall not be 100 % equivalent to Hz, in that "Hz shall only every be used for a frequency"

I am not sure if we will be able to implement that in a generic way that will not look like a workaround.

I don't see how the latter could be disallowed even today if the former is allowed.

The former just scales a unit providing another unit. It does not know anything about the dimension or a reference type. quantity is defined in terms of reference, which in turn is defined in terms of unit. That is why I think a unit should not be defined in terms of a quantity again. Even if we find a hack to make it work, it will look like a bad design. Also in compiler errors, such a unit will look bad/long. Another point here is that Representation is not able to support all of the ratios.

with the exception of 5) "the amount of * required"

This and also the number of additional parentheses needed in the short form. For example: 42 * m / (2 * (m / s)). With the design proposed here we had isq::length[m](42) / isq::speed[m / s](2). Longer but less * and ().

mpusz commented 1 year ago

Another example is joule (J), used for energy, but never for moment of force, the unit of which is newton metre (N•m).

As long as I can see that we can limit J to energy I do not see an easy way to limit N * m to moment of force :-(

mpusz commented 1 year ago

Could we still have the quantity_spec(quantity) as a syntax for that narrowing_quantity_type_cast? Personally, I prefer isq::energy(42 J) over 42 isq::energy[J]

I am not sure. quantity_spec(quantity) will be another thing to standardize and learn. Although, I must admit it looks quite nice to me as well.

To simplify, we could remove 42 * isq::energy[J] syntax, but we still need to preserve isq::energy[J] for the quantity definition, quantity_of, and quantity_cast, so I do not think that is a good idea. Also removing it but leaving isq::energy[J](42) or isq::energy(42, J) is totally inconsistent with proposed 42 * J.

burnpanck commented 1 year ago

I don't share your concerns regarding the implementation: With the proposed syntax, unit instances behave like references in some circumstances. However, a reference is just a designator for a quantity_spec and a unit. The quantity_spec is a narrowing of the concept of a physical dimension (it adds more information), and as I have been trying to point out in the last few days, that narrowing is best though of as a hierarchy (event if it adds "orthogonal" information - it still adds information). The root of that hierarchy is then the quantity_spec with no information added beyond the dimension (which is already in the unit), thus references at the root level are 1:1 with units. Even your glossary says "a reference can be a unit". With that view, only_for<...> units then become equivalent to references which are not at the root of the hierarchy, but rather at the root of the sub-hierarchy where the narrowed quantity_spec is anchored.

Once units are considered references, we achieve a nice symmetric set of syntax:

  1. quantity_spec[reference] -> reference: Narrows the quantity_spec of the given reference.
  2. number * reference -> quantity: Creates a quantity.
  3. quantity_spec(quantity) -> quantity: Narrows quantity_spec of the given quantity.
  4. quantity[unit] -> quantity: Essentially quantity_cast<unit>(quantity).
  5. quantity[reference] -> quantity: Essentially quantity_cast<reference>(quantity).

The two remaining syntaxes are:

JohelEGP commented 1 year ago

Even your glossary says "a reference can be a unit".

Indeed. The definition of quantity contains

NOTE 2 A reference can be a measurement unit, a measurement procedure, a reference material, or a combination of such.

But in mp_units, as I understand it, a reference is an instance of an unit with the additional information of the system of units the reference is part of. So a unit is a generic specification, and a reference an instance of a unit for a given system of units. The idea is to not have to repeat the unit specification for each system of units it is part of. So "m" may be just "metre", the "metre" in SI, or the "metre" in the natural system of units.

So in mp_units a reference is more than just an unit in order to support multiple systems of units.

JohelEGP commented 1 year ago

I imagine some quantities could also be shared between different system of units. So if mp_units were to also treat a quantity_spec like an unit, i.e. as not part of any system of units, a reference would be necessary to know the system of units the quantity is part of.

burnpanck commented 1 year ago

But in mp_units, as I understand it, a reference is an instance of an unit with the additional information of the system of units the reference is part of.

I understood that a reference is really just a quantity_spec together with a unit, and the concept of a "system of units" is not represented at all. That, combined with quantity_spec not storing a direct relation to a "system of unit" would make a "reference to a root quantity_spec" equivalent to a unit as I described above. But I could be wrong here.

If we indeed need to specify an explicit relation to a "system of units", and do not want to include that relation in the unit because we want to share the unit between "systems", then, we obviously need to combine two separate pieces of information to form a reference. A number * unit then form then lacks that "system" information, and a number * unit would be a new concept, different from number * reference = quantity. I believe we don't want yet another concept whose purpose is just to be converted to a quantity once "system of units" information becomes available; that indeed feels like a hack.

Then, w have two options: The first option is indeed, we require the user to always provide create a reference through one of the syntaxes that were under discussion initially. Or we could provide real references that look like units (e.g. static constexpr auto m = isq::length[isq::metre];) but do carry that "system of units" information, and thus enable the syntax and semantics that I described. The disadvantage is that now, implementing a new unit requires at least two steps: Creating the actual unit, and creating a matching reference for each system that should be supported.

Given that both options seem suboptimal to me, let me ask why we even need that "system of units" information, beyond what is already encoded (perhaps implicitly) in the base dimensions of the units we use? Unless my initial understanding was correct all along, and there is no such separate information in current V2 at all.

mpusz commented 1 year ago

Just to make it clear, systems of units are built on top of systems of quantities, and those are built by manipulating base dimensions. As @JohelEGP mentioned some time ago, there is no such thing that would be named as a dimension of energy. There is only a corresponding derived dimension built from base dimension ingredients. This is one additional reason why we have quantity_spec to provide strong names for derived things.

thus references at the root level are 1:1 with units.

No. As I mentioned by definition J = kg⋅m2⋅s−2 = N⋅m = Pa⋅m3 = W⋅s = C⋅V or Hz = 1/s. All of those are the same units but not necessary the same quantities.

the quantity_spec with no information added beyond the dimension (which is already in the unit)

In V2 only base units get information about the base dimensions they are used to measure. This is done to be able to verify if a provided unit is compatible with the current quantity but yes it could work as well as some kind of a reference if we would redesing the framework a bit.

we achieve a nice symmetric set of syntax:

It is a bit different from what we have right now but I can image it to work like that. I am only afraid that with such a logic people will be too eager to just pass units everywhere which will end up in hard to understand (read long) error messages as we will always talk in terms of derived quantities equations rather their names. So we are trading compile time error readability for quantity construction helper robustness here.

But in mp_units, as I understand it, a reference is an instance of an unit with the additional information of the system of units the reference is part of. So a unit is a generic specification, and a reference an instance of a unit for a given system of units. The idea is to not have to repeat the unit specification for each system of units it is part of. So "m" may be just "metre", the "metre" in SI, or the "metre" in the natural system of units.

This was the case in V1. In V2 I didn't find a different way then to define a unit in terms of a system of quantities, to be able verify the correctness of the assigned unit to a specific quantity

I imagine some quantities could also be shared between different system of units.

Yes, this is the main idea of V2. Many systems of units (even natural units system) (for example SI, CGS, FPS) should be able to reuse the same definition of a system of quantities (ISQ).

That, combined with quantity_spec not storing a direct relation to a "system of unit"

That is the main intent here. As stated above quantity_spec defines the "system of quantities" part. When joined with a unit (representing a system of unit) it creates a reference that provides all "domain-specific" knowledge to a number.

A number unit then form then lacks that "system" information, and a number unit would be a new concept, different from number * reference = quantity. I believe we don't want yet another concept whose purpose is just to be converted to a quantity once "system of units" information becomes available; that indeed feels like a hack.

This is what I was initially afraid of.

The disadvantage is that now, implementing a new unit requires at least two steps: Creating the actual unit, and creating a matching reference for each system that should be supported.

This was the case for V1 refrences.

mpusz commented 1 year ago

This is what I meant by trading compile-time errors for quantity construction helpers' robustness:

static_assert(is_of_type<60 * (m / s), quantity<reference<derived_quantity_spec<isq::length, per<isq::time>>, derived_unit<si::metre, per<si::second>>>{}, int>>);
static_assert(is_of_type<60 * isq::speed[m / s], quantity<reference<isq::speed, derived_unit<si::metre, per<si::second>>>{}, int>>);

static_assert(is_of_type<42 * J, quantity<reference<derived_quantity_spec<isq::mass, power<isq::length, 2>, per<power<isq::time, 2>>>{}, si::joule>{}, int>>);
static_assert(is_of_type<42 * isq::energy[J], quantity<reference<isq::energy, si::joule>{}, int>>);

I am worried that people will end up with the first types way too often if we provide such a "lazy" feature.

mpusz commented 1 year ago

We can also discuss if quantity<si::joule, int> should be a valid syntax. This is ground-breaking for mp-units but with the logic provided by @burnpanck in https://github.com/mpusz/units/issues/413#issuecomment-1374911713, it could work.

mpusz commented 1 year ago

If we agree on the above, then how does it relate to #405? The above simplifies the library to the maximum. The #405, on the other hand, wants to add a lot of additional information. The biggest problem with the latter is the fact that we do not know what to do about that additional data. As we discussed, quantity character types are controversial, and we do not know how to enforce quantity kinds in an efficient way.

JohelEGP commented 1 year ago

The above simplifies the library to the maximum.

How does that formulation handle the following?

  1. Defining different quantities of the same dimension.
  2. Conversion between different quantities of the same dimension.
  3. Semantics of additive and multiplicative (dimensional analysis) operators between different quantities of the same dimension.

In one extreme: Nothing is allowed. Everything is in the user's hands, including making sure not to mix quantities of the same dimension in ways that shouldn't be mixed due to properties not expressible by the library.

There's no one other extreme, as evidence by the necessity of making tradeoffs.

burnpanck commented 1 year ago

Playing on the interpretation of a unit as a reference denoting a quantity "of that unit" (i.e. no further restriction than what is encoded in the unit specification), we could support both by allowing both quantity<si::joule, int> as-well as quantity<isq::energy[si::joule], int>. These could remain interoperable using the implicit narrowing and widening rules I described. This assumes that the quantity type concept (or whatever we want to call it) forms a hierarchy. Then, the answers are as follows:

  1. Defining new quantities of the same dimension forms a node in the quantity type hierarchy, signifying a subdivision/narrowing of the set of quantities that it applies to. The most powerful form of the formulation allows both "compatible" and "incompatible" narrowing (the former are really just aliases for most purposes). The syntax can remain something like inline constexpr struct moment_of_force : compatible_narrowing<isq::force * si::length> {} moment_of_force;. Here, si::length is the quantity type describing quantities of the SI dimension length.
  2. Conversion between quantities of the same dimension follows the "compatibility" implied by the hierarchy: Narrowing is allowed implicitly, widening implicitly for "compatible" subdivisions else explicitly. Horizontal could be disallowed completely, or allowed explicitly when moving within the same "compatible" subtree.
  3. Dimensional analysis for multiplication is "naturally" extended; two separate trees on dimensions "length" and "time" define a natural tree on the product space of dimension "length * time". Addition follows the compatibility rules described in (2), allowing only conversions that are implicitly allowed: Result is the most narrow compatible quantity type.

In this design, the quantity type-safety is enforced by the C++ type system to a user-selectable level: The user chooses how narrow/"far down the tree" they want to specify their quantities; since quantities don't implicitly widen, the user is softly "locked-in" unless overridden with explicit widening. The preferred way to "start-off" is by constructing quantities using an instant narrowing of pure units-based quantities: isq::energy(42 * J). However, in cases where a strong type is hand-written (such as a state of a Kalman filter), narrowing does not have to be repeated and a more convenient initialisiation with just 42*J is possible.

mpusz commented 1 year ago

How does that formulation handle the following?

The problem I see here is a question if anyone will be using quantity<isq::energy[si::joule], int> if quantity<si::joule, int> will virtually mean the same thing. By that, I mean that the quantities of compatible kinds may be added/subtracted and compared to each other anyway. So what would be the benefit of having different quantities of the same dimension? I am not fun of a unit-only approach but looking at it realistically, what do we lose in terms of type-safety if we follow this path for the SI system (natural units systems aside)?

I think that the problem here is that we already know how to specify a hierarchy of quantities or specify their character, but we really do not know how to benefit from that information in an efficient way in the C++ language. The only thing we agreed on so far in #405 is that we should probably not allow two quantities of parallel kinds to be compatible with each other. Other than that, there are still controversies if we should somehow enforce the characteristics of a quantity or limit conversions to vertical kinds.

Here, si::length is the quantity type describing quantities of the SI dimension length.

I am against mixing or cross-referencing systems of quantities with systems of units. ISQ should not use or specify any units (just quantity types/specs). SI, CGS, and other systems if units should build on top of ISQ to assign units to quantities. We had quantities redefined for systems of units (i.e. SI) in V1, and it was too expensive from the definition and standardization point of view.

Horizontal could be disallowed completely, or allowed explicitly when moving within the same "compatible" subtree.

This is what we agreed on but even though I really do not like it, ISO 80000-1:2022 says explicitly:

For example, while the diameter of a cylindrical rod can be compared to the height of a block, the diameter of a rod cannot be compared to the mass of a block.

JohelEGP commented 1 year ago

So what would be the benefit of having different quantities of the same dimension?

Depends on what the library would allow. If nothing else, it allows expression of intent.

For example, there are pairs of quantities of equal dimension where one is a factor of 2∏ of the other. So if it wasn't possible to express different quantities of the same dimension, and you chose to stick to what the library allows, you could accidentally mix those without taking into account the factor of 2∏. If it was possible to express different quantities of the same dimension, accidental conversion could be avoided. Furthermore, it opens the path to easing the burden of conversion and book-keeping of the factor of 2∏ required on the part of the user.

Horizontal could be disallowed completely, or allowed explicitly when moving within the same "compatible" subtree.

This is what we agreed on but even though I really do not like it, ISO 80000-1:2022 says explicitly:

For example, while the diameter of a cylindrical rod can be compared to the height of a block, the diameter of a rod cannot be compared to the mass of a block.

As you mention, it's a matter of safety and ease of use. Comparisons between quantities of the same kind that are not in the same vertical hierarchy of kind could be allowed if there are no safety concerns.

If comparing diameter and height should consider the length, what of the following should be true? Remember that radius is half of a diameter.

Also remember that there's a relation between comparison and conversion required by the std::regular concept. And that cross-type comparisons require a std::common_reference_t to model std::equality_comparable_with.

burnpanck commented 1 year ago

I am against mixing or cross-referencing systems of quantities with systems of units. ISQ should not use or specify any units (just quantity types/specs). SI, CGS, and other systems if units should build on top of ISQ to assign units to quantities. We had quantities redefined for systems of units (i.e. SI) in V1, and it was too expensive from the definition and standardization point of view.

I believe we are in agreement. The example I showed for isq::moment_of_force does not reference any units, just types/specs; one of them was the isq::force, the other one was the base spec for the dimension of length. We may of course define a isq::length for that purpose, but eventually, it will have to refer to the SI dimension anyway - we want to restrict the applicable SI units to match the dimension, so we need to know why.

For example, while the diameter of a cylindrical rod can be compared to the height of a block, the diameter of a rod cannot be compared to the mass of a block.

Yeah, from examples like these, (or wavelength and diameter), I gather that ISO 80000 indicates that there are not too many kinds in actual use beyond the base dimensions. To me, that is not necessarily unreasonable.

JohelEGP commented 1 year ago

Yeah, from examples like these, (or wavelength and diameter), I gather that ISO 80000 indicates that there are not too many kinds in actual use beyond the base dimensions. To me, that is not necessarily unreasonable.

Energy does have many, as I alluded to at https://github.com/mpusz/units/issues/405#issuecomment-1372290755. Most *_energy quantities from the CE link I posted are like that.

burnpanck commented 1 year ago

Remember that radius is half of a diameter.

The radius of an object is half the diameter of that same object. That doesn't mean that comparing this object's radius to something else always has to give the same result as comparing it's diameter to that same third thing. Assume you have a publication that uses mathematical notation, and describes an object A, whose radius $r$ is 2 m. It also describes an object B, whose diameter $d$ is 3 m. Would you consider $r > d$ to be true? I certainly wouldn't. So radius(1, m) == diameter(2, m) should either be false or should not compile.

Also remember that there's a relation between comparison and conversion required by the std::regular concept. And that cross-type comparisons require a std::common_reference_t to model std::equality_comparable_with.

This is not an issue with the hierarchical behaviour I have described; comparisons behave like additions regarding dimensional analysis and compatibility. We already have rules how additions behave, and those explicitly mention a common type.

Yeah, from examples like these, (or wavelength and diameter), I gather that ISO 80000 indicates that there are not too many kinds in actual use beyond the base dimensions. To me, that is not necessarily unreasonable.

Energy does have many, as I alluded to at https://github.com/mpusz/units/issues/405#issuecomment-1372290755. Most *_energy quantities from the CE link I posted are like that.

That is fine with me. All I'm suggesting is that we should try to model kinds so they match general use. The better the references, the easier it is for us to explain that behaviour.

mpusz commented 1 year ago

If nothing else, it allows expression of intent.

Yes, but it occupies an identifier and does not add any type safety. For example, right now, I have isq::altitude and isq::distance that occupy the identifiers that were much stronger types in the glide_computer of V1. Right now, I should probably even allow them to compare and add in V2.

you could accidentally mix those without taking into account the factor of 2∏.

Well, I still can mix them as they are interconvertible :-(

what of the following should be true?

I had some strong opinions about that before but now I really do not know how to progress here 😢

but eventually, it will have to refer to the SI dimension anyway

This is exactly my point. There is no such thing as SI dimension. SI (being a system of units and not quantities) specifies units for ISQ quantities. Dimension is a property of a quantity, not a unit. Unit can only be defined for a specific quantity/dimension, and this is why in V2, base units take base quantities so I can check in isq::energy[J] if the dimension/quantity of isq::energy and J are compatible. To make it clear I would reword the above statement in the following way "but eventually, it will have to refer to the ISQ quantity type/spec corresponding to this SI unit anyway"

Energy does have many

I am not so sure about it. ISO 80000 never stated it explicitly, and if it did, I suppose it would say that all *_energy quantities are of kind energy like it does for length.

The more we dig into this subject, I feel that we learn that we can do less and less here in terms of C++ type safety. On the one hand, it heavily simplifies the entire library. On the other hand, it removes all of the things that we hoped to be able to model, which is quite frustrating 😢

burnpanck commented 1 year ago

There is no such thing as SI dimension. SI (being a system of units and not quantities) specifies units for ISQ quantities. Dimension is a property of a quantity, not a unit.

I wasn't aware of that, but you are right of course. In some way, that makes the design even more elegant, as base units are specified in terms of base quantities, and only_for<...> units become defined in terms of narrower quantities. The distinction between a unit and a reference is now almust just a matter of definition: Units tie a name to a measure and a basic quantity. If you want to tie a measure to a narrower quantity, and there is no standardised name for it, it is a reference.

On the other hand, it removes all of the things that we hoped to be able to model, which is quite frustrating 😢.

I believe the machinery we are discussing can model this. It may not match common use of ISQ quantities, where many lengths are comparable, but the user of the library may take those exact same concepts from the standard, and define "stronger" quantities that are not comparable. That is, isq::altitude and isq::distance may be comparable, but glide_computer::altitude and glide_computer::distance may not (simply define them as "incompatible" narrowing of the previous two). Does that cheer you up a little 😉?

mpusz commented 1 year ago

I think I see a reasonable solution providing support for that and most parts of #405 as well. Unfortunately, right now, I provide a week-long training for a customer, so I have limited bandwidth, and after that, I have another week of family vacation. I will be working on it in the background and checking if my assumptions are correct.

mpusz commented 1 year ago

@burnpanck, please check https://github.com/mpusz/units/discussions/426 if that is what you meant? Also, I need help with #427.

mpusz commented 1 year ago

Done in V2.