Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: raise level of abstraction from dimension to structure describing a quantity #405

Closed
JohelEGP opened this issue Nov 29, 2022 · 119 comments
Assignees

Comments

@JohelEGP
Copy link
Collaborator

#401 is too big of a step.

The quantity class template takes a template argument for the quantity dimension. The quantity_kind class template slightly extends them to describe the "is a kind of" property. The same happens for quantity_point and quantity_point_kind.

These are insufficient. And I don't think extending quantity, like quantity_kind, did is the way to go. There are more properties than "is a kind of".

A suitable structure, which describes these properties, should be used in place of the dimension of quantity. For starters:

  • The structure should represent the name of the quantity and carry its dimension. With this, we can describe different quantities with equal dimensions. These should not be interconvertible.
  • Then the structure can be expanded to describe the "is a kind of" property. quantity_kind and quantity_point_kind can go, which are replaced by a converting constructor in quanity and quantity_point (or whatever else according to how we define what a "kind" is).
  • The structure can be expanded with more properties:
    • "Is a factor of", e.g. radius is a factor of half of a diameter.
    • "Is offset by", e.g. a Celsius temperature point is offset by 273.15 K from thermodynamic temperature.

The last two are enough to also resolve #232, e.g. Fahrenheit is a factor of ⁵⁄₉ kelvin, and its points are also offset by 459.67 K.

@mpusz
Copy link
Owner

mpusz commented Nov 30, 2022

I think that might be a good idea, but we have to do some POC based on V2 first and find what suits here and what does not. Please also note that we do not have much time if we want to write the first papers for Issaquah.

@mpusz
Copy link
Owner

mpusz commented Dec 1, 2022

I saw your approach to defining such quantities some time ago. It was really nice and clear. However, the more I think about it the more I am concerned about use case for it.

For example, do we really want the below to compile?

static_assert(quantity_cast<diameter>(2 * radius[m]).number() == 4);

I think it would be really surprising to the users that the conversion between a radius and diameter type happens automatically. Lots of users will like to write this 2x multiplier by themselves in their codebase.

If the information you propose will not be used as above then why to provide it at all?

@JohelEGP
Copy link
Collaborator Author

JohelEGP commented Dec 1, 2022

I saw your approach to defining such quantities some time ago. It was really nice and clear. However, the more I think about it the more I am concerned about use case for it.

For example, do we really want the below to compile?

static_assert(quantity_cast<diameter>(2 * radius[m]).number() == 4);

I think we do.

I think it would be really surprising to the users that the conversion between a radius and diameter type happens automatically. Lots of users will like to write this 2x multiplier by themselves in their codebase.

Not only will they have to write 2, they'll also have to ensure the multiplier is correct, and use a different type or risk introducing bugs.

@mpusz
Copy link
Owner

mpusz commented Dec 1, 2022

Not only will they have to write 2, they'll also have to ensure the multiplier is correct, and use a different type or risk introducing bugs.

I am not sure what you mean here. My assumption is that:

auto r = 5 * radius[m];        // 5 m
quantity<diameter[m]> d = r;   // 10 m

Where do they need to multiply by 2?

@JohelEGP
Copy link
Collaborator Author

JohelEGP commented Dec 1, 2022

That's right. I think I misunderstood #405 (comment) because there's a 2 in the code and "2x" in the text, but they're different things.

@mpusz
Copy link
Owner

mpusz commented Dec 1, 2022

I planned to provide kinds just by deriving from dimensions. For example:

inline constexpr struct radius : decltype(isq::length) {} radius;
inline constexpr struct diameter : decltype(isq::length) {} diameter;

With that the following was not meant to compile:

auto r = 5 * radius[m];
quantity<diameter[m]> d = 2 * r;

and one of the following was meant to be needed:

constexpr quantity_of<diameter> auto to_diameter(quantity_of<radius> auto r)
{
  return quantity_cast<diameter>(2 * quantity_cast<isq::length>(r));
}
constexpr quantity_of<diameter> auto to_diameter(quantity_of<radius> auto r)
{
  return quantity_cast<diameter>(2 * r);
}

depending on how powerful we want a quantity_cast to be (only for interconvertible dimensions or also for all equivalent ones).

@mpusz
Copy link
Owner

mpusz commented Dec 1, 2022

But I am open to other ideas as well. We need a POC here.

@JohelEGP
Copy link
Collaborator Author

JohelEGP commented Dec 1, 2022

I'm not a fan of to_diameter. There's many quantities that are a factor or coefficient of another, like

inline constexpr auto rotation = defn<"𝘕", "𝘕 = 𝜑/(2π)", rotational_displacement("𝜑")>();
inline constexpr auto angular_frequency    = defn<"𝜔", "𝜔 = 2π𝘧", frequency("𝘧")>();
inline constexpr auto magnetic_polarization = defn<"𝙅ₘ", "𝙅ₘ = 𝜇₀𝙈", magnetic_constant("𝜇₀"), magnetization("𝙈")>();
inline constexpr auto power_attenuation_coefficient        = defn<"", "𝘮 = 2𝛼", attenuation_coefficient("")>();
inline constexpr auto rest_energy                  = defn<"", "𝘌₀ = 𝘮₀𝘤₀²", rest_mass(""), speed_of_light("𝘤₀"), "𝘌₀", energy("𝘌")>();
inline constexpr quantity charge_number            = defn<"", "dim 𝘤 = 𝘲𝘦⁻¹", electric_charge("𝘲"), elementary_charge("")>();
inline constexpr auto Larmor_frequency                                       = defn<"", "𝘷_L = 𝜔_L/(2π)", Larmor_angular_frequency("")>();
inline constexpr auto Compton_wavelength                                     = defn<"", "𝜆_C = 𝘩/(𝘮𝘤₀)", Planck_constant("𝘩"), rest_mass(""), speed_of_light("𝘤₀")>();
inline constexpr auto level_width                                            = defn<"", "𝛤 = ℏ/𝜏", reduced_Planck_constant(""), mean_duration_of_life("")>();

