mpusz / mp-units

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

How to support non-linear scale? #35

Open mpusz opened 4 years ago

mpusz commented 4 years ago

Currently, it is unclear on how to handle a logarithmic (or other scales). There is only one library on the market that provides such a built-in support https://github.com/nholthaus/units

emsr commented 4 years ago

I've thought some on this (I had an impl somewhere). decibel(power x) = 10 * log10(x / 1_mW). And add math with levels.

At first I thought we should have the ratio of two quantities whose units cancel to be a number but with things like this I think we might want to keep unit information.

In general, I's like to help with this as time allows. I want this for C++23 and I want to use the implementation right now. I have electromagnetic field units lined up.

Thank you for doing this. Ed

Kered13 commented 4 years ago

It seems like the simplest solution would be to create a new base unit and unit to represent this, and provide conversion functions in the library that internally discard the units to perform the calculations. Other than this limited loss of type safety, what is the drawback of this approach?

oschonrock commented 4 years ago

Looking at not just "non-linear scale" for dB (which is reasonably niche), but also "offset linear", such as the transform between Celsius and Fahrenheit and Kelvin, which is more common.

Firstly, just looking at where this is currently implemented:

The basic logic currently performed is (obviously)

// glossing over the fact it's done via base unit -- see below
ValueInUnitX = ValueInUnitY * k
ValueInUnitY = ValueInUnitX / k 

Where k is currently the units::ratio<num,den,exp>

The current quantity_cast implementation assumes intimate knowledge of units::ratio:

template<typename To, typename CRatio, typename CRep, bool NumIsOne = false, bool DenIsOne = false>
struct quantity_cast_impl {
  template<typename Q>
  static constexpr To cast(const Q& q)
  {
    if constexpr (treat_as_floating_point<CRep>) {
      return To(static_cast<To::rep>(static_cast<CRep>(q.count()) *
                                     static_cast<CRep>(fpow10(CRatio::exp)) *
                                     (static_cast<CRep>(CRatio::num) /
                                      static_cast<CRep>(CRatio::den))));
    } else {
      return To(static_cast<To::rep>(static_cast<CRep>(q.count()) *
                                     static_cast<CRep>(CRatio::num) *
                                     static_cast<CRep>(CRatio::exp > 0 ? ipow10(CRatio::exp) : 1) /
                                     (static_cast<CRep>(CRatio::den) *
                                      static_cast<CRep>(CRatio::exp < 0 ? ipow10(-CRatio::exp) : 1))));
    }
  }
};

However, this logic is not very well abstracted, and some linear multiplication assumptions also exist in (at least) these related places:

src/include/units/quantity_cast.h:71:struct quantity_cast_impl {
src/include/units/quantity_cast.h:73:  static constexpr To cast(const Q& q)
src/include/units/quantity_cast.h:91:struct quantity_cast_impl<To, CRatio, CRep, true, true> {
src/include/units/quantity_cast.h:93:  static constexpr To cast(const Q& q)
src/include/units/quantity_cast.h:104:struct quantity_cast_impl<To, CRatio, CRep, true, false> {
src/include/units/quantity_cast.h:106:  static constexpr To cast(const Q& q)
src/include/units/quantity_cast.h:117:struct quantity_cast_impl<To, CRatio, CRep, false, true> {
src/include/units/quantity_cast.h:119:  static constexpr To cast(const Q& q)
src/include/units/quantity_cast.h:165:[[nodiscard]] constexpr auto quantity_cast(const quantity<D, U, Rep>& q)
src/include/units/quantity_cast.h:173:  using cast = detail::quantity_cast_impl<ret, c_ratio, c_rep, c_ratio::num == 1 && c_ratio::exp == 0, c_ratio::den == 1 && c_ratio::exp == 0>;
src/include/units/quantity_cast.h:174:  return cast::cast(q);

One way to extend, is for the operation to become a generic "function transform" such that, eg:

celcius = 5/9(fahrenheit − 32)  // but also
fahrenheit = celcius × ​9⁄5 + 32 

ie, since we don't have a symbolic "solver", and unless we want to "bake in a special case" for linear and not support dB the same way, then we would need two such unit::transforms for each unit. This would need to always be to/from the base unit for that dimension to avoid a combinatorial explosion (so above example would need to be Fahrenheit to/from Kelvin, and similarly for Celsius). This is already the case with ratio, since even though we hypothetically could, we do not "chain ratios" to "walk" from one unit to another with multiple steps. We always "go via base". So the same basic idea can apply to functions.

The "transform selection" for the simple ratio is currently done here:

template<Dimension FromD, Unit FromU, Dimension ToD, Unit ToU>
struct cast_ratio;

template<BaseDimension FromD, Unit FromU, BaseDimension ToD, Unit ToU>
struct cast_ratio<FromD, FromU, ToD, ToU> {
  using type = ratio_divide<typename FromU::ratio, typename ToU::ratio>;
};

template<DerivedDimension FromD, Unit FromU, DerivedDimension ToD, Unit ToU>
  requires same_unit_reference<FromU, ToU>::value
struct cast_ratio<FromD, FromU, ToD, ToU> {
  using type = ratio_divide<typename FromU::ratio, typename ToU::ratio>;
};

template<DerivedDimension FromD, Unit FromU, DerivedDimension ToD, Unit ToU>
struct cast_ratio<FromD, FromU, ToD, ToU> {
  using from_ratio = ratio_multiply<typename FromD::base_units_ratio, typename FromU::ratio>;
  using to_ratio = ratio_multiply<typename ToD::base_units_ratio, typename ToU::ratio>;
  using type = ratio_divide<from_ratio, to_ratio>;
};

So (at least) all the above extracts would need to change and be generalised to invoking these arbitrary transforms, and their inverses. units::ratio could become either a specialisation of units:transform or be wrapped by the latter.

This could be done by having a linear_unit and non_linear_unit but I would probably favour composition over inheritance here.

Note that I am not addressing any "change in dimension" due to the transform, as suggested with db = log(power) above. Because dB is the "log of a ratio of quantities with the same units" eg

dB(100W) = 10 * log₁₀ (100W / 1W) = 10 * log₁₀ (100) = 20  (no units, it's a "dimensionless scalar")

it is dimensionless and it's therefore not clear where it belongs quite apart from it having a non-linear scale?

Is this the general direction?

mpusz commented 4 years ago

Looking at not just "non-linear scale" for dB (which is reasonably niche), but also "offset linear", such as the transform between Celsius and Fahrenheit and Kelvin, which is more common.

I am not sure if temperature support should be discussed here as a scale. It is about affine types discussed in #1. Those types are not only about the zero offset but also about restricting some arithmetic operations on absolute values. Also, I do not think the way to solve affine types is to add more operations before and after multiplying by a factor in ratio (I tried and failed a long tie ago). I am pretty convinced that the only reasonable solution is to go with a quantity_point class as benri and Boost does.

However, this logic is not very well abstracted, and some linear multiplication assumptions also exist in (at least) these related places:

Please note that all of those special cases are mandated by the C++ standard right now in [time.duration.cast].

As we discuss affine types in another thread (#1) please scope here on logarithmic (and potentially other scales here) that most probably should be implemented in terms of Rep or as an additional layer above the quantity type.

oschonrock commented 4 years ago

Do we have an actual example of a logarithm (or other non-linear) scale of a quantity with unit?

dB is not one in my opinion. It is the division of 2 quantities with the same dimension, resulting in a scalar without units and then 10* log

Where does that fit as a dimensionless quantity? Are there other examples?

Happy to take offset discussion to #1, to see if affine types actually provide a solution to these cases.

i-ky commented 4 years ago

Do we have an actual example of a logarithm (or other non-linear) scale of a quantity with unit?

There are a few in chemistry: pH, pOH, pKw, pKa, etc. Although I have never seen their values written with a unit, the only reason for that is that one absolutely MUST use concentrations in mol/l to calculate them. These quantities are not dimensionless by nature and using different units to express concentrations will result in different numbers.

oschonrock commented 4 years ago

Yeah I am well familiar with the Chemistry ones. I would argue that they are not "units" in the conventional sense for many of the reasons you mention. They have many preconditions, you can't do conventional arithmetic them etc...

I wonder if that is more generally true of such "units".

If you think about what "log" does in the Taylor series expansion sense, you can see that "dimensions" in the traditional sense may not survive that process?

JadeMatrix commented 3 years ago

Do we have an actual example of a logarithm (or other non-linear) scale of a quantity with unit?

dB is not one in my opinion. It is the division of 2 quantities with the same dimension, resulting in a scalar without units and then 10* log

I disagree. (Deci)bels only modify how a value of a certain dimension is measured, with the division simply being part of the calculation of the raw numeric value. "dB" is only shorthand when the reference value is implicit in literature; the more correct form is "dB re \<constant> \<unit>". The distinction is important in my line of work, where we deal with multiple incompatible decibels: dB re 1μPa, dB re 1mW, etc.

Essentially, "dB re \<constant> \<unit>" should be convertible* to & from "\<unit>" (or anything in that dimension) as they measure the same thing.

* Theoretically, that is; numeric precision may limit the usefulness of certain conversions in C++ code

I've been toying around with a design for a wrapper temlate that encodes the reference value & unit in the type, which would look something like this:

template<
    auto ReValue,
    Unit ReUnit,
    ratio R = ratio(1),
    QuantityValue Rep = double
> class bel
{
    // ...
};

// In this domain, SPL = 20 log10( p / 1μPa ) = 2 * dB re 1 μPa
using sound_pressure_level = bel<1, si::micropascal, ratio(2, 1, -1)>;

// Possibly in C++23?
using sound_pressure_level = bel<si::pressure<si::micropascal>(1), ratio(2, 1, -1)>;

Hopefully I translated into mp-units correctly; I'm more familiar with my own units library, which I'm looking to cease development of in favor of this one.

JohelEGP commented 3 years ago

I am not sure if temperature support should be discussed here as a scale. It is about affine types discussed in #1.

Considering that #232 is open and #1 is solved, it seems that temperatures are not a solved problem. So I'm posting this here.

At https://youtu.be/ABkxMSbejZI?t=4852, Peter Sommerlad touches this topic.

JohelEGP commented 1 year ago

ISO 80000-8 (acoustics) has logarithmic quantities. Requirements on how to support non-linear scales could be derived from those. It would also help to have the input from people familiar with the domain.

JohelEGP commented 1 year ago

From https://github.com/mpusz/mp-units/issues/468#issuecomment-1708661124:

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.

mp_units::quantity is supposed to represent a vector space (see https://mpusz.github.io/mp-units/2.1/users_guide/framework_basics/the_affine_space/, or https://www.electropedia.org/iev/iev.nsf/display?openform&ievref=102-03-01). If a logarithmic quantity isn't a vector space, I argue that it needs a new abstraction (e.g., mp_units::logarithmic_quantity).

mpusz commented 11 months ago

Input from Timur Doumler:

Logarithmic units are very common and important in many fields, not just in audio (decibels), but for example also in astronomy where we have the apparent magnitude – the unit we use for measuring the brightness of a star or planet in the sky. It's a reverse logarithmic unit: the brighter an object is, the lower its magnitude number. A difference of 1.0 in magnitude corresponds to a brightness ratio of 100^(1/5) which is approximately 2.5. For example, the full moon has an apparent magnitude somewhere around -12, the star Vega is around 0 (Vega is the reference point), and Pluto is around +13. It's a very common and important unit in astronomy. You do things like converting the apparent magnitude to absolute magnitude with a formula that involves the distance.

Magnitudes are all over the place in observational astronomy: https://en.wikipedia.org/wiki/Apparent_magnitude