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

Suggestion: define conversions as rational values #478

Closed
R2D221 opened this issue Sep 14, 2018 · 18 comments
Closed

Suggestion: define conversions as rational values #478

R2D221 opened this issue Sep 14, 2018 · 18 comments
Labels

Comments

@R2D221
Copy link

R2D221 commented Sep 14, 2018

I recently tried to do a simple conversion between US gallons and oil barrels. The conversion is simple, you just need to multiply or divide by 42. However, with this library, the result is the following:

Volume.FromOilBarrels(1).UsGallons == 42.0000197938929

I have already read the Precision page, so I understand how the conversions are made and how it's not the first priority. However, I want to post this as a suggestion.

Instead of defining the units as a double, they could be defined as rational.

How would this work: Each conversion should have the numerator and denominator. This leaves things almost unchanged for e.g. meters to kilometers. But for conversions between metric and imperial systems, it would look as follows:

So, by storing 473176473, 125000000000 and 9936705933, 62500000000 as the conversion values for gallons and barrels, you can then convert between them like so:

var gallons = barrels * 125000000000.0 * 9936705933.0 / 473176473.0 / 62500000000.0

If you input an integer, it will give you an exact multiple of 42.


Of course, this isn't exclusive to gallons, but pretty much any conversion between imperial and metric systems would benefit from this, especially since most if not all of these units are defined in metric terms anyway (like an inch being exactly 0.0254 meters).

I know maybe this isn't in your plans, but I think the system could internally be adapted to make the calculations like this while improving precision. On the other hand, I don't know the performance impact of this, but I don't think it would be that much of a difference.

@angularsen
Copy link
Owner

Thanks for sharing!
I like the idea, but am traveling the next few days and need more time to read through this to comment further.

@angularsen
Copy link
Owner

This is pretty cool.
My immediate thoughts on how this can be implemented:

Today:

  • double Value (a handful of quantities use decimal): 64 or 128 bits
  • UnitEnum Unit: 32 bits (we could probably use uint8 here if we want)

Proposed:

  • long Numerator 64 bits
  • long Denominator 64 bits
  • UnitEnum Unit 32 bits

Breakdown
Here I attempt to break down size, range and precision for the new approach vs double/decimal.
Please correct me if my calculations are off.

  • Size: 160 bits vs 96/160 bits for double/decimal
  • Range: approx. ±1e19 to ±9e18
  • Precision: Not too sure how to think about this, my understanding is that precision in terms of significant digits can vary depending on the number you are representing while double/decimal has a fixed number of significant digits.

Size-wise, we should be good enough I think.
Range-wise, if I got it right it does look a lot less than both double and decimal? This may be a problem.
Precision ought to be an improvement, but my brain fails to formulate any good comparison of this vs double/decimal.

Reference
Long - 64 bit and range is -9,223,372,036,854,775,808 to 9,223,372,036,854,775,807.
Double - 64 bit (15-16 digits) and range is ±5e−324 to ±1.7e308.
Decimal - 128 bit (28-29 significant digits) and range is ±1e-28 to ±7.9e28.

Let me know what you think and if I got anything wrong :-)

@R2D221
Copy link
Author

R2D221 commented Sep 27, 2018

Hi! Let me check on the weekend so I can elaborate on the idea. :D

@R2D221
Copy link
Author

R2D221 commented Oct 8, 2018

Hi, I've made a pretty simple example in this link: https://repl.it/@ArturoTorres/UnitsNET-rational-conversion

Let me explain how I was thinking about it.

The value themselves should still be stored in a simple value. I think for these calculations decimal is a better fit because of the added precision.

So, the proposed values would be:

Proposed:

  • decimal Value: 128 bits
  • UnitEnum Unit: 32 bits

But then, the numerator and denominator will come into play in the conversions.

First of all, for simplicity, I'm defining a Rational struct:

internal struct Rational
{
    private decimal numerator;
    private decimal denominator;
}

The reason these are decimal and not long is because of the added precision (as you can see from my formulas in the previous comment, the numbers can get quite large), and because this won't be a true Rational implementation, but just a helper for our calculations.

This struct has multiplication and division defined, and also an explicit conversion to decimal. The decimal conversion would just make the division and give the result as decimal.

The next part, then, would be to define the conversions in a static dictionary defined for each class (or, with code generation, a switch would work as well)

private static readonly Dictionary<VolumeUnit, Rational> multipliers = new Dictionary<VolumeUnit, Rational>
{
	[VolumeUnit.CubicMeter] = new Rational(1, 1),
	[VolumeUnit.OilBarrel] = new Rational(62500000000, 9936705933),
	[VolumeUnit.UsGallon] = new Rational(125000000000, 473176473),
};

And then the conversion would be applied as such:

public decimal As(VolumeUnit unit)
{
	return this.value * (decimal)(multipliers[unit] / multipliers[this.unit]);
}