So it'd be better to centralize the handling of factors, rather than having to write 2N functions for converting to and from (and probably even more at the call site because there's no swift transition, e.g. from frequency to a quantity that's a factor of angular_frequency, or having to write more than 2N functions).

depending on how powerful we want a quantity_cast to be (only for interconvertible dimensions or also for all equivalent ones).

quantity_cast should also start looking beyond equivalent dimensions if there's more information at hand. For example, to reject conversions between quantities of equal dimension but different kinds.

@mpusz
Copy link
Owner

mpusz commented Dec 1, 2022

I agree with you. We should definitely look into this. If we can achieve this while still having easy to understand code and compiler error messages it would be great.

@mpusz
Copy link
Owner

mpusz commented Dec 1, 2022

quantity_cast should also start looking beyond equivalent dimensions if there's more information at hand. For example, to reject conversions between quantities of equal dimension but different kinds.

Yes, this is why I provided two to_diamter functions. The first one assumes that quantity_cast cannot cast through different kinds (so has to go through length), and the second one assumes we gave it some superpowers 😉 to be different from what a quantity converting constructor can do by default.

@mpusz
Copy link
Owner

mpusz commented Dec 3, 2022

How would you implement propagation_coefficient, which is equivalent to α + iβ where α denotes attenuation and β the
phase coefficient of a plane wave? As long as we do have support for multiplication and division of quantities, for now, we do not support addition and irrational stuff.

@JohelEGP
Copy link
Collaborator Author

JohelEGP commented Dec 3, 2022

There might be some overlap with BobSteagall/wg21#36.

For now, I just define it as

inline constexpr auto propagation_coefficient = defn<"𝛾", "𝛾 = 𝛼 + i𝛽", attenuation("𝛼"), phase_coefficient("𝛽")>();

𝛽 does not necessarily represent an imaginary number, so if you define i to make it so and give it an appropriate type, you don't have to reuse quantity's additive operators to allow such an expression.

@mpusz
Copy link
Owner

mpusz commented Dec 3, 2022

I am not sure if I follow you. How to actually construct such a quantity from the attenuation and phase coefficient and how to validate such a construction? I mean in the dimensional analysis sense here.

@JohelEGP
Copy link
Collaborator Author

JohelEGP commented Dec 3, 2022

q<attenuation> α;
q<phase_coefficient> β;
q<propagation_coefficient, std::complex<double>> γ = α + i * β;

i could make the above work, but there's no standard interface for complex numbers beyond std::complex.

@mpusz mpusz self-assigned this Dec 5, 2022
@mpusz mpusz added this to the v0.9.0 (V2 framework) milestone Dec 5, 2022
@mpusz
Copy link
Owner

mpusz commented Dec 5, 2022

I started to work on that on the V2 branch already.

@mpusz
Copy link
Owner

mpusz commented Dec 6, 2022

Which of the following should compile directly, which should require quantity_cast, and which should not compile at all? How to enforce that?

quantity<isq::speed[km / h]> s1 = 100 * isq::distance[km] / (2 * isq::duration[h]);        // exact match
quantity<isq::speed[km / h]> s2 = 100 * isq::length[km] / (2 * isq::time[h]);              // different kinds (more generic)
quantity<isq::speed[km / h]> s3 = 100 * isq::diameter[km] / (2 * isq::duration[h])         // different kinds (more specific)
quantity<isq::velocity[km / h]> s4 = 100 * isq::distance[km] / (2 * isq::duration[h]);     // different characteristics (scalar not vector)
quantity<isq::speed[km / h]> s5 = 100 * isq::position_vector[km] / (2 * isq::duration[h]); // different characteristics (vector not scalar)
quantity<isq::velocity[km / h]> s6 = 100 * isq::acceleration[km] * (2 * isq::duration[h]);  // exact match
quantity<isq::velocity[km / h]> s7 = 100 * isq::acceleration[km] * (2 * isq::time[h]);      // time vs duration (should they simplify in the resulting type)
quantity<isq::speed[km / h]> s8 = 100 * isq::acceleration[km] * (2 * isq::duration[h]);     // different characteristics (vector not scalar)

@JohelEGP
Copy link
Collaborator Author

JohelEGP commented Dec 6, 2022

quantity<isq::speed[km / h]> s1 = 100 * isq::distance[km] / (2 * isq::duration[h]);        // different kinds (more specific)
quantity<isq::speed[km / h]> s2 = 100 * isq::length[km] / (2 * isq::time[h]);              // exact match
quantity<isq::speed[km / h]> s3 = 100 * isq::diameter[km] / (2 * isq::duration[h])         // different kinds (more specific)

I fixed the comments. Both distance and diameter and kinds of length, so they can stand in for a length.

quantity<isq::velocity[km / h]> s4 = 100 * isq::distance[km] / (2 * isq::duration[h]);     // different characteristics (scalar not vector)

Dimensional analysis should fail. It can't be left up to the number type, as they could be convertible, like double to complex<double>. Same for scalar = vector.

quantity<isq::velocity[km / h]> s7 = 100 * isq::acceleration[km] * (2 * isq::time[h]);      // time vs duration (should they simplify in the resulting type)

They should be synonyms. For simplicity, one of them should be a reference, rather than a "strong alias".

None of these should require quantity_cast.

