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

Should conversions from the raw numerical value to a dimensionless quantity with a unit one be allowed? #553

Closed
mpusz opened this issue Feb 10, 2024 · 33 comments
Assignees
Labels
design Design-related discussion iso The ISO C++ Committee related work question Further information is requested
Milestone

Comments

@mpusz
Copy link
Owner

mpusz commented Feb 10, 2024

A few parties (including LEWG) were surprised that we have to write 42 * one to assign a raw value to a quantity. See also #497 (comment).

This, of course, is consistent with the rest of the library and tries to prevent the cases where someone will initialize quantities measured in percentages or other dimensionless units from the raw value.

In 0.8.0, we had an exception for the dimensionless quantity of a unit one. Such quantity was implicitly convertible from a raw value. This was inconsistent with the handling of other units and increased the complexity of the design a bit, but it was user-friendly.

Should we add a similar feature to the 2.0 framework?

Should the conversion happen implicitly or explicitly? Should we also provide a conversion from quantity to a numerical value in such cases?

@mpusz mpusz added question Further information is requested design Design-related discussion labels Feb 10, 2024
@JohelEGP
Copy link
Collaborator

For reference, the SI Brochure says:

Such quantities are simply numbers.
The associated unit is the unit one, symbol 1, although this is rarely explicitly written (see 5.4.7).

If it's desired, I think it should be explicit.
ints and doubles don't necessarily represent a dimensionless quantity of unit one.
So f(quantity{1}) and f(quantity{x}) should be required to inspire some confidence.

@mpusz
Copy link
Owner Author

mpusz commented Feb 10, 2024

And what about conversions back to the number?
f(static_cast<double>(q)) is not much shorter than f(q.numerical_value_in(one)) and probably not nicer as well.

@mpusz
Copy link
Owner Author

mpusz commented Feb 10, 2024

BTW, lack of implicit conversion to a quantity would probably prevent us from simplifying expressions like this one:

static_assert(10 * km / (5 * km) == 2 * one);
static_assert(10 * km / (5 * km) + 1 * one == 3 * one);

and I think it is the biggest motivation for such a change.

Unless, we want to explicitly allow comparisons and arithmetics with pure numbers but still prevent conversions.

@NAThompson
Copy link
Contributor

Should we add a similar feature to the 2.0 framework?

It appears this library is targeting application developers. But if I'm not mistaken, a requirement to standardize is the code must generically interoperate with the rest of std. I don't see how that happens without instantiation of dimensionless types from their underlying floating point type. (Actually, I also see a difficulty without allowing instantiation from zero, but perhaps this could be solved with an addition to the floating point types e.g. float::ZERO double::ZERO . . . .)

@mpusz
Copy link
Owner Author

mpusz commented Feb 10, 2024

a requirement to standardize is the code must generically interoperate with the rest of std

This actually is not the requirement of the standardization process.

I don't see how that happens without instantiation of dimensionless types from their underlying floating point type.

Why dimensionless is special here? Why don't you need a speed[m / s] to be constructed from the underlying value as well? Does it "generically interoperate" without such a feature?

My point here is that I do not think that the dimensionless quantities are not that special at all.

Actually, I also see a difficulty without allowing instantiation from zero, but perhaps this could be solved with an addition to the floating point types e.g. float::ZERO double::ZERO . . . .

Regarding zero, please see the #487 (comment).
To get 0 you can always value-initialize the quantity type.

@NAThompson
Copy link
Contributor

NAThompson commented Feb 11, 2024

This actually is not the requirement of the standardization process.

Fair enough, but this is certainly a desiderata.

Why dimensionless is special here?

My context for the discussion is the Buckingham-π theorem, where nondimensionalization produces natural scales for the problem at hand. In this context, dimensionless numbers are special. Statements like "|x| ≪ 1" make sense for dimensionless numbers, but not for dimensioned, and this is reflected in how code is written.

