Interconvertibility of derived quantities

I just added a new discussion thread (#426) about the V2 design rules. As stated in TL;DR, the main idea is to:

Allow comparison and addition/subtraction for quantities of the same kinds. This is trivial and I already implemented it. This is consistent with ISO 80000.
Limit quantities construction to only interconvertible quantities where two quantities are interconvertible if their definitions
are from the same branch in the quantity types hierarchy. This is to provide type safety, so the user can not reorder arguments for foo(length, width).
It turns out that this point is quite hard to define for derived quantities.

Should the following quantities be interconvertible?

length * length and area

this is trivial and most probably should be true

width * height and area

both width and height are interconvertible with length so this probably should be true as well

path_length * distance and pow<2>(path_length)

similar case to the above but this time we have two "unnamed" quantities

width * distance and path_length * width

distance is interconvertible with path_length and not with width

altitude * distance and path_length * height

altitude is interconvertible with height, and distance is interconvertible with path_length

width * length and length * height

depending how you look at it both width and height are interconvertible with length
but width is not interconvertible with height
the final result can depend on the ordering of the "cancelling" operations

length * distance and path_length * altitude

depending how you look at it both distance is interconvertible with path_length and length is interconvertible with altitude
but also path_length is interconvertible with length and then distance is NOT interconvertible with altitude
the final result can depend on the ordering of the "cancelling" operations

Is it better to be on the safe sound and disallow non-trivial conversions?
Or maybe it is better to be user-friendly and disallow only what we are sure that should not be interconvertible?

@burnpanck, @JohelEGP, @chiphogg
Do we have any ideas on how to resolve that?
The only thing that somehow resonates with me here is to allow implicit interconvertibility only if we are sure about the result. This covers the following cases:

we have only one interconvertible quantity of a specific dimension in the equation (i.e. length / time should be interconvertible with speed)
all combinations of quantities of the same dimensions are interconvertible (this covers 1, 2, 3)

In case when some combinations result in a lack of interconvertibility (but there is at least one valid combination) a quantity_cast should be used to make an explicit conversion if needed.
Otherwise, the quantities should not be interconvertible.
What do you think about that?

only if we are sure about the result

we have only one interconvertible quantity of a specific dimension in the equation

(but there is at least one valid combination)

Is that possible without using friend?

I hope so :-) I am going to compare quantity_spec and quantity types. I am thinking about something like that:
namespace detail {

enum class interconvertible_result { no, yes, maybe };

template<typename... Q1, typename... Q2>
interconvertible_result are_ingredients_interconvertible(derived_quantity_spec<Q1...> q1,
                                                         derived_quantity_spec<Q2...> q2)
{
  // order ingredients by their complexity (number of base quantities involved in the definition)
  // if first ingredients are of different complexity extract the most complex one
  // repeat above until first ingredients will have the same complexity and dimension
  // check interconvertibility of quantities with the same dimension and continue to the nex one if successful
  // repeat until the end of the list or not interconvertible quantities are found
}

template<QuantitySpec Q1, QuantitySpec Q2>
[[nodiscard]] consteval interconvertible_result are_interconvertible(Q1 q1, Q2 q2)
{
  using enum interconvertible_result;

  if constexpr (Q1::dimension != Q2::dimension)
    return no;
  else if constexpr (have_common_base(q1, q2))
    return (std::derived_from<Q1, Q2> || std::derived_from<Q2, Q1>) ? yes : no;
  else if constexpr (DerivedQuantitySpec<Q1> && DerivedQuantitySpec<Q2>)
    return are_ingredients_interconvertible(q1, q2);
  else
    return no;
}

}  // namespace detail

template<QuantitySpec Q1, QuantitySpec Q2>
[[nodiscard]] consteval bool interconvertible(Q1 q1, Q2 q2)
{
  return are_are_interconvertible(q1, q2) == interconvertible_result::yes;
}

If are_interconvertible returns maybe, in such case quantities are not implicitly interconvertible, but the explicit conversion can be forced with quantity_cast. I hope that will work as expected, but still, I am not sure how to deal with a list of consecutive quantities of the same dimension but different types/kinds. That is why I started this thread, and I hope that https://github.com/mpusz/units/issues/427#issuecomment-1407367145 might be good enough. Unless you have some better proposals?

Maybe I'm misunderstanding something. Let's consider the given example:

we have only one interconvertible quantity of a specific dimension in the equation (i.e. length / time should be interconvertible with speed)

A user can add a new quantity of dimension equal to those of length / time and speed. So a pure function can't answer whether "we have only one" by looking only at its inputs, when the set of such quantities is extensible. Would you be able to do it using virtual?

By "we have only one interconvertible quantity of a specific dimension in the equation" I mean there is only one ingredient quantity of each dimension in Q1... above, and we have to check if it is interconvertible with the ingredient quantity of the same dimension in Q2.... Q1... and Q2... are the ingredients used in the quantity equation used to define two derived quantities I am comparing for interconvertibility.
So to make this rule happy two quantities of dimension length and two quantities of dimension time should be interconvertible. So length / time will be interconvertible with width / duration, but height / time should not be interconvertible with width / time. I think this is quite straightforward to reason about and easy to implement. The problems start when there is more than one ingredient quantity of the same dimension in the equation (i.e. width * length / time and length * height / time).

So a pure function can't answer whether "we have only one" by looking only at its inputs, when the set of such quantities is extensible.

I do not care if there are any other derived quantities of the same dimension besides the two I am comparing now. In fact. there might be an infinite number of quantities of the same dimension.

I still believe that the hierarchical model I presented previously should be able to give a clear and consistent answer to all of these questions. One important thing to note is that my hierarchical model does not speak of "interconvertibility", because that word implies a symmetric relationship between the two involved quantities. I don't think that the conversion rules should necessarily be symmetric. If two quantities really are interconvertible, then it means you can substitute one for the other. That is an equivalence relation. But as such, it should be transitive: If you can substitute A for B in every place and you can substitute B for C everywhere, then you should be able to substitute A for C. Thus, I believe it is impossible to make both height and length "interconvertible", and simultaneously  width and length "interconvertible", but not width and height. Either they are all "interconvertible", or at least one of them is not "interconvertible" to any of the other.
Given the constraint that we want to make width and height incompatible with each other, but each of them individually to be compatible in some suitable sense with length, I see only one solution: That compatibility necessarily has to be an asymmetric relation. That is, between width and length, we want one to be able to stand in for the other, but not the other way around. In principle we can choose either direction, but the only choice that lets us have width and height incompatible for algebra is when length can stand in for width, but not the other way around. They still remain "compatible" for algebraic purposes, as I will try to highlight below:
Let us have a look at a quantity expression length + width:

If width can be converted to length but not vice versa, then the only consistent interpretation of the expression above is that the width first gets converted to a length, and then two lengths are added, creating a length as a result. With that (width + length) + height also is a length, and consequently we can reorder the terms such that the subterm width + height is valid. width and height are still not "interconvertible", but algebraically "compatible".
However, if length can be converted to width but not the other way around, then the only consistent interpretation leads to a width result, and thus (width + length) + height reduces to width + height, which can remain incompatible, as there is no allowed conversion that brings both arguments to the same quantity.
If length and width were interconvertible/equivalent, we can first convert either to the other, and then perform the addition. Depending on which one we convert first, the result will be a length or a width, but since they are equivalent, it doesn't matter and the distinction is really meaningless. This is a viable choice, but then again, we necessarily have that width and height are equivalent and interconvertible too.

So long story short: "interconvertibility" is not the right concept that allows us model the type of "compatibility" that we are looking for, both in function arguments and algebraic operations. We need to consider asymmetric conversion rules.

If we accept such asymmetric conversion rules, it means we could draw a directed graph of all possible quantities and their allowed (implicit) conversions. That graph would be infinite, as we need to consider all sorts of composed quantities too. Due to the associativity of quantity calculus, the graph further needs to be transitive: if A is convertible to B and B is convertible to C, then A should be convertible to C: The expression (B + A) + C has a result of type C, but algebraically is the same expression as B + (A + C). Thus, once we find a cycle in this directed graph of quantities, we immediately know that each of the involved quantities are convertible to all of them and thus truly are interconvertible or equivalent for all purposes. We follow from this that the graph will form groups or clusters of equivalent quantities and a higher-order directed acyclic graph of cluster emerges (if there were a cycle among the clusters, the clusters were all equivalent and thus a single cluster - proof by contradiction).
So far we have only looked at addition. We also need to make sure that this meta-DAG of equivalent quantities respects the quantity algebra of multiplication. Distributivity says that if A is convertible to B, then A*C should be convertible to B*C, because (A+B) * C is algebraically equivalent to A*C + B*C. So this will be another constraint that limits conversion graphs we may propose without breaking quantity calculus.
Apart from those two constraints, we are free to draw this graph of convertibility such that it matches the expectations of our users, or the scientific and engineering communities in general. There is a simple way to ensure both constraints, both by design and in the implementation: With every convertibility we declare explicitly, we make sure that all other convertibilities that are needed to fulfil those constraints are allowed implicitly. Thus if we declare length can be converted to width (but not vice-versa), then, automatically, length * time needs to become convertible to width * time.

Now finally: Let us explore some potential choices for this graph and what it implies for the examples given in the initial issue description.
Trivial choice 1: Dimensional analysis only
Every quantity is equivalent/interconvertible with every other quantity of the same dimension. This is what most units libraries do. But it makes width + height acceptable, and so will it make frequency + radioactivity (now my favourite examples of incompatible kinds because it is explicitly in the standard).
Trivial choice 2: "Extended dimensional analysis" only
We draw some clusters of equivalent quantities following the constraints given before, but do not allow any conversion  anywhere between non-equivalent quantities. This is essentially the same as dimensional analysis, except that we effectively introduce additional dimensions. For the width + height example, we could make all of length, width and height into a separate, non-equivalent group of quantities. None of them is compatible with any of the other. Crucially, we could define area as either as a) length^2 or as  b) width * height, but the quantities length^2 and width*height necessarily would remain non-equivalent and thus incompatible in this design. Only one of them can be the area.  Note that to "define" area could be interpreted as defining a new quantity area that is equivalent/interconvertible with, say, length^2, or it could simply be an alias. Because interconvertibility is an equivalence relation, it is of no further consequence, as the two should remain substitutable in all sitauations anyway. We can exploit this in the implementation so as to make area a strong type which is a separate but essentially equivalent quantity to length^2.
Non-trivial choices
Instead, we may want to hand-craft some of the convertibilities, and see where it leads us. To explore the examples of the initial issue description, let me just assume the following set of hand-crafted basic conversions:

length implicitly converts to width, but not vice versa: They are compatible, but not interconvertible.
length implicitly converts to height, but not vice versa.
height implicitly converts to altitude. (As a consequence, length also implicitly converts to altitude).
length implicitly converts to distance, but not vice versa.
path_length implicitly converts to distance, and vice versa: they are interconvertible/equivalent. (As a consequence, length implicitly converts to path_length).
area is equivalent to length^2, they are interconvertible/equivalent.
No further conversions are allowed, unless required by consistency with quantity calculus.

Consistency with quantity calculus leads to the following results for the examples provided in the first post:

length * length and area are interconvertible/equivalent by definition.
area is convertible to width * height, but not vice-versa: The expression length * length + width * height has a result of quantity type width * height. Converting that to area loses information and thus should not be allowed without explicit input from the user (quantity cast).
path_length * distance and path_length^2 are equivalent/interconvertible as required by distributivity path_length * (path_length + distance) = path_length^2 + path_length * distance, and the term in brackets on the left-hand side are equivalent by definition.
width * distance and path_length * width are necessarily equivalent by the symmetry of * and distributivity again (and as expected for dimensional analysis; there is no "order" just the power of the dimension matters).
path_length * height is convertible to altitude * distance, but not vice-versa: altitude is "more refined" than height.
width * length and length * height are incompatible / neither is convertible to the other. There is no allowed conversion to any of the "parts" that leads to a common quantity type.
length * distance is convertible to path_length * altitude, but not vice-versa: altitude is more refined than length (transitively through height).

To me, all of these results look reasonable. What do you think? I might be able to sketch an implementation of these rules at the start of the next week.

Crucially, we could define area as either as a) length^2 or as b) width * height, but the quantities length^2 and width*height necessarily would remain non-equivalent and thus incompatible in this design. Only one of them can be the area.

An area has dimension L². Both $l * l$ and $w * h$ (where $l$, $w$, and $h$ are quantities of length, width, and height, respectively) are valid expressions of area. It'd be nice if the library didn't require either to be more verbose just due to the way it defines area.
All those implicit conversions in "non-trivial choices" seem backwards and unsafe. A width is always a length, and so the library could make width implicitly convertible to length. But a length doesn't necessarily represent a width, so a library that allows implicitly converting a length to a width opens up opportunities for logical errors that could be caught by the type system.

It'd be nice if the library didn't require either to be more verbose just due to the way it defines area.

I completely agree. I don't want to implement the trivial choice 2. I was trying to prove that we need a non-trivial choice if we want both "LL" and "WH" to be compatible with "A" (even if not interconvertible!) in the same way that "L" and "W" should be compatible, but at the same time we want "W" and "H" to be incompatible.

All those implicit conversions in "non-trivial choices" seem backwards and unsafe. A width is always a length, and so the library could make width implicitly convertible to length. But a length doesn't necessarily represent a width, so a library that allows implicitly converting a length to a width opens up opportunities for logical errors that could be caught by the type system.

I feel you, that was my first thought too, and in the V2 thread I had initially proposed rules to be the other way around. However, I believe that is a reflex coming from object oriented programming, where refining an object adds instance state. In this case, the state space actually becomes "wider" in some sense. Here, refining a quantity adds a compile-time constraint to the physical phenomenon that is described by the quantity; it does not in fact change the state-space, but narrows down the space of applicability of the quantity. I believe that we have here a case of a contravariant relationship between quantities and expressions involving them. The C++ type hierarchy unfortunately does not implement contravariance even though there are many situations where that would be needed as Arthur O’Dwyer points out in the article I linked. So indeed, if we were to make actual instances of quantities to form an inheritance tree according to these refinement rules, then at least for pointers to quantities, C++ would allow us to provide a width for a length. But we are not talking about quantity instances here, but a hierarchy of quantity types/kinds. We can in fact implement contravariance easily in modern C++ if we do not make the quantity instances form a corresponding class hierarchy.
Let me now discuss why I think that contravariance is in fact intuitively correct for the quantity hierarchy. Consider an expression width + "1 meter". I believe everyone would intuitively agree that the result of this operation is still a width, despite "1 meter" not being a width explicitly. This can be formalised as follows: Replacing a general quantity with a refined quantity in an expression is like adding in a constraint. An expression involving multiple constraints can only be valid if all constraints are fulfilled. Thus, converting from refined to general actually removes a constraint, that is, removing information about the quantity. Therefore, converting from width to length is in fact unsafe, as it removes the explicit statement about a quantity.
That the converse does not lead to the expected result can be illustrated with the width + length + height example. If we do not want to redefine the rules of algebra, then that expression should be equivalent to (width + length) + height. If we discard the width constraint and say it is just a length, then ultimately, we have length + height, which again should become a length. But the rules of algebra also say that the same expression could also be evaluated as (width + height) + length. This is exactly the type of expression you would want to forbid, because width and height each have a constraint on applicable physical phenomenons that are mutually disjunct such that the combined expression is not applicable to any physical phenomenon at all.

@burnpanck Thanks a lot for your answers.
Yeah, we went a long way back and forth with the design options here. I agree with you on most points, and actually, I was the one that said that in order for 1 Hz + 1/s + 1 Bq to not compile, we need to make 1 Hz + 1/s return a frequency. Following this logic, width + length + heigth should not compile as well, and the problem would be solved.
However, ISO 80000 explicitly states in multiple places:

The quantities diameter, circumference, and wavelength are generally considered to be quantities of
the same kind, namely, of the kind of quantity called length.

Quantities may be grouped together into categories of quantities that are mutually comparable. Diameters,
distances, heights, wavelengths and so on would constitute such a category, generally called length. Mutually
comparable quantities are called quantities of the same kind.

Quantities that are comparable are said to be of the same kind and are instances of the same general quantity. Hence, diameters and heights are quantities of the same kind, being instances of the general quantity length.

the diameter of a cylindrical rod can be compared to the height of a block

However, it also states:

Quantities of the same kind within a given system of quantities have the same quantity dimension. However,
quantities of the same dimension are not necessarily of the same kind.

The quantities moment of force and energy are, by convention, not regarded as being of the same kind,
although they have the same dimension. Similarly for heat capacity and entropy, as well as for number of entities,
relative permeability, and mass fraction.

However, in some cases special measurement unit names are restricted
to be used with quantities of specific kind only. For example, the measurement 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. As another
example, the joule (J) is used as a unit of energy, but never as a unit of moment of force, i.e. the newton metre (N · m).

Two or more quantities cannot be added or subtracted unless they belong to the same category of mutually
comparable quantities. Hence, quantities on each side of an equal sign in an equation must also be of the
same kind

Of course, we can ignore the above requirements in ISO 80000 and do mathematical and strong-typing correct things. But I am not sure if that is a good idea. I hoped that we could somehow invent a way to do comparisons and arithmetics in terms of kinds and convertibility in terms of their quantity hierarchy.
Please note that function templates (i.e. op+()) will not do automatic implicit conversions between arguments to make the code compile. So even if we make width and height interconvertible with length, they will not do that conversion by themselves.
The interconvertibility requirement comes from the usability fact that I believe that the following should work fine:
void foo(quantity<isq::length[m]>);
void boo(quantity<isq::height[m]>);

foo(isq::height(42 * m));
boo(42 * m);

otherwise, the users may complain the library is not user-friendly. However, I feel that it should not be allowed to call boo(isq::width(42 * m)) to prevent errors in interfaces.
With the rules you described, it seems that foo(isq::height(42 * m)); should not compile without an explicit cast, but this might be the most common case here. Most generic libraries will define functions like:
quantity_of<isq::speed> auto avg_speed(quantity_of<isq::length> auto, quantity_of<isq::time> auto);

If the user works on more specific types in his/her code, then it will need to explicitly cast them to use such generic interfaces. I think this is what @JohelEGP was worried about above.

But as such, it should be transitive: If you can substitute A for B in every place and you can substitute B for C everywhere, then you should be able to substitute A for C. Thus, I believe it is impossible to make both height and length "interconvertible", and simultaneously width and length "interconvertible", but not width and height. Either they are all "interconvertible", or at least one of them is not "interconvertible" to any of the other.

Not necessary. Even in the C++ language, in most cases, we support only one implicit conversion step at a time. For example, if class X is implicitly convertible from std::string, class X will not be implicitly convertible from const char * as this requires two steps at a time. So a conversion between A and C is possible but not implicitly in one step. A user will have to explicitly provide an intermediate step of B, which will be good for code readability and maintenance.

We need to consider asymmetric conversion rules.

Sure, I am open to it. However, please keep the above foo() and boo() examples in mind. Which one do you think should compile, and which one not?

and so will it make frequency + radioactivity (now my favourite examples of incompatible kinds because it is explicitly in the standard).

This would not work because even though they have the same dimension and are part of the same hierarchy tree, they are different kinds, and we can easily define them like that in the library.

path_length implicitly converts to distance, and vice versa:

I am not sure why they should be interconvertible as distance derives from path_length the same as path_length derives from length.

area is equivalent to length^2, they are interconvertible/equivalent.

Does it mean that energy and moment of force are equivalent as well?

void boo(quantity<isq::height[m]>);
boo(42 * m);

This is what I'm worried about. Conversion from length to height should be explicit.

But this is what we wanted to achieve:
std::array<quantity<isq::height[m]>, 3> a = { 1 * m, 2 * m, 3 * m };
auto h = isq::height(42 * m);

If the conversion is explicit, the first one will not compile. I am not sure about the second one.

I agree with @mpusz here: I think the boo example should compile, and even you @JohelEGP requested this at some point in the V2 review (state-space controller initialisation). As for the foo example, I'm not sure we would want to let that one compile.

And what do you think about the ISO 80000 requirement of comparing and adding/subtracting quantities? Should we allow that or do exactly the opposite, which was suggested above? I have implemented the ISO's behavior so far but still struggle with what to do with conversions. I think we have the following options here:
Comparison and arithmetics:

Follow the ISO and allow width + height but not allow frequency + radioactivity as those will be defined as different kinds.
Implement strict arithmetics described above. With that width + height and frequency + radioactivity and we do not have to play with kinds at all.

