Skip to content

Commit

Permalink
feat(osmomath): log2 approximation (#2788)
Browse files Browse the repository at this point in the history
* feat: osmomath log2 approximation

* lint

* fix comment

* comment

* Update osmomath/decimal.go

* improve accuracy with narrower range

* comment

* implement and test precise log for x >= 1

* tests for negative values

* make non-mutative and test

* bench

* changelog

* remove redundant assignments

* improve comments

(cherry picked from commit 8590b80)

# Conflicts:
#	osmomath/decimal.go
#	osmomath/decimal_test.go
#	osmomath/log2_bench_test.go
  • Loading branch information
p0mvn authored and mergify[bot] committed Oct 24, 2022
1 parent 5a8b313 commit d6a28bf
Show file tree
Hide file tree
Showing 5 changed files with 158 additions and 3 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
65 changes: 65 additions & 0 deletions osmomath/decimal.go
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,9 @@ const (

// max number of iterations in ApproxRoot function
maxApproxRootIterations = 100

// max number of iterations in Log2 function
maxLog2Iterations = 300
)

var (
Expand All @@ -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
Expand All @@ -58,6 +67,7 @@ func init() {
precisionMultipliers[i] = calcPrecisionMultiplier(int64(i))
}

<<<<<<< HEAD
log2LookupTable = buildLog2LookupTable()
}

Expand All @@ -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 {
Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -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))
}
73 changes: 71 additions & 2 deletions osmomath/decimal_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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},
Expand Down Expand Up @@ -654,18 +654,48 @@ 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
expected BigDec

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),
Expand All @@ -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
Expand All @@ -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))
})
})
}
Expand Down
21 changes: 21 additions & 0 deletions osmomath/log2_bench_test.go
Original file line number Diff line number Diff line change
@@ -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
}{
Expand All @@ -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))
}
}
1 change: 0 additions & 1 deletion osmomath/rounding_direction_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down

0 comments on commit d6a28bf

Please sign in to comment.