using System; namespace Amphora.UnitTests { public class PortableMathTests : UnitTestSharp.TestFixture { public static readonly double[] LogInputs = { 0.5, 1.5, 2.0, 3.0, 10.0, 1e-05, 123456.789, 1e+300, 5e-324, 2.2250738585072014e-308, 0.7071067811865476, 1.4142135623730951, 1.000000001, 0.999999, 1.7976931348623157e+308, 4.9e-310, 1.3e+308, }; public static readonly double[] LogExpected = { -0.6931471805599453, 0.4054651081081644, 0.6931471805599453, 1.0986122886681098, 2.302585092994046, -11.512925464970229, 11.723646487185881, 690.7755278982137, -744.4400719213812, -708.3964185322641, -0.3465735902799726, 0.3465735902799727, 1.000000082240371e-09, -1.000000500029089e-06, 709.782712893384, -712.2121436230376, 709.4585729066335, }; public static readonly double[] ExpInputs = { 1.0, -1.0, 10.0, -10.0, 0.5, 700.0, -700.0, 709.7, -745.0, 1e-09, 20.25, -20.25, 100.5, 0.3466, -0.3466, -740.0, 709.78, }; public static readonly double[] ExpExpected = { 2.718281828459045, 0.36787944117144233, 22026.465794806718, 4.5399929762484854e-05, 1.6487212707001282, 1.0142320547350045e+304, 9.85967654375977e-305, 1.6549840276802644e+308, 5e-324, 1.000000001, 622964442.1984454, 1.6052280551856116e-09, 4.4319559098458955e+43, 1.4142509118505286, 0.7070881069410189, 4.2e-322, 1.7928227943945155e+308, }; public static readonly double[] PowBases = { 0.3, 7.0, 1e-05, 3.0, 1.0000001, 0.9, 123.456, 2.0, 0.001, }; public static readonly double[] PowExponents = { 1.7, -2.5, 0.25, 40.5, 10000000.0, 100.25, 0.001, 0.3, 3.3, }; public static readonly double[] PowExpected = { 0.12915348607498026, 0.007713560673657699, 0.05623413251903491, 2.1057694276511797e+19, 2.7182816941320818, 2.5870902026806975e-05, 1.0048274998286035, 1.2311444133449163, 1.2589254117941687e-10, }; public static readonly double[] Expm1Inputs = { 1e-17, 1e-10, -1e-10, 1e-05, 0.1, -0.1, 0.3, -0.3, 0.3466, -0.3466, 0.376, 0.5, -0.5, 1.0, -1.0, 10.0, -10.0, 37.9, -37.9, 38.0, 100.0, 709.0, 1e-300, 5e-324, -37.0, }; public static readonly double[] Expm1Expected = { 1e-17, 1.00000000005e-10, -9.999999999500001e-11, 1.0000050000166668e-05, 0.10517091807564763, -0.09516258196404043, 0.3498588075760031, -0.2591817793182821, 0.41425091185052865, -0.2929118930589812, 0.45644713377108603, 0.6487212707001282, -0.3934693402873666, 1.7182818284590453, -0.6321205588285577, 22025.465794806718, -0.9999546000702375, 2.88244390402365e+16, -1.0, 3.1855931757113756e+16, 2.6881171418161356e+43, 8.218407461554972e+307, 1e-300, 5e-324, -0.9999999999999999, }; public static readonly double[] Log1pInputs = { 1e-17, 1e-10, -1e-10, 1e-05, 0.1, -0.1, 0.4, 0.41, 0.42, -0.29, -0.3, -0.5, -0.9, -0.999999, 1.0, 10.0, 10000000000.0, 1e+300, 5e-324, 2.220446049250313e-16, 0.4099999999, 0.4100000001, -0.2899999999, -0.2900000001, }; public static readonly double[] Log1pExpected = { 1e-17, 9.999999999500001e-11, -1.00000000005e-10, 9.999950000333332e-06, 0.09531017980432487, -0.10536051565782631, 0.33647223662121295, 0.3435897043900769, 0.35065687161316933, -0.3424903089467759, -0.35667494393873234, -0.6931471805599453, -2.302585092994046, -13.815510557935518, 0.6931471805599453, 2.3978952727983707, 23.025850930040455, 690.7755278982137, 5e-324, 2.2204460492503128e-16, 0.3435897043191549, 0.3435897044609989, -0.3424903088059309, -0.342490309087621, }; public static readonly double[] SinCosTurns = { 0.0, 1e-200, 1e-30, 1e-09, 0.00048828125, 0.00048828125000000011, 0.00048828124999999995, 0.0625, 0.1, 0.125, 0.12500000000000003, 0.12499999999999999, 0.2, 0.25, 0.25000000000000006, 0.33333333333333331, 0.49999999999999989, 0.50000000000000011, 0.75, 0.99999999999999989, 1.0000000000000002, -0.1, -0.3, 7.3, 1000000.123, 123456789.98765431, 2147483646.5, 2147483648.25, 1000000000000.5, 1000000000000000.3, 4503599627370495.5, 4503599627370496.0, }; public static readonly double[] SinOfTurns = { 0.0, 6.2831853071795862e-200, 6.2831853071795868e-30, 6.2831853071795872e-09, 0.0030679567629659761, 0.003067956762965977, 0.0030679567629659761, 0.38268343236508978, 0.58778525229247314, 0.70710678118654757, 0.70710678118654768, 0.70710678118654746, 0.95105651629515364, 1.0, 1.0, 0.86602540378443871, 6.9757369960172635e-16, -6.9757369960172635e-16, -1.0, -6.9757369960172635e-16, 1.3951473992034527e-15, -0.58778525229247314, -0.95105651629515364, 0.95105651629515386, 0.69816541908982943, -0.077492467920743852, 0.0, 1.0, 0.0, 1.0, 0.0, 0.0, }; public static readonly double[] CosOfTurns = { 1.0, 1.0, 1.0, 1.0, 0.99999529380957619, 0.99999529380957619, 0.99999529380957619, 0.92387953251128674, 0.80901699437494745, 0.70710678118654757, 0.70710678118654735, 0.70710678118654757, 0.30901699437494734, 0.0, -3.4878684980086318e-16, -0.49999999999999989, -1.0, -1.0, 0.0, 1.0, 1.0, 0.80901699437494745, -0.30901699437494734, -0.30901699437494634, 0.71593648292786616, 0.9969929374953227, -1.0, 0.0, -1.0, 0.0, -1.0, 1.0, }; public static readonly double[] HardSinCosTurns = { 0.060371540937450341, -0.0032893622171726034, 0.1196965866749333, 0.027849406247426156, -0.017364044956709651, -0.07745972957515973, }; public static readonly double[] HardSinOfTurns = { 0.37029407416354931, -0.020666201009081175, 0.68315615546668329, 0.1740913776679377, -0.10888519916418443, -0.46770618312292905, }; public static readonly double[] HardCosOfTurns = { 0.92891458091654466, 0.99978643126212319, 0.73027232403260423, 0.98472950205712817, 0.9940543312128246, 0.88388400045966509, }; public static readonly double[] DeepSinCosTurns = { 0.01573667903559145, 0.046806654429983718, -0.019004306465221915, -0.043762078557798956, -0.057306436281703954, -0.01341632192702219, }; public static readonly double[] DeepSinOfTurns = { 0.098715437339158343, 0.28987371364070064, -0.11912402578551996, -0.27151349155988819, -0.35233689868791296, -0.084197435907713114, }; public static readonly double[] DeepCosOfTurns = { 0.99511570303705821, 0.95706490382844422, 0.99287938163739242, 0.962434633573085, 0.93587323384258791, 0.99644909141740223, }; public static readonly double[] DeepestSinCosTurns = { -0.035107824791691805, 0.10064772733587615, 0.039632965020027472, -0.1152947132302842, 0.059375927516176541, -0.02706567846181554, }; public static readonly double[] DeepestSinOfTurns = { -0.21880435844290033, 0.59107290539370061, 0.2464555309502571, -0.66269973978372865, 0.36447592676639551, -0.16924017622744736, }; public static readonly double[] DeepestCosOfTurns = { 0.97576874961559967, 0.80661813797449988, 0.9691541008859359, 0.74888520808637848, 0.93121281069784312, 0.98557483873651242, }; public static readonly double[] SubnormalSineTurns = { 3.54131503325977e-309, 3.541315033259774e-309, 3.54131503325978e-309, 1.545113682833714e-309, 2.32065760764941e-309, 1.522659206695785e-309, 2.04078049256145e-309, 3.092751508937984e-309, 1.951770428477287e-309, 2.84225637364045e-309, 1.84242613094718e-309, 3.35887791439601e-309, 3.085605750636267e-309, 3.53142043482896e-309, 2.52407648653818e-309, }; public static readonly double[] SubnormalSines = { 2.2250738585071974e-308, 2.2250738585072004e-308, 2.2250738585072034e-308, 9.70823558990293e-309, 1.458112178337729e-308, 9.56714995535268e-309, 1.282260200604083e-308, 1.943233083971664e-308, 1.2263335279196096e-308, 1.7858423486095196e-308, 1.1576304795531044e-308, 2.110445236034301e-308, 1.938743271614663e-308, 2.218856898959106e-308, 1.5859240294414154e-308, }; public static readonly double[] MediumSinCosTurns = { 268435455.5, 268435455.99999991, 268435456.0, 268435456.00000012, 268435456.5, 268435456.75, -268435455.5, -268435456.0, -268435456.5, -268435456.75, 536870912.375, 1073741824.625, }; public static readonly double[] MediumSinOfTurns = { 0.0, -5.6176054243855976e-07, 0.0, 7.4901405658471574e-07, 0.0, -1.0, 0.0, 0.0, 0.0, 1.0, 0.70710678118654757, -0.70710678118654757, }; public static readonly double[] MediumCosOfTurns = { -1.0, 0.99999999999984224, 1.0, 0.99999999999971945, -1.0, 0.0, -1.0, 1.0, -1.0, 0.0, -0.70710678118654757, -0.70710678118654757, }; public static readonly double[] LargeSinCosTurns = { 4398046511103.9995, 4398046511104.0, 4398046511104.001, 4398046511104.0029, 4398046511104.333, 4398046511104.5, 281474976710656.06, 281474976710656.19, 281474976710656.94, 562949953421312.13, 562949953421312.63, 1125899906842624.3, 1125899906842624.8, 2251799813685247.5, 2251799813685248.0, 2251799813685248.5, 2251799813685249.5, 3377699720527872.5, 4503599627370495.5, 4503599627370496.0, -4398046511104.001, -281474976710656.19, -2251799813685248.5, -2251799813685248.0, -4503599627370496.0, -1e+300, }; public static readonly double[] LargeSinOfTurns = { -0.0030679567629659761, 0.0, 0.0061358846491544753, 0.01840672990580482, 0.86704624551569265, 0.0, 0.38268343236508978, 0.92387953251128674, -0.38268343236508978, 0.70710678118654757, -0.70710678118654757, 1.0, -1.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, -0.0061358846491544753, -0.92387953251128674, 0.0, 0.0, 0.0, 0.0, }; public static readonly double[] LargeCosOfTurns = { 0.99999529380957619, 1.0, 0.99998117528260111, 0.9998305817958234, -0.49822766697278187, -1.0, 0.92387953251128674, 0.38268343236508978, 0.92387953251128674, 0.70710678118654757, -0.70710678118654757, 0.0, 0.0, -1.0, 1.0, -1.0, -1.0, -1.0, -1.0, 1.0, 0.99998117528260111, 0.38268343236508978, -1.0, 1.0, 1.0, 1.0, }; public static bool FastSinCosWithinOneUlp(double[] turns, double[] sines, double[] cosines) { for (int i = 0; i < turns.Length; i++) { double sine; double cosine; PortableMath.FastSinCosOfTurns(turns[i], out sine, out cosine); // A zero of either sign is the same value, which counting steps between bit patterns would miss. if ((sines[i] != sine && UlpsApart(sines[i], sine) > 1) || (cosines[i] != cosine && UlpsApart(cosines[i], cosine) > 1)) { return false; } } return true; } public static long UlpsApart(double first, double second) { return Math.Abs(BitConverter.DoubleToInt64Bits(first) - BitConverter.DoubleToInt64Bits(second)); } // Returns the first input whose answer is more than the given distance from the expected, or zero for none. public static double FirstInputOffByMoreThanUlps( Func function, double[] inputs, double[] expected, long ulps) { for (int i = 0; i < inputs.Length; i++) { if (UlpsApart(expected[i], function(inputs[i])) > ulps) { return inputs[i]; } } return 0.0; } public static double RelativeError(double expected, double actual) { return Math.Abs(actual / expected - 1.0); } public class LogTests : UnitTestSharp.TestFixture { public void ReferenceValues_AreWithinOneUlp() { CheckEqual(0.0, FirstInputOffByMoreThanUlps(PortableMath.Log, LogInputs, LogExpected, 1)); } public void One_IsExactlyZero() { CheckEqual(0.0, PortableMath.Log(1.0)); } public void Zero_IsNegativeInfinity() { CheckEqual(double.NegativeInfinity, PortableMath.Log(0.0)); CheckEqual(double.NegativeInfinity, PortableMath.Log(-0.0)); } public void Negative_IsNotANumber() { CheckTrue(double.IsNaN(PortableMath.Log(-1.0))); CheckTrue(double.IsNaN(PortableMath.Log(double.NegativeInfinity))); } public void Infinity_IsInfinity() { CheckEqual(double.PositiveInfinity, PortableMath.Log(double.PositiveInfinity)); } public void NotANumber_IsNotANumber() { CheckTrue(double.IsNaN(PortableMath.Log(double.NaN))); } } public class ExpTests : UnitTestSharp.TestFixture { public void ReferenceValues_AreWithinOneUlp() { CheckEqual(0.0, FirstInputOffByMoreThanUlps(PortableMath.Exp, ExpInputs, ExpExpected, 1)); } public void Zero_IsExactlyOne() { CheckEqual(1.0, PortableMath.Exp(0.0)); } public void JustOverTheLargestFiniteResult_IsInfinity() { CheckEqual(double.PositiveInfinity, PortableMath.Exp(709.79)); } public void JustUnderTheSmallestSubnormalResult_IsZero() { CheckEqual(0.0, PortableMath.Exp(-745.2)); } public void Infinities_AreInfinityAndZero() { CheckEqual(double.PositiveInfinity, PortableMath.Exp(double.PositiveInfinity)); CheckEqual(0.0, PortableMath.Exp(double.NegativeInfinity)); } public void NotANumber_IsNotANumber() { CheckTrue(double.IsNaN(PortableMath.Exp(double.NaN))); } public void NegativeZero_IsOne() { CheckEqual(1.0, PortableMath.Exp(-0.0)); } } public class PowTests : UnitTestSharp.TestFixture { public void ReferenceValues_AreWithinTheLoosePowTolerance() { for (int i = 0; i < PowBases.Length; i++) { CheckLess(RelativeError(PowExpected[i], PortableMath.Pow(PowBases[i], PowExponents[i])), 2e-14); } } public void SmallWholePowers_AreExact() { CheckEqual(1024.0, PortableMath.Pow(2.0, 10.0)); CheckEqual(100000.0, PortableMath.Pow(10.0, 5.0)); CheckEqual(0.125, PortableMath.Pow(0.5, 3.0)); CheckEqual(1.0 / 9.0, PortableMath.Pow(3.0, -2.0)); CheckEqual(2.0, PortableMath.Pow(2.0, 1.0)); } public void OneHalf_IsTheSquareRoot() { CheckEqual(Math.Sqrt(2.0), PortableMath.Pow(2.0, 0.5)); CheckEqual(Math.Sqrt(0.3), PortableMath.Pow(0.3, 0.5)); } public void ManyWholePowersOrNearTheEdgeOfTheRange_AreCloseByTheLogarithm() { CheckLess(RelativeError(Math.Pow(1.0001, 1000.0), PortableMath.Pow(1.0001, 1000.0)), 1e-13); CheckLess(RelativeError(1e300, PortableMath.Pow(10.0, 300.0)), 1e-12); CheckLess(RelativeError(1e-300, PortableMath.Pow(10.0, -300.0)), 1e-12); CheckLess(RelativeError(Math.Pow(2.0, 1023.0), PortableMath.Pow(2.0, 1023.0)), 1e-12); } public void PowerOfZero_IsOne_WhateverTheBase() { CheckEqual(1.0, PortableMath.Pow(0.0, 0.0)); CheckEqual(1.0, PortableMath.Pow(double.NaN, 0.0)); CheckEqual(1.0, PortableMath.Pow(double.PositiveInfinity, 0.0)); CheckEqual(1.0, PortableMath.Pow(-3.0, 0.0)); } public void BaseOfOne_IsOne_WhateverThePower() { CheckEqual(1.0, PortableMath.Pow(1.0, double.NaN)); CheckEqual(1.0, PortableMath.Pow(1.0, double.PositiveInfinity)); CheckEqual(1.0, PortableMath.Pow(1.0, 123.5)); } public void NotANumber_IsNotANumber() { CheckTrue(double.IsNaN(PortableMath.Pow(double.NaN, 2.0))); CheckTrue(double.IsNaN(PortableMath.Pow(2.0, double.NaN))); } public void ZeroBase_IsZeroForPositivePowersAndInfinityForNegative() { CheckEqual(0.0, PortableMath.Pow(0.0, 3.0)); CheckEqual(0.0, PortableMath.Pow(0.0, 0.25)); CheckEqual(double.PositiveInfinity, PortableMath.Pow(0.0, -3.0)); CheckEqual(double.PositiveInfinity, PortableMath.Pow(0.0, -0.25)); } public void NegativeZeroBase_KeepsItsSignForOddPowers() { CheckEqual(double.NegativeInfinity, PortableMath.Pow(-0.0, -3.0)); CheckEqual(double.PositiveInfinity, PortableMath.Pow(-0.0, -2.0)); CheckTrue(BitConverter.DoubleToInt64Bits(PortableMath.Pow(-0.0, 3.0)) < 0); CheckEqual(0L, BitConverter.DoubleToInt64Bits(PortableMath.Pow(-0.0, 2.0))); } public void InfinitePower_DependsOnWhetherTheBaseIsOverOrUnderOne() { CheckEqual(double.PositiveInfinity, PortableMath.Pow(2.0, double.PositiveInfinity)); CheckEqual(0.0, PortableMath.Pow(2.0, double.NegativeInfinity)); CheckEqual(0.0, PortableMath.Pow(0.5, double.PositiveInfinity)); CheckEqual(double.PositiveInfinity, PortableMath.Pow(0.5, double.NegativeInfinity)); CheckEqual(double.PositiveInfinity, PortableMath.Pow(0.0, double.NegativeInfinity)); CheckEqual(0.0, PortableMath.Pow(0.0, double.PositiveInfinity)); } public void InfiniteBase_IsInfinityForPositivePowersAndZeroForNegative() { CheckEqual(double.PositiveInfinity, PortableMath.Pow(double.PositiveInfinity, 2.0)); CheckEqual(0.0, PortableMath.Pow(double.PositiveInfinity, -2.0)); } public void NegativeInfiniteBase_KeepsItsSignForOddPowers() { CheckEqual(double.NegativeInfinity, PortableMath.Pow(double.NegativeInfinity, 3.0)); CheckEqual(double.PositiveInfinity, PortableMath.Pow(double.NegativeInfinity, 2.0)); CheckTrue(BitConverter.DoubleToInt64Bits(PortableMath.Pow(double.NegativeInfinity, -3.0)) < 0); } public void NegativeBase_NeedsAWholePower() { CheckEqual(-8.0, PortableMath.Pow(-2.0, 3.0)); CheckEqual(4.0, PortableMath.Pow(-2.0, 2.0)); CheckEqual(-0.125, PortableMath.Pow(-2.0, -3.0)); CheckTrue(double.IsNaN(PortableMath.Pow(-2.0, 0.5))); } public void NegativeBaseToAHugeEvenPower_IsPositive() { CheckEqual(double.PositiveInfinity, PortableMath.Pow(-2.0, 1e300)); CheckEqual(0.0, PortableMath.Pow(-0.5, 1e300)); } public void NegativeOneToAnInfinitePower_IsOne() { CheckEqual(1.0, PortableMath.Pow(-1.0, double.PositiveInfinity)); } public void NegativeBaseToAnInfinitePower_FollowsTheMagnitude() { CheckEqual(double.PositiveInfinity, PortableMath.Pow(-2.0, double.PositiveInfinity)); CheckEqual(0.0, PortableMath.Pow(-2.0, double.NegativeInfinity)); } } public class Expm1Tests : UnitTestSharp.TestFixture { public void ReferenceValues_AreWithinTwoUlps() { CheckEqual(0.0, FirstInputOffByMoreThanUlps(PortableMath.Expm1, Expm1Inputs, Expm1Expected, 2)); } public void Zero_IsZero() { CheckEqual(0.0, PortableMath.Expm1(0.0)); } public void TinyInput_ComesBackUnchanged() { CheckEqual(1e-300, PortableMath.Expm1(1e-300)); CheckEqual(5e-324, PortableMath.Expm1(5e-324)); } public void SmallInput_KeepsDigitsThatSubtractingFromExpLoses() { CheckEqual(1e-17, PortableMath.Expm1(1e-17)); CheckEqual(0.0, PortableMath.Exp(1e-17) - 1.0); } public void VeryNegativeInput_IsExactlyMinusOne() { CheckEqual(-1.0, PortableMath.Expm1(-38.5)); CheckEqual(-1.0, PortableMath.Expm1(-1000.0)); CheckEqual(-1.0, PortableMath.Expm1(double.NegativeInfinity)); } public void LargeInput_IsExp() { CheckEqual(PortableMath.Exp(50.0), PortableMath.Expm1(50.0)); CheckEqual(PortableMath.Exp(709.0), PortableMath.Expm1(709.0)); } public void OverflowingInput_IsInfinity() { CheckEqual(double.PositiveInfinity, PortableMath.Expm1(710.0)); CheckEqual(double.PositiveInfinity, PortableMath.Expm1(double.PositiveInfinity)); } public void NotANumber_IsNotANumber() { CheckTrue(double.IsNaN(PortableMath.Expm1(double.NaN))); } } public class Log1pTests : UnitTestSharp.TestFixture { public void ReferenceValues_AreWithinTwoUlps() { CheckEqual(0.0, FirstInputOffByMoreThanUlps(PortableMath.Log1p, Log1pInputs, Log1pExpected, 2)); } public void Zero_IsZero() { CheckEqual(0.0, PortableMath.Log1p(0.0)); } public void TinyInput_ComesBackUnchanged() { CheckEqual(1e-300, PortableMath.Log1p(1e-300)); CheckEqual(5e-324, PortableMath.Log1p(5e-324)); } public void SmallInput_KeepsDigitsThatAddingOneLoses() { CheckEqual(1e-17, PortableMath.Log1p(1e-17)); CheckEqual(0.0, PortableMath.Log(1.0 + 1e-17)); } public void MinusOne_IsNegativeInfinity() { CheckEqual(double.NegativeInfinity, PortableMath.Log1p(-1.0)); } public void BelowMinusOne_IsNotANumber() { CheckTrue(double.IsNaN(PortableMath.Log1p(-1.5))); CheckTrue(double.IsNaN(PortableMath.Log1p(double.NegativeInfinity))); } public void Infinity_IsInfinity() { CheckEqual(double.PositiveInfinity, PortableMath.Log1p(double.PositiveInfinity)); } public void NotANumber_IsNotANumber() { CheckTrue(double.IsNaN(PortableMath.Log1p(double.NaN))); } } public class NextDownTests : UnitTestSharp.TestFixture { public void Positive_IsOneStepDown() { CheckEqual(0.9999999999999999, PortableMath.NextDown(1.0)); CheckEqual(5e-324, PortableMath.NextDown(1e-323)); } public void Zero_IsTheSmallestNegative() { CheckEqual(-5e-324, PortableMath.NextDown(0.0)); CheckEqual(-5e-324, PortableMath.NextDown(-0.0)); } public void SmallestPositive_IsZero() { CheckEqual(0L, BitConverter.DoubleToInt64Bits(PortableMath.NextDown(5e-324))); } public void Negative_IsOneStepFurtherFromZero() { CheckEqual(-1.0000000000000002, PortableMath.NextDown(-1.0)); } public void Infinities_StepToTheLargestFiniteOrStay() { CheckEqual(double.MaxValue, PortableMath.NextDown(double.PositiveInfinity)); CheckEqual(double.NegativeInfinity, PortableMath.NextDown(double.NegativeInfinity)); CheckEqual(double.NegativeInfinity, PortableMath.NextDown(-double.MaxValue)); } public void NotANumber_IsNotANumber() { CheckTrue(double.IsNaN(PortableMath.NextDown(double.NaN))); } } public class SinCosOfTurnsTests : UnitTestSharp.TestFixture { public void ReferenceValues_AreTheNearestDoubles() { for (int i = 0; i < SinCosTurns.Length; i++) { double sine; double cosine; PortableMath.SinCosOfTurns(SinCosTurns[i], out sine, out cosine); CheckEqual(SinOfTurns[i], sine); CheckEqual(CosOfTurns[i], cosine); } } public void AnglesTheTableSumsCannotDecide_AreStillTheNearestDoubles() { for (int i = 0; i < HardSinCosTurns.Length; i++) { double sine; double cosine; PortableMath.SinCosOfTurns(HardSinCosTurns[i], out sine, out cosine); CheckEqual(HardSinOfTurns[i], sine); CheckEqual(HardCosOfTurns[i], cosine); } } public void AnglesEvenTheRefinedSumsCannotDecide_AreStillTheNearestDoubles() { for (int i = 0; i < DeepSinCosTurns.Length; i++) { double sine; double cosine; PortableMath.SinCosOfTurns(DeepSinCosTurns[i], out sine, out cosine); CheckEqual(DeepSinOfTurns[i], sine); CheckEqual(DeepCosOfTurns[i], cosine); } } public void AnglesWhoseSineIsSubnormal_GetTheNearestDoubleNotADoubleRounding() { for (int i = 0; i < SubnormalSineTurns.Length; i++) { double sine; double cosine; PortableMath.SinCosOfTurns(SubnormalSineTurns[i], out sine, out cosine); CheckEqual(SubnormalSines[i], sine); CheckEqual(1.0, cosine); PortableMath.SinCosOfTurns(-SubnormalSineTurns[i], out sine, out cosine); CheckEqual(-SubnormalSines[i], sine); CheckEqual(1.0, cosine); } } public void SmallestSubnormalAngle_GivesTheNearestSubnormalSine() { double sine; double cosine; PortableMath.SinCosOfTurns(BitConverter.Int64BitsToDouble(1L), out sine, out cosine); CheckEqual(BitConverter.Int64BitsToDouble(6L), sine); CheckEqual(1.0, cosine); } public void ZeroAngle_KeepsItsSignInTheSine() { double sine; double cosine; PortableMath.SinCosOfTurns(0.0, out sine, out cosine); CheckEqual(0L, BitConverter.DoubleToInt64Bits(sine)); CheckEqual(1.0, cosine); PortableMath.SinCosOfTurns(-0.0, out sine, out cosine); CheckEqual(long.MinValue, BitConverter.DoubleToInt64Bits(sine)); CheckEqual(1.0, cosine); } public void ZerosInTheResult_AreAPlusSignForAPositiveAngle() { foreach (double turns in new[] { 0.5, 0.25, 0.75, 1.0, 1.5, 1.25, 4398046511104.0, 1125899906842624.25, 4503599627370496.0 }) { double sine; double cosine; PortableMath.SinCosOfTurns(turns, out sine, out cosine); CheckFalse(sine == 0.0 && BitConverter.DoubleToInt64Bits(sine) != 0L); CheckFalse(cosine == 0.0 && BitConverter.DoubleToInt64Bits(cosine) != 0L); } } public void ZeroSineOfANegativeAngle_IsMinusZero() { double sine; double cosine; PortableMath.SinCosOfTurns(-0.5, out sine, out cosine); CheckEqual(long.MinValue, BitConverter.DoubleToInt64Bits(sine)); CheckEqual(-1.0, cosine); } public void Quarters_AreExact() { double sine; double cosine; PortableMath.SinCosOfTurns(0.0, out sine, out cosine); CheckEqual(0.0, sine); CheckEqual(1.0, cosine); PortableMath.SinCosOfTurns(0.25, out sine, out cosine); CheckEqual(1.0, sine); CheckEqual(0.0, cosine); PortableMath.SinCosOfTurns(0.5, out sine, out cosine); CheckEqual(0.0, sine); CheckEqual(-1.0, cosine); PortableMath.SinCosOfTurns(0.75, out sine, out cosine); CheckEqual(-1.0, sine); CheckEqual(0.0, cosine); PortableMath.SinCosOfTurns(1.25, out sine, out cosine); CheckEqual(1.0, sine); CheckEqual(0.0, cosine); } public void NegativeAngle_NegatesTheSineAndKeepsTheCosine() { foreach (double turns in new[] { 0.1, 0.3, 7.3, 1000000.123 }) { double sine; double cosine; double negatedSine; double negatedCosine; PortableMath.SinCosOfTurns(turns, out sine, out cosine); PortableMath.SinCosOfTurns(-turns, out negatedSine, out negatedCosine); CheckEqual(-sine, negatedSine); CheckEqual(cosine, negatedCosine); } } public void SquaresAddToOne() { for (int step = 0; step < 1000; step++) { double sine; double cosine; PortableMath.SinCosOfTurns(step / 997.0, out sine, out cosine); CheckLess(Math.Abs(sine * sine + cosine * cosine - 1.0), 3e-16); } } public void AnglesEvenTheCarefulSumsCannotDecide_AreStillTheNearestDoubles() { for (int i = 0; i < DeepestSinCosTurns.Length; i++) { double sine; double cosine; PortableMath.SinCosOfTurns(DeepestSinCosTurns[i], out sine, out cosine); CheckEqual(DeepestSinOfTurns[i], sine); CheckEqual(DeepestCosOfTurns[i], cosine); } } public void AnglesAroundWhereWholeTurnsAreRemovedFirst_AreTheNearestDoubles() { for (int i = 0; i < MediumSinCosTurns.Length; i++) { double sine; double cosine; PortableMath.SinCosOfTurns(MediumSinCosTurns[i], out sine, out cosine); CheckEqual(MediumSinOfTurns[i], sine); CheckEqual(MediumCosOfTurns[i], cosine); } } public void AnglesFromTheTableOnlyRange_AreTheNearestDoubles() { for (int i = 0; i < LargeSinCosTurns.Length; i++) { double sine; double cosine; PortableMath.SinCosOfTurns(LargeSinCosTurns[i], out sine, out cosine); CheckEqual(LargeSinOfTurns[i], sine); CheckEqual(LargeCosOfTurns[i], cosine); } } public void AngleOfWholeTurnsOnly_IsZeroAndOne() { double sine; double cosine; PortableMath.SinCosOfTurns(Math.Pow(2.0, 61.0), out sine, out cosine); CheckEqual(0L, BitConverter.DoubleToInt64Bits(sine)); CheckEqual(1.0, cosine); PortableMath.SinCosOfTurns(Math.Pow(2.0, 52.0), out sine, out cosine); CheckEqual(0L, BitConverter.DoubleToInt64Bits(sine)); CheckEqual(1.0, cosine); } public void NegativeWholeTurns_GiveAMinusZeroSine() { foreach (double turns in new[] { -1.0, -3.0, -4398046511104.0, -Math.Pow(2.0, 52.0), -Math.Pow(2.0, 61.0) }) { double sine; double cosine; PortableMath.SinCosOfTurns(turns, out sine, out cosine); CheckEqual(long.MinValue, BitConverter.DoubleToInt64Bits(sine)); CheckEqual(1.0, cosine); } } public void Infinity_IsNotANumber() { double sine; double cosine; PortableMath.SinCosOfTurns(double.PositiveInfinity, out sine, out cosine); CheckTrue(double.IsNaN(sine) && double.IsNaN(cosine)); } public void NegativeInfinity_IsNotANumber() { double sine; double cosine; PortableMath.SinCosOfTurns(double.NegativeInfinity, out sine, out cosine); CheckTrue(double.IsNaN(sine) && double.IsNaN(cosine)); } public void NotANumber_IsNotANumber() { double sine; double cosine; PortableMath.SinCosOfTurns(double.NaN, out sine, out cosine); CheckTrue(double.IsNaN(sine) && double.IsNaN(cosine)); } } public class FastSinCosOfTurnsTests : UnitTestSharp.TestFixture { public void ReferenceValues_AreWithinOneUlp() { CheckTrue(FastSinCosWithinOneUlp(SinCosTurns, SinOfTurns, CosOfTurns)); } public void AnglesTheExactTiersWorkHardOn_AreWithinOneUlp() { CheckTrue(FastSinCosWithinOneUlp(HardSinCosTurns, HardSinOfTurns, HardCosOfTurns)); CheckTrue(FastSinCosWithinOneUlp(DeepSinCosTurns, DeepSinOfTurns, DeepCosOfTurns)); CheckTrue(FastSinCosWithinOneUlp(DeepestSinCosTurns, DeepestSinOfTurns, DeepestCosOfTurns)); } public void AnglesAroundWhereWholeTurnsAreRemovedFirst_AreWithinOneUlp() { CheckTrue(FastSinCosWithinOneUlp(MediumSinCosTurns, MediumSinOfTurns, MediumCosOfTurns)); } public void AnglesFromTheTableOnlyRange_AreTheNearestDoubles() { for (int i = 0; i < LargeSinCosTurns.Length; i++) { double sine; double cosine; PortableMath.FastSinCosOfTurns(LargeSinCosTurns[i], out sine, out cosine); CheckEqual(LargeSinOfTurns[i], sine); CheckEqual(LargeCosOfTurns[i], cosine); } } public void AnglesWhoseSineIsSubnormal_AreTheNearestDoubles() { for (int i = 0; i < SubnormalSineTurns.Length; i++) { double sine; double cosine; PortableMath.FastSinCosOfTurns(SubnormalSineTurns[i], out sine, out cosine); CheckEqual(SubnormalSines[i], sine); CheckEqual(1.0, cosine); } } public void ZeroAngle_KeepsItsSignInTheSine() { double sine; double cosine; PortableMath.FastSinCosOfTurns(0.0, out sine, out cosine); CheckEqual(0L, BitConverter.DoubleToInt64Bits(sine)); CheckEqual(1.0, cosine); PortableMath.FastSinCosOfTurns(-0.0, out sine, out cosine); CheckEqual(long.MinValue, BitConverter.DoubleToInt64Bits(sine)); CheckEqual(1.0, cosine); } public void ZerosInTheResult_AreAPlusSignForAPositiveAngle() { foreach (double turns in new[] { 0.5, 0.25, 0.75, 1.0, 1.5, 1.25, 4398046511104.0, 1125899906842624.25, 4503599627370496.0 }) { double sine; double cosine; PortableMath.FastSinCosOfTurns(turns, out sine, out cosine); CheckFalse(sine == 0.0 && BitConverter.DoubleToInt64Bits(sine) != 0L); CheckFalse(cosine == 0.0 && BitConverter.DoubleToInt64Bits(cosine) != 0L); } } public void ZeroSineOfANegativeAngle_IsMinusZero() { double sine; double cosine; PortableMath.FastSinCosOfTurns(-0.5, out sine, out cosine); CheckEqual(long.MinValue, BitConverter.DoubleToInt64Bits(sine)); CheckEqual(-1.0, cosine); } public void Quarters_AreExact() { double sine; double cosine; PortableMath.FastSinCosOfTurns(0.25, out sine, out cosine); CheckEqual(1.0, sine); CheckEqual(0.0, cosine); PortableMath.FastSinCosOfTurns(0.5, out sine, out cosine); CheckEqual(0.0, sine); CheckEqual(-1.0, cosine); PortableMath.FastSinCosOfTurns(0.75, out sine, out cosine); CheckEqual(-1.0, sine); CheckEqual(0.0, cosine); PortableMath.FastSinCosOfTurns(1.25, out sine, out cosine); CheckEqual(1.0, sine); CheckEqual(0.0, cosine); } public void NegativeAngle_NegatesTheSineAndKeepsTheCosine() { foreach (double turns in new[] { 0.1, 0.3, 7.3, 1000000.123 }) { double sine; double cosine; double negatedSine; double negatedCosine; PortableMath.FastSinCosOfTurns(turns, out sine, out cosine); PortableMath.FastSinCosOfTurns(-turns, out negatedSine, out negatedCosine); CheckEqual(-sine, negatedSine); CheckEqual(cosine, negatedCosine); } } public void Infinity_IsNotANumber() { double sine; double cosine; PortableMath.FastSinCosOfTurns(double.PositiveInfinity, out sine, out cosine); CheckTrue(double.IsNaN(sine) && double.IsNaN(cosine)); } public void NotANumber_IsNotANumber() { double sine; double cosine; PortableMath.FastSinCosOfTurns(double.NaN, out sine, out cosine); CheckTrue(double.IsNaN(sine) && double.IsNaN(cosine)); } } public class FastLogTests : UnitTestSharp.TestFixture { public void AcrossManySizes_IsWithinAnAbsoluteTwelveMillionths() { double worst = 0.0; for (int power = -1000; power <= 1000; power += 7) { for (int step = 0; step < 61; step++) { double x = Math.Pow(2.0, power) * (1.0 + step / 61.0); worst = Math.Max(worst, Math.Abs(PortableMath.FastLog(x) - PortableMath.Log(x))); } } CheckLess(worst, 1.2e-5); } public void PowerOfTwo_IsTheExponentTimesLn2() { CheckEqual(10 * 0.6931471805599453, PortableMath.FastLog(1024.0)); CheckEqual(0.0, PortableMath.FastLog(1.0)); } public void Zero_NegativeAndInfiniteAndNotANumber_GetTheExactAnswers() { CheckEqual(double.NegativeInfinity, PortableMath.FastLog(0.0)); CheckTrue(double.IsNaN(PortableMath.FastLog(-2.0))); CheckEqual(double.PositiveInfinity, PortableMath.FastLog(double.PositiveInfinity)); CheckTrue(double.IsNaN(PortableMath.FastLog(double.NaN))); } public void Subnormal_IsWithinTheSameBound() { double[] inputs = { 5e-324, 1e-320, 4.9e-310, 2.2250738585072009e-308 }; foreach (double x in inputs) { CheckLess(Math.Abs(PortableMath.FastLog(x) - PortableMath.Log(x)), 1.2e-5); } } } public class FastExpTests : UnitTestSharp.TestFixture { public void AcrossTheNormalResults_IsWithinARelativeTenMillionth() { double worst = 0.0; for (int i = 0; i <= 28300; i++) { double x = -707.9 + i * 0.05; worst = Math.Max(worst, RelativeError(PortableMath.Exp(x), PortableMath.FastExp(x))); } CheckLess(worst, 1e-7); } public void Zero_IsExactlyOne() { CheckEqual(1.0, PortableMath.FastExp(0.0)); } public void WholeMultiplesOfLn2_ArePowersOfTwoToWithinTheFit() { CheckLess(RelativeError(1024.0, PortableMath.FastExp(10 * 0.6931471805599453)), 1e-7); } public void SubnormalResults_AreWithinTheRelativeBoundPlusOneStep() { double[] inputs = { -710.0, -720.0, -730.0, -740.0, -743.9 }; foreach (double x in inputs) { double exact = PortableMath.Exp(x); CheckLess(Math.Abs(PortableMath.FastExp(x) - exact), exact * 1e-7 + 5e-324); } } public void NearOverflowingOrVanishing_GetsTheExactAnswer() { CheckEqual(PortableMath.Exp(709.0), PortableMath.FastExp(709.0)); CheckEqual(PortableMath.Exp(709.5), PortableMath.FastExp(709.5)); CheckEqual(PortableMath.Exp(-744.0), PortableMath.FastExp(-744.0)); CheckEqual(PortableMath.Exp(-745.0), PortableMath.FastExp(-745.0)); CheckEqual(double.PositiveInfinity, PortableMath.FastExp(1000.0)); CheckEqual(0.0, PortableMath.FastExp(-1000.0)); } public void JustInsideTheFastRange_IsStillClose() { CheckLess(RelativeError(PortableMath.Exp(708.99), PortableMath.FastExp(708.99)), 1e-7); } public void NotANumber_IsNotANumber() { CheckTrue(double.IsNaN(PortableMath.FastExp(double.NaN))); } } public class SameBitsOnEveryMachineTests : UnitTestSharp.TestFixture { public void LogExpAndPow_GiveTheBitsTheReferenceImplementationGave() { ulong state = 88172645463325252UL; ulong logHash = 14695981039346656037UL; ulong expHash = 14695981039346656037UL; ulong powHash = 14695981039346656037UL; for (int i = 0; i < 100000; i++) { ulong exponentField = NextWord(ref state) % 2047; ulong mantissa = NextWord(ref state) & 0x000FFFFFFFFFFFFFUL; double positive = BitConverter.Int64BitsToDouble((long)((exponentField << 52) | mantissa)); logHash = Fold(logHash, PortableMath.Log(positive)); expHash = Fold(expHash, PortableMath.Exp(Unit(ref state) * 1500.0 - 750.0)); double powBase = Unit(ref state) * 10.0; double power = Unit(ref state) * 20.0 - 10.0; powHash = Fold(powHash, PortableMath.Pow(powBase, power)); } CheckEqual(16268866722378715347UL, logHash); CheckEqual(15872255864321907476UL, expHash); CheckEqual(11823991732036011076UL, powHash); } public void Expm1AndLog1p_GiveTheBitsTheReferenceImplementationGave() { ulong state = 88172645463325252UL; ulong expm1Hash = 14695981039346656037UL; ulong log1pHash = 14695981039346656037UL; for (int i = 0; i < 100000; i++) { double expm1Input = Unit(ref state) * 80.0 - 40.0; expm1Input *= 1.0 / (double)(1UL << (int)(NextWord(ref state) % 40)); double log1pInput = Unit(ref state) * 6.0 - 1.0; log1pInput *= 1.0 / (double)(1UL << (int)(NextWord(ref state) % 40)); expm1Hash = Fold(expm1Hash, PortableMath.Expm1(expm1Input)); log1pHash = Fold(log1pHash, PortableMath.Log1p(log1pInput)); } CheckEqual(8134355637742807013UL, expm1Hash); CheckEqual(15840705718198487128UL, log1pHash); } public void SinCosOfTurns_GiveTheBitsTheReferenceImplementationGave() { ulong state = 88172645463325252UL; ulong sineHash = 14695981039346656037UL; ulong cosineHash = 14695981039346656037UL; for (int i = 0; i < 100000; i++) { double small = Unit(ref state) * 4.0 - 2.0; double large = Unit(ref state) * 1e12; double sine; double cosine; PortableMath.SinCosOfTurns(small, out sine, out cosine); sineHash = Fold(sineHash, sine); cosineHash = Fold(cosineHash, cosine); PortableMath.SinCosOfTurns(large, out sine, out cosine); sineHash = Fold(sineHash, sine); cosineHash = Fold(cosineHash, cosine); } CheckEqual(5719802040167353131UL, sineHash); CheckEqual(3792827332978950537UL, cosineHash); } public void FastSinCosOfTurns_GiveTheBitsTheReferenceImplementationGave() { ulong state = 88172645463325252UL; ulong sineHash = 14695981039346656037UL; ulong cosineHash = 14695981039346656037UL; for (int i = 0; i < 100000; i++) { double small = Unit(ref state) * 4.0 - 2.0; double large = Unit(ref state) * 1e12; double sine; double cosine; PortableMath.FastSinCosOfTurns(small, out sine, out cosine); sineHash = Fold(sineHash, sine); cosineHash = Fold(cosineHash, cosine); PortableMath.FastSinCosOfTurns(large, out sine, out cosine); sineHash = Fold(sineHash, sine); cosineHash = Fold(cosineHash, cosine); } CheckEqual(1847973215133409229UL, sineHash); CheckEqual(5729743477279778212UL, cosineHash); } public static ulong NextWord(ref ulong state) { state ^= state << 13; state ^= state >> 7; state ^= state << 17; return state; } public static double Unit(ref ulong state) { return (NextWord(ref state) >> 11) * (1.0 / 9007199254740992.0); } public static ulong Fold(ulong hash, double value) { return (hash ^ (ulong)BitConverter.DoubleToInt64Bits(value)) * 1099511628211UL; } } } }