Conversions:
3. Interconvertibility across the same quantity branch
4. Implicit downcasting (length -> width) across the same quantity branch
5. Implicit upcasting (width -> length) across the same quantity branch
It seems that options 1. and 5. are the ISO's preferred approach, but that does not provide any type-safety. With that, I would prefer to abandon quantity types and quantity kinds at all, and just use dimensions. The only problem here is what @JohelEGP
mentioned some time ago that there is no such thing as a speed dimension, so naming things could be harder. Such behavior would be really easy to implement, document, and standardize.
I tried hard to achieve 1. and 3. as a compromise. However, those are inconsistent in mathematical ways, as pointed out above. I am not sure if we should explore this path more?
The most C++ type-safety provides 2. and 4. which is suggested above by @burnpanck. It is consistent as well but not compatible with ISO 80000.

Yeah, you're right. Does the implicit conversion generalizes to kinds (such that a path length implicitly converts to a distance, which is a logic error if the path length is not a distance, i.e. the shortest path length between two points in a metric space) or is it limited somehow?

Implicit downcasting (length -> width) across the same quantity branch
Implicit upcasting (width -> length) across the same quantity branch

It seems that options 1. and 5. are the ISO's preferred approach, but that does not provide any type-safety.

The most C++ type-safety provides 2. and 4. which is suggested above by @burnpanck.

It seems to me you have the type-safety backwards. 4 is not type safe because a length isn't always a width, but 5 is type safe because a width is always a length.

It seems to me you have the type-safety backwards. 4 is not type safe because a length isn't always a width, but 5 is type safe because a width is always a length.

This is why I actually claim that we should provide interconvertibility. One is for usability and the other one is for type safety. And I hope we do not have to extend that to arithmetics and comparison so we can implement ISO's behavior.

Ok, I understand now better what you are trying to achieve. My proposal was geared at clarity and consistency, by using the same set of rules for conversion and arithmetics/comparison. I also applied those same set of rules both to frequency vs. radioactivity (which ISO explicitly states as separate kinds), as well as for width vs. height, which are both of the same kind in ISO. My approach distinguished the latter case from the former only through hierarchy, i.e. by making width and height some sort of sub-kinds which still are both length but not compatible with each other.
I believe clarity and consistency are very valuable, and we should be careful about introducing more concepts, because it makes it even more difficult for us to just even define the behaviour, let alone verify it and communicate it to the users.
Let me think a little more if I can formalise the rules that you have been suggesting under the assumption that conversion may have different rules than compatibility for algebra and comparison, so that we can make width and height the same kind (i.e. equivalent for comparison and algebra purposes), but not equivalent for conversion.

My proposal was geared at clarity and consistency, by using the same set of rules for conversion and arithmetics/comparison.

I love it. I would be for it if only we could make it consistent with ISO and also type-safe. However, the more experience we get, the more it looks like we need some compromise here or be not consistent with ISO 80000 rules.

Let me think a little more if I can formalise the rules that you have been suggesting under the assumption that conversion may have different rules than compatibility for algebra and comparison, so that we can make width and height the same kind (i.e. equivalent for comparison and algebra purposes), but not equivalent for conversion.

Thanks! I tried to state such rules in https://github.com/mpusz/units/discussions/426, but maybe we can find something even better.

Ok, let me recap/restate what we believe to be true and the goal that we want to achieve.
Assumptions:

A1) We want to model ISO kinds exactly; ISO explicitly speaks about compatibility for comparison, but given that A > B <-> A - B > 0, I believe that compatibility for addition should be exactly the same. We will have to define the result of such an operation.
A2) ISO obviously does not speak about convertibility in the C++ sense, so we may want separate rules here.

For comparison/addition:

B1) Different kinds should be incompatible for addition and comparison:frequency + radioactivity should be disallowed.
B2) With a suitable concept of quantity "types", different "types" that are of the same "kind" should be compatible for comparison and algebra by definition of same kind. Assuming width and height are such examples, then width + height should be allowed. The only reasonable result of that operation would be a general length then.

For conversion:

C1) Whenever two things are incompatible for comparison, they should never be implicitly convertible in either direction. (Otherwise I think that would be unintuitive to a C++ programmer: if I can implicitly convert A to B, then I would expect that  the comparison between the two would behave as if one of then is implicitly converted to the other and then compared). Thus, by B1), quantities of different kinds never convert to each other.
C2) The converse shall not generally be true: width and height are compatible for comparison, but they should never convert implicitly, because they are on separate branches of the "quantity type tree" within a single "kind".
C3) There is no consensus on conversion along the "vertical" direction in the "quantity type tree":

"implicit downcasting": @mpusz and @burnpanck are in favour of allowing that, @JohelEGP is against (correct, or did I misunderstand?).
"implicit upcasting": @mpusz wants to allow that, I'm undecided yet. @JohelEGP seems to be in favour, as this is the usual "covariant" compatibility implied by a C++ type hierarchy ("type safety" - though I'm not sure that we should represent the "quantity type hierarchy" as a hierarchy of quantity<...> types in C++. IMHO we should rather try to understand what conversion behaviour best models physical quantities.)

Do we all agree on that?

I agree with all of the above :-)

We want to model ISO kinds exactly

Probably it would be good to do so unless we decide to ignore ISO 80000. One thing to note here is that ISO does specify which quantities are of different kinds. It just provides a few examples and claims that the division into kinds is to some extent arbitrary.

"type safety" - though I'm not sure that this term has any meaning here

By "type safety" I mean that we should not be allowed to call void foo(quantity<isq::width> w, quantity<isq::height> h); with reversed order of arguments. This in my opinion is the best we can do in those circumstances but, of course, I am open to some better proposals.

"implicit narrowing" (length to width)

I wouldn't call that narrowing. It's implicitly adding semantics to a value by type conversion, from a more generic type to a more specific type.

Also, please clarify what your feeling towards path_length and distance is. I assume they are of the general kind of "length", so comparison to width and height should be allowed. So among those four, are they all on separate branches , or are path_length and distance "closer" in some way. (Ideally, draw me the hierarchy).

This is my understanding
        length
   /      |          \
width   height    path_length
                       |
                   distance

"implicit narrowing" (length to width)
I wouldn't call that narrowing. It's implicitly adding semantics to a value by type conversion, from a more generic type to a more specific type.

Ok, then let's not call it narrowing. Please confirm what type of "vertical" comparisons you would like to allow, and which you don't. My understanding is you want to allow width to length but not the other way around?

I agree with all of the above :-)

We want to model ISO kinds exactly

I agree, too.

or are path_length and distance "closer" in some way.

See https://github.com/mpusz/units/issues/405#issuecomment-1339621239.

My understanding is you want to allow width to length but not the other way around?

Only for conversions.
Length to distance, path length to distance, and length to distance. All those would treat the source value as valid for the target type. You could make arguments for length, which is used to construct more specific quantities of length. But a path length doesn't necessarily represent a distance. So permitting such a conversion implicitly, when it can be avoided in the type system, seems like a bad design choice.

But a path length doesn't necessarily represent a distance

I think this exactly the same case as "length doesn't necessarily represent a width" but I assume you would like to be able to write:
quantity<isq::path_length> pl = 42 * m;

or call:
void foo(quantity<isq::path_length>);