As another example, I think you would concede unif(0,1) random numbers are useful when the values being generated are dimensionless, but contextless generation of unif(0,1) would probably be a bug if the "1" had dimensions of m/s.

@mpusz
Copy link
Owner Author

mpusz commented Feb 11, 2024

Do you mean unif(0, 1) to be uniform distribution? If so, I do not see a reason why it should not have units attached to it. We do support such distributions already:

template<Quantity Q>
requires std::floating_point<typename Q::rep>
struct uniform_real_distribution : public std::uniform_real_distribution<typename Q::rep> {

@NAThompson
Copy link
Contributor

NAThompson commented Feb 11, 2024

So even if I use this file, I still have a problem that I cannot write generic code which works with both double and mp-units:

math/include/boost/math/optimization/random_search.hpp:108:62: fatal error: no matching conversion for static_cast from 'int' to 'DimensionlessFloat' (aka 'quantity<mp_units::one{}, double>')
        uniform_real_distribution<DimensionlessFloat> unif01(static_cast<DimensionlessFloat>(0), static_cast<DimensionlessFloat>(1));
                                                             ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

And if I add the 0*one, I break double and all my users who don't care about units.

The second problem is that my users have to know that I'm using <random> in my code in order for them to know that they should include <mp-units/random.h> for ADL. But arguably use of <random> is an implementation detail they shouldn't have to care about-and you can imagine how difficult a compile of a much larger codebase would be if this is the requirment.

@mpusz
Copy link
Owner Author

mpusz commented Feb 11, 2024

It is even worse, class templates do not work with UDL 😉 so including <mp-units/random.h> will not help.

@kwikius
Copy link
Contributor

kwikius commented Feb 18, 2024

The type of the result of a calculation on two quantities Q<d1,v1> op Q<d2,v2> where the result is dimensionless should merely be the type of(v1() op v2()). No conversion required.

I found this the simplest and best solution for my quan library and also my pqs library .
In both the quantity imposes constraints on the underlying type it encapsulates, which simplifies things compared to mp-units I think. https://github.com/kwikius/pqs/wiki/type_function-get_numeric_type

A number is a pure mathematical construct, whereas a physical quantity is not. When you wish to find the ratio between two lengths the ratio is transformed to the pure math domain despite it originated from physical quantities, the satisfying and fundamental difference between maths and physics.

@chiphogg
Copy link
Collaborator

The type of the result of a calculation on two quantities Q<d1,v1> op Q<d2,v2> where the result is dimensionless should merely be the type of(v1() op v2()). No conversion required.

Do you intend for this statement to apply to all dimensionless result types? Or merely to results that are dimensionless and unitless (i.e., the "unit one")?

If the latter, I think it's a defensible design decision. (In fact, it's what Au currently does, although experience has persuaded me to move away from this in the future.)

However, if you're proposing a raw numeric result type for all dimensionless results (such as "cm / m", which is equivalent to "percent"), then I would consider it a grave mistake.

@kwikius
Copy link
Contributor

kwikius commented Feb 20, 2024

The type of the result of a calculation on two quantities Q<d1,v1> op Q<d2,v2> where the result is dimensionless should merely be the type of(v1() op v2()). No conversion required.

Do you intend for this statement to apply to all dimensionless result types? Or merely to results that are dimensionless and unitless (i.e., the "unit one")?

If the latter, I think it's a defensible design decision. (In fact, it's what Au currently does, although experience has persuaded me to move away from this in the future.)

However, if you're proposing a raw numeric result type for all dimensionless results (such as "cm / m", which is equivalent to "percent"), then I would consider it a grave mistake.

Let us avoid phrases designed to excite emotions.

You want one thing, I and from your own comments in your link, your users expect another. Neither is a "grave mistake". We just want different things.

The real problem is: mp-units doesn't provide the flexibility to mould the semantics. In fact the claim in mp-units to be able to represent different systems is not upheld in practise. It provides one system and uses that as the basis for the rest.

