mpusz / mp-units

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

Quantities with the same dimension #32

Closed mpusz closed 1 year ago

mpusz commented 5 years ago

There are a few pairs (or more) quantities that have the same quantity:

It is not clear how to handle those. Should the library:

  1. Should the library work as for any other quantity when only one of the definitions ais imported (header included) to the current translation unit or should there be a special behavior for such case?
  2. What to do when both headers are included? a) Fail to compile b) No implicit downcasting to any of those and demand an explicit cast (how to implement this)?
kwikius commented 4 years ago

Hi,

I have a physical quantities library called "quan" that I have used for many years https://github.com/kwikius/quan-trunk [1]

I solved the problem of distinguishing quantities by adding an id to the quantity, to distinguish what I called "named quantities" from "anonymous quantities"

https://github.com/kwikius/quan-trunk/blob/master/quan/meta/abstract_quantity.hpp#L46

"named quantity" would be torque, energy etc ; anonymous quantity would be the same dimension but without knowing the purpose. In a calc you can attempt to keep the id e.g for add,subtract and mul div by a scalar but for mult div by a dimensioned quantity you generally turn the quantity into an "anonymous quantity", since you cant determine the named type of the result in general, but it is not problematic

I found "named quantities" only useful for I/O and for that purpose a named quantity can be assigned/inited from a dimensionally equivalent anonymous quantity. The output for named quantity can then be specified to override the default for anonymous quantity:

https://github.com/kwikius/quan-trunk/blob/master/quan_matters/examples/doc_output_demo.cpp

I saw your physical quantities library at WG21. Good luck with it!