@mpusz
Copy link
Owner

mpusz commented Dec 6, 2022

I fixed the comments.

I think that speed should be defined in terms of distance, not length.

Please note that every scalar can also be a vector and a tensor. It is not the case in the reverse direction.

@mpusz
Copy link
Owner

mpusz commented Dec 6, 2022

They should be synonyms.

I am also not sure if time and duration should be synonyms. duration is a time difference and time can be used to define a time_point.

@mpusz
Copy link
Owner

mpusz commented Dec 6, 2022

I am also afraid that if most of the above are to compile then it might be hard to prevent compilation of some kinds derived from i.e. speed:

struct speed_kind1 : speed {};
struct speed_kind2 : speed {};

At least I cannot invent the algorithm to cover all the bases here.

@JohelEGP
Copy link
Collaborator Author

JohelEGP commented Dec 6, 2022

I fixed the comments.

I think that speed should be defined in terms of distance, not length.

I'm not sure. ISO just defines speed as "magnitude of the velocity". By a definition that uses "distance", I think the example in https://en.wikipedia.org/wiki/Speed#Difference_between_speed_and_velocity, paragraph 2, would also have the speed be 0, because the distance from the starting and end points are 0.

Please note that every scalar can also be a vector and a tensor. It is not the case in the reverse direction.

In general, I don't think it's so clear-cut. Force is a vector quantity that is prominently used as a scalar in quantity definitions:

inline constexpr auto normal_stress  = defn<"𝜎ₙ", "𝜎ₙ = d𝘍ₙ/d𝘈", force("𝘍ₙ"), area("𝘈")>();
inline constexpr auto shear_stress   = defn<"𝜏ₛ", "𝜏ₛ = d𝘍ₜ/d𝘈", force("𝘍ₜ"), area("𝘈")>();
inline constexpr auto static_friction_factor    = defn<"𝜇ₛ", "𝘍ₘₐₓ = 𝜇ₛ𝘕", static_friction_force("𝘍ₘₐₓ"), force("𝘕")>();
inline constexpr auto kinetic_friction_factor   = defn<"𝜇", "𝘍_μ = 𝜇𝘕", kinetic_friction_force("𝘍_μ"), force("𝘕")>();
inline constexpr auto rolling_resistance_factor = defn<"𝘊ᵣᵣ", "𝘍 = 𝘊ᵣᵣ𝘕", force("𝘍"), force("𝘕")>();
inline constexpr auto drag_coefficient          = defn<"𝘊_D", "𝘍_D = ½𝘊_D𝜌𝘷²𝘈", drag_force("𝘍_D"), mass_density("𝜌"), speed("𝘷"), area("𝘈")>();

Some of these are components of the vector, others their magnitude, IIRC. It's necessary to support these uses.

They should be synonyms.

I am also not sure if time and duration should be synonyms. duration is a time difference and time can be used to define a time_point.

That's indeed how ISO defines it. And it remarks that "duration is often just called time". And indeed, most quantity definitions outside ISO 80000-3 use "time" instead of "difference" for what's a "time difference between to events".

inline constexpr auto force                   = defn<"𝙁", "dim 𝙁 = 𝘮𝘭𝘵⁻²", mass("𝘮"), length("𝘭"), time("𝘵")>(); // Part 4
inline constexpr quantity energy            = defn<"𝘌", "dim 𝘌 = 𝘮𝘭²𝘵⁻²", mass("𝘮"), length("𝘭"), time("𝘵")>(); // Part 5
inline constexpr auto electric_charge              = defn<"𝘘", "d𝘘 = 𝘐d𝘵", electric_current("𝘐"), time("𝘵")>(); // Part 6
inline constexpr auto radiant_flux      = defn<"𝛷ₑ", "𝛷ₑ = d𝘘ₑ/d𝘵", radiant_energy<electromagnetism>("𝘘ₑ"), time("𝘵")>(); // Part 7
inline constexpr auto sound_particle_velocity     = defn<"𝙫", "𝙫 = ∂𝞭/∂𝘵", sound_particle_displacement("𝞭"), time("𝘵")>(); // Part 8
inline constexpr auto reverberation_time = defn<"𝘛_𝘯", duration>(); // Part 9
inline constexpr auto decay_constant                                         = defn<"", "𝜆 = -(1/𝘕)(d𝘕/d𝘵)", number_of_entities<E>("𝘕(X)"), time("𝘵")>(); // Part 10

I am also afraid that if most of the above are to compile then it might be hard to prevent compilation of some kinds derived from i.e. speed:

struct speed_kind1 : speed {};
struct speed_kind2 : speed {};

At least I cannot invent the algorithm to cover all the bases here.

You could allow defining kinds not by deriving.

@mpusz
Copy link
Owner

mpusz commented Dec 6, 2022

I'm not sure. ISO just defines speed as "magnitude of the velocity". By a definition that uses "distance", I think the example in https://en.wikipedia.org/wiki/Speed#Difference_between_speed_and_velocity, paragraph 2, would also have the speed be 0, because the distance from the starting and end points are 0.

I do not agree with that. Actually, the usage of length could mean 0. distance is defined in ISO 80000 as the "shortest path length" and path_length is specified as the length over the path, which should give a non-zero result here, right?

In general, I don't think it's so clear-cut.

Please see the tensor definition here: https://en.wikipedia.org/wiki/Physical_quantity#Size. I think that the framework should assume that scalar is also a vector and a tensor, and that a vector is also a tensor. So the same scalar representation type could be used for all three abstractions. For example, I can imagine a coordinate system where an int may be used to mean a vector (or tensor) over a default axis (i.e. the axis of the airplane). This type could also be implicitly convertible to a dedicated vector type. In such case, the magnitude would be provided by int and the "direction" would be the default one. On the other hand, any tensor or any vector value cannot always be represented as a scalar. So to summarize, I think that we are dealing with a hierarchy of abstraction here that should allow conversion in one but not the other.

