You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
Suggestion: define conversions as rational values #478
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:
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:
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.
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 :-)
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),
};
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.
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.
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:
@tmilnthorp
Continuing my comments from the PR here:
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.
Since it doesn't scale well, I'm not sure this replaces rational value proposal discussed here.
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.
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.
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.
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!
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.
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:
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.
Rational to improve precision for all conversions (unit-base-unit)
Direct conversions to further improve(?) precision of certain, highly-used unit-to-unit conversions
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.
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.
Gotcha. What do you think about bullet point 1, is it still worth pursuing?
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:
A single piece of code to handle all conversions, generically
Route existing unit-base and base-unit conversions through it
Ability to add higher-precision direct conversions
Ability to add cross-quantity conversions
Ability to plug in 3rd party conversions of 3rd party unit enums
@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.
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.
Activity
angularsen commentedon Sep 17, 2018
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 commentedon Sep 20, 2018
This is pretty cool.
My immediate thoughts on how this can be implemented:
Today:
double Value(a handful of quantities usedecimal): 64 or 128 bitsUnitEnum Unit: 32 bits (we could probably use uint8 here if we want)Proposed:
long Numerator64 bitslong Denominator64 bitsUnitEnum Unit32 bitsBreakdown
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.
±1e19 to ±9e18Size-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 commentedon Sep 27, 2018
Hi! Let me check on the weekend so I can elaborate on the idea. :D
R2D221 commentedon 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
decimalis a better fit because of the added precision.So, the proposed values would be:
Proposed:
decimal Value: 128 bitsUnitEnum Unit: 32 bitsBut then, the numerator and denominator will come into play in the conversions.
First of all, for simplicity, I'm defining a
Rationalstruct:The reason these are
decimaland notlongis 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)
And then the conversion would be applied as such:
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 commentedon Oct 8, 2018
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
structtoclass, which would allow the consumer to choose his own numeric type, something along the lines ofLength<float>or using namespacesUnitsNet.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 commentedon Oct 8, 2018
Great! Yes, using
doubleas values would work as well.angularsen commentedon Oct 11, 2018
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=1if the number is irrational. Just make sure to base the PR onv4branch.tmilnthorp commentedon Jan 25, 2019
I think I may have a better idea here. Working on a PR...
tmilnthorp commentedon Jan 25, 2019
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:
In this case, the answer is exactly 42.
angularsen commentedon Jan 26, 2019
@tmilnthorp
Continuing my comments from the PR here:
2*10conversion functions (to base + from base) you have to define9*10functions.tmilnthorp commentedon Jan 28, 2019
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 commentedon 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 commentedon Jan 28, 2019
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
However you can specify an explicit converter for C to F like so:
Then this will give you exactly 32:
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 commentedon Jan 28, 2019
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.345000000000001000I still need to mature the idea in my head a bit, but I think maybe there can be some advantage from both ideas here.
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!
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 commentedon Jan 28, 2019
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.
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 commentedon Jan 30, 2019
Gotcha. What do you think about bullet point 1, is it still worth pursuing?
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:
Quantityin Improve working dynamically with units and quantities #576 for converting dynamicallyLet's move the conversion system discussion to #588 .
angularsen commentedon Feb 15, 2019
@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 commentedon 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.