This would make the calculations more precise while still storing a single value in each Unit. The Rational is just a helper for multiplications.


What do you think? I hope it's not confusing.

@angularsen
Copy link
Owner

Great example, it clarified a lot for me, thanks!

I now understood the part about only using the rationals for representing conversion factors.
I think this would work equally well with double, wouldn't it? I mean, it wouldn't get any worse than today's representation and conversion?
After completing #487 we are discussing the option of moving from struct to class, which would allow the consumer to choose his own numeric type, something along the lines of Length<float> or using namespaces UnitsNet.SinglePrecision.Length. If we go for that, then it's good to know if this solution works with other number types too.

I'll get back to you on this, just wanted to chime in real quick.

@R2D221
Copy link
Author

R2D221 commented Oct 8, 2018

Great! Yes, using double as values would work as well.

@angularsen
Copy link
Owner

Well, if you want, you can start working on a PR for this. I see no downsides to this approach and we can always represent our current conversion factors as a numerator with denominator=1 if the number is irrational. Just make sure to base the PR on v4 branch.

@tmilnthorp tmilnthorp mentioned this issue Nov 6, 2018
25 tasks
@tmilnthorp
Copy link
Collaborator

I think I may have a better idea here. Working on a PR...

@tmilnthorp
Copy link
Collaborator

Take a look at my idea in PR #588.

I think rather than adding this complexity, it would be nice to have all unit conversions held in the UnitConverter class. This PR allows you to also add custom to/from conversions, including direct conversions from unit->target that avoid going from unit->base->target.

Here's the code for going directly to US gallons from oil barrels:

        [Fact]
        public void TryCustomConversionForOilBarrelsToUsGallons()
        {
            ConversionFunction conversionFunction = ( from ) => Volume.FromUsGallons( ((Volume)from).Value * 42 );

            var unitConverter = new UnitConverter();
            unitConverter.SetConversionFunction<Volume>( VolumeUnit.OilBarrel, VolumeUnit.UsGallon, conversionFunction );

            var foundConversionFunction = unitConverter.GetConversionFunction<Volume>( VolumeUnit.OilBarrel, VolumeUnit.UsGallon );
            var converted = foundConversionFunction( Volume.FromOilBarrels( 1 ) );

            Assert.Equal( Volume.FromUsGallons( 42 ), converted );
        }

In this case, the answer is exactly 42.

@angularsen
Copy link
Owner

angularsen commented Jan 26, 2019

@tmilnthorp
Continuing my comments from the PR here:

  1. I like it, but I'm not sure how widely it will be used. Is higher precision conversion the main motivation for this change? Do you imagine replacing all existing conversions or just the ones that could use better precision? I don't think the former doesn't scale very well. If a quantity has 10 units, instead of 2*10 conversion functions (to base + from base) you have to define 9*10 functions.
  2. Since it doesn't scale well, I'm not sure this replaces rational value proposal discussed here.
  3. One usecase I can think of is adding entirely new conversion functions dynamically at runtime, such as your own unit enums and your own conversion functions between them. Again, I think this is very niche and not many will ever use it, but it could be a nice-to-have to offer.

@tmilnthorp
Copy link
Collaborator

The goals here were multifaceted: allow for higher precision conversion, allow conversion functions to be specified between different quantities, and to allow users to implement their own IQuantity and create their own conversions.

I would not plan on inserting all of the permutations, just to/from base units. I need to add some code that checks if a conversion is explicit. If it doesn't exist, we can see if one exists from current units, to base units, to the target units.

@R2D221
Copy link
Author

R2D221 commented Jan 28, 2019

I don't think defining custom conversion functions is the way to go. It seems like a very easy way to get things wrong.

My proposal was more about avoiding having exponentially many conversion pairs while still yielding an exact result.

I'm thinking of another possibility... This all originates from conversions between SI and Imperial units. But, usually, Imperial units have whole numbers in their conversions (like, 1 yard = 3 feet, 1 foot = 12 inches, and so on). So, maybe, we could define to conversion "lines": a metric one and an imperial one, and a way to transition between the two of them. That way, when going from barrels to gallons, you can have an exact 42 multiplier, but when going to liters you can have the little approximation that comes with the division.

@tmilnthorp
Copy link
Collaborator

I believe custom conversions are the only way to do it exactly. Specifying just a numerator/denominator is not enough. Take temperatures for example. It's a numerator/denominator plus an addition. Explicit SI/Imperial factors are also not always valid as some quantities are unitless!

Today if you do this, you get 31.999999999999989

var temp = Temperature.FromDegreesCelsius( 0 );
var converted = temp.As( TemperatureUnit.DegreeFahrenheit );

However you can specify an explicit converter for C to F like so:

ConversionFunction conversionFunction = ( from ) =>
{
    var value = ((Temperature)from).Value * 1.8 + 32;
    return Temperature.FromDegreesFahrenheit(value);
};

