Closed JohelEGP closed 1 year ago
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.
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?
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.
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
?
That's right. I think I misunderstood https://github.com/mpusz/units/issues/405#issuecomment-1333743705 because there's a 2
in the code and "2x" in the text, but they're different things.
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).
But I am open to other ideas as well. We need a POC here.
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.
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.
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.
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.
There might be some overlap with https://github.com/BobSteagall/wg21/issues/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.
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.
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
.
I started to work on that on the V2 branch already.
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)
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
.
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.
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
.
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.
I fixed the comments.
I think that
speed
should be defined in terms ofdistance
, notlength
.
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 andtime
can be used to define atime_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.
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.
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 mean0
.distance
is defined in ISO 80000 as the "shortest path length" andpath_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).
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.
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?
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.
$๐ = ยฝ๐ฎ๐ทยฒ$ is different. If $๐$ were a function taking the variables in the expression $๐ฎ$ and $๐ท$ as parameters (which can be synthesized from the equation), that'd make sense. Notice that you already have to add that pow<2>
call above. The conversion support is only for factors, defined in ISO 80000-1 A.2, when one quantity is a factor of another.
I am not so sure if that is true. m * v^2
is a derived quantity X
with the same dimension as kinetic_energy
. kinetic_energy
is defined as half of X
so in such case 1/2
is a factor which exactly the same case as radius being 1/2 of a diameter.
In my opinion, the only difference between this and diameter/radius problem we discussed before is that this case uses derived quantities and the other base quantities. The rest is exactly the same.
X
can't just be any quantity of the same dimension. $m$ has to be the mass of a body and $v$ its speed. That's different from $๐ณ = ยฝ๐ฅ$, where $๐ฅ$ is a kind of diameter. Or more generally $๐โ = ๐ฅ๐โ$, where $๐ฅ$ is the factor and $๐โ$ a kind of some specific quantity. If the X
was named (and thus expressed in the type system) and specified to exclude the factor ยฝ, converting to $T$ would just be $ยฝ$X
. But unlike the $๐โ = ๐ฅ๐โ$ I'm suggesting to support, where $๐โ$ is well specified, you can't generally assume X
has the desired properties to convert to $T$.
What cases?
template<QuantitySpec Q1, QuantitySpec Q2>
[[nodiscard]] consteval bool interconvertible(Q1, Q2)
{
if constexpr (NamedQuantitySpecaltitude
/ time
-> sink_rate
)
return Q1::dimension == Q2::dimension;
}
}
The above is the biggest issue. A few questions:
derived_quantity_spec
expression preserves the operation that was used to define and create it. Thanks to that, we can for example, infer quantity characters. Also, it allows us to know about the kinds of derived quantities being involved in the expression. However, with that, the types forming a specific quantity from different expressions are not necessarily the same. For example: velocity = position_vector / duration
and velocity
can also be created by acceleration * duration
. Those form two different derived_quantity_spec
types that are not the same and do not inherit from each other but should be interconvertible.width
, height
, position_vector
are equivalent.Do you have any ideas on how to improve that?
- Should quantities of different characters (scalar/vector/tensor) be interconvertible?
We should allow modelling math and physics. So we need examples.
2. In order to be able to validate kinds of quantities, I compare named quantities. But that is not enough. I should be also able to do the same for named and derived quantities.
Please, clarify. Do you mean as in the code's comment, regarding sink_rate
?
3. and
velocity
can also be created byacceleration * duration
This seems like a programming design space issue. I suppose it's about how verbose do we require the user to confirm that $๐ข๐ต$ is indeed $๐ท$.
Please, clarify.
velocity
derives from derived_quantiy_spec<position_vector, per<duration>>
and is and should be interconvertible with it even though the derived quantity spec is not named. However, we can create velocity
in another way (as specified in point 3
) which will not make the result interconvertible with velocity
.
Do you mean as in the code's comment, regarding sink_rate?
This is actually point 4
above.
Looking at this case in isolation, you can do $๐ซ = ๐๐ต = (๐ซ/๐ตโฅ)๐ตโฅ = ๐ซ$. So the problem is having a generalized algorithm, right?
If we "unpack" acceleration
to pieces, we lose some information necessary to detect interconvertibility:
power
which is scalar despite being defined in terms of force
and velocity
So the problem is having a generalized algorithm, right?
Yes, this is exactly what I stated above with "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."
For the implicit conversion target(source)
, each kind ๐โ
in source
should be a kind of ๐โ
present in target
.
For example,
length(width)
, width
should be a kind of length
,q<๐ซ>(q<๐๐ต>)
, the kind ๐งยน
in q<๐๐ต>
is a kind of ๐งยน
in q<๐ซ>
, where ๐ง
is position vector.Yes, I agree with that. "Unpacking" for acceleration would work here. However, if there are derived quantities that are constructed the same way but then are branched to different kinds then "unpacking" will not work. For example drag_coefficient
depends on drag_force
that is a kind of force
. If I "unpack" drag_force
to ingredients than I will not know if it is a drag_force
or a different kind of force
anymore. This is why I could not find the best way to follow here. Any ideas here are welcomed.
Peek into the makeup of the quantity without unpacking.
I am not sure if it will work for all cases either. With this I will be able to create drag_coefficient
from any force
and not only drag_force
.
Why? If the force in the source type is not a kind of drag force, it should fail.
Because you suggested that if quantity is not convertible I should also peek into its makeup to check again. So instead just comparing if we have drag_force
I will also check if force
was created in a correct way which is the same as the drag_force
. I hope I am missing something here. It is hard to talk unless we can share godbolt examples. I hope to be able to merge the branch to the mainline soon. But there is always something that prevents me from doing that and requires more work...
BTW, I still need to make the branch to work on other compilers.
I think the convertibility of "kinds" could be formalised to follow the same rules of dimensional analysis, where kind-specialisations introduce a new dimension for dimensional analysis. Breaking up the kind into it's constituent dimensions (or more general kinds) is only allowed at the source side (up-casting/widening is allowed, down-casting/narrowing isn't). Thus, anything defined in terms of drag_force
needs to be made up from an atomic drag_force
, but never a plain force
. Breaking up a drag_force
into it's parts is considered a widening, so assigning a drag_force / acceleration
to a mass is acceptable, but mass * acceleration
can only be assigned to a plain force
, but not a drag_force
. Of course, that implies that drag_force / force
should not be simplified to a dimensionless quantity within an expression, as it is convertible to a dimensionless quantity but not equivalent: (drag_force / force) * mass * acceleration
should probably remain a drag_force
. This, in turn would require us to be careful to distinguish "plain" dimensions from "kinds"/narrowed dimensions, otherwise force
would be considered a narrowing from it's constituent dimensions $ML/T^2$, and wouldn't be convertible from `mass acceleration`.
As for the other cause of trouble, the scalar/vector/tensor character, there may be a way to follow the same logic, in that conversion from vector->scalar may be allowed, but not the other way around. However, I have to admit that I rarely distinguish the two concepts, so I don't know if that is really helping here.
I think the convertibility of "kinds" could be formalised to follow the same rules of dimensional analysis, where kind-specialisations introduce a new dimension for dimensional analysis.
But a dimension is "a product of powers of factors corresponding to the base quantities". So the only way to extend dimensional analysis would be by adding base quantities. I agree with your safety improvements, but it shouldn't affect the dimensional analysis.
I definitely do not want to change the concept of dimension or the dimensional analysis, or add more base quantities. Instead I was proposing some sort of "kind analysis", which is a refinement of dimensional analysis (i.e. matching kinds should imply matching dimensions). With that, I believe we can solve the problem of the "generalised algorithm" to do the matching:
However, maybe I misunderstood what was the actual discussion/problem here. Before the drag_force
example, you have been discussing diameter vs. radius and kinetic_energy vs. $M L^2 / T^2$. To allow automatic conversion between diameter and radius, those two would have to be quantity specifications of the same "kind", but a different factor. On the other hand, no automatic conversion would occur between quantities matching the dimension of energy, and quantities specified to be of the kind kinetic_energy. So contrary to the comment above by @mpusz, the difference would not be base vs. derived quantities, but matching "kinds":
In my opinion, the only difference between this and diameter/radius problem we discussed before is that this case uses derived quantities and the other base quantities. The rest is exactly the same.
That said, I do question the value of automatic conversion between radius and diameter somewhat, given the risks of unexpected surprises.
(i.e. matching kinds should imply matching dimensions)
That's a truth.
NOTE 2 Quantities of the same kind within a given system of quantities have the same quantity dimension. [...] -- https://jcgm.bipm.org/vim/en/1.2.html
To allow automatic conversion between diameter and radius, those two would have to be quantity specifications of the same "kind", but a different factor.
Indeed. And additionally, one is a factor of the other.
That said, I do question the value of automatic conversion between radius and diameter somewhat, given the risks of unexpected surprises.
That isn't too different from today's master branch. Kilometres and metres are interconvertible. Although those are prefixes, and not different quantities or a factor between them.
That said, I do question the value of automatic conversion between radius and diameter somewhat, given the risks of unexpected surprises.
That isn't too different from today's master branch. Kilometres and metres are interconvertible. Although those are prefixes, and not different quantities or a factor between them.
I claim that only semantically equivalent objects should be interconvertible, but what exactly we consider equivalent is maybe a bit subjective. We probably agree that "1 km" and "1000 m" describe the same physical object, a quantity of dimension length. Do a "radius of 1 m" and a "diameter of 2 m" describe the same physical object? Maybe. As properties of a circle, they certainly describe the same circle (but is a "radius" equivalent to a "circle"?). If a "radius of 1 m" is equivalent to a "diameter of 2 m", then it should definitely not also describe a physical length of 1 m (otherwise it would be transitively equivalent to a "diameter of 1 m"). I think preferring equivalence between radius and diameter over radius and length isn't out of the question, but also isn't that obvious.
When there is a risk of subjective misinterpretation, I tend to prefer to err on the cautious side in accordance with the principle of least surprise. Maybe a radius should neither be considered fully equivalent to a diameter nor a general physical length - instead both of those could require an explicit cast.
We may potentially draw inspiration from the quantities framework within astropy, another python library that I have worked with in the past. The have a concept of equivalencies: Conversion between quantities that are considered equivalent under some circumstances only is opt-in. That said, they do not have a concept of quantity character apart from the dimension. The standard example is conversion between vacuum wavelength and frequency of light. In particular, their dimensionless_angles()
equivalency does not correctly apply the $2 \pi$ factor to convert between angular frequency $\omega$ and frequency $f$ in Hz
. It may be instructing to try to make that work, independent of if the equivalency is implicit or explicit.
Do a "radius of 1 m" and a "diameter of 2 m" describe the same physical object? Maybe. As properties of a circle, they certainly describe the same circle (but is a "radius" equivalent to a "circle"?). If a "radius of 1 m" is equivalent to a "diameter of 2 m", then it should definitely not also describe a physical length of 1 m (otherwise it would be transitively equivalent to a "diameter of 1 m").
Have we discussed this before? The example feels pretty familiar.
There should be no transitivity involved. I admit that something feels off. Perhaps it's because a "quantity kind" is not precisely defined. As a library which provides safety over int
s among other conveniences (one of them interconvertibility between these two quantities), we're trying to conveniently define "quantity kind".
If we look at their definitions, I think that a "radius of 1 m" equals a "diameter of 2 m", although their length is not the same. Here are the definitions of these quantities from ISO 80000-3:
Item No. | Name | Definition |
---|---|---|
3-1.1 | length | linear extent in space between any two points |
3-1.2 | width | minimum length of a straight line segment between two parallel straight lines (in two dimensions) or planes (in three dimensions) that enclose a given geometrical shape |
3-1.5 | diameter | width (item 3โ1.2) of a circle, cylinder or sphere |
3-1.6 | radius | half of a diameter (item 3โ1.5) |
So the library's interpretation of "quantity kind" shouldn't allow a radius to equal two different lengths due to its connection to diameter. As for the length of a "radius of 1 m" not being equal to the length of a "diameter of 2 m", of course not. The linear extent of the circle's radius and diameter are not equal.
When there is a risk of subjective misinterpretation, I tend to prefer to err on the cautious side in accordance with the principle of least surprise.
I agree. There's no escaping subjective misinterpretation. But the case at hand is systematically well specified, so there's a single objective interpretation.
up-casting/widening is allowed, down-casting/narrowing isn't
This is where I started, but it turned out heavily not efficient on the interface side then we got rid of the downcasting facility. Let's see the following code:
void foo1(quantity<isq::speed[m/s], int> s);
void foo2(quantity_of<isq::speed> s);
auto q1 = isq::length[m](1) / isq::time[s](1); // derived_quantity_spec<isq::length, per<isq::time>> (not isq::speed)
quantity<isq::speed[m/s], int> q2 = isq::length[m](1) / isq::time[s](1); // compile-time error
auto q3 = q1 + isq::speed[m / s](2); // derived_quantity_spec<isq::length, per<isq::time>> (not isq::speed)
foo1(q1); // compile-time error
foo2(q1); // compile-time error
Forcing the user to put quantity_cast
everywhere to downcast to a specific type/kind is counterproductive, and a lot of people will complain about such interfaces. This is why at some point, I introduced the opposite, which allowed the programmer to be more efficient:
void foo1(quantity<isq::speed[m/s], int> s);
void foo2(quantity_of<isq::speed> s);
void foo3(weak_quantity_of<isq::speed> s);
auto q1 = isq::length[m](1) / isq::time[s](1); // derived_quantity_spec<isq::length, per<isq::time>> (not isq::speed)
quantity<isq::speed[m/s], int> q2 = isq::length[m](1) / isq::time[s](1); // OK, implicit conversion on initialization
auto q3 = q1 + isq::speed[m / s](2); // isq::speed (consistent with the implicit conversion above)
foo1(q1); // OK
foo2(q1); // compile-time error
foo3(q1); // OK
If you have any ideas on how to improve the above, please let me know. Otherwise, I am afraid that we cannot forbid some of the conversions you mentioned because the library will turn out unusable. Believe me, I tried that already...
There should be no transitivity involved.
Equivalence should definitely be transitive (see equivalence relation) and so is "the equality" (see equality(mathematics); relation with equivalence). There may be multiple equivalence relations, but "equality" is the "finest" of all of them, i.e. equality implies equivalence in "all equivalence relations" on a set. Thus a "radius of 1m" should probably not be considered "equal" to a "diameter of 2m", but potentially equivalent, at least under a certain equivalence relation. We are encountering two relevant but different equivalence relations here
isq::radius(1, m)
โ isq::length(1, m)
, and isq::diameter(1, m)
โ isq::length(1, m)
, but therefore transitively isq::radius(1, m)
โ isq::diameter(1, m)
isq::radius(1, m)
~ย isq::diameter(1, m)
Whenever we convert between equivalent quantities, it should be very clear, which equivalence relation is being followed. If we have an explicit C++ cast, the keyword used to name the cast may be used to disambiguate. But implicit casts and explicit constructor calls should only do one of the two, and I would prefer if it would be "vertical" in the kind hierarchy.
401 is too big of a step.
The
quantity
class template takes a template argument for the quantity dimension. Thequantity_kind
class template slightly extends them to describe the "is a kind of" property. The same happens forquantity_point
andquantity_point_kind
.These are insufficient. And I don't think extending
quantity
, likequantity_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:quantity_kind
andquantity_point_kind
can go, which are replaced by a converting constructor inquanity
andquantity_point
(or whatever else according to how we define what a "kind" is).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.