PQS provides a set of concepts which allows customisation for a particular system, so In pqs the semantics of operations on quantities are customisable. If you want to return a dimensionless_with units entity and I want to return a number then the library allows us to customise it to do so.
PQS is based on concepts, not types. EDIT: some discussion about this here #194

@mpusz
Copy link
Owner Author

mpusz commented Feb 20, 2024

In fact the claim in mp-units to be able to represent different systems is not upheld in practise. It provides one system and uses that as the basis for the rest.

This is actually not true. mp-units is fully capable of providing various independent systems if needed. One such example can be
a natural units system. But even such independent system of units can build on top of the same system of quantities (but they do not have to).

The rest of the systems are defined on top of the SI because this is how they work in reality. But it does not prevent anyone from doing otherwise.

@kwikius
Copy link
Contributor

kwikius commented Feb 20, 2024

In fact the claim in mp-units to be able to represent different systems is not upheld in practise. It provides one system and uses that as the basis for the rest.

This is actually not true. mp-units is fully capable of providing various independent systems if needed. One such example can be a natural units system. But even such independent system of units can build on top of the same system of quantities (but they do not have to).

The rest of the systems are defined on top of the SI because this is how they work in reality. But it does not prevent anyone from doing otherwise.

I will have to look again, as I didn't look in a while, but if you cant change the semantics of operations ( so result type of op) then effectively you are limited to one system, since different systems will have different semantics. If that was not the case then there would be no reason to open this issue, since you create a system with your preferred semantics.

@mpusz
Copy link
Owner Author

mpusz commented Feb 20, 2024

if you cant change the semantics of operations ( so result type of op) then effectively you are limited to one system, since different systems will have different semantics

I do not believe that it is true. The library framework should be independent of systems of units and quantities. The same operation (e.g., division) should always return the same interface no matter which system you use.

Of course, one might decide to define the library in terms of concepts, as you did in PQS. But those should not behave differently for different systems. They may provide different features/behaviors for different user needs, though. My worry with such a solution is that we can end up with an explosion of many types having similar semantics mandated by concepts. This, in turn, will result in interoperability problems between those types and interfacing issues between different vendors.

@kwikius
Copy link
Contributor

kwikius commented Feb 20, 2024

The same operation (e.g., division) should always return the same interface no matter which system you use.

I can't follow what you are trying to say there. What do you mean by interface ?

Let us try some code to agree on some common ground.

assume we have 2 quantities q1, q2.

and 2 concepts ...

// alias to mp_units::Quantity
template <typename Q>
concept quantity =...;

// alias to mp_units:: "quantity of dimension one"
template <typename Q>
concept dimensionless_quantity = ...;

using Q1 = decltype(q1);
using Q2 = decltype(q2);

// verify that q1 and q2 are models of quantity
static_assert(quantity<Q1> && quantity<Q2>);

// verify that they are in the same measurement system
static_assert( get_measurement_system<Q1> == get_measurement_system<Q2>);

The following must now hold for mp-units, quan, pqs ( can be done for std::chrono) and other similar libraries

auto q3 = q1 / q2;

using Q3 = decltype(q3);

static_assert( quantity<Q3> || dimensionless_quantity<Q3> );

if constexpr ( dimensionless_quantity<Q3>){
   ...
}else{
   ...
}

We know very little about q1, q2, q3 so far but assuming the leeway of replacing the aliases names below for your own libraries names for these things, these are are the "interfaces" here

  • quantity<...>,
  • dimensionless_quantity<...>
  • get_measurement_system<...>,

@chiphogg
Copy link
Collaborator

Let us avoid phrases designed to excite emotions.

Quite right --- I unconditionally apologize, and will try to do better in the future.

Besides the needlessly emotionally charged language, my main mistake was not explaining why I came to adopt my viewpoint.