most quantity definitions outside ISO 80000-3 use "time" instead of "difference" for what's a "time difference between to events"

Good point.

You could allow defining kinds not by deriving.

Deriving is just an implementation detail here. In general, I just cannot find a good algorithm that will prevent conversions between those two but will allow all the rest to work.

@JohelEGP
Copy link
Collaborator Author

JohelEGP commented Dec 6, 2022

On the other hand, any tensor or any vector value cannot always be represented as a scalar.

Of course it can't, it loses the information on direction. But you should somehow be able to represent a component or magnitude of a vector. The simplest way would be to allow replacing the number representation type of a vector quantity with a scalar.

I'm not sure. ISO just defines speed as "magnitude of the velocity". By a definition that uses "distance", I think the example in https://en.wikipedia.org/wiki/Speed#Difference_between_speed_and_velocity, paragraph 2, would also have the speed be 0, because the distance from the starting and end points are 0.

I do not agree with that. Actually, the usage of length could mean 0. distance is defined in ISO 80000 as the "shortest path length" and path_length is specified as the length over the path, which should give a non-zero result here, right?

Once you do a rotation and end up in the same starting position, the distance is 0 and the path length $2π𝘳$.

You could allow defining kinds not by deriving.

Deriving is just an implementation detail here. In general, I just cannot find a good algorithm that will prevent conversions between those two but will allow all the rest to work.

I think it's simpler with values (or reflection). So rather than deriving from the speed, derive from something which makes it easy to implement (due to the lack of reflection).

@mpusz
Copy link
Owner

mpusz commented Dec 6, 2022

the distance is 0 and the path length 2pir.

ISO 80000 defines distance as the "shortest path length" so they should have the same value here assuming that your path is circular in your example. I would argue that actually a length could be zero in this case.

derive from something which makes it easy to implement

Again, the problem is not with inheritance but with the algorithm itself. Having all of the recipes for all the quantities I cannot find a good algorithm that covers all of the cases here.

@JohelEGP
Copy link
Collaborator Author

JohelEGP commented Dec 6, 2022

the distance is 0 and the path length 2pir.

ISO 80000 defines distance as the "shortest path length" so they should have the same value here. I would argue that actually a length could be zero in this case.

There are infinite path lengths from point A to point A, one being a rotation around the center of a circle. The shortest one from and to the same point, the distance, is 0.

derive from something which makes it easy to implement

Again, the problem is not with inheritance but with the algorithm itself. Having all of the recipes for all the quantities I cannot find a good algorithm that covers all of the cases here.

What cases?

@mpusz
Copy link
Owner

mpusz commented Dec 12, 2022

As long as I can somehow imagine user not being super confused by:

auto r = 5 * radius[m];        // 5 m
quantity<diameter[m]> d = r;   // 10 m

what about kinetic energy being defined as T = m * v^2 / 2. Does that mean that:

auto mass = 10 * isq::mass[kg];
auto speed = 2 * isq::speed[m/s];
auto e1 = mass * pow<2>(speed);                             // 40 J
auto e2 = quantity_cast<kinetic_energy>(e1);                // 20 J
quantity<kinetic_energy[J]> e3 = e1;                        // 20 J
quantity<kinetic_energy[J]> e4 = mass * pow<2>(speed) / 2;  // 10 J

I think that omitting the need to divide the above by 2 because it will be done automatically by the framework may be really error-prone 😞

What do you think about it? Feedback is welcomed from everyone on this.

@burnpanck
Copy link
Contributor

burnpanck commented Jan 5, 2023

Maybe we should move the discussion about computing/confirming "character" to #409, and keep this thread here focused on some sort of quantity_type in general - even though that may imply "character" in some or all cases.

I am not sure if we agree here. I would say that width is a kind of length, and position_vector is a kind of length, but position_vector is not a kind of width, which makes them incompatible.

As always, I ask for precise language, so we can clearly reason about concepts. Your statements "A is a kind of B" describe a hierarchy of quantities, but it may not match the ISO 80000 definition of kind. For clarity, let us use type (or still, quantity_spec), so we can discuss if that concept is the same as ISO 80000 kind. So to describe the hierarchy in question here, we may say length is a type of a quantity, and so is width. We may also say the type width is a subtype of the type length, and so is position_vector. But the type position_vector is neither a subtype of the type width, nor vice-versa. Does that still hold if I replace type with kind and subtype with subkind? The EXAMPLE 1 of NOTE 2 from the standard that you quoted explicitly 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". Given that ISO 80000-2 also defines diameter as "width (item 3‑1.2) of a circle, cylinder or sphere", which I take as the type diameter is a subtype of the type width. But if the kind of a quantity whose type is diameter is still length, then so must be the kind of a quantity whose type is width: They are of the same kind. So type is not exactly the same concept as kind, but type may still be a refinement of kind (i.e. a more fine-grained hierarchy).

Let me visualise that, together with a few more types, which I'll need later:

graph TD
    subgraph Length["kind: length"]
    length["length := root(dim=L)"]
    length-->width
    width-->diameter
    length-->radius
    radius -.- |equivalent as properties of circle| diameter
    end
    subgraph Position["kind: position_vector"]
    length---->position_vector
    end

    subgraph InvTime["kind: inverse_time"]
    invtime("root(dim=1/T) = 1/time")
    end
    subgraph Freq["kind: frequency"]
    invtime --> freq["frequency"]
    end
    subgraph Activity["kind: radioactivity"]
    invtime --> activity["radioactivity"]
    end

    subgraph LengthByTime["kind: length_by_time"]
    length_by_time("root(dim=L/T) = length * inverse_time")
    length_by_time --> width_by_time("width * inverse_time")
    end

    subgraph LengthFreq["kind: length_times_freq"]
    length_by_time ---> length_times_freq("length*frequency")
    length_times_freq --> width_times_freq("width*frequency")
    end

    subgraph Velocity["kind: velocity"]
    length_by_time ---> velocity["velocity := position_vector * inverse_time"]
    end

    subgraph PosTimesFreq["kind: position_vector*frequency"]
    velocity --> pos_times_freq("position_vector * frequency")
    end
Loading

Note that I do not have a ISO 80000 reference that defines a hierarchy of kinds, but it's definition of quantity types seems to indicate at least a hierarchy of types. (It doesn't use the term quantity type as a concept though, it simply calls it a quantity).

There are a few assumptions here, but bear with me for the moment. One assumption here is that the kind of radius is also length. ISO 80000 states that quantities of the same kind are comparable (we call it "compatible"), such that we can rightfully ask "radius(circle_A) < diameter(circle_B)". This really is a comparison of two geometric lengths (and I'm sure we can come up with a geometry example where that comparison is meaningful for two circles A & B). We should also be able to do computations like "radius(circle_A) + length(line_C) + diameter(circle_B) + width(some_geomertic_object)" or just "radius(circle_A) + diameter(circle_B)". The meaning of these expressions is intuitive according to ISO 80000, and in both cases, these are just quantities of type length again, neither radius not diameter.

Let us compare this with our frequency/radioactivity example. If we believe that ISO 80000 forbids us to compare those two, then they must be of different kinds. I have included that in the illustration above. Because neither frequency nor radioactivity is a subkind of each other, they are not compatible. But for frequency + 1/time, we will still have to come up with a rule (or find a reference in ISO 80000) that matches our intuition. But by defining that such an expression implicitly means frequency + narrowing_quantity_type_cast<frequency>(1/time), we achieve frequency + 1/time -> frequency, and thus further frequency + 1/time + radioactivity still remains a logic error.

So what about the type position_vector? Again, I do not have an ISO 80000 reference about it's kind, but since we set out to formalise how velocity is different from width by time, I went ahead and declared position_vector a separate, different sub-kind of length. The rules of quantity calculus and indeed the expressions templates you have implemented then generate an implicit expansion of the type hierarchy, and I have shown a subset of those "derived" types in the illustration above (generated/"derived" types are shown with rounded corners). We also interpret ISO 80000's definition of the type velocity as to be the same type as any expression of position_vector * inverse_time. But with our decision above that we should implicitly interpret incompatible sub-kinds as a narrowing_quantity_type_cast, then velocity = position_vector * inverse_time still remains compatible with width / time = width * inverse_time (or even position_vector * frequency, but not width*frequency).

We have a couple of options here:

  1. We do not make sub-kinds of each other compatible through implicit up-casts; effect frequency + 1/time is not compatible anymore, neither is velocity + width/time, but radius + diameter still is
  2. We declare we do not consider ISO 80000 kinds at all for the sake of compatibility and instead directly use the type; effect radius + diameter is not compatible anymore; though radius + length still is, frequency + 1/time too, and velocity + width/time too! -> not a solution for our goal for velocity
  3. Combine 1&2. Effect; neither radius + length, frequency + 1/time nor velocity + width/time are compatible.
  4. Declare position_vector not to be a sub-type of length, but both of them to be "parallel" kinds of dimension $L$, potentially sub-types of some "proto-length" type (length != root(dim=L) in the illustration above).

Similar logic applies for energy and enthalpy, as well as it's derived types specific_energy and specific_enthalpy. Either these are parallel kinds (or types if we choose 2) in the options above), or we choose 1) in the options above and they are sub-kinds of each other, or we chose 3) = 1)&2) and they can remain sub-types of each other.

@burnpanck
Copy link
Contributor

So to say, with that quantity_type concept refining ISO 80000 kind, I propose a framework to systematically determine if two quantities are compatible, which is consistent with quantity calculus. So maybe let us decide for each of the examples above what the result type should be or that they should be incompatible, and then we can try to draw up the type hierarchy. If we cannot, then someone will have to describe an alternate framework so that we have a reasonable concept of compatibility.

@JohelEGP
Copy link
Collaborator Author

JohelEGP commented Jan 5, 2023

IMHO not worth the trouble, given that the representation already can capture the "representation character", if not the "quantity character".

That reminds me that the number type can be x_component, which is a scalar but has a direction. What difference would it make if the quantity's characteristic is scalar or vector?

Let me visualise that, together with a few more types, which I'll need later:

Diameter is a kind of width.

Note that I do not have a ISO 80000 reference that defines a hierarchy of kinds, but it's definition of quantity types seems to indicate at least a hierarchy of types. (It doesn't use the term quantity type as a concept though, it simply calls it a quantity).

I was working on expressing that in C++. You can find it here for reference: https://compiler-explorer.com/z/9Ta34nTK7.

@JohelEGP
Copy link
Collaborator Author

JohelEGP commented Jan 5, 2023

Coincidentally, I checked the Wikipedia article for some quantities of latter parts of ISO 80000. Unsurprisingly, they have less general formulas that work in particular cases that involve different quantities. How would the library accommodate for that?

Consider that position vector is defined by ISO 80000-3 as

vector (ISO 80000-2) quantity from the origin of a coordinate system to a point in space

We have