var unitConverter = new UnitConverter();
unitConverter.SetConversionFunction<Temperature>( TemperatureUnit.DegreeCelsius, TemperatureUnit.DegreeFahrenheit, conversionFunction );

Then this will give you exactly 32:

var foundConversionFunction = unitConverter.GetConversionFunction<Temperature>( TemperatureUnit.DegreeCelsius, TemperatureUnit.DegreeFahrenheit );
var convertedByConverter = foundConversionFunction( temp );

Not that I would actually like to route the As/ToUnit methods through the unit converter so that explicitly defined units still work, so it wouldn't look any different.

@angularsen
Copy link
Owner

angularsen commented Jan 28, 2019

Then this will give you exactly 32.

In this example it will be exactly 32, but, please keep in mind that floating precision types are never reliably exact to begin with when performing arithmetic:

(12.345 - 12).ToString("F18") // 0.345000000000001000

I still need to mature the idea in my head a bit, but I think maybe there can be some advantage from both ideas here.

  1. Rational to improve precision for all conversions (unit-base-unit)
  2. Direct conversions to further improve(?) precision of certain, highly-used unit-to-unit conversions
  3. Direct conversions to dynamically convert between units of different quantities

I agree with @R2D221 that there is added risk of getting direct conversions wrong so we need to test these conversions, which means quite a bit more manual work in adding and testing both new and old units. Also, the value of the slightly more precise conversions must be worth it. I personally am not very invested in increasing the precision, but if we can do it without adding a lot more effort, I'm all for it!

I would not plan on inserting all of the permutations, just to/from base units.

Does this improve the precision compared to what we already have with hard coded from/to conversion functions?

It does sound elegant to simply do everything through one system though, which I think is what you are aiming for here.

Performance
If I am reading it right we are looking at 2 method calls instead of 1 in today's conversion properties, sounds reasonable to me.

Memory
With 900 units I think we're looking at 900*2 conversion functions to cover all to/from base conversions. I think each take up at least 32 bytes, which would mean 57 kB of method allocations. I may be way off, but that number at least is insignificant.

Converting between units of different quantities sounds interesting for #576. It would be nice if you passed in two units for different quantities, that were compatible through a direct conversion function, then it would all "just work". But, how should we communicate to consumers what units they can expect to convert between across quantities? Xmldoc on the unit enum values? Wiki-page? It's not very visible/discoverable and I also suspect this feature will not be used by many.

Look forward to what you both think!

@tmilnthorp
Copy link
Collaborator

Indeed! However it is more precise than the cumulative floating point error of unit -> base -> target unit!

I think registering custom conversions would generally be done more frequently for external users of the library. For example, one that comes to mind is converting from 1 cubic centimeter to its weight in water (1g... on earth). As the library stands today, we don't support this, so an external user could then add it for convenience. If we found it was being used a lot, we could add a custom converter for this, and we'd probably add a ToWeightInWater() method on the Volume quantity directly. However keeping the converter is still handy for doing generic calculations where possible.

Does this improve the precision compared to what we already have with hard coded from/to conversion functions?

No, for most cases it would not. We would only add a custom conversion where necessary, an override of sorts.

Performance wise, I think most of the hit is due to creating a Quantity rather than just a double. That said, I think that's important as you need to be able to see what you were converted to if it's a generic mechanism.

@angularsen
Copy link
Owner

Gotcha. What do you think about bullet point 1, is it still worth pursuing?

  1. Rational to improve precision for all conversions (unit-base-unit)

I believe it improves the precision, but it does come at the expense of a lot heavier quantities - roughly twice the size with numerator/denominator instead of value - if we use doubles instead of decimal as suggested.

I don't know yet what I prefer. As I've eluded to before, precision has not been a concern for my uses of the library and this is really the first time anyone ever asks about it for the 5 years this library has existed. I think I need some hard numbers on how much better precision we would get in some concrete examples and see that in light of how many more bytes we need to allocate per quantity, in order to be able to consider it further.

@R2D221 could you draft up a comparison or some kind of benchmark that illustrates the gain in precision vs bytes allocated?

As for direct conversions, I think that sounds like a good idea regardless:

  1. A single piece of code to handle all conversions, generically
  2. Route existing unit-base and base-unit conversions through it
  3. Ability to add higher-precision direct conversions
  4. Ability to add cross-quantity conversions
  5. Ability to plug in 3rd party conversions of 3rd party unit enums
  6. Compatible with the new dynamic Quantity in Improve working dynamically with units and quantities #576 for converting dynamically

Let's move the conversion system discussion to #588 .

@angularsen
Copy link
Owner

@R2D221 Are you still interested in pursuing the rational value proposal? As mentioned above, we need some benchmarks or some kind of metric to compare before/after in terms of precision vs memory use and performance.

@stale
Copy link

stale bot commented Sep 24, 2019

This issue has been automatically marked as stale because it has not had recent activity. It will be closed if no further activity occurs. Thank you for your contributions.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

No branches or pull requests

3 participants