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

Provide support for non-negative quantities #468

Open
mpusz opened this issue Jun 19, 2023 · 60 comments
Open

Provide support for non-negative quantities #468

mpusz opened this issue Jun 19, 2023 · 60 comments
Labels
design Design-related discussion enhancement New feature or request
Milestone

Comments

@mpusz
Copy link
Owner

mpusz commented Jun 19, 2023

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).

@mpusz mpusz changed the title Provide support for nen-negative quantities Provide support for non-negative quantities Jun 19, 2023
@mpusz
Copy link
Owner Author

mpusz commented Jun 19, 2023

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.

@JohelEGP
Copy link
Collaborator

What does the library need to do,
besides permitting user-defined representation types that can catch negative values?

@mpusz
Copy link
Owner Author

mpusz commented Jun 19, 2023

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.

@mpusz mpusz added design Design-related discussion enhancement New feature or request labels Jun 24, 2023
@JohelEGP
Copy link
Collaborator

JohelEGP commented Sep 4, 2023

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:

  • Position vector and displacement (Part 3, §3-1.10, §3-1.11).
  • Electric potential, electric potential difference, and voltage (Part 6, §6-11).

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.

@mpusz
Copy link
Owner Author

mpusz commented Sep 4, 2023

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?

@JohelEGP
Copy link
Collaborator

JohelEGP commented Sep 4, 2023

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).

@JohelEGP
Copy link
Collaborator

JohelEGP commented Sep 4, 2023

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.

@mpusz
Copy link
Owner Author

mpusz commented Sep 4, 2023

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?

@JohelEGP
Copy link
Collaborator

JohelEGP commented Sep 4, 2023

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.

@mpusz
Copy link
Owner Author

mpusz commented Sep 4, 2023

I would do something like:

QUANTITY_SPEC(pos_x, isq::length);
QUANTITY_SPEC(pos_y, isq::length);
QUANTITY_SPEC(pos_z, isq::length);

@mpusz
Copy link
Owner Author

mpusz commented Sep 4, 2023

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 😜

@JohelEGP
Copy link
Collaborator

JohelEGP commented Sep 4, 2023

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?

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.

@JohelEGP
Copy link
Collaborator

JohelEGP commented Sep 4, 2023

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.

@JohelEGP
Copy link
Collaborator

JohelEGP commented Sep 4, 2023

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?

@mpusz
Copy link
Owner Author

mpusz commented Sep 4, 2023

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.

@JohelEGP
Copy link
Collaborator

JohelEGP commented Sep 4, 2023

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).

@JohelEGP
Copy link
Collaborator

JohelEGP commented Sep 5, 2023

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.

@JohelEGP
Copy link
Collaborator

JohelEGP commented Sep 5, 2023

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.

@mpusz
Copy link
Owner Author

mpusz commented Sep 6, 2023

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).

@mpusz
Copy link
Owner Author

mpusz commented Sep 6, 2023

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.

@JohelEGP
Copy link
Collaborator

JohelEGP commented Sep 6, 2023

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

@mpusz
Copy link
Owner Author

mpusz commented Sep 6, 2023

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?

@mpusz
Copy link
Owner Author

mpusz commented Sep 6, 2023

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.

@mpusz
Copy link
Owner Author

mpusz commented Sep 6, 2023

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?

@JohelEGP
Copy link
Collaborator

JohelEGP commented Sep 6, 2023

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 #468 (comment).
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 for quantity?

I don't think so, since you can have negative points.
I really think it's a new abstraction: Magnitude.

@mpusz
Copy link
Owner Author

mpusz commented Sep 6, 2023

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 #468 (comment).
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?

@JohelEGP
Copy link
Collaborator

JohelEGP commented Sep 6, 2023

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.

@mpusz
Copy link
Owner Author

mpusz commented Sep 6, 2023

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).

@chiphogg
Copy link
Collaborator

chiphogg commented Sep 6, 2023

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.

@JohelEGP
Copy link
Collaborator

JohelEGP commented Sep 6, 2023

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.