inline constexpr auto length              = defn<"𝘭">();
inline constexpr auto width               = defn<"𝘣", "𝘣 = {𝘹 ∈ 𝘭 | {𝘹} ≥ 0}", length("𝘭")>();
inline constexpr auto diameter            = defn<"𝘥", width>();
inline constexpr auto radius              = defn<"𝘳", "𝘳 = ½𝘥", diameter("𝘥")>();
inline constexpr auto time      = defn<"𝘵">();
inline constexpr auto velocity     = defn<"𝙫", "𝙫 = d𝙧/d𝘵", position_vector("𝙧"), duration("𝘵")>();
inline constexpr auto speed        = defn<"𝘷", "𝘷 = |𝙫|", velocity("𝙫")>();

𝘷 = 𝘳/𝘵, where 𝘷 is speed, 𝘳 is radius, and 𝘵 is time, can be a valid formula for calculating speed. That can happen when movement is in a straight line. The origin of the coordinate system, as present in the definition of velocity, would be the center of a circle. And so the traveled length is the radius.

That formula for speed could be the speed formula for a domain. So I've had my reservations about enforcing the "one true way".

@JohelEGP
Copy link
Collaborator Author

JohelEGP commented Jan 5, 2023

There are also quantities defined by ISO 80000 without a formula, and with multiple formulas, sometimes none of general application.

Famously, there's energy, with myriads of quantities of its kind, and no formula itself. The Wikipedia article does list plenty of formulas for energy itself and not one of its kinds. So energy is really little more than a strong type for tagging quantity expressions with the right dimension as being a quantity of energy.

@burnpanck
Copy link
Contributor

burnpanck commented Jan 5, 2023

Diameter is a kind of width.

Please clarify; NOTE 2 uses the language "the kind of [QUANTITY] is [KIND]"; never "[QUANTITY] is a kind of [WHAT-IS-THIS?]". Their language does not create a hierarchy, but the latter language does. Because of that, I suggested to use the term type instead, and draw that hierarchy using the language of type. As I have drawn it, diameter is a sub-type of width matching your statement. Where do we disagree here?

𝘷 = 𝘳/𝘵, where 𝘷 is speed, 𝘳 is radius, and 𝘵 is time, can be a valid formula for calculating speed. That can happen when movement is in a straight line. The origin of the coordinate system, as present in the definition of velocity, would be the center of a circle. And so the traveled length is the radius.

Actually, in my field of physics, we have regularly used r as a symbol for a position_vector, and r as it's magnitude, i.e. a distance. No circles involved, no straight lines involved, and the name "radius" probably inappropriate, even though it incidentally is used with the same symbol, r.

@burnpanck
Copy link
Contributor

burnpanck commented Jan 5, 2023

I was working on expressing that in C++. You can find it here for reference: https://compiler-explorer.com/z/9Ta34nTK7.

Nice work! But I don't see how this defines a hierarchy, or "compatibility" as in the sense of ISO 80000 kind, or more generically as required for the semantics of quantity calculus in mp-units. It defines relations between quantities. How do I use that to answer questions of the form "is every diameter compatible with path_length"? I also don't see the definition of defn, which I'd love to see. Together with a suitable is_subtype predicate, that may indeed create a hierarchy.

@JohelEGP
Copy link
Collaborator Author

JohelEGP commented Jan 5, 2023

Please clarify

NOTE 1 The division of 'quantity' according to 'kind of quantity' is to some extent arbitrary.

We define what a kind is in such a way that it enables safe manipulation of quantities. I don't believe it's necessary to introduce more terms. It should suffice that width and height are kinds of length, but width is not a kind of height, height is not a kind of width. And so certain expressions involving width and length are allowed by the library, whereas the same with width and height are not.

@JohelEGP
Copy link
Collaborator Author

JohelEGP commented Jan 5, 2023

I was working on expressing that in C++. You can find it here for reference: https://compiler-explorer.com/z/9Ta34nTK7.

Nice work! But I don't see how this defines a hierarchy, or "compatibility" as in the sense of ISO 80000 kind, or more generically as required for the semantics of quantity calculus in mp-units. It defines relations between quantities. How do I use that to answer questions of the form "is every diameter compatible with path_length"? I also don't see the definition of defn, which I'd love to see. Together with a suitable is_subtype predicate, that may indeed create a hierarchy.

https://gcc.godbolt.org/z/va6fMsd75 is more complete, but it doesn't have any of that. That's why my suggestions in the matter are not accompanied with working code.

@burnpanck
Copy link
Contributor

burnpanck commented Jan 5, 2023

I don't believe it's necessary to introduce more terms. It should suffice that width and height are kinds of length, but width is not a kind of height, height is not a kind of width. And so certain expressions involving width and length are allowed by the library, whereas the same with width and height are not.

I believe that is not what ISO 80000 is trying to tell us. Specifically, chapter 4.2 that @mpusz quoted says:

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. Mathematic operations can be performed on quantities other than ordinal quantities, as explained below. Two or more quantities cannot be added or subtracted unless they belong to the same category of mutually comparable quantities.

So diameter and height are comparable, and we agree that diameter is a width, so width and height are comparable. I believe that this makes sense: If I want to determine if a circle C3 fits between two concentric circles C1 and C2, I check for radius(C1) + diameter(C3) <= radius(C2).

So if we want to allow that, then we need to introduce more terms. Otherwise, you are right, we don't need separate terms (this was my option (2) at the end of my latest essay).

https://gcc.godbolt.org/z/va6fMsd75 is more complete, but it doesn't have any of that. That's why my suggestions in the matter are not accompanied with working code.

Thanks! I'll check it out - maybe I can come up with a suitable is_subtype.

