Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(osmomath): Power with decimal exponent #3731

Merged
merged 20 commits into from
Dec 21, 2022
Merged
Show file tree
Hide file tree
Changes from 15 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
* [#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) `Exp2` function to compute 2^decimal.
* [#3693](https://github.com/osmosis-labs/osmosis/pull/3693) Add `EstimateSwapExactAmountOut` query to stargate whitelist
* [#3731](https://github.com/osmosis-labs/osmosis/pull/3731) Power and PowerMut functions with decimal exponent
p0mvn marked this conversation as resolved.
Show resolved Hide resolved

### API breaks

Expand Down
42 changes: 42 additions & 0 deletions osmomath/decimal.go
Original file line number Diff line number Diff line change
Expand Up @@ -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))
nicolaslara marked this conversation as resolved.
Show resolved Hide resolved

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
}
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Unsure if there is value in this

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

err this is backwards. We should write the mutative method here, and then have Power just make a copy then run PowerMut

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Or just delete power mut for now

Unless the idea is to keep this as an API stub, to optimize later?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm happy to delete this for now.

Since we rely on Exp2 function for the underlying implementation, making a mutative PowerMut is not as trivial as making MulMut.

167 changes: 166 additions & 1 deletion osmomath/decimal_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import (
"github.com/stretchr/testify/suite"
"gopkg.in/yaml.v2"

"github.com/osmosis-labs/osmosis/v13/app/apptesting/osmoassert"
"github.com/osmosis-labs/osmosis/v13/osmomath"
gammtypes "github.com/osmosis-labs/osmosis/v13/x/gamm/types"
)
Expand All @@ -20,6 +21,12 @@ type decimalTestSuite struct {
suite.Suite
}

var (
zeroAdditiveErrTolerance = osmomath.ErrTolerance{
AdditiveTolerance: sdk.ZeroDec(),
}
)

func TestDecimalTestSuite(t *testing.T) {
suite.Run(t, new(decimalTestSuite))
}
Expand Down Expand Up @@ -1144,9 +1151,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))
}
})
}
}
Expand Down Expand Up @@ -1219,7 +1235,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() {

Expand Down Expand Up @@ -1260,3 +1276,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 osmomath.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: osmomath.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: osmomath.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: osmomath.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: osmomath.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: osmomath.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)
nicolaslara marked this conversation as resolved.
Show resolved Hide resolved

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)
})
}
}