From 1cd49285fe8234c2de8700b96c8500f9d9db4114 Mon Sep 17 00:00:00 2001 From: Roman Date: Wed, 14 Dec 2022 13:45:55 -0800 Subject: [PATCH] feat(osmomath): Power and PowerMut functions with decimal exponent --- osmomath/decimal.go | 42 ++++++++++ osmomath/decimal_test.go | 167 ++++++++++++++++++++++++++++++++++++++- 2 files changed, 208 insertions(+), 1 deletion(-) diff --git a/osmomath/decimal.go b/osmomath/decimal.go index 955b6229f58..6ab557b98df 100644 --- a/osmomath/decimal.go +++ b/osmomath/decimal.go @@ -997,3 +997,45 @@ func (d BigDec) PowerIntegerMut(power uint64) BigDec { return d.MulMut(tmp) } + +// Power returns a result of raising the given big dec to +// a positive decimal power. Panics if the power is negative. +// Panics if the base is negative. Does not mutate the receiver. +// N.B.: support for negative power can be added when needed. +func (d BigDec) Power(power BigDec) BigDec { + if d.IsNegative() { + panic(fmt.Sprintf("negative base is not supported for Power(), base was (%s)", d)) + } + if power.IsNegative() { + panic(fmt.Sprintf("negative power is not supported for Power(), power was (%s)", power)) + } + if power.Abs().GT(maxSupportedExponent) { + panic(fmt.Sprintf("integer exponent %s is too large, max (%s)", power, maxSupportedExponent)) + } + if power.IsInteger() { + return d.PowerInteger(power.TruncateInt().Uint64()) + } + if power.IsZero() { + return OneDec() + } + if d.IsZero() { + return ZeroDec() + } + if d.Equal(twoBigDec) { + return Exp2(power) + } + + // d^power = exp2(power * log_2{base}) + result := Exp2(d.LogBase2().Mul(power)) + + return result +} + +// PowerMut returns a result of raising the given big dec to +// a positive decimal power. Panics if the power is negative. +// Panics if the base is negative. Mutates the receiver. +// N.B.: support for negative power can be added when needed. +func (d BigDec) PowerMut(power BigDec) BigDec { + d.i.Set(d.Power(power).i) + return d +} diff --git a/osmomath/decimal_test.go b/osmomath/decimal_test.go index c6983617c6c..8ea125a681b 100644 --- a/osmomath/decimal_test.go +++ b/osmomath/decimal_test.go @@ -14,6 +14,7 @@ import ( "github.com/osmosis-labs/osmosis/v13/app/apptesting/osmoassert" "github.com/osmosis-labs/osmosis/v13/osmomath" + "github.com/osmosis-labs/osmosis/v13/osmoutils" gammtypes "github.com/osmosis-labs/osmosis/v13/x/gamm/types" ) @@ -21,6 +22,12 @@ type decimalTestSuite struct { suite.Suite } +var ( + zeroAdditiveErrTolerance = osmoutils.ErrTolerance{ + AdditiveTolerance: sdk.ZeroDec(), + } +) + func TestDecimalTestSuite(t *testing.T) { suite.Run(t, new(decimalTestSuite)) } @@ -1149,9 +1156,18 @@ func (s *decimalTestSuite) TestPowerInteger() { tolerance = tc.expectedToleranceOverwrite } + // Main system under test actualResult := tc.base.PowerInteger(tc.exponent) require.True(osmomath.DecApproxEq(s.T(), tc.expectedResult, actualResult, tolerance)) + // Secondary system under test. + // To reduce boilerplate from the same test cases when exponent is a + // positive integer, we also test Power(). + // Negative exponent and base are not supported for Power() + if tc.exponent >= 0 && !tc.base.IsNegative() { + actualResult2 := tc.base.Power(osmomath.NewDecFromInt(osmomath.NewIntFromUint64(tc.exponent))) + require.True(osmomath.DecApproxEq(s.T(), tc.expectedResult, actualResult2, tolerance)) + } }) } } @@ -1225,7 +1241,7 @@ func (s *decimalTestSuite) TestMul_Mutation() { } } -// TestMul_Mutation tests that PowerIntegerMut mutates the receiver +// TestPowerInteger_Mutation tests that PowerIntegerMut mutates the receiver // while PowerInteger is not. func (s *decimalTestSuite) TestPowerInteger_Mutation() { @@ -1266,3 +1282,152 @@ func (s *decimalTestSuite) TestPowerInteger_Mutation() { }) } } + +func (s *decimalTestSuite) TestPower() { + tests := map[string]struct { + base osmomath.BigDec + exponent osmomath.BigDec + expectedResult osmomath.BigDec + expectPanic bool + errTolerance osmoutils.ErrTolerance + }{ + // N.B.: integer exponents are tested under TestPowerInteger. + + "3 ^ 2 = 9 (integer base and integer exponent)": { + base: osmomath.NewBigDec(3), + exponent: osmomath.NewBigDec(2), + + expectedResult: osmomath.NewBigDec(9), + + errTolerance: zeroAdditiveErrTolerance, + }, + "2^0.5 (base of 2 and non-integer exponent)": { + base: osmomath.MustNewDecFromStr("2"), + 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, + RoundingDir: osmomath.RoundDown, + }, + }, + "3^0.33 (integer base other than 2 and non-integer exponent)": { + base: osmomath.MustNewDecFromStr("3"), + exponent: osmomath.MustNewDecFromStr("0.33"), + + // https://www.wolframalpha.com/input?i=3%5E0.33+37+digits + expectedResult: osmomath.MustNewDecFromStr("1.436977652184851654252692986409357265"), + + errTolerance: osmoutils.ErrTolerance{ + AdditiveTolerance: minDecTolerance, + RoundingDir: osmomath.RoundDown, + }, + }, + "e^0.98999 (non-integer base and non-integer exponent)": { + base: osmomath.EulersNumber, + exponent: osmomath.MustNewDecFromStr("0.9899"), + + // https://www.wolframalpha.com/input?i=e%5E0.9899+37+digits + expectedResult: osmomath.MustNewDecFromStr("2.690965362357751196751808686902156603"), + + errTolerance: osmoutils.ErrTolerance{ + AdditiveTolerance: minDecTolerance, + RoundingDir: osmomath.RoundUnconstrained, // TODO: understand if rounding behavior is acceptable. + }, + }, + "10^0.001 (small non-integer exponent)": { + base: osmomath.NewBigDec(10), + exponent: osmomath.MustNewDecFromStr("0.001"), + + // https://www.wolframalpha.com/input?i=10%5E0.001+37+digits + expectedResult: osmomath.MustNewDecFromStr("1.002305238077899671915404889328110554"), + + errTolerance: osmoutils.ErrTolerance{ + AdditiveTolerance: minDecTolerance, + RoundingDir: osmomath.RoundUnconstrained, // TODO: understand if rounding behavior is acceptable. + }, + }, + "13^100.7777 (large non-integer exponent)": { + base: osmomath.NewBigDec(13), + exponent: osmomath.MustNewDecFromStr("100.7777"), + + // https://www.wolframalpha.com/input?i=13%5E100.7777+37+digits + expectedResult: osmomath.MustNewDecFromStr("1.822422110233759706998600329118969132").Mul(osmomath.NewBigDec(10).PowerInteger(112)), + + errTolerance: osmoutils.ErrTolerance{ + MultiplicativeTolerance: minDecTolerance, + RoundingDir: osmomath.RoundDown, + }, + }, + "large non-integer exponent with large non-integer base - panics": { + base: osmomath.MustNewDecFromStr("169.137"), + exponent: osmomath.MustNewDecFromStr("100.7777"), + + expectPanic: true, + }, + "negative base - panic": { + base: osmomath.NewBigDec(-3), + exponent: osmomath.MustNewDecFromStr("4"), + + expectPanic: true, + }, + "negative exponent - panic": { + base: osmomath.NewBigDec(1), + exponent: osmomath.MustNewDecFromStr("-4"), + + expectPanic: true, + }, + } + + for name, tc := range tests { + tc := tc + s.Run(name, func() { + osmoassert.ConditionalPanic(s.T(), tc.expectPanic, func() { + + actualResult := tc.base.Power(tc.exponent) + + s.Require().Equal(0, tc.errTolerance.CompareBigDec(tc.expectedResult, actualResult)) + }) + }) + } +} + +// TestPower_Mutation tests that PowerMut mutates the receiver +// while Power is not. +func (s *decimalTestSuite) TestPower_Mutation() { + + exponent := osmomath.NewBigDec(2) + + tests := map[string]struct { + startValue osmomath.BigDec + expectedResult osmomath.BigDec + }{ + "1": { + startValue: osmomath.OneDec(), + expectedResult: osmomath.OneDec(), + }, + "0": { + startValue: osmomath.ZeroDec(), + expectedResult: osmomath.ZeroDec(), + }, + "4": { + startValue: osmomath.MustNewDecFromStr("4.5"), + expectedResult: osmomath.MustNewDecFromStr("20.25"), + }, + } + + for name, tc := range tests { + s.Run(name, func() { + + startMut := tc.startValue.Clone() + startNonMut := tc.startValue.Clone() + + resultMut := startMut.PowerMut(exponent) + resultNonMut := startNonMut.Power(exponent) + + s.assertMutResult(tc.expectedResult, tc.startValue, resultMut, resultNonMut, startMut, startNonMut) + }) + } +}