@JohelEGP
Copy link
Collaborator Author

JohelEGP commented Jan 5, 2023

So diameter and height are comparable, and we agree that diameter is a width, so width and height are comparable. I believe that this makes sense: If I want to determine if a circle C3 fits between two concentric circles C1 and C2, I check for radius(C1) + diameter(C3) <= radius(C2).

That last expression doesn't involve height. Only kinds of width.

@burnpanck
Copy link
Contributor

That last expression doesn't involve height. Only kinds of width.

Where does it say that radius is a width?

Either way, then take wavelength(evanescent_field_in_SCHOTT_glass(HeNe_laser)) < diameter(optical_fiber). Explicitly allowed in that statment. wavelength is a width too?

@JohelEGP
Copy link
Collaborator Author

JohelEGP commented Jan 5, 2023

inline constexpr auto diameter            = defn<"𝘥", width>();
inline constexpr auto radius              = defn<"𝘳", "𝘳 = ½𝘥", diameter("𝘥")>();

Diameter is a width and radius is half of a diameter. Both are kinds of width.

inline constexpr auto wavelength          = defn<"𝜆", length>();

Wavelength is a kind of length. I don't know whether 𝜆 < 𝘥, where 𝜆 is wavelength and 𝘥 is diameter, should be valid when expressed in C++.

@burnpanck
Copy link
Contributor

Wavelength is a kind of length. I don't know whether 𝜆 < 𝘥, where 𝜆 is wavelength and 𝘥 is diameter, should be valid when expressed in C++.

ISO 80000 explicitly says that 𝜆 < 𝘥 is a valid physical expression. Do we want to model this in C++ or not? I think we should.

Which of the following expressions should be disallowed?

  • diameter(nose_wheel) < height(nose_wheel_stand)
  • time(delay_time_of_geiger_counter_electronics) + 1/radioactivity(U238_sample)

Give me a rule of what is compatible (and what the resulting kind of the expression is)! There are many options, such as:

  1. [Easy] Nothing is allowed. All quantities are different unless they are the exact same type/kind/whatever. frequency + 1/time is inacceptable. velocity + sink_rate is inacceptable. velocity + position_vector/time is allowed by definition of velocity.
  2. [Easy] Everything compatible with dimensional analysis is allowed. frequency + radioactivity is fine.
  3. [Elegant] There is a hierarchy of kinds where somehow all of width, diameter, radius are vertically below each other, but height and wavelength are not. Let us define is_subkind<A,B> for defn, then generate the hierarchy and test a few examples of compatibility (and result kind). No need to have any other term than kind.
  4. [Less elegant, but maybe better?] There is a hierarchy of types that subdividies kinds. In addition to is_subtype<A,B>, we also need at least is_subkind_root<A>, which we probably cannot derive just from the current defn information. We may need to handcraft the latter based on interpretation of the accompanying text in ISO 80000.
  5. Something else.

Let me know what you want to do with mp-units, and if I can help with any of that (such as continuing to ask uncomfortable questions like why the diameter of the nose wheel should be comparable to the wingspan, but not the height of the vertical stabilizer).

@burnpanck
Copy link
Contributor

PS: I don't think that we'll get a reasonable hierarchy from any is_subkind derived from defn, as the defining equations simply do not define an "is-a" relationship.

@JohelEGP
Copy link
Collaborator Author

JohelEGP commented Jan 5, 2023

PS: I don't think that we'll get a reasonable hierarchy from any is_subkind derived from defn, as the defining equations simply do not define an "is-a" relationship.

Yeah. The code mostly has formulas. But ISO 80000 usually has more, including definitions and remarks. For example, the definitions quoted at #405 (comment) describe "is-a" relationships. The intent is that the way they're defined in code are in line with the ISO 80000 definitions, so considering:

inline constexpr auto height              = defn<"𝘩", length>();
inline constexpr auto radius              = defn<"𝘳", "𝘳 = ½𝘥", diameter("𝘥")>();

defn describes height as a kind of length, and radius as a factor of diameter and a kind of width.

@mpusz
Copy link
Owner

mpusz commented Jan 5, 2023

Note that I do not have a ISO 80000 reference that defines a hierarchy of kinds
We declare we do not consider ISO 80000 kinds at all

There is no such thing in ISO 80000 as a hierarchy or even a list of kinds. ISO 80000 never specifies exactly a kind of quantity and even says "The division of the concept ‘quantity’ into several kinds is to some extent arbitrary." It does not say if a kind is equivalent to a dimension or to the more fine-grained hierarchy of named quantities. The only thing that ISO 80000 says is that kind is an "aspect common to mutually comparable quantities", but again, does not specify which quantities should be comparable. This is up to us to specify how much type-safety and how many casts we will introduce that will still not make the library unusable. We have to find some compromise here.

Practically all quantities of the same dimensions are comparable and could be added and subtracted. It is trivial to implement as well. But is it enough to say our library is type-safe? In such a case, why bother defining all ISO 80000 quantities and not only one quantity per dimension?

As @burnpanck correctly listed in #405 (comment) it is up to us (and all other users) to define what is good for a C++ type-safe library and what is not. I hope that as a result of this thread, we will come to some common conclusions that could result in a successful logic for a library. 😃

@burnpanck
Copy link
Contributor

burnpanck commented Jan 5, 2023

For example, the definitions quoted at #405 (comment) describe "is-a" relationships.

Yeah I'm not fully convinced by that - "radius is a diameter" just doesn't feel right. But I'm willing to accept that if that is what prevents us to at least agree on some definition that is self-consistent under quantity calculus.

