diff --git a/CHANGELOG.md b/CHANGELOG.md index 38fcdbd2794..ee35bcc74d2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -44,6 +44,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Features +* [#2788](https://github.com/osmosis-labs/osmosis/pull/2788) Add logarithm base 2 implementation. * [#2739](https://github.com/osmosis-labs/osmosis/pull/2739) Add pool type query ### Bug fixes diff --git a/osmomath/decimal.go b/osmomath/decimal.go index 761238fd536..bb10b45dc47 100644 --- a/osmomath/decimal.go +++ b/osmomath/decimal.go @@ -31,6 +31,9 @@ const ( // max number of iterations in ApproxRoot function maxApproxRootIterations = 100 + + // max number of iterations in Log2 function + maxLog2Iterations = 300 ) var ( @@ -41,7 +44,13 @@ var ( oneInt = big.NewInt(1) tenInt = big.NewInt(10) +<<<<<<< HEAD log2LookupTable map[uint16]BigDec +======= + // initialized in init() since requires + // precision to be defined. + twoBigDec BigDec +>>>>>>> 8590b80f (feat(osmomath): log2 approximation (#2788)) ) // Decimal errors @@ -58,6 +67,7 @@ func init() { precisionMultipliers[i] = calcPrecisionMultiplier(int64(i)) } +<<<<<<< HEAD log2LookupTable = buildLog2LookupTable() } @@ -79,6 +89,9 @@ func buildLog2LookupTable() map[uint16]BigDec { 18000: MustNewDecFromStr("0.847996906554950015037158458406242841"), 19000: MustNewDecFromStr("0.925999418556223145923199993417444246"), } +======= + twoBigDec = NewBigDec(2) +>>>>>>> 8590b80f (feat(osmomath): log2 approximation (#2788)) } func precisionInt() *big.Int { @@ -883,6 +896,7 @@ func DecApproxEq(t *testing.T, d1 BigDec, d2 BigDec, tol BigDec) (*testing.T, bo return t, diff.LTE(tol), "expected |d1 - d2| <:\t%v\ngot |d1 - d2| = \t\t%v", tol.String(), diff.String() } +<<<<<<< HEAD // ApproxLog2 returns the approximation of log_2 {x}. // Rounds down by truncating and right shifting during // calculations. @@ -933,4 +947,55 @@ func (x BigDec) ApproxLog2() BigDec { panic(fmt.Sprintf("no matching value for key (%s) in the lookup table", lookupKey)) } return NewBigDec(y).Add(tableValue) +======= +// LogBase2 returns log_2 {x}. +// Rounds down by truncations during division and right shifting. +// Accurate up to 32 precision digits. +// Implementation is based on: +// https://stm32duinoforum.com/forum/dsp/BinaryLogarithm.pdf +func (x BigDec) LogBase2() BigDec { + // create a new decimal to avoid mutating + // the receiver's int buffer. + xCopy := ZeroDec() + xCopy.i = new(big.Int).Set(x.i) + if xCopy.LTE(ZeroDec()) { + panic(fmt.Sprintf("log is not defined at <= 0, given (%s)", xCopy)) + } + + // Normalize x to be 1 <= x < 2. + + // y is the exponent that results in a whole multiple of 2. + y := ZeroDec() + + // repeat until: x >= 1. + for xCopy.LT(OneDec()) { + xCopy.i.Lsh(xCopy.i, 1) + y = y.Sub(OneDec()) + } + + // repeat until: x < 2. + for xCopy.GTE(twoBigDec) { + xCopy.i.Rsh(xCopy.i, 1) + y = y.Add(OneDec()) + } + + b := OneDec().Quo(twoBigDec) + + // N.B. At this point x is a positive real number representing + // mantissa of the log. We estimate it using the following + // algorithm: + // https://stm32duinoforum.com/forum/dsp/BinaryLogarithm.pdf + // This has shown precision of 32 digits relative + // to Wolfram Alpha in tests. + for i := 0; i < maxLog2Iterations; i++ { + xCopy = xCopy.Mul(xCopy) + if xCopy.GTE(twoBigDec) { + xCopy.i.Rsh(xCopy.i, 1) + y = y.Add(b) + } + b.i.Rsh(b.i, 1) + } + + return y +>>>>>>> 8590b80f (feat(osmomath): log2 approximation (#2788)) } diff --git a/osmomath/decimal_test.go b/osmomath/decimal_test.go index 92b886d1d9e..f89e3f0735d 100644 --- a/osmomath/decimal_test.go +++ b/osmomath/decimal_test.go @@ -204,8 +204,8 @@ func (s *decimalTestSuite) TestBigDecFromSdkDec() { func (s *decimalTestSuite) TestBigDecFromSdkDecSlice() { tests := []struct { - d []sdk.Dec - want []BigDec + d []sdk.Dec + want []BigDec expPanic bool }{ {[]sdk.Dec{sdk.MustNewDecFromStr("0.000000000000000000")}, []BigDec{NewBigDec(0)}, false}, @@ -654,6 +654,10 @@ func BenchmarkMarshalTo(b *testing.B) { } func (s *decimalTestSuite) TestLog2() { +<<<<<<< HEAD +======= + var expectedErrTolerance = MustNewDecFromStr("0.000000000000000000000000000000000100") +>>>>>>> 8590b80f (feat(osmomath): log2 approximation (#2788)) tests := map[string]struct { initialValue BigDec @@ -661,11 +665,37 @@ func (s *decimalTestSuite) TestLog2() { expectedPanic bool }{ +<<<<<<< HEAD "log_2{0.99}; not supported; panic": { initialValue: NewDecWithPrec(99, 2), expectedPanic: true, }, +======= + "log_2{-1}; invalid; panic": { + initialValue: OneDec().Neg(), + expectedPanic: true, + }, + "log_2{0}; invalid; panic": { + initialValue: ZeroDec(), + expectedPanic: true, + }, + "log_2{0.001} = -9.965784284662087043610958288468170528": { + initialValue: MustNewDecFromStr("0.001"), + // From: https://www.wolframalpha.com/input?i=log+base+2+of+0.999912345+with+33+digits + expected: MustNewDecFromStr("-9.965784284662087043610958288468170528"), + }, + "log_2{0.56171821941421412902170941} = -0.832081497183140708984033250637831402": { + initialValue: MustNewDecFromStr("0.56171821941421412902170941"), + // From: https://www.wolframalpha.com/input?i=log+base+2+of+0.56171821941421412902170941+with+36+digits + expected: MustNewDecFromStr("-0.832081497183140708984033250637831402"), + }, + "log_2{0.999912345} = -0.000126464976533858080645902722235833": { + initialValue: MustNewDecFromStr("0.999912345"), + // From: https://www.wolframalpha.com/input?i=log+base+2+of+0.999912345+with+37+digits + expected: MustNewDecFromStr("-0.000126464976533858080645902722235833"), + }, +>>>>>>> 8590b80f (feat(osmomath): log2 approximation (#2788)) "log_2{1} = 0": { initialValue: NewBigDec(1), expected: NewBigDec(0), @@ -674,20 +704,36 @@ func (s *decimalTestSuite) TestLog2() { initialValue: NewBigDec(2), expected: NewBigDec(1), }, +<<<<<<< HEAD +======= + "log_2{7} = 2.807354922057604107441969317231830809": { + initialValue: NewBigDec(7), + // From: https://www.wolframalpha.com/input?i=log+base+2+of+7+37+digits + expected: MustNewDecFromStr("2.807354922057604107441969317231830809"), + }, +>>>>>>> 8590b80f (feat(osmomath): log2 approximation (#2788)) "log_2{512} = 9": { initialValue: NewBigDec(512), expected: NewBigDec(9), }, +<<<<<<< HEAD "log_2{600} = 9": { initialValue: NewBigDec(580), // TODO: true value is: 9.179909090014934468590092754117374938 // Need better lookup table. expected: MustNewDecFromStr("9.137503523749934908329043617236402782"), +======= + "log_2{580} = 9.179909090014934468590092754117374938": { + initialValue: NewBigDec(580), + // From: https://www.wolframalpha.com/input?i=log+base+2+of+600+37+digits + expected: MustNewDecFromStr("9.179909090014934468590092754117374938"), +>>>>>>> 8590b80f (feat(osmomath): log2 approximation (#2788)) }, "log_2{1024} = 10": { initialValue: NewBigDec(1024), expected: NewBigDec(10), }, +<<<<<<< HEAD "log_2{1024.987654321} = 10": { initialValue: NewDecWithPrec(1024987654321, 9), // TODO: true value is: 10.001390817654141324352719749259888355 @@ -699,14 +745,37 @@ func (s *decimalTestSuite) TestLog2() { // TODO: true value is: 99.525973560175362367047484597337715868 // Need better lookup table expected: MustNewDecFromStr("99.485426827170241759571649887742440632"), +======= + "log_2{1024.987654321} = 10.001390817654141324352719749259888355": { + initialValue: NewDecWithPrec(1024987654321, 9), + // From: https://www.wolframalpha.com/input?i=log+base+2+of+1024.987654321+38+digits + expected: MustNewDecFromStr("10.001390817654141324352719749259888355"), + }, + "log_2{912648174127941279170121098210.92821920190204131121} = 99.525973560175362367047484597337715868": { + initialValue: MustNewDecFromStr("912648174127941279170121098210.92821920190204131121"), + // From: https://www.wolframalpha.com/input?i=log+base+2+of+912648174127941279170121098210.92821920190204131121+38+digits + expected: MustNewDecFromStr("99.525973560175362367047484597337715868"), +>>>>>>> 8590b80f (feat(osmomath): log2 approximation (#2788)) }, } for name, tc := range tests { s.Run(name, func() { osmoassert.ConditionalPanic(s.T(), tc.expectedPanic, func() { +<<<<<<< HEAD res := tc.initialValue.ApproxLog2() s.Require().Equal(tc.expected, res) +======= + // Create a copy to test that the original was not modified. + // That is, that LogbBase2() is non-mutative. + initialCopy := ZeroDec() + initialCopy.i.Set(tc.initialValue.i) + + // system under test. + res := tc.initialValue.LogBase2() + require.True(DecApproxEq(s.T(), tc.expected, res, expectedErrTolerance)) + require.Equal(s.T(), initialCopy, tc.initialValue) +>>>>>>> 8590b80f (feat(osmomath): log2 approximation (#2788)) }) }) } diff --git a/osmomath/log2_bench_test.go b/osmomath/log2_bench_test.go index 1a723be1643..ef3a6edaa8c 100644 --- a/osmomath/log2_bench_test.go +++ b/osmomath/log2_bench_test.go @@ -1,10 +1,15 @@ package osmomath import ( +<<<<<<< HEAD +======= + "math/rand" +>>>>>>> 8590b80f (feat(osmomath): log2 approximation (#2788)) "testing" ) func BenchmarkLog2(b *testing.B) { +<<<<<<< HEAD tests := []struct { value BigDec }{ @@ -30,5 +35,21 @@ func BenchmarkLog2(b *testing.B) { for _, test := range tests { test.value.ApproxLog2() } +======= + tests := []BigDec{ + MustNewDecFromStr("1.2"), + MustNewDecFromStr("1.234"), + MustNewDecFromStr("1024"), + NewBigDec(2048 * 2048 * 2048 * 2048 * 2048), + MustNewDecFromStr("999999999999999999999999999999999999999999999999999999.9122181273612911"), + MustNewDecFromStr("0.563289239121902491248219047129047129"), + } + + for i := 0; i < b.N; i++ { + b.StopTimer() + test := tests[rand.Int63n(int64(len(tests)))] + b.StartTimer() + _ = test.LogBase2() +>>>>>>> 8590b80f (feat(osmomath): log2 approximation (#2788)) } } diff --git a/osmomath/rounding_direction_test.go b/osmomath/rounding_direction_test.go index 8668c84c677..3fac4b62c9d 100644 --- a/osmomath/rounding_direction_test.go +++ b/osmomath/rounding_direction_test.go @@ -42,7 +42,6 @@ func TestDivIntByU64ToBigDec(t *testing.T) { addTCForAllRoundingModes("odd divided by 2", sdk.NewInt(5), 2, NewDecWithPrec(25, 1)) for name, tt := range tests { - fmt.Println("start") t.Run(name, func(t *testing.T) { got, err := DivIntByU64ToBigDec(tt.i, tt.u, tt.round) require.Equal(t, tt.want, got)