[1] ( I have used the library in many other projects. An example use in a drone osd https://github.com/kwikius/quantracker/blob/master/air/osd/aircraft.hpp

Toy examples https://github.com/kwikius/quan-trunk/blob/master/quan_matters/examples/capacitor_time_curve.cpp https://github.com/kwikius/quan-trunk/blob/master/quan_matters/examples/windturbine.hpp )

kwikius commented 4 years ago

In the example below, including the energy header changes the outputs of the force and torque to newtons and Joules. Not sure if that is scaleable The behaviour without inclusion of the headers seems right, what I refer to above as "anonymous quantity" I think that conversion to energy or torque etc should be explicit made explicit by assigning/initing the rvalue to a named quantity

https://github.com/kwikius/units/blob/andy_master/example/torque_example.cpp

mpusz commented 4 years ago

The problem with the inclusion is that if someone will include both headers (torque and work) we will end up in compilation error. I have to figure out how to fix the downcasting facility to silently do nothing when more than one downcast target is defined.

kwikius commented 4 years ago

My concern is that compilation doesnt fail when neither header is included, but inclusion of either or both headers changes the behaviour of output. Since it is difficult to predict which headers are visible in a large project over time, so surprising breaking changes in behaviour may occur without an obvious reason. For example, including units in a file provides a satisfying, strongly typed and user friendly text output format: https://github.com/kwikius/f3res/blob/master/outer_panel.wing_stl The ideal is that non inclusion of a required header causes compilation to fail surely?

I also refer back to the source I linked above https://github.com/kwikius/quan-trunk/blob/master/quan_matters/examples/doc_output_demo.cpp where all three quantities are used in the same file

quan style output is\ :

1 kg.m+2.s-2 1 J 1 N.m

kwikius commented 4 years ago

Comment above edited for clarity re 'explicit'.

kwikius commented 4 years ago

As continued from https://github.com/mpusz/units/issues/41

 constexpr dim_mass<1> mass;
   constexpr dim_length<1> length;
   constexpr dim_time<1> time;
   constexpr auto velocity = length / time; 
   struct dim_velocity = derived_dimension<dim_velocity,  metre_per_second, decltype(velocity)>
   constexpr auto acceleration = velocity / time; 
   struct dim_acceleration = derived_dimension<dim_acceleration, metre_per_second_sq, decltype(acceleration)>
   constexpr auto force = mass * acceleration;

There is a misunderstanding there Bear with me, I will see if I can explain this better.

(N.B I avoided c++ syntax markdown op here as I dont know how to do emphasis in it)

constexpr dim_mass<1> mass; //mass is an _abstractquantity

constexpr dim_length<1> length; //length is an _abstractquantity

constexpr dim_time<1> time; // time is an _abstractquantity

constexpr auto velocity = length / time; //velocity is an _abstractquantity

// dim_velocity is not an _abstractquantity. (abstract_quantity universe does not have any units, ie numeric values or conversions factors etc) struct dim_velocity = derived_dimension<dim_velocity, metre_per_second , decltype(velocity)> // #####shouldnt compile ############################ constexpr auto acceleration = velocity / time; //############################################# // dim_acceleration is not an _abstractquantity. struct dim_acceleration = derived_dimension<dim_acceleration, metre_per_second_sq, decltype(acceleration)> // ###### should fail to compile *no operator (_abstract_quantity , quantity_with_unit_)* #### constexpr auto force = mass acceleration; //#########################################################

I have worked this up in current working branch of pqs which uses exp<base_dim,N,D> syntax which may be clearer

#include <pqs/base_quantity/length.hpp>
#include <pqs/base_quantity/time.hpp>
#include <pqs/base_quantity/mass.hpp>
#include <pqs/exposition/base_quantity_exp.hpp>
#include <pqs/bits/quantity.hpp>
#include "make_quantity.hpp"

using pqs_exposition::exp;
using pqs::base_length;
using pqs::base_time;
using pqs::base_mass;

namespace exposition{

   // base_quantity exponent aka base_dimension exponent
   constexpr exp<base_length,1> length;
   constexpr exp<base_time,1> time;
   constexpr exp<base_mass,1> mass;

   // dimension<exp<base_length,1>,exp<base_time,-1> >
   constexpr auto velocity = length / time;

   // derived_dimension
   struct vertical_velocity_t : decltype(velocity) {};
   constexpr vertical_velocity_t  vertical_velocity ;
}

namespace {

   template <typename T>
   struct is_abstract_quantity_t :
      pqs::meta::or_<
        pqs::is_base_quantity_exp<T>,
        pqs::is_dimension<T>,
        pqs::is_derived_dimension<T>
      >{};

   template <typename T>
   constexpr bool is_abstract_quantity(T t)
   {
      return is_abstract_quantity_t<T>::value;
   }

   template <typename T> 
   constexpr bool is_base_quantity_exp(T t)
   {
      return pqs::is_base_quantity_exp<T>::value;
   }

   template <typename T> 
   constexpr bool is_dimension(T t)
   {
      return pqs::is_dimension<T>::value;
   }

   template <typename T> 
   constexpr bool is_derived_dimension(T t)
   {
      return pqs::is_derived_dimension<T>::value;
   }
}

int main()
{ 
   static_assert(is_base_quantity_exp(exposition::length),"");
   static_assert(is_abstract_quantity(exposition::length),"");

   static_assert(is_base_quantity_exp(exposition::mass),"");
   static_assert(is_abstract_quantity(exposition::mass),"");

   static_assert(is_base_quantity_exp(exposition::time),"");
   static_assert(is_abstract_quantity(exposition::time),"");

   static_assert(is_dimension(exposition::velocity),"");
   static_assert(is_abstract_quantity(exposition::velocity),"");
   static_assert(not is_derived_dimension(exposition::velocity),"");

   static_assert(is_derived_dimension(exposition::vertical_velocity),"");
   static_assert(is_abstract_quantity(exposition::vertical_velocity ),"");

   constexpr auto q1 = make_quantity<3>(exposition::length,20.0);
   constexpr auto q2 = make_quantity<3>(exposition::velocity,20.0);
   constexpr auto q3 = make_quantity<3>(exposition::vertical_velocity,20.0);

   static_assert ( not is_abstract_quantity(q1),"");
   static_assert ( not is_abstract_quantity(q2),"");
   static_assert ( not is_abstract_quantity(q3),"");
}
kwikius commented 4 years ago

We can look at the problem of what the result of an operation on two abstract quantities should be If Qa Qb are two abstract_quantities

Qa [+,-] Qb if (same_type(Qa,Qb) --> Qa if ( not same_type(Qa,Qb) but dimensionally_equivalent(Qa,Qb) ) --> ? ( my answer is anonymous_abstract_quantity(Qa) yours is to lookup some match or fail?

Qa [*,/] Qb if (same type[(Qa,/,Qb) ,(Qa,*,1/Qb)]-> dimensionless if ( not same but dimensionally_equivalent[(Qa,/,Qb),(Qa,,1/Qb)] -> ? (my answer is dimensionless but could do something cleverer there) if ( not dimensionally_equivalent[(Qa,/, Qb),(Qa,*,1/Qb)] -> ( my answer anonymous_abstract_quantity(Qa [\,/] Qb, again you do lookup or fail? )

anonymous_abstract_quantity(Q) is then probably same as dimension_of(Q) maybe dimension<exp<base,n,d>...> or exp<base,n,d> if can be so reduced

mpusz commented 4 years ago

I just figured out how to disambiguate downcasting for conflicting definitions: https://godbolt.org/z/uSrLDm

I will integrate this into the framework soon. The idea is to provide automated downcasting only if it is unambiguous and otherwise ask the user to manually quantity_cast to a specific dimension/unit.

kwikius commented 4 years ago

It sounds promising.

In my experience making the type unambiguous most often happens simply by assignment/initialisation.

MyQuantity v = ambiguous_expression();

std::cout << v << '\n';

in which case writing quantity_cast should not be a common occurrence in user level code

mpusz commented 4 years ago

Fun is over. It seems that even though it works for one translation unit it is a possible ODR violation in a program scope. It seems that we will have to get rid of downcasting facility as we cannot expect our users to include the same set of headers and in the same order in every translation unit. :-(

kwikius commented 4 years ago

I realise that it would be disappointing to remove downcasting however removing it simplifies things I think.

The 4 useful features of the library 1) Dimensional analysis 2) Tracking of numerical values and unit conversions according to semantic unit conversion rules 3) I/O in some preferred format 4) Self documenting code

The first 2 features are unaffected by removing downcasting. Only required attributes are dimension and conversion_factor.

The third feature requires an extra attribute and the library cant guess what that attribute is, since it just doesn't have that information. Looked at another way, A function frequently returns only partial information about its inputs, so the operations on 2 quantities looks in some cases only at the dimension and conversion factor and return an anonymous quantity with a dimension and a conversion factor according to semantic rules for dimensional anlysis and unit conversion , unless there is input information to do otherwise ( an example of where the function can do better is addition of two named_quantities with the same name). (Again I suggest "anonymous quantity" versus "named quantity" to express the different categories in docs. The named_quantity can be seen as wrapping the anonymous quantity with an extra name attribute which is plucked out of the air by the programmer : "torque", "area", "energy" etc etc)

The 4th feature is actually enhanced by removing downcasting because now it is left to the user to impose the extra output attribute name (and specify a conversion_factor for the quantity FWIW) explicitly at I/O HMI and that helps code as documentation, which is a really important but often overlooked feature of a units library.

EDIT: And I would expect compilation speed to improve without downcasting, which is a big plus to remove it.

kwikius commented 4 years ago

I would suggest a way to make progress on the downcasting issue is to use a define and a config.h header for the alpha versions of the library, specifying clearly in docs and source that this is only a temporary solution and at some point one option or the other will need to be chosen for the final library.

define MPUSZ_UNITS_NO_AUTOMATIC_DOWNCASTING

or some such ( or #define MPUSZ_UNITS_YES_AUTOMATIC_DOWNCASTING if you prefer ...)

and take appropriate steps in the source to do downcasting or not based on this define, then it is possible to get some experience of the two options.

mpusz commented 4 years ago

Thanks, this is exactly what I plan to do. I will do this as soon as I find some time as I am in a constant lack of it ;-)

JohelEGP commented 4 years ago

Fun is over. It seems that even though it works for one translation unit it is a possible ODR violation in a program scope. It seems that we will have to get rid of downcasting facility as we cannot expect our users to include the same set of headers and in the same order in every translation unit. :-(

Is that the case even with modules? If you can provide an example, I can test with GCC branch devel/c++-modules. The ones mentioned in the OP only come in a single form.

mpusz commented 4 years ago

Yeah, this is what I hope for. Modules will hopefully solve the issue. Just one module with all SI definitions without the possibility to split it into smaller pieces.

BTW, it is an ODR violation even without collisions. It is enough to divide the length by time in 2 translation units where only one of them includes speed definition. In such a case you will get the same generic function (i.e. divide_and_print(length, time)) to have different implementation and behavior in each TU).

mpusz commented 4 years ago

On the other hand, I still look for alternatives. I made downcasting_2.0 branch (https://github.com/mpusz/units/tree/downcasting_2.0) where the user may turn on or off the downcasting facility. It also provides auto mode which will enable the feature when no collisions are detected.

However, I still do not have an idea how to make dimension-specific concepts to work there (i.e. units::physical::Speed) 😞 In case you have an idea, please let me know.

JohelEGP commented 4 years ago

BTW, it is an ODR violation even without collisions. It is enough to divide the length by time in 2 translation units where only one of them includes speed definition.

Ah, yes. I remember that. I ran into it when I came up with the "strong units" at nholthaus/units (branch v3.x), which are downcasting on the quantity (there named unit) rather than the unit (there more like unit_conversion or conversion_factor). We talk about that at https://github.com/nholthaus/units/issues/200 and https://github.com/nholthaus/units/pull/150#issuecomment-428280402.

Later in https://github.com/nholthaus/units/issues/200, I mention "strong unit conversions", which is more like the downcasting facility (applies to the unit rather than the quantity). That doesn't have the problems we discuss there. In other words, wouldn't a solution be forward declare part of the downcasting in <quantity.h> so that if a user attempts to do 2q_m * 2q_m without including the area header, it sees the declaration of the area dimension and/or unit, but not its definition, and so gets an incomplete type error.

mpusz commented 4 years ago

wouldn't a solution be forward declare part of the downcasting in so that if a user attempts to do 2q_m * 2q_m without including the area header, it sees the declaration of the area dimension and/or unit, but not its definition, and so gets an incomplete type error.

I do not think it is possible to forward declare all dimensions and units before the quantity. Quantity is the part of a generic library. Units and dimensions are parts of concrete library. External vendors would like to provide their own libraries of dimensions and units. It is impossible to account for everything in forward declarations.

JohelEGP commented 4 years ago

Not before quantity, just after it. It's true that it's a hack that might not scale. I was hoping that providers of systems of quantities would forward declare their dimensions and/or units. Like having all of SI include a detail/si_fwd.hpp header with the forward declarations. The problem would still be there with program-defined derived dimensions and units. Like si/international if it weren't part of the library.

Hopefully modules makes this unnecesary.

mpusz commented 4 years ago

Hopefully modules makes this unnecesary.

This is exactly what I hope for.

In the meantime, if we can find out how to make dimension-specific concepts to work on downcasting_2.0 branch with MODE set to OFF than we would have some alternative (and usage experience) if ISO C++ Committee would not agree to include the downcasting facility to the standard.

mpusz commented 4 years ago

I forced the usage of si.h with all the definitions and wanted to make a release today. However, it turns out that the inclusion of all SI definitions every time increased the CI build time by at least the factor of 2 (https://travis-ci.com/github/mpusz/units/builds). What is even worse it made the Compiler Explorer to time out on even the easiest code samples: https://godbolt.org/z/qE7Wx6. I am afraid that we cannot release something like this...

If you have a good idea of how to improve this before modules will come please let me know. For now, I am reverting most of those changes and plan to try doing the release tomorrow again.

dwmckee commented 3 years ago

In principle energy and torque have different tensor character (scalar and pseudo-vector respectively) and you could tell them apart by introducing another layer of concepts to represent that. But I don't think that is a complete solution because there are the issues of cyclic-frequency versus angular-frequency (which you've alluded to in a different issue) and worse still periodic frequency (Hz) and random frequency (Bq). Th SI committee basically punted on this stuff figuring that people are smart enough to work it out in context (true enough once they've gotten past the first few courses), but that leaves the problems still to be solved by people trying to automate these issues.

Aside: I think that the difference between state quantities like energy and interaction quantities like work belong at a different semantic level than a units/dimension library (AKA your users should be responsible for that one).

AndreasLokko commented 2 years ago

Hi,

I would like to argue the issue here is not with the library and lies within the standard unit of torque.

For linear motion work is done via applying a force along a displacement, in which case dimensional analysis yields expected units.

For rotational motion work is done via applying a torque about an angular displacement. If radians are treated as a base unit instead of a dimensionless unit then it follows that torque should be expressed as newton-metre per radian. The has been argued for before (a citation I found from skimming wikipedia article for torque):

Page, Chester H. (1979). "Rebuttal to de Boer's "Group properties of quantities and units"". American Journal of Physics. 47 (9): 820. doi:10.1119/1.11704

In the wikipedia article for newton-metre I found this gem:

The unit is also used less commonly as a unit of work, or energy, in which case it is equivalent to the more common and standard SI unit of energy, the joule.[2] In this usage the metre term represents the distance travelled or displacement in the direction of the force, and not the perpendicular distance from a fulcrum as it does when used to express torque. This usage is generally discouraged,[3] since it can lead to confusion as to whether a given quantity expressed in newton-metres is a torque or a quantity of energy.[4] However, since torque represents energy transferred or expended per angle of revolution, one newton-metre of torque is equivalent to one joule per radian.[4]

Unfortunately the SI-unit for torque seems to have fallen victim to tradition. Also an interesting fact is that although one radian is one over two pi and pi emerged some 4000 years ago radian only became an SI derived unit in 1995.

PS: I just realized this is how the library already has the unit of torque as newton-metre per radian and #99 has a lot of discussion regarding this. Maybe this issue should be closed since the original problem example has been resolved?

JohelEGP commented 2 years ago

Maybe this issue should be closed since the original problem example has been resolved?

No. I believe this is more general. There are more quantities with the same dimension.

JohelEGP commented 2 years ago

There are a few pairs (or more) quantities that have the same quantity:

Shouldn't this read "quantities that have the same unit"?


The SI Brochure[1] says

It is important to emphasize that each physical quantity has only one coherent SI unit, even though this unit can be expressed in different forms by using some of the special names and symbols. The converse, however, is not true, because in general several different quantities may share the same SI unit. For example, for the quantity heat capacity as well as for the quantity entropy the SI unit is joule per kelvin. Similarly, for the base quantity electric current as well as the derived quantity magnetomotive force the SI unit is the ampere. It is therefore important not to use the unit alone to specify the quantity. This applies not only to technical texts, but also, for example, to measuring instruments (i.e. the instrument read-out needs to indicate both the unit and the quantity measured). In practice, with certain quantities, preference is given to the use of certain special unit names to facilitate the distinction between different quantities having the same dimension. When using this freedom, one may recall the process by which this quantity is defined. For example, the quantity torque is the cross product of a position vector and a force vector. The SI unit is newton metre. Even though torque has the same dimension as energy (SI unit joule), the joule is never used for expressing torque.

Problems arise when 2 units are defined in terms of the same units. I think that's because mp_units ties units to quantities. For example, when declaring the unit metre_per_second, the library machinery makes it so that a result of metres per second is tied to the quantity tied to the unit metre_per_second. You can't declare another first-class citizen unit defined in terms of the units metre_per_second is defined. Of course, there are workarounds, like alias units and whatnot.

A possible solution is to separate quantities from units. Most generally, the library could traffic only in units of base dimensions and only assume derived quantities when the users specifies so. For example, 1 * (m / s) could be quantity<metre(per<second>)>, whereas quantity<metre_per_second>(1 * (m / s)) is what it says.

[1]: https://www.bipm.org/en/publications/si-brochure

mpusz commented 2 years ago

Most libraries always tie a unit to a specific dimension (i.e. https://www.boost.org/doc/libs/1_62_0/doc/html/boost_units/Units.html), but as stated above, several quantities can use the same unit (it is even more true in natural systems of quantities). This library was designed with that in mind and makes a unit independent from the quantity/dimension. Thanks to that more than one quantity can use the same unit and they are properly handled in the dimensional analysis.

It is important to emphasize that each physical quantity has only one coherent SI unit

Also the above is the key point of the library's design. Each dimension in this library requires providing a coherent unit for it.

mpusz commented 2 years ago

As long as we have aliased unit we do not have aliased dimension in the design. We can use a C++ alias (as used in some examples) but it is not the same. I hope to solve it with #281 when I will finally have enough time to work on it.

JohelEGP commented 2 years ago

Right. I suppose the problem I described arises specifically when defining units in the same system with equivalent dimensions, like the ones commented out in si.h:

// TODO Add when downcasting issue is solved (collides with pressure)
// #include <units/isq/si/energy_density.h>
// TODO Add when downcasting issue is solved (collides with frequency)
// #include <units/isq/si/radioactivity.h>

The ones in the OP and some others can also be solved by not being pedantic with the SI definition and insisting in even stronger types.

"moment of force/torque" vs "energy/work"

According to https://en.wikipedia.org/wiki/Work_(physics):

In its simplest form, it is often represented as the product of force and displacement.

[...] a displacement is a vector [...]

So this can be solved with an abstraction representing a vector. I think this particular point has been raised before.

Now consider radioactivity.

The International System of Units (SI) unit of radioactive activity is the becquerel (Bq), named in honor of the scientist Henri Becquerel. One Bq is defined as one transformation (or decay or disintegration) per second. -- https://en.wikipedia.org/wiki/Radioactive_decay#Units

This other one can be solved by giving the numerator a unit, and thus a stronger type. I'll use one I'm more familiar with for exposition: https://en.wikipedia.org/wiki/Clock_rate.

In computing, the clock rate or clock speed typically refers to the frequency at which the clock generator of a processor can generate pulses, which are used to synchronize the operations of its components,[1] and is used as an indicator of the processor's speed. It is measured in clock cycles per second or its equivalent, the SI unit hertz (Hz).

Although it measures "clock cycles per second", its typical representation using Hz loses that information. This is arguably fine in text, where you specify that the quantity is a clock rate. Not so much in programming.

You could choose to represent "clock cycles" however you want. But mpusz/units already has a good example of my preference. Consider this from the SI Brochure:

Because of this, it is recommended that quantities called “frequency”, “angular frequency”, and “angular velocity” always be given explicit units of Hz or rad/s and not s^−1.

rad/s already means something different from Hz and s^−1. That's because angle.h defines

struct radian : named_unit<radian, "rad", isq::si::prefix> {};

template<Unit U = radian>
struct dim_angle : base_dimension<"A", U> {};

So in code “angular frequency” is different from “frequency”, which is a good thing.

i-ky commented 2 years ago

There is a curious case of absorbed dose vs. equivalent dose vs. effective dose. First one is measured in grays (Gy), other two in sieverts (Sv), all are equivalent dimension-wise to J/kg. But one needs additional information to convert between them.

mpusz commented 1 year ago

Addressed with V2.