@mpusz
Copy link
Owner Author

mpusz commented Sep 6, 2023

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.

@mpusz
Copy link
Owner Author

mpusz commented Sep 6, 2023

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.

@JohelEGP
Copy link
Collaborator

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 magnitudes as vectors.
So I overloaded unary * on magnitude to return the vector.
Using that, I avoid having to overload vector functions for magnitudes.

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.

@JohelEGP
Copy link
Collaborator

JohelEGP commented Sep 19, 2023

#428, and #353 (comment) in particular,
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.

@JohelEGP
Copy link
Collaborator

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).

@JohelEGP
Copy link
Collaborator

#428, and #353 (comment) 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 -.

@mpusz
Copy link
Owner Author

mpusz commented Sep 19, 2023

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?

@JohelEGP
Copy link
Collaborator

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:

  • Not allowing users to explicitly define their quantity_specs as non-negative.
  • Having a 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).
  • 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 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.

@JohelEGP
Copy link
Collaborator

Based on all the feedback, I'm leaning towards this:

  • Not allowing users to explicitly define their quantity_specs as non-negative.
  • Having a 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).
  • 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 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.

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).

@JohelEGP
Copy link
Collaborator

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.

@mpusz
Copy link
Owner Author

mpusz commented Oct 11, 2023

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?

@mpusz
Copy link
Owner Author

mpusz commented Oct 11, 2023

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.

@JohelEGP
Copy link
Collaborator

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.

@mpusz
Copy link
Owner Author

mpusz commented Oct 11, 2023

But negative altitude is possible. Nearly all Netherlands apply here ;-)

@mpusz
Copy link
Owner Author

mpusz commented Oct 11, 2023

Not allowing users to explicitly define their quantity_specs as non-negative.

I do not think it is correct. For example, I think it would be perfectly fine to define:

inline constexpr struct passenger_height : quantity_spec<isq::height, non_negative> {} passenger_height;

@JohelEGP
Copy link
Collaborator

Yeah.
But the height of a person can't be negative (nor zero).
But being able to represent that in code will help catch errors in calculations.

@mpusz
Copy link
Owner Author

mpusz commented Oct 11, 2023

hat aliases quantity<Reference.non_negative(), Rep>

I would not like to increase the number of template parameters for a quantity for this "niche" use case.

@mpusz
Copy link
Owner Author

mpusz commented Oct 11, 2023

But the height of a person can't be negative (nor zero).
But being able to represent that in code will help catch errors in calculations.

This is why we need a dedicated/custom quantity type for such use cases. See #468 (comment).

@JohelEGP
Copy link
Collaborator

passenger_height makes sense.

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? 😉

If speed can't be negative, how do we represent changes in speed?
For example, the change in average speed of a F1 car between its adjacent races could be 0 m/s, 1 m/s, -1 m/s.

@mpusz
Copy link
Owner Author

mpusz commented Oct 11, 2023

Right. The same logic could be applied to passenger_height: "The second passenger is lower by 3 cm than the first one.".

I think that quantity<Reference, non_negative<Rep>> could work, and dropping non_negative part on subtraction may make sense.

@JohelEGP
Copy link
Collaborator

As you say, the same can be done for passenger_height.
Do we go back to measuring in height,
even thought they're still heights of passengers?

It might make sense to have a delta of a quantity_spec that is non-negative so we can measure that stuff.
But that's just adding more abstractions.

This is similar to force being a vector quantity, and the force normal a scalar quantity.
I remember seeing some ISO/IEC 80000 quantities
where some formulas used a delta of a quantity,
wondering if that'd have to be a new abstraction
to not accidentally mix "plain" differences from some kind of more specific "change" in quantity.

@JohelEGP
Copy link
Collaborator

So yeah, I think that

  • if subtraction that includes a non-negative quantity returns a quantity that can be negative, and
  • we can name this delta quantity, e.g., quanity<width.delta[m], int>,

then we're set to be able to enable the safer default of
quantity<width[m], int> and quantity<speed[m/s], int>
being contract-checked to reject negative values.