One good resource is this discussion page on dimensionless units. In particular, the last section explains the drawbacks of supporting implicit conversion between non-unity dimensionless numbers (such as percent) and the raw numeric types. The risks are more than just hypothetical, too: the nholthaus/units library has this behaviour, and it has led to fiendishly thorny problems such as nholthaus/units#328, nholthaus/units#276, and nholthaus/units#275.

Of course, I don't think implicit conversions are exactly the same thing you're talking about: I think you're proposing that the library should simply produce a raw number from a computation whose result is dimensionless. (Let me know if I'm wrong.) This is what Au currently does for unitless only, although aurora-opensource/au#185 tracks our goal to remove even this, as we've already discussed.

For other dimensionless units, I think the main difficulty is the choice between following your proposed policy exactly, and avoiding surprising and very lossy conversions. I don't think we can have both, unless I've missed something! For example: what should (75 * cm) / (1 * m) return? According to this policy:

The type of the result of a calculation on two quantities Q<d1,v1> op Q<d2,v2> where the result is dimensionless should merely be the type of(v1() op v2()). No conversion required.

The result type should be int / int, which is int. But this is equivalent to percent(75), which works out to 0. Is this acceptable? Or is there some better alternative which adheres to this policy that I haven't considered?

@mpusz
Copy link
Owner Author

mpusz commented Feb 20, 2024

// alias to mp_units:: "quantity of dimension one"
template
concept dimensionless_quantity = ...;

I do not think that having a separate concept for a dimensionless quantity is a good idea, especially when there is no subsumption relationship between them. For example, quantity<Q3> || dimensionless_quantity<Q3> is a big issue for generic interfaces that take any quantity. But this is a choice each library author has to make by him/her-self...

// verify that they are in the same measurement system
static_assert( get_measurement_system == get_measurement_system);

