Open mpusz opened 1 year ago
We should probably provide some exceptions from the rules for engineering purposes. For example, speed is defined as a magnitude of velocity which implies that it is non-negative.
For engineering purposes, speed probably should be defined as length / time
which improves usability. For example, we can easily calculate height / length
without mandating users to use a linear algebra library. In such a case a negative height can result in a negative speed.
What does the library need to do, besides permitting user-defined representation types that can catch negative values?
The check implementation part is easy. Just do the compile-time branch with gsl_Assert()
.
However, first, the logic (when and where) has to be figured out. Then we have to find out how to store the information about this in a derived quantity based on the quantity equation.
I think I have an answer.
Either we do nothing, or provide a way to opt-into having a non-negative quantity type.
For example, I wrap a width and a height in a Cartesian vector. I should be able to add and subtract these values without triggering assertions. Sometimes, the Cartesian vector represents a size, in which case both of the components should be non-negative. In this case, the width and height components are equally constrained. So there isn't anything particularly special about width.
It seems to me that ISO/IEC 80000 sometimes calls out special cases like this. However, that shouldn't prevent us from applying the maths to manipulate these values. As for
or provide a way to mark a given value's type as non-negative
it seems to me that we actually want an interface that both applies abs
when first converting the quantity
and updates the quantity_spec
to require the numerical value to be non-negative from there on.
So I can do
Quantity_of<width> auto q = (entity1.pos.x - entity2.pos.x).non_negative();
when I require the result to be the width that is non-negative, and an equivalent interface to easily name such a constrained type.
It seems to me that ISO/IEC 80000 sometimes calls out special cases like this.
Others include quantity difference and quantity point pairs.
So where we might use quantity<time[s]>
and quantity_point<time[s]>
as a difference and point, respectively,
ISO/IEC 80000 might actually have different names for those.
For example, I get the impression that these come in groups of points and corresponding difference:
There may be a few others, but I don't remember. I think I may have mentioned this before. I also don't remember if V2 does anything in particular with this information.
For example, I wrap a width and a height in a Cartesian vector. I should be able to add and subtract these values without triggering assertions.
Sure, please note that subtracting width from height is a length that is not constrained by itself.
ISQ defines only width, thickness, diameter, and radius to be non-negative. Additionally, probably quantities that are defined as magnitudes of vectors could be non-negative as well.
However, subtracting width and width will give you width as a result, and we have to decide what to do with that... ๐
So I can do
Quantity_of<width> auto q = (entity1.pos.x - entity2.pos.x).non_negative();
when I require the result to be the width that is non-negative, and an equivalent interface to easily name such a constrained type.
I actually never thought of that, but I am not sure if that is needed. The temporary result probably does not need constraints. The check has to be done in the width constructor if the value is non-negative anyway.
I also don't remember if V2 does anything in particular with this information.
I am not sure if we should constrain some ISQ types to be used only for either quantity
or quantity_point
. It could be too much, but maybe we should discuss this option in a dedicated issue? Another question is who would be able to properly identify those?
For example, I wrap a width and a height in a Cartesian vector. I should be able to add and subtract these values without triggering assertions.
Sure, please note that subtracting width from height is a length that is not constrained by itself.
I meant the vector values, which does the component-wise operation (so no width plus height, but rather width plus width and height plus height).
However, subtracting width and width will give you width as a result, and we have to decide what to do with that... ๐
This is why I answered
Either we do nothing, or provide a way to opt-into having a non-negative quantity type.
I forgot that to mention that it might just be the case of a missing abstraction.
Which is why I mentioned enhancing quantity_spec
to recognize the constrained case.
We still want to be able to have differences and points of "negative" quantities that aren't actually constrained.
If we force the property for all uses of the quantity, it becomes engineering unfriendly.
Please note that isq::height
can be negative.
Probably, you are abusing isq::width
for your coordinate needs. Width is something that you may measure with a
caliper. There is no physical way for it to be negative. Maybe you should derive your own quantity_spec
from isq::length
to denote X axis?
Yeah, I'm considering that. I need to do it for depth, anyways, which in ISQ is a synonym for height. However, I'm not convinced that my abuse of width means that my opinions on how it should be handled by the library are invalid. If it's constrained, the only reasonable way to change width values is to work on lengths before converting to widths.
I would do something like:
QUANTITY_SPEC(pos_x, isq::length);
QUANTITY_SPEC(pos_y, isq::length);
QUANTITY_SPEC(pos_z, isq::length);
If it's constrained, the only reasonable way to change width values is to work on lengths before converting to widths.
Yes, if we really want to subtract two widths measured by the caliper and we really need to subtract the bigger value from the smaller one we would probably need something like that:
QuantityOf<isq::length> auto q = isq::length(width1) - isq::length(width2);
which I think may have some sense in a "twisted" physical way ๐
I also don't remember if V2 does anything in particular with this information.
I am not sure if we should constrain some ISQ types to be used only for either
quantity
orquantity_point
. It could be too much, but maybe we should discuss this option in a dedicated issue? Another question is who would be able to properly identify those?
I remember that we did discuss this before. I think it was something about how a quantity difference can have different associated points. The associated points could be different by definition, and thus not share an origin among themselves. And I think I concluded that a quantity difference doesn't necessarily have a default associated point. So it's the user who should opt-into using the correct quantity point for their use case.
Probably, you are abusing
isq::width
for your coordinate needs.
The reason I haven't convinced myself of that is that the definition of width and height in ISO seem to match that of the components of a Cartesian vector.
My Cartesian vectors, represented with cartesian_vector๐d
and cartesian_point๐d
in code,
are more specific quantities of displacement and position vector, respectively.
However, I believe it has an even more specific representation as a tuple of (width, height, depthโโโ).
Only when a Cartesian vector represents a size, do all of its components need to be non-negative.
I think it's perfectly valid for a difference of widths to be negative.
A quantity
is supposed to represent a quantity difference.
And quantity_point
is just a more specific version representing a quantity offset from an origin.
So I think it's fine for those to be negative.
So if a quantity of width is supposed to be non-negative, what is the missing abstraction?
Is it that quantity
doesn't always represent a quantity difference?
What are the implications of making it so, and enhancing quantity_spec
to represent this use case?
I think it's perfectly valid for a difference of widths to be negative.
The ISQ defines width as:
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
How can a length between two parallel lines be negative? I do not think it has anything to do with points here. If we subtract two widths, we actually do not subtract the widths but their lengths. That is why I think that the cast I mentioned above makes a lot of sense from the physical point of view.
That's why this time I'm referring to a difference of widths, which shouldn't be the same as a width (the non-negative quantity).
Maybe it'd make more sense if quantity
was named quantity_vector
.
Then we could add quantity_magnitude
for the times we want the non-negative quantity
that doesn't represent a vector.
It could be implemented on top of quantity_vector
(like quantity_point
is),
but without operations (other than an accessor for the vector),
and possibly constrained to non-negative quantities.
So yeah, as a generalization of std::chrono::duration
,
I was under the impression that mp_units::quantity
was supposed to represent a vector space.
But widths don't have inverse elements, so it doesn't form a vector space.
It's still useful to have width vectors, and have their magnitude result in an actual width.
So I think quantity_magnitude
is the missing abstraction.
But widths don't have inverse elements, so it doesn't form a vector space. It's still useful to have width vectors, and have their magnitude result in an actual width. So I think quantity_magnitude is the missing abstraction.
I do not think that we miss something here. We might just misuse what we have. Notice that isq::width
is defined as a scalar quantity so trying to represent it with vectors or expect it to model a vector space is wrong.
As I wrote before, width is something that you can measure with the caliper, and physically you can't measure negative widths, right? ๐ If we subtract a longer displacement from a shorter one and take a magnitude of it, it would not be a width in a physical sense.
Let's forget about vectors for now and see the following:
// measured with caliper
quantity<isq::diameter[mm]> outside_pipe_diameter = 20 * mm;
quantity<isq::diameter[mm]> inside_pipe_diameter = 16 * mm;
quantity<isq::thickness[mm]> pipe_wall = quantity_cast<isq::thickness>((outside_pipe_diameter - inside_pipe_diameter) / 2);
std::cout << "Thickness of the pipe wall: " << pipe_wall << "\n";
If I accidentally subtract the outside diameter from the inside one I will get a negative value which does not make sense and the entire purpose of this Issue is to help find such problems (unfortunately only at runtime).
Now, let's come back to vectors.
If we deal with vectors, we should use isq::position_vector
(if our vector originates from the center of the coordinate system and expresses a point/position) or isq::displacement
otherwise. If isq::displacement
is not expressive enough, we can derive more specific quantities from them if we need additional safety.
Having those, we can subtract them, take the magnitude of the result, and cast it to isq::width
if that is what we actually deal with. But again, if we subtract a longer displacement from a shorter one and take a magnitude of the result (which will yield a positive value) we will still have an error in our calculations as this should not express a width. However, this time, the library will not find the issue at runtime because the magnitude(q)
operation will hide the issue.
The above is probably not a correct approach as well. So what we should do is the following:
quantity<isq::displacement[mm], la_vector> outside_pipe_diameter = {2, 3, 4} * mm;
quantity<isq::displacement[mm], la_vector> inside_pipe_diameter = {1, 2, 3} * mm;
quantity<isq::thickness[mm]> pipe_wall =
quantity_cast<isq::thickness>(isq::diameter(magnitude(outside_pipe_diameter)) -
isq::diameter(magnitude(inside_pipe_diameter)) / 2);
std::cout << "Thickness of the pipe wall: " << pipe_wall << "\n";
We have #463 opened to discuss how to handle vector quantities in the library correctly.
But widths don't have inverse elements, so it doesn't form a vector space. It's still useful to have width vectors, and have their magnitude result in an actual width. So I think quantity_magnitude is the missing abstraction.
I do not think that we miss something here. We might just misuse what we have. Notice that
isq::width
is defined as a scalar quantity so trying to represent it with vectors or expect it to model a vector space is wrong.
Rather than vector space, it seems like I meant more specifically affine space. This is the vector I'm talking about: https://mpusz.github.io/mp-units/2.0/users_guide/framework_basics/the_affine_space/#vector-is-modeled-by-quantity. And a width doesn't support this operation listed at https://mpusz.github.io/mp-units/2.0/users_guide/framework_basics/the_affine_space/#operations-in-the-affine-space:
- -vector -> vector
OK, I was confused because you mentioned taking the magnitude of a vector several times. What does the magnitude of such a vector/difference quantity mean?
Also, I think I was wrong in claiming that a negative width has no physical sense. For example, let's assume that we measure the width of brake pads in my car with a caliper today and after half a year. The second measurement will be smaller, which should give me the answer that the pads shrunk by this amount. It is still a quantity of width and its value probably should be negative.
Does it mean that in case we will add support for non-negative quantities they should actually apply only to the values in the quantity_point
and not for quantity
?
If quantity
represents a vector,
then it can't represent negative quantities like width.
But you can indeed represent changes in width.
Like you mention with https://github.com/mpusz/mp-units/issues/468#issuecomment-1708157645.
That's what vectors are for, after all.
I'm struggling more finding something that you can't measure with quantity
or a vector.
That's besides nominal properties and ordinal quantities.
And of course, algebraic structures that don't form a vector space.
Does it mean that in case we will add support for non-negative quantities they should actually apply only to the values in the
quantity_point
and not forquantity
?
I don't think so, since you can have negative points. I really think it's a new abstraction: Magnitude.
If quantity represents a vector, then it can't represent negative quantities like width. But you can indeed represent changes in width. Like you mention with https://github.com/mpusz/mp-units/issues/468#issuecomment-1708157645. That's what vectors are for, after all.
I am not sure if I understand what you meant here. First, you say that a vector can't represent negative quantities and then you say that is obvious ๐
I don't think so, since you can have negative points. I really think it's a new abstraction: Magnitude.
What does a negative point of width mean?
Depends on your origin, I guess.
With ideal_waist_size_of_adult_supermodel
,
it'd mean your waist size is less than that of the ideal for an adult supermodel.
quantity
always expresses the change of something, and quantity_point
provides the actual point/position (https://mpusz.github.io/mp-units/2.0/users_guide/framework_basics/the_affine_space/#vector-is-modeled-by-quantity). We are just not using points often enough, thinking that they are mostly used for temperatures.
Think about std::duration
and std::time_point
.
We are just not using points often enough thinking that they are mostly used for temperatures.
Well, I do use them a lot, and not for temperatures at all.
Here's a sample implementation of magnitude
: https://cpp2.godbolt.org/z/xP9fhM9Mf.
The lower pane has generated Cpp1.
The only operations I permit are with other magnitudes, and only in the direction of addition.
We are just not using points often enough, thinking that they are mostly used for temperatures.
I totally wouldn't want to mix positions and sizes/magnitudes in the type system.
I once tried to merge quantity
and quantity_point
,
disabling unwanted overloads with constraints.
The result was unsightly compared to each being its own template,
so I wouldn't want to do the same with quantity_point
.
Depends on your origin, I guess. With
ideal_waist_size_of_adult_supermodel
, it'd mean your waist size is less than that of the ideal for an adult supermodel.
Right ๐ Do you think such an origin should be defined as absolute
or relative
? Maybe then, absolute values should be non-negative?
Maybe then, absolute values should be non-negative?
With something like mean_sea_level
,
you'd want negative points.
And regardless of it being an absolute or relative origin,
you should be able to have negative points
(if you can do the same with any single other related origin).
But height
is not specified as non-negative. Only width
and quantities derived from it are defined as non-negative. See the tree of quantities of kind length
for reference: https://mpusz.github.io/mp-units/2.0/users_guide/framework_basics/systems_of_quantities/#system-of-quantities-is-not-only-about-kinds.
Yeah, I said before that width is no different than height in respect to being negative or non-negative.
You can have an object with a width and a height. These are magnitudes, because a negative width or height for an object doesn't make sense. So both are equally non-negative in this case, even if ISQ defines both from different applications (as a magnitude for width, and as a vector for height).
You can also have the object change its width or height. A vector applied to one of the two parallel straight lines that define a width as a magnitude can represent this change in the width of the object. The same applies to the primitives that define the height of the object.
The object can also be in a space. Conveniently, I use the same space of the primitives used to define its width and height. So I use these primitives to the define points of the object in the space (e.g., it's center), and changes to the points of the object (in the type system using the same vectors that describe the width and height of the object).
I said before that width is no different than height in respect to being negative or non-negative.
I am not so convinced here. It was hard for me to realize how to measure a negative width, there are plenty of negative heights/altitudes on a map. Height points are typically measured relative to MSL so it is easy to get a negative value.
Widths are mostly measured against 0
(but your ideal_waist_size_of_adult_supermodel
example is probably also valid).
Terminology is hard. "Height" can mean "height relative to a reference altitude" (which can be positive or negative), or "minimum clearance to pass over an object on the ground" (which must be non-negative). It's important to be clear about which we're discussing when we say "height"... although I have no good ideas for how to make this distinction in code.
how to measure a negative width
I agree with ISQ; there are no negative widths.
As currently defined, mp_units
can't represent a width.
mp_units::quantity
is a vector.
But a width isn't a vector because its negative is undefined.
mp_units::quantity_point
is a point.
But a width has no origin.
Or you could say it has the two parallel lines in its definition.
But those are very dynamic in nature; they enclose an object in a space.
Then what do mp_units::quantity<width[m]>
and mp_units::quantity_point<width[m]>
measure?
Is it something we want to prohibit?
And instead allow only through a new abstraction mp_units::magnitude<width[m]>
?
My application has sizes, points, and vectors.
I can define a size as the magnitude of a vector.
And the vector can be the difference of two points.
So if magnitude(point_a - point_b)
can give me a size,
then it makes sense that I can add a vector like point_a - point_b
to any point of the same "quantity" to change an eventual magnitude.
Do note that ISO/IEC 80000 already defines "quantities" that are points. So it's no surprise that it also defines "quantites" that are magnitudes. Part 8 (Acoustics) also defines logarithmic quantities, which we haven't figured out yet (#35). It might be that they also are their own new abstraction. Knowing whether they make a vector space could be a starting point to figuring that out.
[ Note: Even though ISQ remarks that height "is usually signed", I can have non-negative heights (i.e., magnitudes, e.g., to describe the height of an object or space, which isn't a point or a vector). -- end note ]
[ Note:
In reality, some of my objects are represented with a single point offset from some origin
(e.g., the top-right of a GUI button offset from the window's top-right).
Their representation also include a size (a pair of magnitudes of width and height).
Those can be enough to manipulate all its points.
Those primitives currently go by the name of
vector<width, pixel>
, magnitude<width, pixel>
, and point<window, width, pixel>
,
which currently respectively correspond to mp_units
's
quantity<width[pixel]>
, quantity<width[pixel]>
, and quantity_point<window, width[pixel]>
.
-- end note ]
although I have no good ideas for how to make this distinction in code.
This is why I'm contemplating using a magnitude
abstraction for this.
I'm still rewriting things to use magnitude
.
And mp_units
also has some examples that use widths and heights.
So looking at those with this new information in mind might help in figuring things out.
I already have my own application, which is how I got here in the first place.
I wouldn't like to use the term vector
here, because I prefer to leave it to quantity character so saying that width
might be a vector is really misleading as ISQ says that it is a scalar quantity. ISQ defines only two vector quantities of kind length
(position_vector
and displacement
). Using the same term for two different things leads to confusion. Can we find a better name for it (i.e. difference, offset, shift, ...)?
Also, magnitude
is already taken for the "factor" of a unit. We should rename either of those to avoid the confusion. For vector quantities, we plan to use norm()
to calculate the magnitude of a vector as described in #463, but this may change as well.
Terminology is hard. "Height" can mean "height relative to a reference altitude" (which can be positive or negative), or "minimum clearance to pass over an object on the ground" (which must be non-negative). It's important to be clear about which we're discussing when we say "height"... although I have no good ideas for how to make this distinction in code.
ISQ is not that specific, but if some life-critical aviation application needs to distinguish those, it can always derive its own quantity kinds from isq::height
and make one of them non-negative if needed.
This is what I've settled with for now: https://cpp2.godbolt.org/z/sWj1K5rc8.
With that, I specify arguments and members that are magnitudes.
It's clear from context what's a magnitude,
so I chose for vector
to be implicitly convertible to magnitude
.
I still need to use magnitude
s as vector
s.
So I overloaded unary *
on magnitude
to return the vector
.
Using that, I avoid having to overload vector
functions for magnitude
s.
So far, I've been able to rely on the implicit conversion of vector
to magnitude
.
But when implicit conversion doesn't happen, a named conversion might be need.
If Cpp2 had conversion operators, I'd have used that, too.
reminded me that we can just use a constrained number type. When it comes to saying whether such a type would be a vector space, I think it's the same as using an unsigned integer type. That means that the domains of some operations are more restricted.
No, sorry, that not entirely right. A number type constrained to not be negative isn't the same as an unsigned integer type. Unsigned integer types have more defined operations (see the last sentence of the "Note 1 to entry" of IEV 102-01-11). Using an unsigned integer type for representing magnitudes is as problematic as using unsigned integer types for representing sizes (e.g., of C++ standard library containers).
428, and https://github.com/mpusz/mp-units/issues/353#issuecomment-1084679393 in particular,
reminded me that we can just use a constrained number type. [...] That means that the domains of some operations are more restricted.
This is also doubtful.
Elements of a vector space have an opposite.
But with such a constrained number type, all elements are out the domain of unary -
.
I wouldn't recommend using unsigned type for that, but a custom wrapper for representation type could do the work. However, I do not see how it is different than adding proper checks to the constructor and assignment of quantity
?
IIUC, the consensus on yesterday's meeting was that, for non-negative quantities, contract-checking the number suffices and that anything else should prove the safety it brings.
I have been thinking that, indeed, that should suffice.
But that doesn't answer how the library should handle non-negative quantities.
Right now, we have to use a number type that is contract-checked to catch negative values.
IIRC, @rothmichaels wanted to be able to specify a quantity_spec
as non-negative to reduce boilerplate.
In that case, mp_units::quantity
could be contract-checked with .is_non_zero()
.
And a quantity_point
with a non-negative quantity_spec
would also be contract-checked for free.
We also talked about being able to subtract quantities specified as non-negative. That could result in a negative value depending on the order of the arguments. But that there are conceivable use cases where such manipulation is useful (or the lack thereof restrictive). @mpusz also mentioned having to specify speed not as the magnitude of velocity (as in ISO 80000), which would make it implicitly non-negative, but as something else because negative speeds are entirely reasonable.
I still view quantity
as a vector for which we should be able to always subtract in any order.
We also talked about the value of having non-negative quantities
of quantities that aren't necessarily specified as being non-negative.
Let's consider height, not specified by ISO 80000 as non-negative.
Some amusement park rides requires have a minimum height requirement on its riders.
The convenience of generalized non-negative quantities is being able to catch math errors.
Did the user get a sign wrong or arguments inverted, or simply forgot to call abs
or magnitude
?
So even if amusement park goers never have 0 height, we can help catch logic errors.
Based on all the feedback, I'm leaning towards this:
quantity_spec
s as non-negative.magnitude<Reference, Rep>
template alias that "sets the non-negative bit in quantity_spec
",
that aliases quantity<Reference.
_non_negative
_(), Rep>
(I think a magnitude_point
might also be useful).magnitude
is mainly useful in interfaces.
So in order to allow using their values as vectors,
they should drop the "non-negative bit" at the drop of a hat.
For example, given a magnitude
x
:
x -= y
works and checks that the resulting number of x
is non-negative.x - y
results in a quantity with the "non-negative bit" unset.As an aside, I originally gave the example of frequency instead of height. But that might not be entirely right, as it's specified in terms of period (IEV 103-06-01). Although it being non-negative isn't clear from the ISO 80000 definition: "duration (item 3-9) of one cycle of a periodic event". @mpusz Here's an example of what you were wondering about other non-negative quantities in ISO/IEC 80000.
Based on all the feedback, I'm leaning towards this:
- Not allowing users to explicitly define their
quantity_spec
s as non-negative.- Having a
magnitude<Reference, Rep>
template alias that "sets the non-negative bit inquantity_spec
", that aliasesquantity<Reference.
_non_negative
_(), Rep>
(I think amagnitude_point
might also be useful).- I think
magnitude
is mainly useful in interfaces. So in order to allow using their values as vectors, they should drop the "non-negative bit" at the drop of a hat. For example, given amagnitude
x
:
x -= y
works and checks that the resulting number ofx
is non-negative.x - y
results in a quantity with the "non-negative bit" unset.
More simply, magnitude<Reference, Rep>
aliases quantity<Reference, non_negative<Rep>>
.
Where mp-units defines non_negative
because it wants to support the good stuffs above.
Accordingly, -=
is checked, and -
returns the Rep
(i.e., not wrapped in non_negative
).
I think it was @mpusz who said that we wouldn't be having this discussion if std
had something like non_negative
.
I'm going to suggest that the non_negative
I'm suggesting that mp-units needs
should be named magnitude_rep
to highlight its special behavior.
Other constrained numbers keep their constrains when operating on them.
I'm suggesting that magnitude_rep<Rep>
should instead return Rep
from those.
Now, a question of safety.
Is it clear that quantity<width[m], int>
can be negative?
Today's mp-units could be a source of logic errors due to users who expect naively it to be contract-checked.
I see two ways forward.
The first one is the very constraining choice of all non-negative quantities always being contract-checked.
The second one is to attempt to educate users.
My suggestion of adding magnitude
is one such attempt in this direction.
because negative speeds are entirely reasonable.
The more I think of it, the more I am convinced that negative speeds should indeed be velocities. A negative value is to represent a vector in the "backward" direction, right? ๐
However, I think that it should be perfectly fine to obtain speed by dividing i.e. height by time, and not always through a magnitude of position vector. Do we have any idea how to handle that?
Let's consider height, not specified by ISO 80000 as non-negative.
Height and altitude are the same quantities in ISQ and negative altitudes are perfectly fine.
Yeah. That's why I'm saying there's value in having it checked as non-negative when a negative altitudes would be a logic error.
ISO 80000 explicitly specifies quantities that have to be non-negative (width, thickness, diameter, radius).
Some of the quantities are implicitly defined as non-negative. For example, "path length" is defined as
hypot(dx, dy, dz)
. Others are defined as the magnitude of a vector which also is always non-negative.Some quantities are also explicitly defined as signed (i.e. height), so they should not be treated as a
norm(position_vector)
.