From f442bce78b02324d4303706af61d3d5c39034c8c Mon Sep 17 00:00:00 2001 From: Roman Date: Tue, 13 Dec 2022 16:34:57 -0800 Subject: [PATCH 01/17] feat(osmomath): exp2 function --- osmomath/exp2.go | 93 +++++++ osmomath/exp2_test.go | 298 +++++++++++++++++++++++ osmomath/export_test.go | 13 + scripts/approximations/approximations.py | 27 +- scripts/approximations/main.py | 60 ++++- 5 files changed, 476 insertions(+), 15 deletions(-) create mode 100644 osmomath/exp2.go create mode 100644 osmomath/exp2_test.go create mode 100644 osmomath/export_test.go diff --git a/osmomath/exp2.go b/osmomath/exp2.go new file mode 100644 index 00000000000..baa278cee2b --- /dev/null +++ b/osmomath/exp2.go @@ -0,0 +1,93 @@ +package osmomath + +import "fmt" + +var ( + // Truncated at precision end. + // See scripts/approximations/main.py exponent_approximation_choice function for details. + numeratorCoefficients13Param = []BigDec{ + MustNewDecFromStr("1.000000000000000000000044212244679434"), + MustNewDecFromStr("0.352032455817400196452603772766844426"), + MustNewDecFromStr("0.056507868883666405413116800969512484"), + MustNewDecFromStr("0.005343900728213034434757419480319916"), + MustNewDecFromStr("0.000317708814342353603087543715930732"), + MustNewDecFromStr("0.000011429747507407623028722262874632"), + MustNewDecFromStr("0.000000198381965651614980168744540366"), + } + + // Rounded up at precision end. + // See scripts/approximations/main.py exponent_approximation_choice function for details. + denominatorCoefficients13Param = []BigDec{ + OneDec(), + MustNewDecFromStr("0.341114724742545112949699755780593311").Neg(), + MustNewDecFromStr("0.052724071627342653404436933178482287"), + MustNewDecFromStr("0.004760950735524957576233524801866342").Neg(), + MustNewDecFromStr("0.000267168475410566529819971616894193"), + MustNewDecFromStr("0.000008923715368802211181557353097439").Neg(), + MustNewDecFromStr("0.000000140277233177373698516010555916"), + } + + // maxSupportedExponent = 2^10. The value is chosen by finding + // at which value some of the underlying internal functions overflow. + // If needed, exp2 can be reimplemented to allow for greater exponents. + maxSupportedExponent = MustNewDecFromStr("2").PowerInteger(9) +) + +// exp2 takes 2 to the power of a given non-negative decimal exponent. +// and returns the result. +// The computation is performed by using th following property: +// 2^decimal_exp = 2^{integer_exp + fractional_exp} = 2^integer_exp * 2^fractional_exp +// The max supported exponent is 2^10. If greater exponent is given, the function panics. +// Panics if the exponent is negative. +func exp2(exponent BigDec) BigDec { + if exponent.Abs().GT(maxSupportedExponent) { + panic(fmt.Sprintf("integer exponent %s is too large, max (%s)", exponent, maxSupportedExponent)) + } + if exponent.IsNegative() { + panic(fmt.Sprintf("negative exponent %s is not supported", exponent)) + } + + integerExponentDec := exponent.TruncateDec() + integerExponent := integerExponentDec.TruncateInt() + integerResult := twoBigDec.PowerInteger(integerExponent.Uint64()) + + fractionalExponent := exponent.Sub(integerExponentDec) + fractionalResult := exp2ChebyshevRationalApprox(fractionalExponent) + + result := integerResult.Mul(fractionalResult) + + return result +} + +// exp2ChebyshevRationalApprox takes 2 to the power of a given decimal exponent. +// The result is approximated by a 13 parameter Chebyshev rational approximation. +// f(x) = h(x) / p(x) (7, 7) terms. We set the first term of p(x) to 1. +// As a result, this ends up being 7 + 6 = 13 parameters. +// The numerator coefficients are truncated at precision end. The denominator +// coefficients are rounded up at precision end. +// See scripts/approximations/README.md for details of the scripts used +// to compute the coefficients. +// CONTRACT: exponent must be in the range [0, 1], panics if not. +func exp2ChebyshevRationalApprox(x BigDec) BigDec { + if x.LT(ZeroDec()) || x.GT(OneDec()) { + panic(fmt.Sprintf("exponent must be in the range [0, 1], got %s", x)) + } + if x.IsZero() { + return OneDec() + } + if x.Equal(OneDec()) { + return twoBigDec + } + + h_x := numeratorCoefficients13Param[0].Clone() + p_x := denominatorCoefficients13Param[0].Clone() + x_exp_i := OneDec() + for i := 1; i < len(numeratorCoefficients13Param); i++ { + x_exp_i.MulMut(x) + + h_x = h_x.Add(numeratorCoefficients13Param[i].Mul(x_exp_i)) + p_x = p_x.Add(denominatorCoefficients13Param[i].Mul(x_exp_i)) + } + + return h_x.Quo(p_x) +} diff --git a/osmomath/exp2_test.go b/osmomath/exp2_test.go new file mode 100644 index 00000000000..e85300e0938 --- /dev/null +++ b/osmomath/exp2_test.go @@ -0,0 +1,298 @@ +package osmomath_test + +import ( + "testing" + + sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/stretchr/testify/require" + + "github.com/osmosis-labs/osmosis/v13/app/apptesting/osmoassert" + "github.com/osmosis-labs/osmosis/v13/osmomath" + "github.com/osmosis-labs/osmosis/v13/osmoutils" +) + +var ( + // minDecTolerance minimum tolerance for sdk.Dec, given its precision of 18. + minDecTolerance = sdk.MustNewDecFromStr("0.000000000000000001") +) + +func TestExp2ChebyshevRationalApprox(t *testing.T) { + + smallValue := osmomath.MustNewDecFromStr("0.00001") + smallerValue := osmomath.MustNewDecFromStr("0.00000000000000000001") + + tests := map[string]struct { + exponent osmomath.BigDec + expectedResult osmomath.BigDec + errTolerance osmoutils.ErrTolerance + expectPanic bool + }{ + "exp2(0.5)": { + exponent: osmomath.MustNewDecFromStr("0.5"), + // https://www.wolframalpha.com/input?i=2%5E0.5+37+digits + expectedResult: osmomath.MustNewDecFromStr("1.414213562373095048801688724209698079"), + + errTolerance: osmoutils.ErrTolerance{ + AdditiveTolerance: minDecTolerance, + MultiplicativeTolerance: minDecTolerance, + RoundingDir: osmomath.RoundDown, + }, + }, + "exp2(0)": { + exponent: osmomath.ZeroDec(), + expectedResult: osmomath.OneDec(), + + errTolerance: osmoutils.ErrTolerance{ + AdditiveTolerance: sdk.ZeroDec(), + MultiplicativeTolerance: sdk.ZeroDec(), + RoundingDir: osmomath.RoundDown, + }, + }, + "exp2(1)": { + exponent: osmomath.OneDec(), + expectedResult: osmomath.MustNewDecFromStr("2"), + + errTolerance: osmoutils.ErrTolerance{ + AdditiveTolerance: sdk.ZeroDec(), + MultiplicativeTolerance: sdk.ZeroDec(), + RoundingDir: osmomath.RoundDown, + }, + }, + "exp2(0.00001)": { + exponent: smallValue, + // https://www.wolframalpha.com/input?i=2%5E0.00001+37+digits + expectedResult: osmomath.MustNewDecFromStr("1.000006931495828305653209089800561681"), + + errTolerance: osmoutils.ErrTolerance{ + AdditiveTolerance: minDecTolerance, + MultiplicativeTolerance: minDecTolerance, + // TODO: confirm if rounding behavior is acceptable. + // Note, that a Python estimate passes RoundDown but not Wolfram. + RoundingDir: osmomath.RoundUnconstrained, + }, + }, + "exp2(0.99999)": { + exponent: osmomath.OneDec().Sub(smallValue), + // https://www.wolframalpha.com/input?i=2%5E0.99999+37+digits + expectedResult: osmomath.MustNewDecFromStr("1.999986137104433991477606830496602898"), + + errTolerance: osmoutils.ErrTolerance{ + AdditiveTolerance: sdk.MustNewDecFromStr("0.00000000000000007"), + MultiplicativeTolerance: minDecTolerance.Mul(sdk.NewDec(100)), + RoundingDir: osmomath.RoundDown, + }, + }, + "exp2(0.99999...)": { + exponent: osmomath.OneDec().Sub(smallerValue), + // https://www.wolframalpha.com/input?i=2%5E%281+-+0.00000000000000000001%29+37+digits + expectedResult: osmomath.MustNewDecFromStr("1.999999999999999999986137056388801094"), + + errTolerance: osmoutils.ErrTolerance{ + AdditiveTolerance: minDecTolerance, + MultiplicativeTolerance: minDecTolerance, + RoundingDir: osmomath.RoundDown, + }, + }, + "exp2(0.0000...1)": { + exponent: osmomath.ZeroDec().Add(smallerValue), + // https://www.wolframalpha.com/input?i=2%5E0.00000000000000000001+37+digits + expectedResult: osmomath.MustNewDecFromStr("1.000000000000000000006931471805599453"), + + errTolerance: osmoutils.ErrTolerance{ + AdditiveTolerance: minDecTolerance, + MultiplicativeTolerance: minDecTolerance, + RoundingDir: osmomath.RoundUnconstrained, // TODO: confirm if this is acceptable. + }, + }, + "exp2(0.3334567)": { + exponent: osmomath.MustNewDecFromStr("0.3334567"), + // https://www.wolframalpha.com/input?i=2%5E0.3334567+37+digits + expectedResult: osmomath.MustNewDecFromStr("1.260028791934303989065848870753742298"), + + errTolerance: osmoutils.ErrTolerance{ + AdditiveTolerance: sdk.MustNewDecFromStr("0.00000000000000007"), + MultiplicativeTolerance: minDecTolerance.Mul(sdk.NewDec(10)), + RoundingDir: osmomath.RoundDown, + }, + }, + "exp2(0.84864288)": { + exponent: osmomath.MustNewDecFromStr("0.84864288"), + // https://www.wolframalpha.com/input?i=2%5E0.84864288+37+digits + expectedResult: osmomath.MustNewDecFromStr("1.800806138872630518880998772777747572"), + + errTolerance: osmoutils.ErrTolerance{ + AdditiveTolerance: sdk.MustNewDecFromStr("0.00000000000000002"), + MultiplicativeTolerance: minDecTolerance.Mul(sdk.NewDec(10)), + RoundingDir: osmomath.RoundUnconstrained, // TODO: confirm if this is acceptable. + }, + }, + "exp2(0.999999999999999999999999999999999956)": { + exponent: osmomath.MustNewDecFromStr("0.999999999999999999999999999999999956"), + // https://www.wolframalpha.com/input?i=2%5E0.999999999999999999999999999999999956+37+digits + expectedResult: osmomath.MustNewDecFromStr("1.999999999999999999999999999999999939"), + + errTolerance: osmoutils.ErrTolerance{ + AdditiveTolerance: minDecTolerance, + MultiplicativeTolerance: minDecTolerance, + RoundingDir: osmomath.RoundDown, + }, + }, + // out of bounds. + "exponent < 0 - panic": { + exponent: osmomath.ZeroDec().Sub(smallValue), + expectPanic: true, + }, + "exponent > 1 - panic": { + exponent: osmomath.OneDec().Add(smallValue), + expectPanic: true, + }, + } + + for name, tc := range tests { + tc := tc + t.Run(name, func(t *testing.T) { + osmoassert.ConditionalPanic(t, tc.expectPanic, func() { + + // System under test. + result := osmomath.Exp2ChebyshevRationalApprox(tc.exponent) + + // Reuse the same test cases for exp2 that is a wrapper around Exp2ChebyshevRationalApprox. + // This is done to reduce boilerplate from duplicating test cases. + resultExp2 := osmomath.Exp2(tc.exponent) + require.Equal(t, result, resultExp2) + + require.Equal(t, 0, tc.errTolerance.CompareBigDec(tc.expectedResult, result)) + }) + }) + } +} + +func TestExp2(t *testing.T) { + tests := map[string]struct { + exponent osmomath.BigDec + expectedResult osmomath.BigDec + errTolerance osmoutils.ErrTolerance + expectPanic bool + }{ + "exp2(28.5)": { + exponent: osmomath.MustNewDecFromStr("28.5"), + // https://www.wolframalpha.com/input?i=2%5E%2828.5%29+45+digits + expectedResult: osmomath.MustNewDecFromStr("379625062.497006211556423566253288543343173698"), + + errTolerance: osmoutils.ErrTolerance{ + AdditiveTolerance: minDecTolerance, + MultiplicativeTolerance: minDecTolerance, + RoundingDir: osmomath.RoundUnconstrained, // TODO: confirm if this is acceptable. + }, + }, + "exp2(63.84864288)": { + exponent: osmomath.MustNewDecFromStr("63.84864288"), + // https://www.wolframalpha.com/input?i=2%5E%2863.84864288%29+56+digits + expectedResult: osmomath.MustNewDecFromStr("16609504985074238416.013387053450559984846024066925604094"), + + errTolerance: osmoutils.ErrTolerance{ + AdditiveTolerance: sdk.MustNewDecFromStr("0.00042"), + MultiplicativeTolerance: minDecTolerance, + RoundingDir: osmomath.RoundUnconstrained, // TODO: confirm if this is acceptable. + }, + }, + "exp2(64.5)": { + exponent: osmomath.MustNewDecFromStr("64.5"), + // https://www.wolframalpha.com/input?i=2%5E%2864.5%29+56+digits + expectedResult: osmomath.MustNewDecFromStr("26087635650665564424.699143612505016737766552579185717157"), + + errTolerance: osmoutils.ErrTolerance{ + AdditiveTolerance: sdk.MustNewDecFromStr("0.000000000000000008"), + MultiplicativeTolerance: minDecTolerance, + RoundingDir: osmomath.RoundUnconstrained, // TODO: confirm if this is acceptable. + }, + }, + "exp2(80.5)": { + exponent: osmomath.MustNewDecFromStr("80.5"), + // https://www.wolframalpha.com/input?i=2%5E%2880.5%29+61+digits + expectedResult: osmomath.MustNewDecFromStr("1709679290002018430137083.075789128776926268789829515159631571"), + + errTolerance: osmoutils.ErrTolerance{ + AdditiveTolerance: sdk.MustNewDecFromStr("0.0000000000006"), + MultiplicativeTolerance: minDecTolerance, + RoundingDir: osmomath.RoundUnconstrained, // TODO: confirm if this is acceptable. + }, + }, + "exp2(100.5)": { + exponent: osmomath.MustNewDecFromStr("100.5"), + // https://www.wolframalpha.com/input?i=2%5E%28100.5%29+67+digits + expectedResult: osmomath.MustNewDecFromStr("1792728671193156477399422023278.661496394239222564273688025833797661"), + + errTolerance: osmoutils.ErrTolerance{ + AdditiveTolerance: sdk.MustNewDecFromStr("0.0000006"), + MultiplicativeTolerance: minDecTolerance, + RoundingDir: osmomath.RoundUnconstrained, // TODO: confirm if this is acceptable. + }, + }, + "exp2(128.5)": { + exponent: osmomath.MustNewDecFromStr("128.5"), + // https://www.wolframalpha.com/input?i=2%5E%28128.5%29+75+digits + expectedResult: osmomath.MustNewDecFromStr("481231938336009023090067544955250113854.229961482126296754016435255422777776"), + + errTolerance: osmoutils.ErrTolerance{ + AdditiveTolerance: sdk.MustNewDecFromStr("146.5"), + MultiplicativeTolerance: minDecTolerance, + RoundingDir: osmomath.RoundUnconstrained, // TODO: confirm if this is acceptable. + }, + }, + "exp2(127.999999999999999999999999999999999999)": { + exponent: osmomath.MustNewDecFromStr("127.999999999999999999999999999999999999"), + // https://www.wolframalpha.com/input?i=2%5E%28127.999999999999999999999999999999999999%29+75+digits + expectedResult: osmomath.MustNewDecFromStr("340282366920938463463374607431768211220.134236774486705862055857235845515682"), + + errTolerance: osmoutils.ErrTolerance{ + AdditiveTolerance: sdk.MustNewDecFromStr("15044647266406936"), + MultiplicativeTolerance: minDecTolerance, + RoundingDir: osmomath.RoundDown, + }, + }, + "exp2(127.84864288)": { + exponent: osmomath.MustNewDecFromStr("127.84864288"), + // https://www.wolframalpha.com/input?i=2%5E%28127.84864288%29+75+digits + expectedResult: osmomath.MustNewDecFromStr("306391287650667462068703337664945630660.398687487527674545778353588077174571"), + + errTolerance: osmoutils.ErrTolerance{ + AdditiveTolerance: sdk.MustNewDecFromStr("7707157415597963"), + MultiplicativeTolerance: minDecTolerance, + RoundingDir: osmomath.RoundUnconstrained, + }, + }, + "panic, too large - positive": { + exponent: osmomath.MaxSupportedExponent.Add(osmomath.OneDec()), + expectPanic: true, + }, + "panic - negative exponent": { + exponent: osmomath.OneDec().Neg(), + expectPanic: true, + }, + "at exponent boundary - positive": { + exponent: osmomath.MaxSupportedExponent, + // https://www.wolframalpha.com/input?i=2%5E%282%5E9%29 + expectedResult: osmomath.MustNewDecFromStr("13407807929942597099574024998205846127479365820592393377723561443721764030073546976801874298166903427690031858186486050853753882811946569946433649006084096"), + + errTolerance: osmoutils.ErrTolerance{ + AdditiveTolerance: minDecTolerance, + MultiplicativeTolerance: minDecTolerance, + RoundingDir: osmomath.RoundDown, + }, + }, + } + + for name, tc := range tests { + tc := tc + t.Run(name, func(t *testing.T) { + osmoassert.ConditionalPanic(t, tc.expectPanic, func() { + + // System under test. + result := osmomath.Exp2(tc.exponent) + + require.Equal(t, 0, tc.errTolerance.CompareBigDec(tc.expectedResult, result)) + }) + }) + } +} diff --git a/osmomath/export_test.go b/osmomath/export_test.go new file mode 100644 index 00000000000..a018e369b9f --- /dev/null +++ b/osmomath/export_test.go @@ -0,0 +1,13 @@ +package osmomath + +var ( + MaxSupportedExponent = maxSupportedExponent +) + +func Exp2(exponent BigDec) BigDec { + return exp2(exponent) +} + +func Exp2ChebyshevRationalApprox(exponent BigDec) BigDec { + return exp2ChebyshevRationalApprox(exponent) +} diff --git a/scripts/approximations/approximations.py b/scripts/approximations/approximations.py index 19849698088..1a434509667 100644 --- a/scripts/approximations/approximations.py +++ b/scripts/approximations/approximations.py @@ -84,9 +84,9 @@ def compute_max_error(y_approximation, y_actual) -> sp.Float: max = sp.Max(max, cur_abs) return max -def compute_error_range(y_approximation: list, y_actual: list) -> list[sp.Float]: +def compute_absolute_error_range(y_approximation: list, y_actual: list) -> list[sp.Float]: """ Given an approximated list of y values and actual y values, computes and returns - error deltas between them. + absolute error between them, computed as | y_approximation[i] - y_actual[i] |. CONTRACT: - y_approximation and y_actual must be the same length @@ -101,6 +101,29 @@ def compute_error_range(y_approximation: list, y_actual: list) -> list[sp.Float] result.append(cur_abs) return result +def compute_relative_error_range(y_approximation: list, y_actual: list) -> list[sp.Float]: + """ Given an approximated list of y values and actual y values, computes and returns + relative error between them, computed as | y_approximation[i] - y_actual[i] | / y_actual[i]. + + For y_actual[i] = 0, relative error is defined as 0. + + CONTRACT: + - y_approximation and y_actual must be the same length + - for every i in range(len(y_approximation)), y_approximation[i] and y_actual[i] must correspond to the + same x coordinate + """ + result = [] + for i in range(len(y_approximation)): + if y_actual[i] == 0: + result.append(0) + continue + + cur_relative_error = sp.Abs(y_approximation[i] - y_actual[i]) / y_actual[i] + if cur_relative_error is sp.nan: + raise ValueError(F"cur_abs is nan. y_approximation[i] ({y_approximation[i]}) and y_actual[i] ({y_actual[i]})") + result.append(cur_relative_error) + return result + def equispaced_poly_approx(fn, x_start: sp.Float, x_end: sp.Float, num_terms: int): """ Returns the coefficients for an equispaced polynomial between x_start and x_end with num_terms terms. diff --git a/scripts/approximations/main.py b/scripts/approximations/main.py index b91433855c9..47875ed7b24 100644 --- a/scripts/approximations/main.py +++ b/scripts/approximations/main.py @@ -20,10 +20,10 @@ num_parameters_errors = 30 # number of (x,y) coordinates used to plot the resulting approximation. -num_points_plot = 100000 +num_points_plot = 10000 # function to approximate -approximated_fn = lambda x: sp.Pow(sp.E, x) +approximated_fn = lambda x: sp.Pow(2, x) # fixed point precision used in Osmosis `osmomath` package. osmomath_precision = 36 @@ -48,16 +48,23 @@ # Plots if true. shouldPlotMaxError = True -def plot_error_range(x_coordinates, y_approximation, y_actual): +def plot_error_range(x_coordinates, y_approximation, y_actual, is_absolute: bool): """ Given x coordinates that correspond to approximated y coordinates and actual y coordinates, - compute the deltas between y approximated and y actual and plot them in log scale on y. + computes the error between y approximated and y actual and plot them in log scale on y. + + If is_absolute, plots the absolute error. Otherwise, plots the relative error. """ - error_deltas = approximations.compute_error_range(y_approximation, y_actual) + if is_absolute: + error_kind_str = "Absolute" + error_deltas = approximations.compute_absolute_error_range(y_approximation, y_actual) + else: + error_kind_str = "Relative" + error_deltas = approximations.compute_relative_error_range(y_approximation, y_actual) plt.semilogy(x_coordinates, error_deltas) plt.grid(True) - plt.title(f"Chebyshev Rational e^x Errors on [{x_start}, {x_end}]. {num_parameters} params, {num_points_plot} points") + plt.title(f"Chebyshev Rational e^x {error_kind_str} Errors on [{x_start}, {x_end}]. {num_parameters} params, {num_points_plot} points") plt.show() # This script does the following: @@ -171,14 +178,41 @@ def main(): def exponent_approximation_choice(): # Equispaced x coordinates to be used for plotting every approximation. x_coordinates = approximations.linspace(x_start, x_end, num_points_plot) - x_coordinates = [sp.Float(sp.N(coef, osmomath_precision + 1), osmomath_precision + 1) for coef in x_coordinates] + x_coordinates = [sp.N(sp.Float(coef, osmomath_precision), n=osmomath_precision) for coef in x_coordinates] - # Chebyshev Rational Approximation to get the coefficients. - coef_numerator, coef_denominator = approximations.chebyshev_rational_approx(approximated_fn, x_start, x_end, num_parameters) + print(x_coordinates) - # Truncate the coefficients to osmomath precision. - coef_numerator = [sp.Float(sp.N(coef, osmomath_precision + 1), osmomath_precision + 1) for coef in coef_numerator] - coef_denominator = [sp.Float(sp.N(coef, osmomath_precision + 1), osmomath_precision + 1) for coef in coef_denominator] + # Chebyshev Rational Approximation to get the coefficients. + # coef_numerator, coef_denominator = approximations.chebyshev_rational_approx(approximated_fn, x_start, x_end, num_parameters) + # coef_numerator = [sp.N(coef, osmomath_precision + 2) for coef in coef_numerator] + # coef_denominator = [sp.N(coef, osmomath_precision + 2) for coef in coef_denominator] + + # Hard code and round up numerator coefficientst that are to be used in production + # Hard code and round down numerator coefficientst that are to be used in production + # Both of these are calculated us=ing the above commented out code. + + coef_numerator = [ + sp.N(sp.Float("1.000000000000000000000044212244679434", osmomath_precision), n=osmomath_precision), + sp.N(sp.Float("0.352032455817400196452603772766844426", osmomath_precision), n=osmomath_precision), + sp.N(sp.Float("0.056507868883666405413116800969512484", osmomath_precision), n=osmomath_precision), + sp.N(sp.Float("0.005343900728213034434757419480319916", osmomath_precision), n=osmomath_precision), + sp.N(sp.Float("0.000317708814342353603087543715930732", osmomath_precision), n=osmomath_precision), + sp.N(sp.Float("0.000011429747507407623028722262874632", osmomath_precision), n=osmomath_precision), + sp.N(sp.Float("0.000000198381965651614980168744540366", osmomath_precision), n=osmomath_precision), + ] + + coef_denominator = [ + sp.N(sp.Float("1.0000000000000000000000000000000000000", osmomath_precision), n=osmomath_precision), + sp.N(sp.Float("-0.341114724742545112949699755780593311", osmomath_precision), n=osmomath_precision), + sp.N(sp.Float("0.052724071627342653404436933178482287", osmomath_precision), n=osmomath_precision), + sp.N(sp.Float("-0.004760950735524957576233524801866342", osmomath_precision), n=osmomath_precision), + sp.N(sp.Float("0.000267168475410566529819971616894193", osmomath_precision), n=osmomath_precision), + sp.N(sp.Float("-0.000008923715368802211181557353097439", osmomath_precision), n=osmomath_precision), + sp.N(sp.Float("0.000000140277233177373698516010555916", osmomath_precision), n=osmomath_precision), + ] + + print(coef_numerator) + print(coef_denominator) # Evaluate approximation. y_chebyshev_rational = rational.evaluate(x_coordinates, coef_numerator, coef_denominator) @@ -186,7 +220,7 @@ def exponent_approximation_choice(): # Compute Actual Values y_actual = approximations.get_y_actual(approximated_fn, x_coordinates) - plot_error_range(x_coordinates, y_chebyshev_rational, y_actual) + plot_error_range(x_coordinates, y_chebyshev_rational, y_actual, True) if __name__ == "__main__": # Uncomment to run the main script. From 025feb0b44cdf4995d1d773e2ce85d29a679c6c7 Mon Sep 17 00:00:00 2001 From: Roman Date: Tue, 13 Dec 2022 16:38:04 -0800 Subject: [PATCH 02/17] export exp2 --- osmomath/exp2.go | 4 ++-- osmomath/export_test.go | 4 ---- 2 files changed, 2 insertions(+), 6 deletions(-) diff --git a/osmomath/exp2.go b/osmomath/exp2.go index baa278cee2b..d27c08a11a3 100644 --- a/osmomath/exp2.go +++ b/osmomath/exp2.go @@ -33,13 +33,13 @@ var ( maxSupportedExponent = MustNewDecFromStr("2").PowerInteger(9) ) -// exp2 takes 2 to the power of a given non-negative decimal exponent. +// Exp2 takes 2 to the power of a given non-negative decimal exponent. // and returns the result. // The computation is performed by using th following property: // 2^decimal_exp = 2^{integer_exp + fractional_exp} = 2^integer_exp * 2^fractional_exp // The max supported exponent is 2^10. If greater exponent is given, the function panics. // Panics if the exponent is negative. -func exp2(exponent BigDec) BigDec { +func Exp2(exponent BigDec) BigDec { if exponent.Abs().GT(maxSupportedExponent) { panic(fmt.Sprintf("integer exponent %s is too large, max (%s)", exponent, maxSupportedExponent)) } diff --git a/osmomath/export_test.go b/osmomath/export_test.go index a018e369b9f..2331e90dbbe 100644 --- a/osmomath/export_test.go +++ b/osmomath/export_test.go @@ -4,10 +4,6 @@ var ( MaxSupportedExponent = maxSupportedExponent ) -func Exp2(exponent BigDec) BigDec { - return exp2(exponent) -} - func Exp2ChebyshevRationalApprox(exponent BigDec) BigDec { return exp2ChebyshevRationalApprox(exponent) } From a54806899bca0db8a42c1ab68c0c1cdf1dfa620e Mon Sep 17 00:00:00 2001 From: Roman Date: Tue, 13 Dec 2022 18:03:04 -0800 Subject: [PATCH 03/17] changelog --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 813327ef9da..1bfbcfedec4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -49,6 +49,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 * [#3677](https://github.com/osmosis-labs/osmosis/pull/3677) Add methods for cloning and mutative multiplication on osmomath.BigDec. * [#3676](https://github.com/osmosis-labs/osmosis/pull/3676) implement `PowerInteger` function on `osmomath.BigDec` * [#3678](https://github.com/osmosis-labs/osmosis/pull/3678) implement mutative `PowerIntegerMut` function on `osmomath.BigDec`. +* [#3708](https://github.com/osmosis-labs/osmosis/pull/3708) mutative `Exp2` function to compute 2^decimal. ### Bug fixes From ea6c719bbec0a7f78063b3c5f812c1f7344624a7 Mon Sep 17 00:00:00 2001 From: Roman Date: Tue, 13 Dec 2022 18:13:50 -0800 Subject: [PATCH 04/17] refactor ErrTolerance to use Dec instead of Int for additive tolerance --- osmoutils/binary_search.go | 8 ++-- osmoutils/binary_search_test.go | 38 +++++++++---------- x/gamm/pool-models/balancer/pool_test.go | 2 +- x/gamm/pool-models/internal/cfmm_common/lp.go | 2 +- x/gamm/pool-models/stableswap/amm.go | 2 +- x/gamm/pool-models/stableswap/amm_test.go | 2 +- 6 files changed, 27 insertions(+), 27 deletions(-) diff --git a/osmoutils/binary_search.go b/osmoutils/binary_search.go index 27ea4b4598f..03016a395a6 100644 --- a/osmoutils/binary_search.go +++ b/osmoutils/binary_search.go @@ -23,7 +23,7 @@ import ( // RoundingDir = RoundUnconstrained. // Note that if AdditiveTolerance == 0, then this is equivalent to a standard compare. type ErrTolerance struct { - AdditiveTolerance sdk.Int + AdditiveTolerance sdk.Dec MultiplicativeTolerance sdk.Dec RoundingDir osmomath.RoundingDirection } @@ -33,7 +33,7 @@ type ErrTolerance struct { // returns 1 if not, and expected > actual. // returns -1 if not, and expected < actual func (e ErrTolerance) Compare(expected sdk.Int, actual sdk.Int) int { - diff := expected.Sub(actual).Abs() + diff := expected.ToDec().Sub(actual.ToDec()).Abs() comparisonSign := 0 if expected.GT(actual) { @@ -71,7 +71,7 @@ func (e ErrTolerance) Compare(expected sdk.Int, actual sdk.Int) int { } // Check multiplicative tolerance equations if !e.MultiplicativeTolerance.IsNil() && !e.MultiplicativeTolerance.IsZero() { - errTerm := diff.ToDec().Quo(sdk.MinInt(expected.Abs(), actual.Abs()).ToDec()) + errTerm := diff.Quo(sdk.MinInt(expected.Abs(), actual.Abs()).ToDec()) if errTerm.GT(e.MultiplicativeTolerance) { return comparisonSign } @@ -117,7 +117,7 @@ func (e ErrTolerance) CompareBigDec(expected osmomath.BigDec, actual osmomath.Bi } } - if diff.GT(osmomath.BigDecFromSDKDec(e.AdditiveTolerance.ToDec())) { + if diff.GT(osmomath.BigDecFromSDKDec(e.AdditiveTolerance)) { return comparisonSign } } diff --git a/osmoutils/binary_search_test.go b/osmoutils/binary_search_test.go index 95f7d5afb56..505f71ec5b9 100644 --- a/osmoutils/binary_search_test.go +++ b/osmoutils/binary_search_test.go @@ -12,7 +12,7 @@ import ( ) var ( - withinOne = ErrTolerance{AdditiveTolerance: sdk.OneInt()} + withinOne = ErrTolerance{AdditiveTolerance: sdk.OneDec()} withinFactor8 = ErrTolerance{MultiplicativeTolerance: sdk.NewDec(8)} zero = osmomath.ZeroDec() ) @@ -29,10 +29,10 @@ func TestBinarySearch(t *testing.T) { output := sdk.Int(result) return output, nil } - noErrTolerance := ErrTolerance{AdditiveTolerance: sdk.ZeroInt()} - testErrToleranceAdditive := ErrTolerance{AdditiveTolerance: sdk.NewInt(1 << 20)} - testErrToleranceMultiplicative := ErrTolerance{AdditiveTolerance: sdk.ZeroInt(), MultiplicativeTolerance: sdk.NewDec(10)} - testErrToleranceBoth := ErrTolerance{AdditiveTolerance: sdk.NewInt(1 << 20), MultiplicativeTolerance: sdk.NewDec(1 << 3)} + noErrTolerance := ErrTolerance{AdditiveTolerance: sdk.ZeroDec()} + testErrToleranceAdditive := ErrTolerance{AdditiveTolerance: sdk.NewDec(1 << 20)} + testErrToleranceMultiplicative := ErrTolerance{AdditiveTolerance: sdk.ZeroDec(), MultiplicativeTolerance: sdk.NewDec(10)} + testErrToleranceBoth := ErrTolerance{AdditiveTolerance: sdk.NewDec(1 << 20), MultiplicativeTolerance: sdk.NewDec(1 << 3)} tests := map[string]struct { f func(sdk.Int) (sdk.Int, error) lowerbound sdk.Int @@ -82,13 +82,13 @@ func lineF(a osmomath.BigDec) (osmomath.BigDec, error) { return a, nil } func cubicF(a osmomath.BigDec) (osmomath.BigDec, error) { - return a.Power(3), nil + return a.PowerInteger(3), nil } -var negCubicFConstant = osmomath.NewBigDec(1 << 62).Power(3).Neg() +var negCubicFConstant = osmomath.NewBigDec(1 << 62).PowerInteger(3).Neg() func negCubicF(a osmomath.BigDec) (osmomath.BigDec, error) { - return a.Power(3).Add(negCubicFConstant), nil + return a.PowerInteger(3).Add(negCubicFConstant), nil } type searchFn func(osmomath.BigDec) (osmomath.BigDec, error) @@ -153,9 +153,9 @@ var fnMap = map[string]searchFn{"line": lineF, "cubic": cubicF, "neg_cubic": neg // This function tests that any value in a given range can be reached within expected num iterations. func TestIterationDepthRandValue(t *testing.T) { tests := map[string]binarySearchTestCase{} - exactEqual := ErrTolerance{AdditiveTolerance: sdk.ZeroInt()} - withinOne := ErrTolerance{AdditiveTolerance: sdk.OneInt()} - within32 := ErrTolerance{AdditiveTolerance: sdk.OneInt().MulRaw(32)} + exactEqual := ErrTolerance{AdditiveTolerance: sdk.ZeroDec()} + withinOne := ErrTolerance{AdditiveTolerance: sdk.OneDec()} + within32 := ErrTolerance{AdditiveTolerance: sdk.OneDec().Mul(sdk.NewDec(32))} createRandInput := func(fnName string, lowerbound, upperbound int64, errTolerance ErrTolerance, maxNumIters int, errToleranceName string) { @@ -227,12 +227,12 @@ func runBinarySearchTestCases(t *testing.T, tests map[string]binarySearchTestCas } func TestBinarySearchBigDec(t *testing.T) { - testErrToleranceAdditive := ErrTolerance{AdditiveTolerance: sdk.NewInt(1 << 30)} - errToleranceBoth := ErrTolerance{AdditiveTolerance: sdk.NewInt(1 << 30), MultiplicativeTolerance: sdk.NewDec(1 << 3)} + testErrToleranceAdditive := ErrTolerance{AdditiveTolerance: sdk.NewDec(1 << 30)} + errToleranceBoth := ErrTolerance{AdditiveTolerance: sdk.NewDec(1 << 30), MultiplicativeTolerance: sdk.NewDec(1 << 3)} twoTo50 := osmomath.NewBigDec(1 << 50) twoTo25PlusOne := osmomath.NewBigDec(1 + (1 << 25)) - twoTo25PlusOneCubed := twoTo25PlusOne.Power(3) + twoTo25PlusOneCubed := twoTo25PlusOne.PowerInteger(3) tests := map[string]binarySearchTestCase{ "cubic f, no err tolerance, converges": {cubicF, zero, twoTo50, twoTo25PlusOneCubed, withinOne, 51, twoTo25PlusOne, false}, @@ -279,7 +279,7 @@ func TestBinarySearchBigDec(t *testing.T) { } func TestBinarySearchRoundingBehavior(t *testing.T) { - withinTwoTo30 := ErrTolerance{AdditiveTolerance: sdk.NewInt(1 << 30)} + withinTwoTo30 := ErrTolerance{AdditiveTolerance: sdk.NewDec(1 << 30)} twoTo50 := osmomath.NewBigDec(1 << 50) // twoTo25PlusOne := osmomath.NewBigDec(1 + (1 << 25)) @@ -318,10 +318,10 @@ func TestBinarySearchRoundingBehavior(t *testing.T) { } func TestErrTolerance_Compare(t *testing.T) { - ZeroErrTolerance := ErrTolerance{AdditiveTolerance: sdk.ZeroInt(), MultiplicativeTolerance: sdk.Dec{}} - NonZeroErrAdditive := ErrTolerance{AdditiveTolerance: sdk.NewInt(10), MultiplicativeTolerance: sdk.Dec{}} - NonZeroErrMultiplicative := ErrTolerance{AdditiveTolerance: sdk.Int{}, MultiplicativeTolerance: sdk.NewDec(10)} - NonZeroErrBoth := ErrTolerance{AdditiveTolerance: sdk.NewInt(1), MultiplicativeTolerance: sdk.NewDec(10)} + ZeroErrTolerance := ErrTolerance{AdditiveTolerance: sdk.ZeroDec(), MultiplicativeTolerance: sdk.Dec{}} + NonZeroErrAdditive := ErrTolerance{AdditiveTolerance: sdk.NewDec(10), MultiplicativeTolerance: sdk.Dec{}} + NonZeroErrMultiplicative := ErrTolerance{AdditiveTolerance: sdk.Dec{}, MultiplicativeTolerance: sdk.NewDec(10)} + NonZeroErrBoth := ErrTolerance{AdditiveTolerance: sdk.NewDec(1), MultiplicativeTolerance: sdk.NewDec(10)} tests := []struct { name string tol ErrTolerance diff --git a/x/gamm/pool-models/balancer/pool_test.go b/x/gamm/pool-models/balancer/pool_test.go index 8cfa372137e..2adbf032a11 100644 --- a/x/gamm/pool-models/balancer/pool_test.go +++ b/x/gamm/pool-models/balancer/pool_test.go @@ -569,7 +569,7 @@ func (suite *BalancerTestSuite) TestBalancerCalculateAmountOutAndIn_InverseRelat suite.Require().NotNil(pool) errTolerance := osmoutils.ErrTolerance{ - AdditiveTolerance: sdk.OneInt(), MultiplicativeTolerance: sdk.Dec{}} + AdditiveTolerance: sdk.OneDec(), MultiplicativeTolerance: sdk.Dec{}} sut := func() { test_helpers.TestCalculateAmountOutAndIn_InverseRelationship(suite.T(), ctx, pool, poolAssetIn.Token.Denom, poolAssetOut.Token.Denom, tc.initialCalcOut, swapFeeDec, errTolerance) } diff --git a/x/gamm/pool-models/internal/cfmm_common/lp.go b/x/gamm/pool-models/internal/cfmm_common/lp.go index 61d4f526c2d..47cbf4ee9ed 100644 --- a/x/gamm/pool-models/internal/cfmm_common/lp.go +++ b/x/gamm/pool-models/internal/cfmm_common/lp.go @@ -155,7 +155,7 @@ func BinarySearchSingleAssetJoin( } // We accept an additive tolerance of 1 LP share error and round down - errTolerance := osmoutils.ErrTolerance{AdditiveTolerance: sdk.OneInt(), MultiplicativeTolerance: sdk.Dec{}, RoundingDir: osmomath.RoundDown} + errTolerance := osmoutils.ErrTolerance{AdditiveTolerance: sdk.OneDec(), MultiplicativeTolerance: sdk.Dec{}, RoundingDir: osmomath.RoundDown} numLPShares, err = osmoutils.BinarySearch( estimateCoinOutGivenShares, diff --git a/x/gamm/pool-models/stableswap/amm.go b/x/gamm/pool-models/stableswap/amm.go index cdbdbf5a24c..8081fcb6164 100644 --- a/x/gamm/pool-models/stableswap/amm.go +++ b/x/gamm/pool-models/stableswap/amm.go @@ -327,7 +327,7 @@ func solveCFMMBinarySearchMulti(xReserve, yReserve, wSumSquares, yIn osmomath.Bi maxIterations := 256 // we use a geometric error tolerance that guarantees approximately 10^-12 precision on outputs - errTolerance := osmoutils.ErrTolerance{AdditiveTolerance: sdk.Int{}, MultiplicativeTolerance: sdk.NewDecWithPrec(1, 12)} + errTolerance := osmoutils.ErrTolerance{AdditiveTolerance: sdk.Dec{}, MultiplicativeTolerance: sdk.NewDecWithPrec(1, 12)} // if yIn is positive, we want to under-estimate the amount of xOut. // This means, we want x_out to be rounded down, as x_out = x_init - x_final, for x_init > x_final. diff --git a/x/gamm/pool-models/stableswap/amm_test.go b/x/gamm/pool-models/stableswap/amm_test.go index dacdbd8315d..332fdbfada4 100644 --- a/x/gamm/pool-models/stableswap/amm_test.go +++ b/x/gamm/pool-models/stableswap/amm_test.go @@ -727,7 +727,7 @@ func (suite *StableSwapTestSuite) Test_StableSwap_CalculateAmountOutAndIn_Invers pool := createTestPool(suite.T(), tc.poolLiquidity, swapFeeDec, exitFeeDec, tc.scalingFactors) suite.Require().NotNil(pool) errTolerance := osmoutils.ErrTolerance{ - AdditiveTolerance: sdk.Int{}, MultiplicativeTolerance: sdk.NewDecWithPrec(1, 12)} + AdditiveTolerance: sdk.Dec{}, MultiplicativeTolerance: sdk.NewDecWithPrec(1, 12)} test_helpers.TestCalculateAmountOutAndIn_InverseRelationship(suite.T(), ctx, pool, tc.denomIn, tc.denomOut, tc.initialCalcOut, swapFeeDec, errTolerance) }) } From 5aba207c23dbe1c2025fdc47b7efa3691e1e4d9a Mon Sep 17 00:00:00 2001 From: Roman Date: Tue, 13 Dec 2022 21:23:18 -0500 Subject: [PATCH 05/17] Update osmomath/exp2.go --- osmomath/exp2.go | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/osmomath/exp2.go b/osmomath/exp2.go index d27c08a11a3..8bd57fb58b1 100644 --- a/osmomath/exp2.go +++ b/osmomath/exp2.go @@ -27,9 +27,9 @@ var ( MustNewDecFromStr("0.000000140277233177373698516010555916"), } - // maxSupportedExponent = 2^10. The value is chosen by finding - // at which value some of the underlying internal functions overflow. - // If needed, exp2 can be reimplemented to allow for greater exponents. + // maxSupportedExponent = 2^10. The value is chosen by benchmarking + // when the underlying internal functions overflow. + // If needed in the future, Exp2 can be reimplemented to allow for greater exponents. maxSupportedExponent = MustNewDecFromStr("2").PowerInteger(9) ) From b72f11492509f2c4a3fa90d30bd8fb345a398bd9 Mon Sep 17 00:00:00 2001 From: Roman Date: Tue, 13 Dec 2022 21:23:23 -0500 Subject: [PATCH 06/17] Update osmomath/exp2.go --- osmomath/exp2.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osmomath/exp2.go b/osmomath/exp2.go index 8bd57fb58b1..02548f60544 100644 --- a/osmomath/exp2.go +++ b/osmomath/exp2.go @@ -33,7 +33,7 @@ var ( maxSupportedExponent = MustNewDecFromStr("2").PowerInteger(9) ) -// Exp2 takes 2 to the power of a given non-negative decimal exponent. +// Exp2 takes 2 to the power of a given non-negative decimal exponent // and returns the result. // The computation is performed by using th following property: // 2^decimal_exp = 2^{integer_exp + fractional_exp} = 2^integer_exp * 2^fractional_exp From 7af4d5c76e7e4d11085705cad2803a4c6d20c230 Mon Sep 17 00:00:00 2001 From: Roman Date: Tue, 13 Dec 2022 21:23:28 -0500 Subject: [PATCH 07/17] Update osmomath/exp2.go --- osmomath/exp2.go | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/osmomath/exp2.go b/osmomath/exp2.go index 02548f60544..a22e5b8c407 100644 --- a/osmomath/exp2.go +++ b/osmomath/exp2.go @@ -37,7 +37,8 @@ var ( // and returns the result. // The computation is performed by using th following property: // 2^decimal_exp = 2^{integer_exp + fractional_exp} = 2^integer_exp * 2^fractional_exp -// The max supported exponent is 2^10. If greater exponent is given, the function panics. +// The max supported exponent is defined by the global maxSupportedExponent. +// If a greater exponent is given, the function panics. // Panics if the exponent is negative. func Exp2(exponent BigDec) BigDec { if exponent.Abs().GT(maxSupportedExponent) { From 8e081a61ff58994dc32e60236cee52a50c91509d Mon Sep 17 00:00:00 2001 From: Roman Date: Tue, 13 Dec 2022 21:23:33 -0500 Subject: [PATCH 08/17] Update osmomath/exp2_test.go --- osmomath/exp2_test.go | 1 - 1 file changed, 1 deletion(-) diff --git a/osmomath/exp2_test.go b/osmomath/exp2_test.go index e85300e0938..51afa61a6ef 100644 --- a/osmomath/exp2_test.go +++ b/osmomath/exp2_test.go @@ -17,7 +17,6 @@ var ( ) func TestExp2ChebyshevRationalApprox(t *testing.T) { - smallValue := osmomath.MustNewDecFromStr("0.00001") smallerValue := osmomath.MustNewDecFromStr("0.00000000000000000001") From 3a7a39cb50181f2eaf24c295e21e765266e9106d Mon Sep 17 00:00:00 2001 From: Roman Date: Tue, 13 Dec 2022 21:23:38 -0500 Subject: [PATCH 09/17] Update osmomath/exp2_test.go --- osmomath/exp2_test.go | 1 - 1 file changed, 1 deletion(-) diff --git a/osmomath/exp2_test.go b/osmomath/exp2_test.go index 51afa61a6ef..3a7e0bc0c8c 100644 --- a/osmomath/exp2_test.go +++ b/osmomath/exp2_test.go @@ -151,7 +151,6 @@ func TestExp2ChebyshevRationalApprox(t *testing.T) { tc := tc t.Run(name, func(t *testing.T) { osmoassert.ConditionalPanic(t, tc.expectPanic, func() { - // System under test. result := osmomath.Exp2ChebyshevRationalApprox(tc.exponent) From f73b3f2d4866d3928afa2a6d906c7970d3c1f6f5 Mon Sep 17 00:00:00 2001 From: Roman Date: Fri, 16 Dec 2022 14:36:27 -0800 Subject: [PATCH 10/17] do bit shift instead of multiplication --- osmomath/exp2.go | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/osmomath/exp2.go b/osmomath/exp2.go index a22e5b8c407..e3e01c33977 100644 --- a/osmomath/exp2.go +++ b/osmomath/exp2.go @@ -48,16 +48,14 @@ func Exp2(exponent BigDec) BigDec { panic(fmt.Sprintf("negative exponent %s is not supported", exponent)) } - integerExponentDec := exponent.TruncateDec() - integerExponent := integerExponentDec.TruncateInt() - integerResult := twoBigDec.PowerInteger(integerExponent.Uint64()) + integerExponent := exponent.TruncateDec() - fractionalExponent := exponent.Sub(integerExponentDec) + fractionalExponent := exponent.Sub(integerExponent) fractionalResult := exp2ChebyshevRationalApprox(fractionalExponent) - result := integerResult.Mul(fractionalResult) + fractionalResult.i = fractionalResult.i.Lsh(fractionalResult.i, uint(integerExponent.TruncateInt().Uint64())) - return result + return fractionalResult } // exp2ChebyshevRationalApprox takes 2 to the power of a given decimal exponent. From 0ac7811980caf1d9de6568c08a3334bdc721b675 Mon Sep 17 00:00:00 2001 From: Roman Date: Fri, 16 Dec 2022 14:42:50 -0800 Subject: [PATCH 11/17] godoc about error bounds --- osmomath/exp2.go | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/osmomath/exp2.go b/osmomath/exp2.go index e3e01c33977..86a2e81c333 100644 --- a/osmomath/exp2.go +++ b/osmomath/exp2.go @@ -40,6 +40,8 @@ var ( // The max supported exponent is defined by the global maxSupportedExponent. // If a greater exponent is given, the function panics. // Panics if the exponent is negative. +// The answer is correct up to a factor of 10^-18. +// Meaning, result = result * k for k in [1 - 10^(-18), 1 + 10^(-18)] func Exp2(exponent BigDec) BigDec { if exponent.Abs().GT(maxSupportedExponent) { panic(fmt.Sprintf("integer exponent %s is too large, max (%s)", exponent, maxSupportedExponent)) @@ -67,6 +69,8 @@ func Exp2(exponent BigDec) BigDec { // See scripts/approximations/README.md for details of the scripts used // to compute the coefficients. // CONTRACT: exponent must be in the range [0, 1], panics if not. +// The answer is correct up to a factor of 10^-18. +// Meaning, result = result * k for k in [1 - 10^(-18), 1 + 10^(-18)] func exp2ChebyshevRationalApprox(x BigDec) BigDec { if x.LT(ZeroDec()) || x.GT(OneDec()) { panic(fmt.Sprintf("exponent must be in the range [0, 1], got %s", x)) From b878401f9ed248b7561d84dfe25a626b3aa8a4df Mon Sep 17 00:00:00 2001 From: Roman Date: Fri, 16 Dec 2022 14:44:45 -0800 Subject: [PATCH 12/17] comment about bit shift equivalency --- osmomath/exp2.go | 1 + 1 file changed, 1 insertion(+) diff --git a/osmomath/exp2.go b/osmomath/exp2.go index 86a2e81c333..32e77ef2190 100644 --- a/osmomath/exp2.go +++ b/osmomath/exp2.go @@ -55,6 +55,7 @@ func Exp2(exponent BigDec) BigDec { fractionalExponent := exponent.Sub(integerExponent) fractionalResult := exp2ChebyshevRationalApprox(fractionalExponent) + // Left bit shift is equivalent to multiplying by 2^integerExponent. fractionalResult.i = fractionalResult.i.Lsh(fractionalResult.i, uint(integerExponent.TruncateInt().Uint64())) return fractionalResult From 9451599984e347a8f48cb57207a0cff436f9f0dd Mon Sep 17 00:00:00 2001 From: Roman Date: Fri, 16 Dec 2022 14:50:51 -0800 Subject: [PATCH 13/17] merge conflict --- osmomath/exp2_test.go | 43 +++++++++++++++++++++---------------------- 1 file changed, 21 insertions(+), 22 deletions(-) diff --git a/osmomath/exp2_test.go b/osmomath/exp2_test.go index 3a7e0bc0c8c..6935bf346fc 100644 --- a/osmomath/exp2_test.go +++ b/osmomath/exp2_test.go @@ -8,7 +8,6 @@ import ( "github.com/osmosis-labs/osmosis/v13/app/apptesting/osmoassert" "github.com/osmosis-labs/osmosis/v13/osmomath" - "github.com/osmosis-labs/osmosis/v13/osmoutils" ) var ( @@ -23,7 +22,7 @@ func TestExp2ChebyshevRationalApprox(t *testing.T) { tests := map[string]struct { exponent osmomath.BigDec expectedResult osmomath.BigDec - errTolerance osmoutils.ErrTolerance + errTolerance osmomath.ErrTolerance expectPanic bool }{ "exp2(0.5)": { @@ -31,7 +30,7 @@ func TestExp2ChebyshevRationalApprox(t *testing.T) { // https://www.wolframalpha.com/input?i=2%5E0.5+37+digits expectedResult: osmomath.MustNewDecFromStr("1.414213562373095048801688724209698079"), - errTolerance: osmoutils.ErrTolerance{ + errTolerance: osmomath.ErrTolerance{ AdditiveTolerance: minDecTolerance, MultiplicativeTolerance: minDecTolerance, RoundingDir: osmomath.RoundDown, @@ -41,7 +40,7 @@ func TestExp2ChebyshevRationalApprox(t *testing.T) { exponent: osmomath.ZeroDec(), expectedResult: osmomath.OneDec(), - errTolerance: osmoutils.ErrTolerance{ + errTolerance: osmomath.ErrTolerance{ AdditiveTolerance: sdk.ZeroDec(), MultiplicativeTolerance: sdk.ZeroDec(), RoundingDir: osmomath.RoundDown, @@ -51,7 +50,7 @@ func TestExp2ChebyshevRationalApprox(t *testing.T) { exponent: osmomath.OneDec(), expectedResult: osmomath.MustNewDecFromStr("2"), - errTolerance: osmoutils.ErrTolerance{ + errTolerance: osmomath.ErrTolerance{ AdditiveTolerance: sdk.ZeroDec(), MultiplicativeTolerance: sdk.ZeroDec(), RoundingDir: osmomath.RoundDown, @@ -62,7 +61,7 @@ func TestExp2ChebyshevRationalApprox(t *testing.T) { // https://www.wolframalpha.com/input?i=2%5E0.00001+37+digits expectedResult: osmomath.MustNewDecFromStr("1.000006931495828305653209089800561681"), - errTolerance: osmoutils.ErrTolerance{ + errTolerance: osmomath.ErrTolerance{ AdditiveTolerance: minDecTolerance, MultiplicativeTolerance: minDecTolerance, // TODO: confirm if rounding behavior is acceptable. @@ -75,7 +74,7 @@ func TestExp2ChebyshevRationalApprox(t *testing.T) { // https://www.wolframalpha.com/input?i=2%5E0.99999+37+digits expectedResult: osmomath.MustNewDecFromStr("1.999986137104433991477606830496602898"), - errTolerance: osmoutils.ErrTolerance{ + errTolerance: osmomath.ErrTolerance{ AdditiveTolerance: sdk.MustNewDecFromStr("0.00000000000000007"), MultiplicativeTolerance: minDecTolerance.Mul(sdk.NewDec(100)), RoundingDir: osmomath.RoundDown, @@ -86,7 +85,7 @@ func TestExp2ChebyshevRationalApprox(t *testing.T) { // https://www.wolframalpha.com/input?i=2%5E%281+-+0.00000000000000000001%29+37+digits expectedResult: osmomath.MustNewDecFromStr("1.999999999999999999986137056388801094"), - errTolerance: osmoutils.ErrTolerance{ + errTolerance: osmomath.ErrTolerance{ AdditiveTolerance: minDecTolerance, MultiplicativeTolerance: minDecTolerance, RoundingDir: osmomath.RoundDown, @@ -97,7 +96,7 @@ func TestExp2ChebyshevRationalApprox(t *testing.T) { // https://www.wolframalpha.com/input?i=2%5E0.00000000000000000001+37+digits expectedResult: osmomath.MustNewDecFromStr("1.000000000000000000006931471805599453"), - errTolerance: osmoutils.ErrTolerance{ + errTolerance: osmomath.ErrTolerance{ AdditiveTolerance: minDecTolerance, MultiplicativeTolerance: minDecTolerance, RoundingDir: osmomath.RoundUnconstrained, // TODO: confirm if this is acceptable. @@ -108,7 +107,7 @@ func TestExp2ChebyshevRationalApprox(t *testing.T) { // https://www.wolframalpha.com/input?i=2%5E0.3334567+37+digits expectedResult: osmomath.MustNewDecFromStr("1.260028791934303989065848870753742298"), - errTolerance: osmoutils.ErrTolerance{ + errTolerance: osmomath.ErrTolerance{ AdditiveTolerance: sdk.MustNewDecFromStr("0.00000000000000007"), MultiplicativeTolerance: minDecTolerance.Mul(sdk.NewDec(10)), RoundingDir: osmomath.RoundDown, @@ -119,7 +118,7 @@ func TestExp2ChebyshevRationalApprox(t *testing.T) { // https://www.wolframalpha.com/input?i=2%5E0.84864288+37+digits expectedResult: osmomath.MustNewDecFromStr("1.800806138872630518880998772777747572"), - errTolerance: osmoutils.ErrTolerance{ + errTolerance: osmomath.ErrTolerance{ AdditiveTolerance: sdk.MustNewDecFromStr("0.00000000000000002"), MultiplicativeTolerance: minDecTolerance.Mul(sdk.NewDec(10)), RoundingDir: osmomath.RoundUnconstrained, // TODO: confirm if this is acceptable. @@ -130,7 +129,7 @@ func TestExp2ChebyshevRationalApprox(t *testing.T) { // https://www.wolframalpha.com/input?i=2%5E0.999999999999999999999999999999999956+37+digits expectedResult: osmomath.MustNewDecFromStr("1.999999999999999999999999999999999939"), - errTolerance: osmoutils.ErrTolerance{ + errTolerance: osmomath.ErrTolerance{ AdditiveTolerance: minDecTolerance, MultiplicativeTolerance: minDecTolerance, RoundingDir: osmomath.RoundDown, @@ -169,7 +168,7 @@ func TestExp2(t *testing.T) { tests := map[string]struct { exponent osmomath.BigDec expectedResult osmomath.BigDec - errTolerance osmoutils.ErrTolerance + errTolerance osmomath.ErrTolerance expectPanic bool }{ "exp2(28.5)": { @@ -177,7 +176,7 @@ func TestExp2(t *testing.T) { // https://www.wolframalpha.com/input?i=2%5E%2828.5%29+45+digits expectedResult: osmomath.MustNewDecFromStr("379625062.497006211556423566253288543343173698"), - errTolerance: osmoutils.ErrTolerance{ + errTolerance: osmomath.ErrTolerance{ AdditiveTolerance: minDecTolerance, MultiplicativeTolerance: minDecTolerance, RoundingDir: osmomath.RoundUnconstrained, // TODO: confirm if this is acceptable. @@ -188,7 +187,7 @@ func TestExp2(t *testing.T) { // https://www.wolframalpha.com/input?i=2%5E%2863.84864288%29+56+digits expectedResult: osmomath.MustNewDecFromStr("16609504985074238416.013387053450559984846024066925604094"), - errTolerance: osmoutils.ErrTolerance{ + errTolerance: osmomath.ErrTolerance{ AdditiveTolerance: sdk.MustNewDecFromStr("0.00042"), MultiplicativeTolerance: minDecTolerance, RoundingDir: osmomath.RoundUnconstrained, // TODO: confirm if this is acceptable. @@ -199,7 +198,7 @@ func TestExp2(t *testing.T) { // https://www.wolframalpha.com/input?i=2%5E%2864.5%29+56+digits expectedResult: osmomath.MustNewDecFromStr("26087635650665564424.699143612505016737766552579185717157"), - errTolerance: osmoutils.ErrTolerance{ + errTolerance: osmomath.ErrTolerance{ AdditiveTolerance: sdk.MustNewDecFromStr("0.000000000000000008"), MultiplicativeTolerance: minDecTolerance, RoundingDir: osmomath.RoundUnconstrained, // TODO: confirm if this is acceptable. @@ -210,7 +209,7 @@ func TestExp2(t *testing.T) { // https://www.wolframalpha.com/input?i=2%5E%2880.5%29+61+digits expectedResult: osmomath.MustNewDecFromStr("1709679290002018430137083.075789128776926268789829515159631571"), - errTolerance: osmoutils.ErrTolerance{ + errTolerance: osmomath.ErrTolerance{ AdditiveTolerance: sdk.MustNewDecFromStr("0.0000000000006"), MultiplicativeTolerance: minDecTolerance, RoundingDir: osmomath.RoundUnconstrained, // TODO: confirm if this is acceptable. @@ -221,7 +220,7 @@ func TestExp2(t *testing.T) { // https://www.wolframalpha.com/input?i=2%5E%28100.5%29+67+digits expectedResult: osmomath.MustNewDecFromStr("1792728671193156477399422023278.661496394239222564273688025833797661"), - errTolerance: osmoutils.ErrTolerance{ + errTolerance: osmomath.ErrTolerance{ AdditiveTolerance: sdk.MustNewDecFromStr("0.0000006"), MultiplicativeTolerance: minDecTolerance, RoundingDir: osmomath.RoundUnconstrained, // TODO: confirm if this is acceptable. @@ -232,7 +231,7 @@ func TestExp2(t *testing.T) { // https://www.wolframalpha.com/input?i=2%5E%28128.5%29+75+digits expectedResult: osmomath.MustNewDecFromStr("481231938336009023090067544955250113854.229961482126296754016435255422777776"), - errTolerance: osmoutils.ErrTolerance{ + errTolerance: osmomath.ErrTolerance{ AdditiveTolerance: sdk.MustNewDecFromStr("146.5"), MultiplicativeTolerance: minDecTolerance, RoundingDir: osmomath.RoundUnconstrained, // TODO: confirm if this is acceptable. @@ -243,7 +242,7 @@ func TestExp2(t *testing.T) { // https://www.wolframalpha.com/input?i=2%5E%28127.999999999999999999999999999999999999%29+75+digits expectedResult: osmomath.MustNewDecFromStr("340282366920938463463374607431768211220.134236774486705862055857235845515682"), - errTolerance: osmoutils.ErrTolerance{ + errTolerance: osmomath.ErrTolerance{ AdditiveTolerance: sdk.MustNewDecFromStr("15044647266406936"), MultiplicativeTolerance: minDecTolerance, RoundingDir: osmomath.RoundDown, @@ -254,7 +253,7 @@ func TestExp2(t *testing.T) { // https://www.wolframalpha.com/input?i=2%5E%28127.84864288%29+75+digits expectedResult: osmomath.MustNewDecFromStr("306391287650667462068703337664945630660.398687487527674545778353588077174571"), - errTolerance: osmoutils.ErrTolerance{ + errTolerance: osmomath.ErrTolerance{ AdditiveTolerance: sdk.MustNewDecFromStr("7707157415597963"), MultiplicativeTolerance: minDecTolerance, RoundingDir: osmomath.RoundUnconstrained, @@ -273,7 +272,7 @@ func TestExp2(t *testing.T) { // https://www.wolframalpha.com/input?i=2%5E%282%5E9%29 expectedResult: osmomath.MustNewDecFromStr("13407807929942597099574024998205846127479365820592393377723561443721764030073546976801874298166903427690031858186486050853753882811946569946433649006084096"), - errTolerance: osmoutils.ErrTolerance{ + errTolerance: osmomath.ErrTolerance{ AdditiveTolerance: minDecTolerance, MultiplicativeTolerance: minDecTolerance, RoundingDir: osmomath.RoundDown, From 180ba8ce1d3bf3e6a46efeed025a94c91a78330b Mon Sep 17 00:00:00 2001 From: Roman Date: Fri, 16 Dec 2022 14:53:10 -0800 Subject: [PATCH 14/17] improve godoc --- osmomath/exp2.go | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/osmomath/exp2.go b/osmomath/exp2.go index 32e77ef2190..d0dbacab124 100644 --- a/osmomath/exp2.go +++ b/osmomath/exp2.go @@ -42,6 +42,8 @@ var ( // Panics if the exponent is negative. // The answer is correct up to a factor of 10^-18. // Meaning, result = result * k for k in [1 - 10^(-18), 1 + 10^(-18)] +// Note: our Python script plots show accuracy up to a factor of 10^22. +// However, in Go tests we only test up to 10^18. Therefore, thos is the guarantee. func Exp2(exponent BigDec) BigDec { if exponent.Abs().GT(maxSupportedExponent) { panic(fmt.Sprintf("integer exponent %s is too large, max (%s)", exponent, maxSupportedExponent)) @@ -72,6 +74,8 @@ func Exp2(exponent BigDec) BigDec { // CONTRACT: exponent must be in the range [0, 1], panics if not. // The answer is correct up to a factor of 10^-18. // Meaning, result = result * k for k in [1 - 10^(-18), 1 + 10^(-18)] +// Note: our Python script plots show accuracy up to a factor of 10^22. +// However, in Go tests we only test up to 10^18. Therefore, thos is the guarantee. func exp2ChebyshevRationalApprox(x BigDec) BigDec { if x.LT(ZeroDec()) || x.GT(OneDec()) { panic(fmt.Sprintf("exponent must be in the range [0, 1], got %s", x)) From 017166826a68baece50fc18ae662ba9508a6e61d Mon Sep 17 00:00:00 2001 From: Roman Date: Fri, 16 Dec 2022 14:53:42 -0800 Subject: [PATCH 15/17] typo --- osmomath/exp2.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/osmomath/exp2.go b/osmomath/exp2.go index d0dbacab124..b7973b3bc62 100644 --- a/osmomath/exp2.go +++ b/osmomath/exp2.go @@ -43,7 +43,7 @@ var ( // The answer is correct up to a factor of 10^-18. // Meaning, result = result * k for k in [1 - 10^(-18), 1 + 10^(-18)] // Note: our Python script plots show accuracy up to a factor of 10^22. -// However, in Go tests we only test up to 10^18. Therefore, thos is the guarantee. +// However, in Go tests we only test up to 10^18. Therefore, this is the guarantee. func Exp2(exponent BigDec) BigDec { if exponent.Abs().GT(maxSupportedExponent) { panic(fmt.Sprintf("integer exponent %s is too large, max (%s)", exponent, maxSupportedExponent)) @@ -75,7 +75,7 @@ func Exp2(exponent BigDec) BigDec { // The answer is correct up to a factor of 10^-18. // Meaning, result = result * k for k in [1 - 10^(-18), 1 + 10^(-18)] // Note: our Python script plots show accuracy up to a factor of 10^22. -// However, in Go tests we only test up to 10^18. Therefore, thos is the guarantee. +// However, in Go tests we only test up to 10^18. Therefore, this is the guarantee. func exp2ChebyshevRationalApprox(x BigDec) BigDec { if x.LT(ZeroDec()) || x.GT(OneDec()) { panic(fmt.Sprintf("exponent must be in the range [0, 1], got %s", x)) From cafedbeb1e4b0aa0b50de59cb6a82bd12b240af5 Mon Sep 17 00:00:00 2001 From: Roman Date: Fri, 16 Dec 2022 14:54:49 -0800 Subject: [PATCH 16/17] remove TODOs - confirmed obsolete --- osmomath/exp2_test.go | 20 +++++++++----------- 1 file changed, 9 insertions(+), 11 deletions(-) diff --git a/osmomath/exp2_test.go b/osmomath/exp2_test.go index 6935bf346fc..156d5bca241 100644 --- a/osmomath/exp2_test.go +++ b/osmomath/exp2_test.go @@ -64,9 +64,7 @@ func TestExp2ChebyshevRationalApprox(t *testing.T) { errTolerance: osmomath.ErrTolerance{ AdditiveTolerance: minDecTolerance, MultiplicativeTolerance: minDecTolerance, - // TODO: confirm if rounding behavior is acceptable. - // Note, that a Python estimate passes RoundDown but not Wolfram. - RoundingDir: osmomath.RoundUnconstrained, + RoundingDir: osmomath.RoundUnconstrained, }, }, "exp2(0.99999)": { @@ -99,7 +97,7 @@ func TestExp2ChebyshevRationalApprox(t *testing.T) { errTolerance: osmomath.ErrTolerance{ AdditiveTolerance: minDecTolerance, MultiplicativeTolerance: minDecTolerance, - RoundingDir: osmomath.RoundUnconstrained, // TODO: confirm if this is acceptable. + RoundingDir: osmomath.RoundUnconstrained, }, }, "exp2(0.3334567)": { @@ -121,7 +119,7 @@ func TestExp2ChebyshevRationalApprox(t *testing.T) { errTolerance: osmomath.ErrTolerance{ AdditiveTolerance: sdk.MustNewDecFromStr("0.00000000000000002"), MultiplicativeTolerance: minDecTolerance.Mul(sdk.NewDec(10)), - RoundingDir: osmomath.RoundUnconstrained, // TODO: confirm if this is acceptable. + RoundingDir: osmomath.RoundUnconstrained, }, }, "exp2(0.999999999999999999999999999999999956)": { @@ -179,7 +177,7 @@ func TestExp2(t *testing.T) { errTolerance: osmomath.ErrTolerance{ AdditiveTolerance: minDecTolerance, MultiplicativeTolerance: minDecTolerance, - RoundingDir: osmomath.RoundUnconstrained, // TODO: confirm if this is acceptable. + RoundingDir: osmomath.RoundUnconstrained, }, }, "exp2(63.84864288)": { @@ -190,7 +188,7 @@ func TestExp2(t *testing.T) { errTolerance: osmomath.ErrTolerance{ AdditiveTolerance: sdk.MustNewDecFromStr("0.00042"), MultiplicativeTolerance: minDecTolerance, - RoundingDir: osmomath.RoundUnconstrained, // TODO: confirm if this is acceptable. + RoundingDir: osmomath.RoundUnconstrained, }, }, "exp2(64.5)": { @@ -201,7 +199,7 @@ func TestExp2(t *testing.T) { errTolerance: osmomath.ErrTolerance{ AdditiveTolerance: sdk.MustNewDecFromStr("0.000000000000000008"), MultiplicativeTolerance: minDecTolerance, - RoundingDir: osmomath.RoundUnconstrained, // TODO: confirm if this is acceptable. + RoundingDir: osmomath.RoundUnconstrained, }, }, "exp2(80.5)": { @@ -212,7 +210,7 @@ func TestExp2(t *testing.T) { errTolerance: osmomath.ErrTolerance{ AdditiveTolerance: sdk.MustNewDecFromStr("0.0000000000006"), MultiplicativeTolerance: minDecTolerance, - RoundingDir: osmomath.RoundUnconstrained, // TODO: confirm if this is acceptable. + RoundingDir: osmomath.RoundUnconstrained, }, }, "exp2(100.5)": { @@ -223,7 +221,7 @@ func TestExp2(t *testing.T) { errTolerance: osmomath.ErrTolerance{ AdditiveTolerance: sdk.MustNewDecFromStr("0.0000006"), MultiplicativeTolerance: minDecTolerance, - RoundingDir: osmomath.RoundUnconstrained, // TODO: confirm if this is acceptable. + RoundingDir: osmomath.RoundUnconstrained, }, }, "exp2(128.5)": { @@ -234,7 +232,7 @@ func TestExp2(t *testing.T) { errTolerance: osmomath.ErrTolerance{ AdditiveTolerance: sdk.MustNewDecFromStr("146.5"), MultiplicativeTolerance: minDecTolerance, - RoundingDir: osmomath.RoundUnconstrained, // TODO: confirm if this is acceptable. + RoundingDir: osmomath.RoundUnconstrained, }, }, "exp2(127.999999999999999999999999999999999999)": { From 82bb60f1228d9381fb176878d0bacfe0c9f5ec1a Mon Sep 17 00:00:00 2001 From: Roman Date: Fri, 16 Dec 2022 14:59:48 -0800 Subject: [PATCH 17/17] Runge's phenomenon comment --- osmomath/exp2_test.go | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/osmomath/exp2_test.go b/osmomath/exp2_test.go index 156d5bca241..0955251dd76 100644 --- a/osmomath/exp2_test.go +++ b/osmomath/exp2_test.go @@ -16,6 +16,14 @@ var ( ) func TestExp2ChebyshevRationalApprox(t *testing.T) { + // These values are used to test the approximated results close + // to 0 and 1 boundaries. + // With other types of approximations, there is a high likelyhood + // of larger errors clsoer to the boundaries. This is known as Runge's phenomenon. + // https://en.wikipedia.org/wiki/Runge%27s_phenomenon + // + // Chebyshev approximation should be able to handle this better. + // Tests at the boundaries help to validate there is no Runge's phenomenon. smallValue := osmomath.MustNewDecFromStr("0.00001") smallerValue := osmomath.MustNewDecFromStr("0.00000000000000000001")