Skip to content

Commit

Permalink
Unit unification (#27)
Browse files Browse the repository at this point in the history
  • Loading branch information
atmoos authored Sep 23, 2023
2 parents 8f1ed5e + 781b919 commit a793cc6
Show file tree
Hide file tree
Showing 39 changed files with 175 additions and 120 deletions.
24 changes: 14 additions & 10 deletions quantities.benchmark/PolynomialBenchmark.cs
Original file line number Diff line number Diff line change
Expand Up @@ -11,27 +11,31 @@ public class PolynomialBenchmark
private const Double offset = Math.Tau + Math.E;
private const Double argument = 0.1321;
private static readonly Polynomial polynomial = Poly(nominator: scale, denominator: Math.PI, offset: offset);
private static readonly Polynomial polynomialWithoutOffset = Poly(nominator: scale, denominator: Math.PI);

[Benchmark(Baseline = true)]
public Double EvaluateTrivial() => Trivial(argument);
[Benchmark]
public Double EvaluatePolynomial() => polynomial * argument;
[Benchmark]
public Double EvaluatePolynomialWithoutOffset() => polynomialWithoutOffset * argument;

[MethodImpl(MethodImplOptions.NoInlining | MethodImplOptions.NoOptimization)]
private static Double Trivial(Double value) => scale * value / Math.PI + offset;
}

/*
// * Summary *
BenchmarkDotNet=v0.13.5, OS=arch
BenchmarkDotNet v0.13.8, Arch Linux
Intel Core i7-8565U CPU 1.80GHz (Whiskey Lake), 1 CPU, 8 logical and 4 physical cores
.NET SDK=7.0.110
[Host] : .NET 7.0.10 (7.0.1023.41001), X64 RyuJIT AVX2
DefaultJob : .NET 7.0.10 (7.0.1023.41001), X64 RyuJIT AVX2
| Method | Mean | Error | StdDev | Ratio | RatioSD |
|------------------- |----------:|----------:|----------:|------:|--------:|
| EvaluateTrivial | 1.1944 ns | 0.0361 ns | 0.0320 ns | 1.00 | 0.00 |
| EvaluatePolynomial | 0.3643 ns | 0.0216 ns | 0.0180 ns | 0.31 | 0.02 |
.NET SDK 7.0.111
[Host] : .NET 7.0.11 (7.0.1123.46301), X64 RyuJIT AVX2
DefaultJob : .NET 7.0.11 (7.0.1123.46301), X64 RyuJIT AVX2
| Method | Mean | Error | StdDev | Ratio | RatioSD |
|-------------------------------- |----------:|----------:|----------:|------:|--------:|
| EvaluateTrivial | 0.9766 ns | 0.0133 ns | 0.0117 ns | 1.00 | 0.00 |
| EvaluatePolynomial | 0.4931 ns | 0.0413 ns | 0.0442 ns | 0.52 | 0.04 |
| EvaluatePolynomialWithoutOffset | 0.4047 ns | 0.0076 ns | 0.0067 ns | 0.41 | 0.01 |
*/
2 changes: 1 addition & 1 deletion quantities.test/AreaTest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ public void SquareMetresToSquareKilometres()
public void SquareMilesToSquareKilometres()
{
Area squareMiles = Area.Of(2).Square.Imperial<Mile>();
Area expected = Area.Of(2 * squareMileInSquareKilometres).Square.Si<Kilo, Metre>();
Area expected = Area.Of(5.179976220672).Square.Si<Kilo, Metre>();

Area actual = squareMiles.To.Square.Si<Kilo, Metre>();

Expand Down
19 changes: 17 additions & 2 deletions quantities.test/Convenience.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@
using Quantities.Numerics;
using Xunit.Sdk;

using static Quantities.Extensions;

namespace Quantities.Test;
public static class Convenience
{
Expand All @@ -16,7 +18,7 @@ public static class Convenience
public static String Join(String leftUnit, String rightUnit) => $"{leftUnit}\u200C{rightUnit}";
internal static void IsSameAs(this Quantity actual, Quantity expected, Int32 precision = fullPrecision)
{
PrecisionIsBounded(expected, actual, precision);
ReformatEqualMessage((e, a, p) => PrecisionIsBounded(e, a, p), expected, actual, precision);
Assert.True(actual.HasSameMeasure(in expected), $"Measure mismatch: {actual} != {expected}");
}
public static void Matches<TQuantity>(this TQuantity actual, TQuantity expected)
Expand All @@ -27,7 +29,7 @@ public static void Matches<TQuantity>(this TQuantity actual, TQuantity expected)
public static void Matches<TQuantity>(this TQuantity actual, TQuantity expected, Int32 precision)
where TQuantity : struct, IQuantity<TQuantity>, Dimensions.IDimension
{
Equals(actual, expected, precision);
ReformatEqualMessage((e, a, p) => a.Equals(e, p), expected, actual, precision);
Assert.True(actual.Value.HasSameMeasure(expected.Value), $"Measure mismatch: {actual} != {expected}");
}
public static void Equals<T>(this T actual, T expected, Int32 precision)
Expand Down Expand Up @@ -71,4 +73,17 @@ internal static Polynomial Poly(in Double nominator = 1, in Double denominator =
var value = new Transformation();
return Polynomial.Of(nominator * value / denominator + offset);
}

private static void ReformatEqualMessage<T>(Action<T, T, Int32> assertion, T expected, T actual, Int32 precision)
where T : IFormattable
{
try {
assertion(expected, actual, precision);
}
catch (EqualException) {
var actualValue = actual.ToString(RoundTripFormat);
var expectedValue = expected.ToString(RoundTripFormat);
throw new EqualException(expectedValue, actualValue, precision, precision + 1);
}
}
}
2 changes: 1 addition & 1 deletion quantities.test/VelocityTest.cs
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
using Quantities.units.NonStandard.Length;
using Quantities.units.NonStandard.Velocity;
using Quantities.Units.NonStandard.Velocity;
using Quantities.Units.Si.Metric;

namespace Quantities.Test;
Expand Down
1 change: 0 additions & 1 deletion quantities.test/prefixes/TestPrefix.cs
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,5 @@ public sealed class TestPrefix<TPrefix> : ITestPrefix
public TestPrefix(Double factor = Double.NaN) => Factor = Double.IsNaN(factor) ? prefix * 1d : factor;
public Double ToSi(Double value) => prefix * value;
public Double FromSi(Double value) => prefix / value;

public override String ToString() => TPrefix.Representation;
}
12 changes: 10 additions & 2 deletions quantities/Extensions.cs
Original file line number Diff line number Diff line change
@@ -1,14 +1,22 @@
using System.Globalization;
using Quantities.Measures;
using Quantities.Numerics;
using Quantities.Prefixes;
using Quantities.Units;
using Quantities.Units.Si;