It leaves the mouth a bit salty that the checks are done at runtime.
So it's not completely safe (maybe you never tested extreme conditions and crashed [physically] at runtime).
And specially not in the other direction, where quantity<width[m], int> is used,
but negative values would have worked fine, but you instead crashed in production.

But there's only so much safety that type-safety can buy you,
and I think this is already close to the pinnacle.
At least, it might be a good balance of safety and usability.
There are more extreme number types, as shown in #492, that go 100% safety, 0% "care about usability".

@rothmichaels
Copy link

Still working through my ideas about non-negative (as well as other constrained quantities) but finally made it through this whole thread in detail so wanted to leave a few notes.

Earlier this year when discussing (DSP) level and power (i.e level^2) (both non-negative quantities) I initially thought that maybe the quantities should be allowed to be negative so you could do signed math on them and use just "non-negative" as an API precondition. I had some colleagues make some arguments in favor of these being explicitly non-negative quantities because if level were allowed to be negative then the relationship between level and power would not be isomorphic. Recently with doing various quantity/unit experiments I do think I agree with this but at the same time think being able to do signed math that could result in negative values with these quantities (thinking about ES.102: Use signed types for arithmetic). I think using a signed representation type with non-negative preconditions would violate that guideline. I'm still puzzling through ways to both follow this guideline and put preconditions on quantities that shouldn't be allowed to be negative.

First some comments older comments:

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.

I would agree that a "width" with this definition cannot be negative. At the same time it may be useful to be able to do math with widths that could result in negative values as intermediate values in a calculation as long as the final result is non-negative when it needs to be "used" similar to the benefits of signed container sizes and signed arguments to array subscripts: if you are doing math on indexes you want to do signed math that could go negative but this is okay as long as your final result is non-negative when used as a subscript.

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).

I think we might be stuck with a runtime check. The only other solution I could think other than runtime checks would be to disallow subtraction but this would not help with multiplication/division which would still have a runtime precondition that the quantity is being scaled by a positive value. I guess thinking out loud, addition of a negative value would also need a precondition or not be allowed. Maybe we should not do math on non-negative quantities? This would seem to limiting I think.

So yeah, I think that

  • if subtraction that includes a non-negative quantity returns a quantity that can be negative, and
  • we can name this delta quantity, e.g., quanity<width.delta[m], int>,

then we're set to be able to enable the safer default of quantity<width[m], int> and quantity<speed[m/s], int> being contract-checked to reject negative values.

I had thought about something like a this width.delta idea but based on some internal experiments I'm not sure that I'm sold on "delta" quantity types—I'm of two minds about it. I also had thought about a variation of this idea where doing math on a width would result in a length which could then be then used to construct a width with the precondition that you are constructing it with a negative value.

It leaves the mouth a bit salty that the checks are done at runtime. So it's not completely safe (maybe you never tested extreme conditions and crashed [physically] at runtime). And specially not in the other direction, where quantity<width[m], int> is used, but negative values would have worked fine, but you instead crashed in production.

But there's only so much safety that type-safety can buy you, and I think this is already close to the pinnacle. At least, it might be a good balance of safety and usability. There are more extreme number types, as shown in #492, that go 100% safety, 0% "care about usability".

I am curious to check out the safe-numerics stuff presented at CppCon this year because maybe there is something I'm missing but it seems like run-time checks are the best solution for this although of course doesn't provide 100% safety.

IIRC, @rothmichaels wanted to be able to specify a quantity_spec as non-negative to reduce boilerplate

Reducing boilerplate was what made me first think about this but I later realized that my current approach with using non-negative representation types when creating a quantity isn't ideal because non-negative is a property of the quantity in general and if it is specified on a specific quantity template instantiation then it still allows negative values to to be used in other places in the code. If it were specified as part of the quantity_spec for "width" then all values of "width" throughout the program would be non-negative.

@mpusz mpusz added this to the v2.4.0 milestone Jun 22, 2024
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
design Design-related discussion enhancement New feature or request
Projects
None yet
Development

No branches or pull requests

4 participants