I am not sure what this means? Do you mean things like SI and CGS? Those are separate systems (both defined in terms of SI), but their quantities can be compared. I can also imagine systems where units from even the same system might not be compatible (see https://github.com/mpusz/mp-units/blob/master/example/currency.cpp).

if constexpr ( dimensionless_quantity<Q3>){
  ...
}else{
  ...
}

The above issue is the result of treating dimensionless quantities separately. This is on of many reasons that I prefer the approach used by mp-units.

I can't follow what you are trying to say there. What do you mean by interface ?

In your previous message, you wrote, "if you can't change the semantics of operations ( so result type of op), then effectively you are limited to one system, since different systems will have different semantics." I thought that you meant that you want a design where dividing length/length in one system may provide a number but may yield a dimensionless quantity in another system. I don't think that the system of units being used should affect this. I may imagine two class templates (e.g., quantity and weak_quantity) where each will have different behavior, but both will satisfy concepts in your library.

As I wrote this a long time ago in answer to your ideas about concepts, I think that one of the biggest problems is deciding what to return if we add two different quantity class templates for the same dimension (e.g., mp-units::quantity< meter> + pqs::quantity<meter>). Should it result in mp-units::quantity, pqs::quantity, or maybe yet another boost::quantityclass template? Moreover, it is really easy to end up with ambiguous calls if both types will allow adding another thing that matches aQuantity` concept).

@kwikius
Copy link
Contributor

kwikius commented Feb 21, 2024

I think you're proposing that the library should simply produce a raw number from a computation whose result is dimensionless. (Let me know if I'm wrong.)

I will quote what I actually said here

The real problem is: mp-units doesn't provide the flexibility to mould the semantics. ...

PQS provides a set of concepts which allows customisation for a particular system, so In pqs the semantics of operations on quantities are customisable. If you want to return a dimensionless_with units entity and I want to return a number then the library allows us to customise it to do so.

EDIT : I should say the library should allow us to customise.... I haven't tried customising to return a dimensionless quantity with units in PQS yet

@kwikius
Copy link
Contributor

kwikius commented Feb 21, 2024

I do not think that having a separate concept for a dimensionless quantity is a good idea, especially when there is no subsumption relationship between them.

Surely mp-units has a means to distinguish a dimensionless quantity from a dimensioned quantity? If not a concept, then a trait surely, but that is splitting hairs:

template <typename V>
concept dimensionless_quantity = quan::meta::is_dimensionless<V>::value;

@kwikius
Copy link
Contributor

kwikius commented Feb 21, 2024

I thought that you meant that you want a design where dividing length/length in one system may provide a number but may yield a dimensionless quantity in another system.

A number is a dimensionless quantity.

   static_assert( dimensionless_quantity<double>);

And this seems to be at the heart of this issue. You want to encapsulate a number in a dimensionless_quantity_with_units type, perhaps like pqs::scaled_value and sure that might be a useful optimisation, but the feedback you have in the LEWG, from @chiphogg's users, from myself, from others is that returning a dimensionless quantity with units is not intuitive, and so doesnt fulfill the "ease of use" requirement.

So I would suggest allowing use of such a type , but as a default to do the simplest, most intuitive thing. For most of use the ratio of two lengths is a number, not a "dimensionless quantity type with a string of units".

@mpusz
Copy link
Owner Author

mpusz commented Feb 21, 2024

It depends on what we call a "quantity" here. In generic programming, you may expect that dividing two quantity types will yield a quantity type as well. After that, you may want to access part of its interface:

  • res.unit
  • res.dimension
  • res.quantity_spec
  • res_type::rep
  • res.zero()
  • ...

None of that will work when a raw number is returned, which complicates all the generic programming logic as you need to do special cases in each such function.

Also, at the very beginning of my journey with standardizing the library in the ISO Committee, I was warned that we should not repeat three std::chrono::duration mistakes:

  • not return std::common_type from arithmetic operations (as this prevents the usage of "smart" arithmetic types),
  • not return a value from the division of two quantities (as this truncates the value for big ratios),
  • do not allow construction from the raw value (is considered unsafe).

Those are considered serious issues by many experts, and I agree with them.

returning a dimensionless quantity with units is not intuitive

I think that people actually do not complain about returning such quantities. They complain that we can't call "legacy" functions taking a value with such a resulting type. This is why I created this Issue to discuss if we should consider making an exception here and allow such conversion, but only if the unit is one.

@kwikius
Copy link
Contributor

kwikius commented Feb 21, 2024

  • res.unit

  • res.dimension

  • res.quantity_spec

  • res_type::rep

  • res.zero()

  • ...

Using functions would be preferable so allowing use of pod ...

template <typename T> requires ( quantity<T> || dimensionless_quantity<T>)
void f( T res) 
{
  get_unit(res);
  get_dimension(res);
  get_quantity_spec(res);
  get_rep_type(res);
  get_zero(res);
}

@kwikius
Copy link
Contributor

kwikius commented Feb 21, 2024

Also, at the very beginning of my journey with standardizing the library in the ISO Committee, I was warned that we should not repeat three std::chrono::duration mistakes:

* not return `std::common_type` from arithmetic operations (as this prevents the usage of "smart" arithmetic types),

* not return a value from the division of two quantities (as this truncates the value for big ratios),

* do not allow construction from the raw value (is considered unsafe).

Those are considered serious issues by many experts, and I agree with them.

That is actually funnier to me than you can ever know. :)

Though again there is emotive language being used above, rather than rational argument ( "mistakes" -> design decisions, "serious issues" -> sub optimal etc) . But yes, std::chrono was rushed in without any proper discussion AFAICS. Its compile time units looked on the surface much like my old version of PQS but...

FWIW pqs ( even in 2005) used binary_op<Q, op,Q>::type to provide a result type of an op https://github.com/kwikius/pqs/blob/master/src/include/pqs/bits/impl/binary_op_impl.hpp . This allows customisation for udts

FWIW Pqs (even in 2005) used a ratio and exponent https://github.com/kwikius/pqs/blob/master/src/include/pqs/bits/conversion_factor_def.hpp , whereas the std::chrono authors believed that a std::ratio would hold all the range you would ever need :)

std::chrono is in common use and I don't personally have a major issue with the explicit value constructor, nor have I heard others complain about it. It is an intuitive way to initialise from a number. There are problems I seem to remember that it assumes everything is a double. To be honest I don't really use it as I generally use the same functionality in my own quan library.

@kwikius
Copy link
Contributor

kwikius commented Feb 21, 2024

It depends on what we call a "quantity" here. In generic programming, you may expect that dividing two quantity types will yield a quantity type as well. After that, you may want to access part of its interface:

* `res.unit`

* `res.dimension`

* `res.quantity_spec`

* `res_type::rep`

* `res.zero()`

* ...

None of that will work when a raw number is returned, which complicates all the generic programming logic as you need to do special cases in each such function.

This mp-units design decision is to me sub-optimal , for the reasons you yourself state ;)

@NAThompson
Copy link
Contributor

I think that people actually do not complain about returning such quantities. They complain that we can't call "legacy" functions taking a value with such a resulting type.

@mpusz : This is indeed a correct description of my particular problem-I only need decltype(Real()/Real() to provide a type that works generically with floating point numbers and with dimensioned quantities-it is of little relevance what this type actually is.

@mpusz mpusz added the iso The ISO C++ Committee related work label Jun 22, 2024
@mpusz mpusz added this to the v2.3.0 milestone Jun 22, 2024
@mpusz mpusz self-assigned this Jun 22, 2024
@mpusz
Copy link
Owner Author

mpusz commented Jul 12, 2024

@NAThompson, I plan to work on this in the next days. There are two ways to somehow address it:

  1. Allow implicit conversion from the raw number to a quantity of a unit one.
  2. Allow implicit conversion of a quantity of a unit one to a raw number.

We can't have both because it breaks std::common_type and the ternary operator.

In the second case, the following and similar would not work:

(5. * km / (10 * km) + 0.1).in(percent)

This is why I prefer the first solution.

In any case, I plan to provide the other conversion as well. It will just be explicit.

Please let me know your thoughts.

@NAThompson
Copy link
Contributor

Please let me know your thoughts.

My goal has always been generic interoperability with units so that I can check the dimensional consistency of my code while allowing my users to just use floats, not needing to add mp-units to their CMakeLists.txt. Doesn't 1) require me to add mp-units as a dependency to my code?

@mpusz
Copy link
Owner Author

mpusz commented Jul 12, 2024

It is difficult for me to answer this question as I do not know your code. Can you provide a specific code snippets that do not work right now but that you would like to enable in the future?

@NAThompson
Copy link
Contributor

NAThompson commented Jul 12, 2024

@mpusz : Here is an example; here is another one.

N.B.: While trying to distinguish between dimensionless numbers and dimensioned, I actually found an error in the original literature. So this is an important activity, even if I didn't succeed getting mp-units to work generically.

@mpusz
Copy link
Owner Author

mpusz commented Jul 12, 2024

I may not fully understand your code, but it seems that the main issue you have right now is to make DimensionlessReal(Number) work. Also, 1-c_sigma could be tricky right now if c_sigma is a quantity.

I think that with the proposed changes in point 1. above, both of those cases should work.

@mpusz
Copy link
Owner Author

mpusz commented Jul 12, 2024

Also, uniform_real_distribution<DimensionlessFloat> unif01(static_cast<DimensionlessFloat>(0), static_cast<DimensionlessFloat>(1)); should work without the need to depend on mp-units, assuming that you want to generate dimensionless values.

@mpusz mpusz closed this as completed in fcc16ae Jul 14, 2024
@mpusz
Copy link
Owner Author

mpusz commented Jul 14, 2024

@NAThompson, it is done. You check the changes in the "Superpowers of the unit one" chapter of our docs. Please let me know if this solves most of your issues.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
design Design-related discussion iso The ISO C++ Committee related work question Further information is requested
Projects
None yet
Development

No branches or pull requests

5 participants