with 42 * m without any additional casts. The question is should it be possible to call with isq::distance<42 * m> but I think that for the sake of consistency, it should. The reason why is that I think that all of the 42 * m, isq::length(42 * m), and 42 * isq::length[m] should mean exactly the same. However, they will result in a quantity different type. The first case will return quantity<si::metre, int> while the other two will return quantity<reference<isq::length, si::metre>, int>. So, in theory, we could create a rule that quantity can be downcasted only if it is specified in terms of a unit and not a referencetype. Giving it a specific reference could mean something more than just a unit, even if thequantity_spec` is exactly the same in both cases.

Following on the above:

quantity<si::metre> being a result of 42 * m could mean quantity of a length kind and be downcastable to any quantity type of that kind.
quantity<isq::length[si::metre]> being a result of 42 * isq::length[m] or isq::length(42 * m) can mean a concrete quantity type in a hierarchy and could not be downcastable (as @JohelEGP suggests).

That can actually work. So every quantity has a kind, but there are quantity that do not have a "quantity type*, and there are quantities that do have a "quantity type". Rules for compatibility for comparison and algebra are only specified in terms of the kind and never take into account the "quantity type". Rules for implicit casting are as follows:

Quantities without a quantity type implicitly convert to a quantity of the same kind and any type (assuming the representation type also qualified for implicit casting).
Quantities with a quantity type do only implicitly cast "upwards".

This does indeed seem to tick all the boxes.
What we will still have to figure out however is how to define the kind of a product of two quantities, with or without type. I believe the "quantity type hierarchy" is still this infinite DAG that follows the rules of quantity calculus as discussed before, and as such I don't believe there is any problem here. My gut feeling is that we may have to put some extra work in still to make the kinds work in the case of energy and moment of force. But I'm confident we will find a solution here!

Some questions to consider:

Do we want to allow energy + force*length? If we don't, how do I integrate the energy stored in an elastic material given it's stress/strain curve?
Do we want to allow energy + current*voltage*time? If we don't, how do I integrate the energy stored in a capacitor?
Do we want to allow count/time < frequency? If we don't, then how do I produce the output of a "frequency counter"?
Do we want to allow count/time < radioactivity? If we don't, then how do we compare the result from a geiger counter against an exposure limit?
Do we want to allow count < frequency*time? IMHO it should be allowed exactly iff 3. is allowed.
We definitely don't want to allow frequency < radioactivity.
We most probably also don't want to allow frequency*length < radioactivity*length.
We may not want to allow frequency*time < radioactivity*time either.

What are your thoughts here?

Just to provide some examples:

Quantities without a quantity type implicitly convert to a quantity of the same kind and any type (assuming the representation type also qualifies for implicit casting).

void foo(quantity<isq::width[m]> w, quantity<isq::height[m]>);

foo(42 * m, 42 * m);   // OK
foo(42 * isq::width[m], isq::height(42 * m)); // OK
foo(isq::height(42 * m), 42 * isq::width[m]); // ERROR
foo(isq::length(42 * m), 42 * isq::length[m]); // ERROR

Quantities with a quantity type do only implicitly cast "upwards".

I do not think that is good. I think it would be good to allow the following to work without any explicit casts:
quantity<isq::speed[m/s]> avg_speed(quantity<isq::length[m]>, quantity<isq::time[s]>);

avg_speed(isq::distance(42 * m), isq::duration(2 * s));

What we will still have to figure out however is how to define the kind of a product of two quantities, with or without type

The way I have implemented it is that the kind of a product of two quantities is always a product of their kinds. So both length / time and distance / duration will create a quantity of length / time kind. This works already, and I believe is correct.
The more interesting question is what should be a kind of 42 * N. Should it be mass * length / time^2, or should it be force? In the latter case, we will have to provide a quantity kind for every named SI unit, which is fine by me as we wanted to do it for Hz, Bq, and J anyway. Extending this requirement to all named derived units in the SI system seems reasonable,

Do we want to allow energy + force*length? If we don't, how do I integrate the energy stored in an elastic material given it's stress/strain curve?

Yes, this is tricky. As force * length will be of a different kind (but the same dimension) than energy according to ISO 80000 it should not compile. To make it compile user will have to do energy + isq::energy(force * length).
An alternative approach could be the one I started at https://github.com/mpusz/units/issues/429. We could assume that kinds also for a hierarchy and then energy + force*length will produce a quantity that is of force * length kind. Such a quantity could be converted to energy again. Implicit conversion would be allowed for 42 * J + 42 * N * (2 * m) and explicit if we write isq::energy(42 * J) + isq::force(42 * N) * isq::height(2 * m).
Which one do you prefer?

3-5 are tricky indeed. ISO claims those should be not comparable if we define them as being of different kinds. Although users may be confused by the fact that they do not compile and the error message they get might not be the easiest one to understand.
I agree with 6-8.

Another interesting point is what should we get from isq::energy(42 * J) + 42 * N * (2 * m) or 42 * J + 42 * N * isq::height(2 * m). I believe that in such cases the quantities should use their kinds as types and form a quantity with a specific type. Does it make sense?

isq::path_length(42 * m)

If isq::path_length were a type, that'd be able to invoke an explicit constructor, so it's syntax I'd expect to work.

but I assume you would like to be able to write:
quantity<isq::path_length> pl = 42 * m;

or call:
void foo(quantity<isq::path_length>);

Those would require implicit conversions. As I replied at https://github.com/mpusz/units/issues/427#issuecomment-1412417240:

You could make arguments for length, which is used to construct more specific quantities of length. But a path length doesn't necessarily represent a distance. So permitting such a conversion implicitly, when it can be avoided in the type system, seems like a bad design choice.

Replying to https://github.com/mpusz/units/issues/427#issuecomment-1412551747.

Some questions to consider:

For 1-4, the answer can be "explicitly cast the lhs to the rhs' type". Otherwise, it should be a question of how much verbosity we require (because those operations should be possible) and possible safety concerns in allowing those.

The way I have implemented it is that the kind of a product of two quantities is always a product of their kinds. So both length / time and distance / duration will create a quantity of length / time kind. This works already, and I believe is correct.

I agree. To be extra precise, this is the behaviour I propose: Dividing a quantity of "type" distance (which is of kind length) by a quantity of "type" duration (which is of kind time) results in a quantity of "type" distance*duration^-1, which is of kind length*time^-1.

Yes, this is tricky. As force * length will be of a different kind (but the same dimension) than energy according to ISO 80000 it should not compile.

There is a loophole that we could invoke: We may argue that multiplying a force by a length is not exactly the same as specifying a quantity in "newtonmetre". Thus, while the latter clearly is a quantity of ISO 80000 kind moment_of_force, there is some room of interpretation what the former product actually is. One interpretation could be exactly as you say:

An alternative approach could be the one I started at https://github.com/mpusz/units/issues/429. We could assume that kinds also form a hierarchy

This allows us to declare compatibility rules for summation of non-equivalent kinds of quantities, making use of that hierarchy. Then the product of a force and a length could be a "base" kind force*length, while both energy and moment_of_force are refined/narrower kinds. But as discussed earlier, to prevent moment_of_force from being compatible with energy do to the transitivity implied by the associativity of summation, the sum force*length with a energy would have to produce a energy, not a force*length. So while this is not a case of conversions, the result would be the same as if we had explicitly down-casted force*length to an energy in the operation.
We have established that we want to decouple the conversion rules from the compatibility rules for addition, and furthermore that conversions shall be determined by the "quantity type" while compatibility for addition shall be determined by the "quantity kind" which do not necessarily need to overlap. Thus converting only upwards in the quantity type hierarchy and additions resulting in the most refined type in the quantity kind hierarchy is not necessarily a problem.
With that arrangement we can also make both frequency and radioactivity sub-kinds of time^-1 in the quantity kind hierarchies and we can allow all of 1-5 while still prevent 6-8. But that may be too complicated.

For 1-4, the answer can be "explicitly cast the lhs to the rhs' type". Otherwise, it should be a question of how much verbosity we require (because those operations should be possible) and possible safety concerns in allowing those.

The questions were specifically meant to be understood as "implicitly allowed" without explicit cast being required. I would rather not have to write quantity_cast in those cases, because quantity_cast is a vastly too powerful beast for this purpose (you know my feeling towards the all-powerful quantity_cast by now ;-D). But I agree that isq::energy(force*time) is clear here, and I'm fine with that. I have a feeling that making a separate hierarchy for kinds that behaves somewhat differently to the quantity type hierarchy may be confusing. So if we are happy that 1-5 requires an explicit cast, then we don't need a hierarchy of kinds as two kinds are either exactly the same, or they are incompatible.

additions resulting in the most refined type in the quantity kind hierarchy is not necessarily a problem

In such a case, what would be the result of the width + length + height that ISO 80000 requires to work?

As I stated in #429, the following is really confusing:
quantity<isq::frequency, int> f = 1 * Hz + 1 * Bq + 1 * Bd;

width + length + height is easy. All of them are of the ISO kind of length, so it actually doesn't matter if we are implementing a hierarchy of kinds or not. As for the "quantity type", we all agree that width and height are both more restricted than length. So the result is still a quantity of kind length and also "type" length.

you know my feeling towards the all-powerful quantity_cast by now ;-D

I am not sure if you have seen 5.2.1 in https://github.com/mpusz/units/discussions/426? 😉 With that quantity_cast could be left only to explicitly cast quantity types and kinds.

Ahh OK. I thought that by the "additions resulting in the most refined type in the quantity kind hierarchy is not necessarily a problem" you meant that we should find a common the most refined base kind for incompatible kinds.

Things are different if we look at (1/(1*s)) + 1*Hz, 1/time + frequency and 1/(1*s) + frequency though. Let us first discuss the "quantity type" assuming the expression were allowed, because that one is easy. The former expression still has no type. The second is the sum of a quantity of "type" time^-1 with a quantity of "type" frequency (and we're still assuming the kinds are somehow compatible). Assuming frequency is a more refined "type" than time^-1, then the result is of the more general "type" time^-1, consistent with the width + height -> length example and our conversion rules that we defined for the "type". The third example finally would be the sum of a quantity with no type with a quantity of type frequency, for which I propose the result to be a quantity of "type" frequency (consistent with our conversion rule exception that allows no-type quantities to down-cast to any refined type).

So I if we want to declare frequency and 1/time compatible for addition, we can come up with consistent rules for the "quantity type". The challenge however is the rule that tells us these are in fact compatible, while still keeping frequency and radioactivity incompatible. We know we cannot make 1/time compatible with frequency by making them the same kind, because we should then equally do the same between 1/time and radioactivity, and then frequency and radioactivity would be transitively compatible too. So we have to make 1/time and frequency different kinds that are still compatible. This either contradicts ISO 80000 right away, unless we call up the loophole that we define a hierarchy of kinds which ISO 80000 doesn't mention. Then, we can achieve our desired behaviour by making the resulting kind of 1/time + frequency be frequency (even though we have established that the resulting "type" is time^-1). But, as you see that this can quickly become confusing, as we now have a general "type" for a refined kind, and I haven't thought it through if that is really going to work out. We may be better off to just make frequency and 1/time incompatible.

Yeah, I have been asking similar questions to myself for a few weeks now. Thanks a lot for your help! Hopefully, together we will be able to find something reasonable.

For what little it's worth (I have not followed this library at all) — I naturally agreed with everything @burnpanck said in the 4 comments starting at https://github.com/mpusz/units/issues/427#issuecomment-1411120164 , until the end of the 4th comment, where I realized that your domain is even trickier than I'd assumed. (That's a verbose way of saying: I have opinions, but many of these opinions are recently formed, weakly held, and unlikely to be defended if challenged!  By dimension I mean the exact same thing ISO calls general quantity IIUC. m and cm have different units but the same dimension (which SI calls "length"). m/s and furlongs/fortnight have different units but the same dimension (which SI calls "length / time").  By direction I mean the difference between one quantity and another: width[m] and height[m] have the same units and dimension (namely "meters" and "length") but different directions. Don't take the word "direction" literally: altitude[m] and height[m] have different directions, because the direction is exactly the part that tells us that an altitude is not a height (even though they have the same dimension and units in this case, their direction is different).
Each possible unit and quantity is associated with one fixed dimension. meters is a unit of length, not time; altitude is a quantity of length, not time. Note that directions do not have names per se (I'm going to try to be rigorous here, although I might slip up): altitude as an identifier names a quantity; altitude (the quantity) has a certain ineffable direction that distinguishes it from other quantities such as height or width or depth or radius or perimeter or wingspan. However, all six of these quantities have the same dimension (length) — again, ISO confusingly calls length their common "general quantity," but I'm going to call it their dimension to avoid overloading the word "quantity" with multiple meanings.
As a C++ expression, myAltitude < myWingspan should be a type error, because altitudes and wingspans are different quantities. Even though they have the same dimension, we still don't want the programmer to do mixed-mode comparisons or math on them: that is the entire point of using a type-safe units library.
Now, in https://github.com/mpusz/units/issues/427#issuecomment-1411196149 , @burnpanck said:

area [should be] convertible to width * height, but not vice-versa: The expression length * length + width * height has a result of quantity type width * height. Converting that to area loses information and thus should not be allowed without explicit input from the user [...]

I had naïvely seen this the other way around: a width*height is an area, but not all areas are width*heights. In particular, length * length + width * height looks like it might be part of a surface-area computation: a surface-area IS-AN area but it certainly is not a width*height. Calling length*length + width*height a width*height seems to add information that is absent from the actual computation!
Relations can be context-dependent: if you're talking about a rectangular prism, then horizontal-surface-area is the same thing as width * depth, and width * width is a type error; but if you're talking about a cylinder, then width * width (times M_PI) is horizontal-surface-area. Worse, on that same rectangular prism, height*depth might correspond to either surface-area-on-left-side or surface-area-on-right-side depending on context!

I suggest that there should be two "kinds" of C++ types in the system:

"general-quantity types" such as length and time and speed(=length/time) possessing dimension and units but not direction: these objects are ephemeral, do not support math at all, and are implicitly-convertible-from directed-quantity types
"directed-quantity types" such as altitude and airspeed possessing all three of dimension, units, and direction: these objects are useful as variables, do support (non-mixed-mode) math, and are explicitly-castable-from general-quantity types

(For my purposes I'm lazily ignoring the units part here. I hope that doesn't cause any technical difficulties.)
The programmer should be able to define their own meaningful directed-quantity types, such as surface_area (distinct from cross_sectional_area).  Completely separate from this, the library must be able to deal with composed/derived types, such as decltype(myWidth * myHeight): the type of that expression is not the general quantity type area; it's the directed-quantity type width*height. This means you can write if (myHeight * myWidth == hisHeight * hisWidth) with no casts.
surface_area (the specific, meaningful, directed-quantity type) should be implicitly convertible to the general directionless type area. But you shouldn't be able to do math on areas, nor compare them. You want mySurfaceArea == myCrossSection to be ill-formed.
But, vice versa, surface_area should also be explicitly constructible from area. You want surface_area(myHeight) to be ill-formed, because it's nonsense. But as long as the dimension is appropriate, you want it to compile: e.g. surface_area(myHeight * myWidth) should be valid. This means you can write if (mySurfaceArea == surface_area(myHeight * myWidth)). (That does an implicit conversion from width*height to area, and then an explicit cast from area to surface_area.)
myHeight + hisHeight is valid, because both sides of the + are of type height. myHeight + myWidth is ill-formed, because mixed-mode arithmetic is not allowed. (We do not say that it's well-formed with a result of type length! We just disallow it.) If the programmer wants to add a height plus a width, he needs to warrant to the library that he knows what he's doing; that means coming up with a (possibly user-defined) directed-quantity type, such as perimeter. perimeter(myHeight) + perimeter(myWidth) is well-formed, and gives a result of type perimeter.
In https://github.com/mpusz/units/issues/427#issuecomment-1411923702 @mpusz quotes ISO:

Quantities that are comparable are said to be of the same kind and are instances of the same general quantity.
Hence, diameters and heights are quantities of the same kind, being instances of the general quantity length.
[...] the diameter of a cylindrical rod can be compared to the height of a block

This corresponds to my notion, as long as you do not take the word "comparable" literally in C++. In my notion, the diameter d of a rod can be compared to the height h of a block by writing a new strong type that represents the common specific abstract notion of "length" that makes us think we can compare these measurements — for example, perhaps the rod and the block both need to fit through the same aperture, so we user-define a type aperture and then we can write aperture(d) == aperture(h). Contrariwise, d == h by itself should be a type error, because in general, diameters are not the same thing as heights. ISO calls them "comparable," but in C++ the way you would compare them is via an explicit conversion that explains why you believe they are comparable.

Now, there's another issue brought up later in the thread ( https://github.com/mpusz/units/issues/427#issuecomment-1411609823 ). Consider myWidth + 1_m. That's clearly still a width. But if 1_m is a general quantity length, on which you can't do math, then myWidth + 1_m would be a type error; you'd have to write myWidth + width(1_m), which is awkward.  Likewise, we want to permit myWidth == 1_m.
There is a possible solution here, inspired by C's own arithmetic rules. float f = {i} is a narrowing conversion, but float f = {2.5} is not: literals behave more magically than variables. So, build that into your literal types: let 1_m be of a magic type that is convertible to any directed-quantity type of the correct dimension, even though the user is (by convention) not supposed to create variables of that magic type because that would break type-safety. Then myWidth + 1_m can be a width, and myWidth + myLength can still be a type error, because 1_m is secretly a-little-more-magic than a plain old length. For people who don't like UDLs (including me), you could have a factory function units::literal_meters(1) distinct from the type-cast units::meters(x). To lock it down even further, this factory function could be consteval in C++20. (Should it be? I don't know much about consteval.)

My answers to @burnpanck's questions in https://github.com/mpusz/units/issues/427#issuecomment-1412551747 will depend greatly on which identifiers identify general-quantity types like length on which you can't do math or comparisons, versus directed-quantity types like altitude on which you can. Which kind of identifier is force? Which kind of identifier is frequency? and so on.
I suggest that the library (or at least, a well-designed client codebase) should make the distinction between general-quantity types and directed-quantity types very clear, e.g. by putting all general-quantity types in a separate namespace, or even better, giving them a common underscored-prefix (general_length, general_time, general_frequency) that will never be detached even by a using-directive. My impression is that these general-quantity types should be named extremely rarely — even more rarely than string_view — and so their names can be cumbersome by design. Someone storing a data member of type general_length instead of altitude should be made to feel bad... I think.

Don't take the word "direction" literally: altitude[m] and height[m] have different directions, because the direction is exactly the part that tells us that an altitude is not a height (even though they have the same dimension and units in this case, their direction is different).

I get it, but altitude and height are synonyms in ISO 80000.  A very appropriate name for something so complicating.

However, all six of these quantities have the same dimension (length) — again, ISO confusingly calls length their common "general quantity," but I'm going to call it their dimension to avoid overloading the word "quantity" with multiple meanings.

That's why it actually calls it their kind.

As a C++ expression, myAltitude < myWingspan should be a type error, because altitudes and wingspans are different quantities. Even though they have the same dimension, we still don't want the programmer to do mixed-mode comparisons or math on them: that is the entire point of using a type-safe units library.

I don't disagree. Perhaps it'd be best to always disallow such mixtures, and instead provide a "kind" accessor (or as_kind<unit>, see #434). Then, if a user really only cares about the common kind part, they'd write myAltitude.kind() < myWingspan.kind() (or Altitude{myAltitude.kind() + myWingspan.kind()} for expressions returning a quantity, if the value of myWingspan in question is also an altitude).

I suggest that there should be two "kinds" of C++ types in the system:

I like this.

 A very appropriate name for something so complicating.

Perhaps, although off the top of my head I don't think "convention" is any better than "direction." My intuition with "direction" was that we all seem to be treating "width" and "height" (or as ISO 80000 calls them, "breadth" and "altitude" ;)) as two distinct quantities, and there the difference is literally what direction you're measuring in.

However, all six of these quantities have the same dimension (length) — again, ISO confusingly calls length their common "general quantity," but I'm going to call it their dimension to avoid overloading the word "quantity"

That's why it actually calls it their kind.

Ah, I stand corrected. "Kind" is an acceptable word to describe "the parts of a quantity that aren't its direction." (Although, grump grump, we'll now have to avoid talking about kinds of types — perhaps we move to families of types? I've only barely gotten used to talking about notions rather than concepts!)

As a C++ expression, myAltitude < myWingspan should be a type error, because altitudes and wingspans are different quantities. Even though they have the same dimension, we still don't want the programmer to do mixed-mode comparisons or math on them: that is the entire point of using a type-safe units library.

I don't disagree. Perhaps it'd be best to always disallow such mixtures, and instead provide a "kind" accessor (or as_kind<unit>, see #434). Then, if a user really only cares about the common kind part, they'd write myAltitude.kind() < myWingspan.kind()

(Nitpick: a.kind() < b.kind() looks like you're comparing the kinds, where really you want to compare a.asLength() < b.asLength().  So I'll call this getter .withoutDirection().) Off the top of my head, I don't see why it's beneficial to let the programmer write myAltitude.withoutDirection() < myWingspan.withoutDirection(). It feels useful to force them to explain (via casts) whether they mean myAltitude < altitude(myWingspan) ("am I flying so low that my wingspan tipped sideways would hit the ground") or wingspan(myAltitude) < myWingspan ("suppose there were a plane with a wingspan equal to my current altitude, would that hypothetical plane actually be less wide than myself"). Same mechanical result, different physical interpretations.

(or Altitude{myAltitude.kind() + myWingspan.kind()} for expressions returning a quantity, if the value of myWingspan in question is also an altitude).

This would certainly be better expressed as myAltitude + altitude(myWingspan). Although that opens yet another can of worms: affine types. Adding one height to another presumably produces a new height, but adding one altitude to another is probably meaningless. I have no thoughts on that subject yet. :)

(or Altitude{myAltitude.kind() + myWingspan.kind()} for expressions returning a quantity, if the value of myWingspan in question is also an altitude).

This would certainly be better expressed as myAltitude + altitude(myWingspan). Although that opens yet another can of worms: affine types. Adding one height to another presumably produces a new height, but adding one altitude to another is probably meaningless. I have no thoughts on that subject yet. :)

In main, that'd be myAltitude + altitude::quantity_type(myWingspan), if altitude is a quantity_point.

I have been following the discussions on the library for quite a while now but honestly I was mainly lost with the new terminology where the length dimension now seems to be the kind of the quantities width and height.
The original terminology in V1 (if I remember correctly from the first talks of @mpusz) where width and height were different quantity kinds with the same dimension lenght was much more intuitive IMHO.
It seems that a lot of the new terminology comes from ISO which is first of all difficult to get because it is not publicly available and second my feeling is that the wish to stick with ISO might not be the best choice starting from the terminologies for e.g. quantity kinds and the liberal use of "comparable" in the ISO text which seems to imply certain rules for C++ operations but where I am fairly certain that the authors have never thought about something like implementing this in a programming language.
It feels a bit like the issue with radian which the SI is not considering a proper physical unit but where it seems to make a lot of sense to consider it as one when actually implementing such a library as it can simplify things.
Having said that, I am starting to post again because

@Quuxplusone 's post was the first time in this discussion where I had the feeling that I again understood what the discussion is about. His terminology might not be as precise as that of the contributors here, but it was much easier to understand for me than the ISO terminology before
the discussion is currently going in the direction of directions ;-) and this is very much related to what I did for my (physical untis aware) linear algebra library where each entry is not only annotated with a dimension / physical unit, but also with the proper physical meaning, i.e. whether something is an x-, y-, z-position / velocity / acceleration and in which coordinate frame it is. I will explain the connection in the following, in case you need more background you can check out  https://www.youtube.com/watch?v=SLSTS-EvOx4

So starting with direction / convention: such a type can definitely represent something like a direction, however I think that we should generalize this to being a strong type that describes the physical quantity with the "appropriate" detail the user currently needs. In all cases it would have to be a more detailed description than just the dimension, e.g. more detailed than length.
Now, which level of detail can we imagine for such quantities? The user might want to distinguish

a generic x-position from a y-position / z-position in a Cartesian coordinate frame (without specifying in which coordinate frame it is)
an affine quantity like a displacement between 2 x-positions
similar for x-/y-/z-velocity and accelerations and angles in a Cartesian frame (still without a coordinate frame annotation)
an x-position in Cartesian coordinate frame A from an x-position in Cartesian frame B (they both have identical dimension and maybe unit)
spherical coordinates (radial distance, azimuth, elevation) https://en.wikipedia.org/wiki/Spherical_coordinate_system , again either generic or tagged with a coordinate fram
cylindrical coordinates (radial distance, azimuth, height) https://en.wikipedia.org/wiki/Cylindrical_coordinate_system
cone coordinates https://en.wikipedia.org/wiki/Conical_coordinates

While all of these strong quantity types have "directions" / are annotated with an axis or something similar, there should also be the possibility to define strong quantity types that do not contain such a notion of "direction". They should still be treated like strong types that cannot be mixed with other quantities of the same dimension.
Examples of such strong quantity types:

a generic length (as in length of an object, not the length dimension) /  width / height quantity (though these can also be considered as direction-like quantities by considering them as differences of x-positions (length), y-positions (width), z-position (height) in an object relative coordinate frame; however, theoretically each object would then need its own coordinate frame as they have different orientations in 3d space)
a half-width / half-height (this is actually something that we use as it is more efficient when e.g. representing a bounding box)
the previously mentioned path_length, radius, diameter quantities are also something that I would like to represent without an attached coordinate frame as it is either very difficult or impossible to specify a coordinate frame for these

IMHO the choice of representation granularity of these strong types should be up to the user of such a library.
Regarding the calculation rules I also prefer a mixture of @Quuxplusone 's and @JohelEGP 's suggestion (these calculations are then very much in line with what I implemented in my library / what we are using in production currently):

Strong types like x-position in frame A should only provide operations with the exact same strong type (with maybe the exception of positions and position differences if we want affine calculus, see my points on linear algebra below); this is what I understood @Quuxplusone proposed
when mixing incompatible types, I strongly prefer @JohelEGP 's suggestion where the user explicitly has to request to do the calculations on a less type-safe level on which only compatible dimensions (or maybe even compatible dimensions and identical units) count
the reason for this is that I don't think forcing the user to write something like x_pos + treat_as_x_pos(y_pos) makes much sense in most cases because there is not unique direction in which to convert and it is also unclear what should happen when doing width + treat_as_width(half_width)? does this automatically multiply by 2 or not? Something like width.asUnit() + half_width.asUnit() ("Unit" might be a bad name here, I mean only consider the underlying dimension plus unit without the strong type-safety that we added with width and half_width) is clearer for me (I immediately would assume here that the factor of 2 is not considered, we only add the plain unit values in e.g. [m])

Now, coming from the linear algebra stand-point there is even much more type-safety that could be considered. I am not saying that we should model all of this but I think it makes sense to be aware of the full problem space and then we can decide what exactly we want to be able to model.
If we say that the strong types in this library should be able to model to model everything, then this would also include being able to model a single entry in a vector or matrix with the following properties:

is the entry / quantity from a point or displacement vector (distinction between points / vectors in linear algebra which maps to absolute and relative / affine quantities)
is it from a normal vector or an information vector (information vector entries have the opposite unit of what the entry describes, e.g. an entry describing x-position information would have a dimension of length^(-1))
is the entry from a jacobian / covariance or information matrix (or the transposed versions of that); this also plays into the area argument: not everything with a dimension of length^2 should be considered an area, it could just as well be an entry from a covariance matrix describing either the variance of an x-position or the covariance between x-position and y-position

However, I think that a units library should probably not try to model the last linear algebra considerations.
To summarize, I think strong quantity types that distinguish different "types" of quantities with the same dimension (still inclined to call them "kinds") are a valuable addition to the library, but only in a fairly straightforward way where such a strong type can only be added / compared to the exact same type. If the user wants to do operations on different strong types with identical dimension, he should be able to do that by explicitly requesting a lower type-safety where only the dimension / unit is considered.
Things like automatic conversions in a type-hierarchy where it isn't even clear in which direction the conversions should go are too cumbersome for the average user IMHO and honestly even too complex to understand for me in some cases.

Thanks a lot for your comments and feedback. I spent the last few days on different prototypes of this subject. I tried different approaches and observed how it affects safety and usability. It is still not a ready design as I need a few more days to finish all the checks and implementation I think we are close to finding a reasonable solution. But before I provide the current status, I would like to provide some comments.

@Quuxplusone, thanks for joining the discussion! 😄 It is good to have you on board here. As you see a physical units library is not as trivial subject as it may seem 😉 No of the libraries I am aware of moved outside of the dimension + unit domain which is trivial. We made an approach to quantity kinds in V1 but it was not scalable and not consistent with what ISO defines.

I suggest that there should be two "kinds" of C++ types in the system

I agree with that. See more in the current status post below.

I would like to encourage everyone to use a consistent naming convention because we will be lost in those discussions pretty quickly. Two biggest organizations in the domain ISO and BIPM are using consistent nomenclature so we probably should do the same 😉. Glossary of terms are provided free of charge by both of them:

https://jcgm.bipm.org/vim/en/
https://www.iso.org/obp/ui#iso:std:iso-iec:guide:99:ed-1:v2:en.

This corresponds to my notion, as long as you do not take the word "comparable" literally in C++. In my notion, the diameter d of a rod can be compared to the height h of a block by writing a new strong type that represents the common specific abstract notion of "length" that makes us think we can compare these measurements

This is something I am not so sure about. Of course, we can consider forcing all the types to be consistent but in such cases the following would have much more boilerplate:
bool is_square(quantity_of<isq::width> auto w, quantity_of<isq::height> auto h) { return w == h; }

quantity_of<isq::length> auto gift_wrapping_paper_length(quantity_of<isq::width> auto w, quantity_of<isq::height> auto h)
{
  return 2 * w + 2 * h + min(w, h);
}

if (isq::distance(42 * m) < isq::path_length(200 * m))
  std::cout << "Congratulations! You were moving in a right direction on our circular track.\n";

auto speed = isq:::speed(10 * (m / s));
if (speed > 120 * m / (2 * h))
  slow_down();

The first three cases seem to be mandated by ISO, and C++ is also an ISO standard so I expect a lot of pressure in the C++ Committee to be consistent between standards within the same organization.
The fourth one is tricky because if we define speed as a separate kind of quantity, then it should not be comparable with something as generic as length / time.

I was mainly lost with the new terminology where the length dimension now seems to be the kind of the quantities width and height.
The original terminology in V1 (if I remember correctly from the first talks of @mpusz) where width and height were different quantity kinds with the same dimension lenght was much more intuitive IMHO.

I initially had the same feeling and that is why we implemented this like that in V1. However, after that I was reading all the definitions and standard many times and I started to prefer the official nomenclature. Let me try to explain. ISO says:

The quantities diameter, circumference, and wavelength are generally considered to be quantities of the same kind, namely of the kind of quantity called length.

"length" above is not a dimension, it is a quantity. But it is somehow special because it also starts a new kind in the hierarchy of quantities. Following the ISO and my wrapping paper example above if I add 2x width and 2x height of a box I will get its circumference over a specic side. The resulting quantity will not be width or height. It will be some length that I can even meassure with an elastic measurement tape. So I can only add or remove quantities of the same kind.
How a kind relates to a dimension? In many cases it is a similar thing. Actually, I believe that every new dimension should implicitly start a new kind so you do not have to be too verbose in the C++ definitions. However, frequency, activity, and modulation rate have the same dimension but probably it should be an error to add or compare those quantities, right? Another example here could be an energy and a moment of force. And this bring us to a similar problem to the last example from my previous post:
auto q1 = isq::energy(42 * J) + isq::moment_of_force(42 * (N * m));  // should not compile as those are clearly different kinds
auto q2 = isq::energy(42 * J) + isq::force(20 * N) * isq::mass(60 * kg); // should it compile?

Ok, I realize that you are thinking in terms of concepts and it seems you are forming a concept hierarchy where lower Levels convert to higher Levels in the hierarchy.
This here is a nice example:

bool is_square(quantity_of<isq::width> auto w, quantity_of<isq::height> auto h) { return w == h; }

now Imagine that we Not only want to distinguish between width and height but also between width in Frame A and width in Frame B, same for height.
Then it would be possible to call this method with width in Frame A and height in Frame B as these are more specialized than width and height and I guess they satisfy the width and height Concept.
However, does it make sense to ask whether width and height in different frames are square? I would say this is a clear logic error if it were callable like this.

Now, which level of detail can we imagine for such quantities? The user might want to distinguish

I think that is more about the representation type rather than a quantity type/kind. Altough some quantities are explicitly speciffied by ISO as vectors or tensors (all the rest are scalars). For example, there is a position vector quantity of kind length that is explicitly defined as a "vector quantity".

I also can't see how such a hierarchy would work in practice if users can add arbitrary new strong quantity types which would then have to be properly located in the type hierarchy tree again by the user.

Yes, this might be a potential issue. However, a user does not have to use the definitions of the ISQ system of quantites (specified by the ISO 80000). For specific needs, a project-specific system of quantities can be created either form scratch or by building on top of only some selected quantities of ISQ.

now Imagine that we Not only want to distinguish between width and height but also between width in Frame A and width in Frame B, same for height.

Then it would be possible to call this method with width in Frame A and height in Frame B as these are more specialized than width and height and I guess they satisfy the width and height Concept.

This is my intent. I will write about it soon below 😉

However, does it make sense to ask whether width and height in different frames are square? I would say this is a clear logic error if it were callable like this.

Does it make sense to call std::sort(it1, it2); on two iterators from two different containers? 😛 Sometimes we can't enforce anything. If a user has specific ocnstraints about its hierarchy, custom functions should be provided.

OK, so here is where I am right now with my prototyping. As we agreed before and what was clearly stated by @Quuxplusone, we have two different categories of quantities.
Quantities of a specific type

For example length, width, height, radius, path length, distance, etc
Such quantities form a hierarchy
They should be implicitly convertible upwards to other compatible quantities. By that I mean that every width is a length, every distance is a (shortest) path length which by itself is also a length.
Downcasting on compatible quantities should be explicit as not every length is a width.
Compatible quantities are the ones that can be found on the same hierarchy branch. width is a length and height is a length so those pairs are compatible. However, width is not a height so such conversion should not be allowed even with explicit cast.

Quantities of a specific kind

They form logical sets of quantity types.
In a hierarchy of quantities of a specific type such a set could be represented as a subtree starting from some node. The root node of the hierarchy always starts a new kind and spans across entire tree - this is a quantity of a unique dimension. However, for some quantities it make sense to define more constrained subtree. For example frequency, activity, and modulation rate are all quantities of the 1 / dim_time  dimension but probably such quantities should not be possible to be added, subtracted, or compared. Another example here could be energy and moment of force both having the same dimension again.
Quantity of a specific kind can be explicitly converted to any quantity type in its set.

I played with many ideas in the code and this is what I ended up with:

quantity type
quantity kind

C++ type
quantity<isq::width[m]> , quantity<isq::length[m]>
quantity<si::metre>, quantity<kind_of<isq::length>[m]>

instance creation
42 * isq::width[m], isq::length(42 * m)
42 * m, 42 * kind_of<isq::length>[m], kind_of<isq::length>(42 * m), kind_of<get_kind(isq::width)>(42 * m)>

A user in most cases will probably just use the first option to create an instance or type of a quantity kind. The other verbose options will be needed for:

natural units
cases when a user do not want to use/print a dedicated named unit (for example if a user wants to create a quantity of frequency kind but use 1/s as a unit then the following will be needed kind_of<isq::frequency>(42 * (1/s)))

As ISO states quantities of the same kind should be comparable, addable, and subtractable. I provided some examples about why I think it is useful in examples in the posts above. However, I think that the two categories of quantities should behave differently here.
A width + 3 m is a quantity of width
quantity_of<isq::width> auto w = isq::width(42 * m) + 3 * m;

This is the case when we are adding a quantity kind to a specific quantity type. Such a quantity kind behaves as any quantity type from the set of quantities of the same kind.
A width + height is a quantity of length
quantity<width[m]> w = isq::width(42 * m) + isq::height(42 * m);  // Compile-time error as the result is a quantity
                                                                  // of length which is not implicitly convertible
                                                                  // to width

Adding or subtracting quantities of specific types results in the closest common node/base in the hierarchy. It might be the root of the subtree (i.e. length like above) but does not have to be:
quantity_of<isq::path_length> auto p = isq::distance(42 * m) + isq::path_length(42 * m);

Please let me know how you like the above and share your thoughts and suggestions. I would like to ensure that we end up with the best possible design here that is easy to use, easy to document and understand, type-safe, and free of surprises. If we can't achieve all of those, we have to find the best compromise.

The first line of
quantity_of<isq::width> auto q1 = isq::width(42 * m) + 123 * m;    // the same result for 123 * kind_of<isq::length>[m] quantity_of<isq::length> auto q2 = isq::width(42 * m) + isq::length(123 * m);
is ok from my point of view. For the 2nd line this seems ok when written exactly like this where you nicely specify the type of width and length and also the concept for the resulting value.
However, out in the wild (in real codebases) it is often not obvious what types the 2 arguments have (might be badly named variables) and the resulting type could just be deduced by auto because the author is lazy (in reality all these names are longer due to additional namespaces) or it is directly returned from a method with auto deduced return type.
If we were to write
auto q2 = a.as_kind() + b.as_kind();
Instead then it is much easier to directly grasp what is happening here, we strip off parts of the strong type safety and only care about the dimension + unit of the quantity.
The same ambiguity might also arise with the first code line but there it is less problematic.

Regarding quantities of the same type: what is the Gil of the library, do we see the types as a fixed set or are these open for extension?
I presume they should be open for extension. Now even the ISO itself calls the categorization they list as somewhat arbitrary and from my experience with such strong types I would definitely want to use a different set of types for e.g. the length kinds.
=> users probably rarely want to use these predefined types like width, height. Instead, they either only work with lengths or they crate their own hierarchy.
This means that we have to assume that there is more fine-grained hierarchy and if this already Leeds to inconsistent code as in the  is_squareexample then we might have a problem with how we model the domain (just my opinion here).
Taking a closer look at your comment

Does it make sense to call std::sort(it1, it2); on two iterators from two different containers? 😛 Sometimes we can't enforce anything. If a user has specific ocnstraints about its hierarchy, custom functions should be provided.
here I think that the proper concept here would require that the 2 iterators are from the same container. Of course this cannot easily be modeled in C++, but that would be the “true” concept here.

If we now use concepts for quantities that are designed for a hierarchy of types and it already breaks down with a small extension where we distinguish between quantities in different coordinate frames (which is a reasonable hierarchy) as seen by the is_square method then this (IMHO) calls this approach into question.

However, out in the wild (in real codebases) it is often not obvious what types the 2 arguments have (might be badly named variables) and the resulting type could just be deduced by auto because the author is lazy (in reality all these names are longer due to additional namespaces) or it is directly returned from a method with auto deduced return type.

Anyway, it will be a quantity of isq::length type and not a quantity of a kind. So for example this will fail:
auto q2 = isq::width(42 * m) + isq::length(123 * m);
quantity<isq::width> q3 = q2;  // does not compile

If we were to write
auto q2 = a.as_kind() + b.as_kind();
Instead then it is much easier to directly grasp what is happening here, we strip off parts of the strong type safety and only care about the dimension + unit of the quantity.

The above is incorrect. The q2 is not a quantity of a kind but a quantity of a type isq::length. Thanks to that q3 does not compile. If q2 was indeed a quantity of kind_of<isq::length> as you state above, then the q3 would compile which I think is really wrong.

I presume they should be open for extension

You are right.

Now even the ISO itself calls the categorization they list as somewhat arbitrary

This is not true. The quantity types they provided are strict definitions so it is not arbitrary. The thing that is arbitrary is the division of types into kinds. ISO does not provide any definitions for such divisions besides some examples in the description part.

users probably rarely want to use these predefined types like width, height. Instead, they either only work with lengths or they crate their own hierarchy.

And that is a really reasonable way to do if a project has such constraints. In such case a user may provide something like:
namespace my_namespace {

inline constexpr auto width = quantity_spec<isq::length, is_kind<width>> {} width;
inline constexpr auto height = quantity_spec<isq::length, is_kind<width>> {} height;

}

and with the above width will not be possible to be added or compared to height.

The above is incorrect. The q2 is not a quantity of a kind but a quantity of a type isq::length. Thanks to that q3 does not compile. If q2 was indeed a quantity of kind_of<isq::length> as you state above, then the q3 would compile which I think is really wrong.
Yes, you caught my on my imprecise terminology here and the as_kind name is not a good name anyways. I now realized that you consider everything a type, even a quantity of length.

So what is missing for me is an intermediate level of type-safety other than the types that only compares dimension (and potentially unit). Then a user could explicitly ask to switch calculations to that level and ignore the additional quantity type information. This would make it always explicit on which level of type-safety we are.
As a summary we would have 3 different levels of safety:

plain scalars (or whatever the underlying type is), meaning almost no type safety
Dimension / unit-aware calculus which only considers the dimension / unit
Full quantity type calculus where only identical types can be added
And it would always be explicit where we are. We already say that the user has to explicitly request this when we want to operate on the least typesafe underlying type, this does not work with implicit conversions. Why not require the same thing
when going from the most typesafe level to the middle level?

Just my thoughts based on how we use these things in our codebase. I think it is always valuable to think about what an inexperienced / lazy programmer would produce with a library and also design for this. Otherwise we just keep on adding more sharp edges in C++ ;-)

Full quantity type calculus where only identical types can be added and it would always be explicit where we are

Yes, we are missing such a level of safety. I am not sure if and how to support that.

Maybe something like this?
auto q = strict<isq::width>(42 * m) + strict<isq::height>(42 * m); // Compile-time error

Alternatively, we could make the third mode (strict) the default and then provide weak<QuantitySpec> modifier for the second level of safety? But that would be incompatible with ISO 80000.

Maybe it makes sense to take a step back…
Can you maybe elaborate a bit why you are putting so much emphasis on exactly following ISO 80000?
Is it because

there were comments in the committee that this should be followed?
You presume that this makes it more likely to get ihr through the committee?
you think that this is the best solution?

If you don’t want to answer that or not publicly then this is also fine, just let me know. I am currently trying to see what we deem the best technical solution without restricting the direction to much with such constraints. Afterwards it still makes sense to compare this with ISO and see how this fits together.
In my understanding there is no requirement from ISO to make their mentioned quantity types like radius, width, height actually distinct types in the C++ type system. This could just as well be represented with different variable names that then are automatically comparable because they are of the same C++ type, couldn’t it?
And as it seems that there is a requirement from ISO to make the types frequency and activity distinct types that cannot be compared, couldn’t this be handled by the mentioned 3rd layer of type safety where only quantities of the same type are comparable?
I just don’t think we are getting much in terms of type safety from using distinct C++ types for width and height if they are comparable anyways. The real big additional benefit (compared to just modeling quantity kinds) in my opinion comes from the 3rd layer of type-safety which makes distinct types incompatible by default.

Maybe something like this?
auto q = strict<isq::width>(42 * m) + strict<isq::height>(42 * m); // Compile-time error

Alternatively, we could make the third mode (strict) the default and then provide weak<QuantitySpec> specifier for the second level of safety? But that would be incompatible with ISO 80000.

Yes, having the strict mode as default is what I would wish for. Otherwise I (personally) would rather stick with just one C++ type per quantity kind and be done with it. The definition of quantity types for width, height from ISO and their operations is IMHO just a mushy gray area that is difficult to implement and does not add much value.
But I think I’ve distracted the discussion enough for now, thanks a lot for your detailed explanations so far @mpusz , have a great weekend!

I think that one the most important benefit of having this hierarchy over one type per kind are strongly-typed conversions.
void f1(quantity<isq::length[m]>, quantity<isq::length[m]>);
void f2(quantity<isq::width[m]>, quantity<isq::height[m]>);

f1(42 * m, 42 * m);. // Ok
f2(42 * m, 42 * m);. // Ok
f1(isq::height(42 * m), isq::width(42 * m)); // Ok
f2(isq::height(42 * m), isq::width(42 * m)); // Error

Yes, having the strict mode as default is what I would wish for.

If we decide this is the best solution then we can go this way. But first I would like to hear some more opinions on the above options.

I think that one the most important benefit of having this hierarchy over one type per kind are strongly-typed conversions.
void f1(quantity<isq::length[m]>, quantity<isq::length[m]>);
void f2(quantity<isq::width[m]>, quantity<isq::height[m]>);

f1(42 * m, 42 * m);. // Ok
f2(42 * m, 42 * m);. // Ok
f1(isq::height(42 * m), isq::width(42 * m)); // Ok
f2(isq::height(42 * m), isq::width(42 * m)); // Error

Wouldn’t then most programmers pick the versions with only f1(42 * m, 42 * m); f2(42 * m, 42 * m) because they are shorter? This version again does not protect against mixing up the order. At the call site such a call is just as if there were no type safety at all.

I think that one the most important benefit of having this hierarchy over one type per kind are strongly-typed conversions.
void f1(quantity<isq::length[m]>, quantity<isq::length[m]>);
void f2(quantity<isq::width[m]>, quantity<isq::height[m]>);

f1(42 * m, 42 * m);. // Ok
f2(42 * m, 42 * m);. // Ok
f1(isq::height(42 * m), isq::width(42 * m)); // Ok
f2(isq::height(42 * m), isq::width(42 * m)); // Error

Wouldn’t then most programmers pick the versions with only f1(42 * m, 42 * m); f2(42 * m, 42 * m) because they are shorter? This version again does not protect against mixing up the order. At the call site such a call is just as if there were no type safety at all.

I think the intent was that the height and width versions would typically be pre-existing variables of that type, not literal values created inline with the call.

Ok, that makes sense then!

FWIW, I find the code snippets hard to understand for two reasons (besides that I don't know this library at all ;)) —

I see both quantity<foo> x and quantity_of<foo> auto x: so this is a library of concrete types like absl::Duration, but also parameterized types (like std::time_point<foo>), but also a library of concepts (like I-don't-know-what)? FWIW, my mental model of how I'd like to use a units library exactly matches absl::Duration: it's just a thin wrapper around a double, but strongly typed, and with factories to conveniently apply scaling factors (e.g. my::Feet(3) == my::Yards(1)). (See the Godbolt below.)
I believe I understand correctly now that isq::length is a completely different sort of animal from isq::width — namely the former is a general quantity and the latter is a directed quantity — but the continual use of "length" and "width" in example code is unnecessarily confusing to the reader IMO, because it's too easy in English to think of "length and width" as if they were two coequal directed quantities of a rectangle or a box. Could @mpusz suggest some less confusable identifiers for use in examples? E.g. width is to isq::length as ____ is to isq::mass? I don't think anyone would casually mistake isq::mass for a directed quantity.

Alternatively: I see in https://raw.githubusercontent.com/mpusz/units/master/docs/framework/arithmetics.rst that there is some sort of convention of using affixes _kind and dim_: that doc uses rate_of_climb_kind, dim_length. Now, dim_length exactly matches my original usage of dimension to mean "the kind of thing that length and mass are." However, @mpusz, you then claimed that kind is a synonym for dimension! If I accept that, then I certainly can't refer to rate_of_climb_kind: "rate of climb" is clearly a quantity, whose dimension (or kind) is "length / time". "Rate of climb" itself is not a kind; rather, it is a quantity which shares the same kind (namely "length / time") with many other quantities, such as "airspeed" and "rate of descent."
@mpusz writes:

So we can compare and add all types of lengths in one expression. This is what is required by ISO 80000

I think you're reading too much C++ jargon into ISO 80000's English. I think it would be more pragmatically useful to read "comparable" as "able to be compared." It doesn't need to mean specifically "directly supports C++'s < operator with no type-casts involved on either side."
bool is_square(quantity_of<isq::width> auto w, quantity_of<isq::height> auto h) { return w == h; }

strikes me as just as unacceptably non-type-safe as
bool is_cromulent(quantity_of<isq::airspeed> auto as, quantity_of<isq::rate_of_descent> auto rod) { return as == rod; }

(By the way, per my first bullet, I'm aware that I don't understand the programming model here. These functions are templates, which AFAIC means maybe the == will compile and maybe it won't; we can't possibly tell until we instantiate the template with some particular types. Could we stick to non-template function with concrete types, like
bool is_cromulent2(quantity<isq::airspeed> as2, quantity<isq::rate_of_descent> rod2) { return as2 == rod2; }

or would that be a pointlessly unrealistic usage of your library as you envision it?)
Your next example is similarly over-templated for my mental model:
quantity_of<isq::length> auto gift_wrapping_paper_length(quantity_of<isq::width> auto w, quantity_of<isq::height> auto h) {
  return 2 * w + 2 * h + min(w, h);
}

Notice that std::min actually requires pw and ph to have the same type or else deduction fails, so the idea of mixed-mode arithmetic between widths and heights doesn't seem sustainable even in this simple example. (Unless you propose to supply a new-and-improved min implementation in your units library!)
I ended up getting horribly nerdsniped by this, so here's a Godbolt sketch of how I'd personally do it, again heavily inspired by the very concrete style of absl::Duration.
https://godbolt.org/z/Y7zaTGKeY
I know you can't do it exactly this way (hard-coded double as the representation type, impossible to add new dimensions/kinds), but FWIW, this is basically the style that I'm picturing in my head and implicitly comparing everything else to, and if I misinterpret something you're describing, I'll likely be erring in this direction.

see both quantity x and quantity_of auto x: so this is a library of concrete types like absl::Duration, but also parameterized types (like std::time_point), but also a library of concepts (like I-don't-know-what)?

Yes, we do have quantity and quantity_point class templates to model affine space. quantity_of<> (or QuantityOf<> in my current not yet pushed code) is a concept that verifies if the given quantity if consistent with the second argument being one of dimension, quantity_spec, or reference.

I believe I understand correctly now that isq::length is a completely different sort of animal from isq::width — namely the former is a general quantity and the latter is a directed quantity

Actually, not :wink: Both isq::length and isq::width are strong quantity types where isq::width derives from isq::length. This means that every width is a length, but not the other way around. Think about it like a tree of quantity types of the same dimension. Additionally, isq::length is the name of the quantity kind that gathers all quantities of length including the length itself, width, height, ... into a set of quantities that are comparable. In the library specifier kind_of can be used to say I am referring to kind rather than to a strongly typed quantity.
So isq::length, and isq::width are strongly types quantities, and kind_of<isq::length> and kind_of<get_kind(isq::width)> are referring to the same quantity kind of length.

Alternatively: I see in https://raw.githubusercontent.com/mpusz/units/master/docs/framework/arithmetics.rst that there is some sort of convention of using affixes kind and dim: that doc uses rate_of_climb_kind, dim_length

This was V1 and this was wrong, not scalable, and not consistent with ISO 80000. In V2 we are trying to do better, hence this discussion...

Could we stick to non-template function with concrete types, like

bool is_cromulent2(quantity<isq::airspeed> as2, quantity<isq::rate_of_descent> rod2) { return as2 == rod2; }

Yes, we can, but the above is incorrect because it does not specify a unit. With quantity_of concept, I accept every unit of this kind and any representation type as well. That is why I tend to mix the usage of strong types and concepts to show how things interact and how consent they are.

Notice that std::min actually requires pw and ph to have the same type or else deduction fails, so the idea of mixed-mode arithmetic between widths and heights doesn't seem sustainable even in this simple example. (Unless you propose to supply a new-and-improved min implementation in your units library!)

Well, this was a simplification, and that is why I did not provide std:: before min(). I could depend on ADL here.

Just a short update. I am attending now the ISO C++ Commitee meeting in Issaquah. Through the last 2 weeks I tried to make covariant case work really hard but I failed:

the design was too complicated with two categories of quantities (https://github.com/mpusz/units/issues/427#issuecomment-1416581125)
I ended up creating a lot of hardcoded kinds in the ISQ hierarchy even though the division "can be arbitrary" (my choices probably would not be OK for others)
some units serve more than one quantity kind (heat vs energy vs enthalpy for J, permeance vs inductance for T, conductance vs admittance vs susceptance for S, ...)
I needed to make thermodynamic_temperature and Celsius_themperature different kinds to serve K and deg_C

Now I am going to try the contravariant approach...

Regarding the contravariant approach. Even though width + 3 m seems tempting, how to calculate mechanical_energy = potential_energy + kinetic_energy?

@mpusz wrote:

Even though width + 3 m seems tempting, how to calculate mechanical_energy = potential_energy + kinetic_energy?

FWIW, in my mental model / toy implementation, that looks isomorphic to the gift-wrap example:
    struct potential_energy : quantity<joules> {};
    struct kinetic_energy : quantity<joules> {};
    struct mechanical_energy : quantity<joules> {};

    potential_energy myPotentialEnergy = meters_per_s2(9.8) * myMass * myHeight;
    kinetic_energy myKineticEnergy = 0.5 * myMass * mySpeed * mySpeed;
    mechanical_energy myTotalEnergy = mechanical_energy(myPotentialEnergy) + mechanical_energy(myKineticEnergy);

where each strong type is castable to any other strong type with the same dimension/kind, but never implicitly convertible.
Or if all three of these variables have the same type, then the math isn't mixed-mode and there's no trouble at all, right?:
    struct energy : quantity<joules> {};
    energy myPotentialEnergy = meters_per_s2(9.8) * myMass * myHeight;
    energy myKineticEnergy = 0.5 * myMass * mySpeed * mySpeed;
    energy myMechanicalEnergy = myPotentialEnergy + myKineticEnergy;

Here's the bigger problem I'm just realizing now: My first snippet above wants meters_per_s2(G) * myMass * myHeight to be implicitly convertible to potential_energy. That's easy, if the multiplication operation returns me a suitcase full of unmarked joules (the unit by itself, with no semantic quantity attached). But then I would be able to write essentially mixed-mode things like myKineticEnergy += meters_per_s2(G) * myMass * myHeight or even myKineticEnergy += myPotentialEnergy * meters(1) / meters(1), or  which seems dumb. This is similar to @burnpanck's "Trivial choice 1: Dimensional analysis only": it looks stronger on the surface, but as soon as you do any math, you're right back to weak dimension-only types. I have no idea how to get around this, other than to push the problem onto the programmer and say "well, use more helper functions" (Godbolt):
    struct potential_energy : quantity<joules> {};
    struct kinetic_energy : quantity<joules> {};
    potential_energy PEOfMassAtHeight(quantity_of<dim_mass> auto m, quantity_of<dim_length> auto h) {
        return meters_per_s2(9.8) * m * h;
    }
    potential_energy myPotentialEnergy = PEOfMassAtHeight(myMass, myHeight);  // OK
    kinetic_energy myKineticEnergy = PEOfMassAtHeight(myMass, myHeight);  // ill-formed, no implicit conversion

(quantity_of<dim_mass> auto is pretty gross, but I did that to myself by deliberately making kilograms a factory and not a type. I thought that was a good idea because "people shouldn't be storing raw kilograms in variables," but maybe for function parameters that turns out to be a good idea. Godbolt. Yeah, that looks fine, right?)

Done in V2.