namespace Quantities;

public static class Extensions
{
internal static String RoundTripFormat = "G17"; // https://learn.microsoft.com/en-us/dotnet/standard/base-types/standard-numeric-format-strings#RFormatString
internal static Double ValueOf<T>(Int32 exponent = 1) where T : ITransform => Math.Pow(Polynomial.Of<T>() * 1d, exponent);
internal static Transformation RootedIn<TSi>(this Transformation self) where TSi : ISiUnit => self;
internal static Transformation From<TBasis>(this Transformation self) where TBasis : IPrefix => TBasis.ToSi(self);
internal static Transformation DerivedFrom<TBasis>(this Transformation self) where TBasis : IUnit, ITransform => TBasis.ToSi(self);
public static String ToString(this IFormattable formattable, String format) => formattable.ToString(format, CultureInfo.InvariantCulture);
internal static Quantity To<TMeasure>(this in Double value)
where TMeasure : IMeasure
=> Quantity.Of<TMeasure>(in value);
where TMeasure : IMeasure => Quantity.Of<TMeasure>(in value);
public static void Serialize<TQuantity>(this IQuantity<TQuantity> quantity, IWriter writer)
where TQuantity : struct, IQuantity<TQuantity>, Dimensions.IDimension
{
Expand Down
13 changes: 13 additions & 0 deletions quantities/ISystems.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
using Quantities.Units.Imperial;
using Quantities.Units.NonStandard;
using Quantities.Units.Si;

namespace Quantities;

public interface ISystems<in TConstraint, out TResult>
{
public TResult Si<TInjectedUnit>() where TInjectedUnit : ISiUnit, TConstraint;
public TResult Metric<TInjectedUnit>() where TInjectedUnit : IMetricUnit, TConstraint;
public TResult Imperial<TInjectedUnit>() where TInjectedUnit : IImperialUnit, TConstraint;
public TResult NonStandard<TInjectedUnit>() where TInjectedUnit : INoSystemUnit, TConstraint;
}
16 changes: 16 additions & 0 deletions quantities/numerics/Algorithms.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
using System.Numerics;

namespace Quantities.Numerics;

internal static class Algorithms
{
// Implements: https://en.wikipedia.org/wiki/Euclidean_algorithm
public static T Gcd<T>(T a, T b)
where T : INumberBase<T>, IModulusOperators<T, T, T>
{
var big = T.MaxMagnitude(a, b);
var small = T.MinMagnitude(a, b);
return Impl(T.Abs(big), T.Abs(small));
static T Impl(T max, T min) => T.IsZero(min) ? max : Impl(min, max % min);
}
}
33 changes: 17 additions & 16 deletions quantities/numerics/Polynomial.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using System.Numerics;
using static Quantities.Numerics.Algorithms;

namespace Quantities.Numerics;

Expand All @@ -14,13 +15,21 @@ namespace Quantities.Numerics;
public Polynomial() => (this.nominator, this.denominator, this.offset) = (1, 1, 0);
private Polynomial(in Double nominator, in Double denominator, in Double offset)
{
this.offset = offset;
(this.nominator, this.denominator) = denominator >= 0 ? (nominator, denominator) : (-nominator, -denominator);
(this.nominator, this.denominator, this.offset) = (nominator, denominator, offset);
}
public static Polynomial Of(Transformation transformation)
{
var (nominator, denominator, offset) = transformation;
return new(in nominator, in denominator, in offset);
var (n, d, offset) = transformation;
(n, d) = Simplify(n, d);
return d >= 0 ? new(n, d, offset) : new(-n, -d, offset);
static (Double n, Double d) Simplify(Double n, Double d)
{
if (Double.IsInteger(n) && Double.IsInteger(d) && Double.Abs(Double.MaxMagnitude(n, d)) < Int64.MaxValue) {
Int64 gcd = Gcd((Int64)n, (Int64)d);
return gcd <= 1 ? (n, d) : (n / gcd, d / gcd);
}
return (n, d);
}
}
public static Polynomial Of<TTransform>()
where TTransform : ITransform => Cache<TTransform>.Polynomial;
Expand All @@ -32,24 +41,16 @@ public static Double Convert<TFrom, TTo>(in Double value)
where TFrom : ITransform where TTo : ITransform => Converter<TFrom, TTo>.Polynomial * value;

public static Double operator *(Polynomial left, Double right)
{
return Double.FusedMultiplyAdd(left.nominator, right, left.denominator * left.offset) / left.denominator;
}
=> Double.FusedMultiplyAdd(left.nominator, right, left.denominator * left.offset) / left.denominator;

public static Polynomial operator *(Polynomial left, Polynomial right)
{
return new(left.nominator * right.nominator, left.denominator * right.denominator, left * right.offset);
}
=> new(left.nominator * right.nominator, left.denominator * right.denominator, left * right.offset);

public static Polynomial operator /(Polynomial left, Polynomial right)
{
return new(left.nominator * right.denominator, left.denominator * right.nominator, right / left.offset);
}
=> new(left.nominator * right.denominator, left.denominator * right.nominator, right / left.offset);

public static Double operator /(Polynomial left, Double right)
{
return left.denominator * (right - left.offset) / left.nominator;
}
=> left.denominator * (right - left.offset) / left.nominator;

public override String ToString()
{
Expand Down
37 changes: 21 additions & 16 deletions quantities/prefixes/BinaryPrefixes.cs
Original file line number Diff line number Diff line change
Expand Up @@ -13,59 +13,64 @@ zebi Zi 1180591620717411303424 2^70
yobi Yi 1208925819614629174706176 2^80
*/

file static class Binary
{
public const Int32 Base = 1024;
}

[DebuggerDisplay(nameof(Kibi))]
public readonly struct Kibi : IBinaryPrefix, IScaleUp
{
internal const Double Factor = 1024;
static Transformation ITransform.ToSi(Transformation self) => Factor * self;
internal const Double Factor = Binary.Base;
static Transformation ITransform.ToSi(Transformation self) => Binary.Base * self;
public static String Representation => "Ki";
}
[DebuggerDisplay(nameof(Mebi))]
public readonly struct Mebi : IBinaryPrefix, IScaleUp
{
internal const Double Factor = 1048576;
static Transformation ITransform.ToSi(Transformation self) => Factor * self;
internal const Double Factor = Kibi.Factor * Kibi.Factor;
static Transformation ITransform.ToSi(Transformation self) => Binary.Base * self.From<Kibi>();
public static String Representation => "Mi";
}
[DebuggerDisplay(nameof(Gibi))]
public readonly struct Gibi : IBinaryPrefix, IScaleUp
{
internal const Double Factor = 1073741824;
static Transformation ITransform.ToSi(Transformation self) => Factor * self;
internal const Double Factor = Kibi.Factor * Mebi.Factor;
static Transformation ITransform.ToSi(Transformation self) => Binary.Base * self.From<Mebi>();
public static String Representation => "Gi";
}
[DebuggerDisplay(nameof(Tebi))]
public readonly struct Tebi : IBinaryPrefix, IScaleUp
{
internal const Double Factor = 1099511627776;
static Transformation ITransform.ToSi(Transformation self) => Factor * self;
internal const Double Factor = Mebi.Factor * Mebi.Factor;
static Transformation ITransform.ToSi(Transformation self) => Binary.Base * self.From<Gibi>();
public static String Representation => "Ti";
}
[DebuggerDisplay(nameof(Pebi))]
public readonly struct Pebi : IBinaryPrefix, IScaleUp
{
internal const Double Factor = 1125899906842624;
static Transformation ITransform.ToSi(Transformation self) => Factor * self;
internal const Double Factor = Kibi.Factor * Tebi.Factor;
static Transformation ITransform.ToSi(Transformation self) => Binary.Base * self.From<Tebi>();
public static String Representation => "Pi";
}
[DebuggerDisplay(nameof(Exbi))]
public readonly struct Exbi : IBinaryPrefix, IScaleUp
{
internal const Double Factor = 1152921504606846976;
static Transformation ITransform.ToSi(Transformation self) => Factor * self;
internal const Double Factor = Mebi.Factor * Tebi.Factor;
static Transformation ITransform.ToSi(Transformation self) => Binary.Base * self.From<Pebi>();
public static String Representation => "Ei";
}
[DebuggerDisplay(nameof(Zebi))]
public readonly struct Zebi : IBinaryPrefix, IScaleUp
{
internal const Double Factor = Gibi.Factor * Tebi.Factor; // 1180591620717411303424
static Transformation ITransform.ToSi(Transformation self) => Factor * self;
internal const Double Factor = Gibi.Factor * Tebi.Factor;
static Transformation ITransform.ToSi(Transformation self) => Binary.Base * self.From<Exbi>();
public static String Representation => "Zi";
}
[DebuggerDisplay(nameof(Yobi))]
public readonly struct Yobi : IBinaryPrefix, IScaleUp
{
internal const Double Factor = Tebi.Factor * Tebi.Factor; // 1208925819614629174706176
static Transformation ITransform.ToSi(Transformation self) => Factor * self;
internal const Double Factor = Tebi.Factor * Tebi.Factor;
static Transformation ITransform.ToSi(Transformation self) => Binary.Base * self.From<Zebi>();
public static String Representation => "Yi";
}
2 changes: 1 addition & 1 deletion quantities/units/Imperial/Length/Chain.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,6 @@ namespace Quantities.Units.Imperial.Length;

public readonly struct Chain : IImperialUnit, ILength
{
public static Transformation ToSi(Transformation self) => 20.1168 * self;
public static Transformation ToSi(Transformation self) => 22 * self.DerivedFrom<Foot>();
public static String Representation => "ch";
}
4 changes: 2 additions & 2 deletions quantities/units/Imperial/Length/Foot.cs
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
using Quantities.Dimensions;
using Quantities.Units.Si;

namespace Quantities.Units.Imperial.Length;

public readonly struct Foot : IImperialUnit, ILength
{
public const Double ToMetre = 0.3048;
public static Transformation ToSi(Transformation self) => ToMetre * self;
public static Transformation ToSi(Transformation self) => 3048 * self.RootedIn<Metre>() / 1e4;
public static String Representation => "ft";
}
2 changes: 1 addition & 1 deletion quantities/units/Imperial/Length/Furlong.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,6 @@ namespace Quantities.Units.Imperial.Length;

public readonly struct Furlong : IImperialUnit, ILength
{
public static Transformation ToSi(Transformation self) => 201.168 * self;
public static Transformation ToSi(Transformation self) => self.DerivedFrom<Mile>() / 8;
public static String Representation => "fur";
}
4 changes: 2 additions & 2 deletions quantities/units/Imperial/Length/Inch.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ namespace Quantities.Units.Imperial.Length;

public readonly struct Inch : IImperialUnit, ILength
{
public const Double ToMetre = 0.0254;
public static Transformation ToSi(Transformation self) => ToMetre * self;
public const Double ToMetre = 0.0254; // ToDo: Remove!
public static Transformation ToSi(Transformation self) => self.DerivedFrom<Foot>() / 12;
public static String Representation => "in";
}
2 changes: 1 addition & 1 deletion quantities/units/Imperial/Length/Mile.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,6 @@ namespace Quantities.Units.Imperial.Length;

public readonly struct Mile : IImperialUnit, ILength
{
public static Transformation ToSi(Transformation self) => 1609.344 * self;
public static Transformation ToSi(Transformation self) => 5280 * self.DerivedFrom<Foot>();
public static String Representation => "mi";
}
2 changes: 1 addition & 1 deletion quantities/units/Imperial/Length/Rod.cs
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,6 @@ namespace Quantities.Units.Imperial.Length;
// https://en.wikipedia.org/wiki/Imperial_units
public readonly struct Rod : IImperialUnit, ILength
{
public static Transformation ToSi(Transformation self) => 5.0292 * self;
public static Transformation ToSi(Transformation self) => 66 * self.DerivedFrom<Foot>() / 4;
public static String Representation => "rod";
}
2 changes: 1 addition & 1 deletion quantities/units/Imperial/Length/Yard.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,6 @@ namespace Quantities.Units.Imperial.Length;

public readonly struct Yard : IImperialUnit, ILength
{
public static Transformation ToSi(Transformation self) => 0.9144 * self;
public static Transformation ToSi(Transformation self) => 3 * self.DerivedFrom<Foot>();
public static String Representation => "yd";
}
5 changes: 3 additions & 2 deletions quantities/units/Imperial/Temperature/Farenheit.cs
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
using Quantities.Dimensions;
using Quantities.Units.Si;

namespace Quantities.Units.Imperial.Temperature;

// [K] ≡ ([°F] + 459.67) × ​5⁄9
// See: https://en.wikipedia.org/wiki/Conversion_of_units#Temperature
// See: https://en.wikipedia.org/wiki/Conversion_of_scales_of_temperature
public readonly struct Fahrenheit : IImperialUnit, ITemperature
{
public static Transformation ToSi(Transformation self) => 5 * (self + 459.67) / 9;
public static Transformation ToSi(Transformation self) => 5 * (self.RootedIn<Kelvin>() + 459.67) / 9;
public static String Representation => "°F";
}
2 changes: 1 addition & 1 deletion quantities/units/Imperial/Temperature/GasMark.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
namespace Quantities.Units.Imperial.Temperature;

// [K] ≡ [GM] × ​125⁄9 + 394.261
// See: https://en.wikipedia.org/wiki/Conversion_of_units#Temperature
// See: https://en.wikipedia.org/wiki/Gas_mark
public readonly struct GasMark : IImperialUnit, ITemperature
{
public static Transformation ToSi(Transformation self) => self.FusedMultiplyAdd(125, 5d * 218d + 9 * 273.15d) / 9;
Expand Down
Loading

0 comments on commit a793cc6

Please sign in to comment.