Practically all quantities of the same dimensions are comparable and could be added and subtracted. It is trivial to implement as well. But is it enough to say our library is type-safe? In such a case, why bother defining all ISO 80000 quantities and not only one quantity per dimension?

Exactly. In fact, I don't know of any package in any language that imposes compatibility restrictions on ISO 80000 quantities beyond dimensional analysis - probably because anything else appears to be much more difficult. As a user of mp-units "classic", I only sparingly made use of the kind concept so far, so even having such a "units-only" package with a nice, modern C++ interface is great for me, even more so if it is C++2x STD. I have a feeling that the committee will be afraid to standardise anything that isn't obviously correct anyway, so chances are ISO 80000 quantities will not make it and we only get SI units and dimensions in STD. Which would be great nonetheless!

So why bother? You have provided me with a few examples where the ISO 80000 is rather clear, first and foremost frequency and radioactivity - we even have units that are restricted to that kind of quantities. It would be great to support that.

IMHO, we should do the following: We keep most of the quantities compatible with each other, unless the case is clear - based on examples that we should collect somewhere. For those, we handcraft a suitable hierarchy of kind, and make the machinery available as a customisation point. Then, users can further subdivide the kind hierarchy with what feels right in their domain. We may still define all of the ISO 80000 quantities as a more fine-grained subdivision of quantity types, which can serve as basis for that domain-specific customisation. Terms kind and type up for bikeshedding, I don't care, as long as we have separate terms that we can reason about.

@JohelEGP
Copy link
Collaborator Author

JohelEGP commented Jan 5, 2023

Yeah I'm not fully convinced by that - "radius is a diameter" just doesn't feel right.

That's why I said

and radius as a factor of diameter and a kind of width

Thought I misspelled "as" instead of "is".

@mpusz
Copy link
Owner

mpusz commented Jan 6, 2023

As a user of mp-units "classic", I only sparingly made use of the kind concept so far

I really do not know how to proceed now. Should we strive to provide ISO 80000 quantities to provide vocabulary types for users or just limit the implementation to the simplest cases and leave the rest for users to define based on their requirements? Also, should we continue to support quantity kinds as we do for now in ISO 80000, which does not introduce much type safety (at least, it seems we do not know how to make it work), or should we restore the V1 design with a dedicated quantity_kind class which was really strongly types? Do you have any ideas here on how to move on now?

@JohelEGP
Copy link
Collaborator Author

JohelEGP commented Jan 9, 2023

For some context, see #413 (comment) and the last part of #413 (comment).

A radius is half of a diameter. @burnpanck correctly points out that the factor between these quantities only applies when converting one of these quantities to the other. So it is dependent on the object being measured. That is different from the conversion factor between temperatures differences, which apply between units.

With that in mind, it seems that comparisons should only look at the (number, unit) pair of the quantity arguments, as is customary. So $2\ m = 2\ m$ irrespective of the quantity kind either side of the equation represents.

Similarly, construction would only look at the (number, unit) pair of its input argument. Just like we commonly formulate that the radius 𝘢 equals $2\ m$, or the distance 𝘣 equals the width of bridge C.

@JohelEGP
Copy link
Collaborator Author

It was my mistake to define radius like

inline constexpr auto length              = defn<"𝘭">();
inline constexpr auto width               = defn<"𝘣", "𝘣 = {𝘹 ∈ 𝘭 | {𝘹} ≥ 0}", length("𝘭")>();
inline constexpr auto diameter            = defn<"𝘥", width>();
inline constexpr auto radius              = defn<"𝘳", "𝘳 = ½𝘥", diameter("𝘥")>();

ISO 80000 literally defines it as

half of a diameter (item 3‑1.5)

It goes out of its way to not define a formula for a reason. I had once tried turning such a quantity definition into a formula. Then I read up a bit about it on Wikipedia, and realized it was wrong to do that. Since then, I've refrained from doing that kind of transformation. That happened in a much later part of the ISO 80000 series, and I didn't realize I had earlier mistakes to fix.

P.S. I think #405 (comment) is going in the same, wrong direction, by deviating from ISO 80000, as I argument in latter comments.

So the definition of a radius should be like that of diameter, i.e. a kind of width.

@JohelEGP
Copy link
Collaborator Author

JohelEGP commented Jan 10, 2023

At least until partway through ISO 80000-10, the remaining quantities that are a factor of another (and not dependent on the measurand, and not a factor between units like the temperature units) are angular quantities, all with the factor $2∏^{\pm1}$. If angular measure were a base quantity, it would be not a factor, but part of a coefficient along with cotes angle, I think.

inline constexpr auto rotation = defn<"𝘕", "𝘕 = 𝜑/(2π)", rotational_displacement("𝜑")>();
inline constexpr auto angular_frequency    = defn<"𝜔", "𝜔 = 2π𝘧", frequency("𝘧")>();
inline constexpr auto angular_repetency   = defn<"𝘬", "𝘬 = |𝙠| = 2π/𝜆", wave_vector("𝙠"), wavelength("𝜆")>();

@mpusz
Copy link
Owner

mpusz commented Jan 10, 2023

@JohelEGP
Copy link
Collaborator Author

To answer #405 (comment), with #405 (comment) in mind, I don't think the library should apply the factor ½ when converting to kinetic energy. You can't know if the input quantity has already had ½ applied. And I insist in #405 (comment), that if 𝘛 were a function with the input quantities of mass and speed, 𝘛(𝘮, 𝘷) = ½𝘮𝘷², then the library should apply both ½ and ².

@mpusz
Copy link
Owner

mpusz commented Jun 15, 2023

Addressed in V2 with quantity_spec.

@mpusz mpusz closed this as completed Jun 15, 2023
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

3 participants