mpusz / mp-units

The quantities and units library for C++
https://mpusz.github.io/mp-units/
MIT License
1.1k stars 90 forks source link

Provide support for non-negative quantities #468

Open mpusz opened 1 year ago

mpusz commented 1 year ago

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 commented 1 year ago

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

mpusz commented 1 year ago

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 commented 1 year ago

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 commented 1 year ago

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 commented 1 year ago

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 https://github.com/mpusz/mp-units/issues/468#issuecomment-1758273432.

JohelEGP commented 1 year ago

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 commented 1 year ago

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 commented 1 year ago

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 commented 1 year ago

So yeah, I think that

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 commented 1 year ago

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.