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): log2 approximation #2788

Merged
merged 16 commits into from
Oct 24, 2022
68 changes: 67 additions & 1 deletion osmomath/decimal.go
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,8 @@ var (
zeroInt = big.NewInt(0)
oneInt = big.NewInt(1)
tenInt = big.NewInt(10)

log2LookupTable map[uint8]BigDec
)

// Decimal errors
Expand All @@ -54,6 +56,27 @@ func init() {
for i := 0; i <= Precision; i++ {
precisionMultipliers[i] = calcPrecisionMultiplier(int64(i))
}

log2LookupTable = buildLog2LookupTable()
}

// buildLog2LookupTable returns a lookup table for log values
// ranging from [1, 2)
// the keys are multiplied by 10 to simplify the rounding logic
// in the log function.s
p0mvn marked this conversation as resolved.
Show resolved Hide resolved
func buildLog2LookupTable() map[uint8]BigDec {
return map[uint8]BigDec{
10: ZeroDec(),
11: MustNewDecFromStr("0.137503523749934908329043617236402782"),
12: MustNewDecFromStr("0.263034405833793833583419514458426332"),
13: MustNewDecFromStr("0.378511623253729812526493224767304557"),
14: MustNewDecFromStr("0.485426827170241759571649887742440632"),
15: MustNewDecFromStr("0.584962500721156181453738943947816508"),
16: MustNewDecFromStr("0.678071905112637652129680570510609824"),
17: MustNewDecFromStr("0.765534746362977060383746581321014178"),
18: MustNewDecFromStr("0.847996906554950015037158458406242841"),
19: MustNewDecFromStr("0.925999418556223145923199993417444246"),
}
}

func precisionInt() *big.Int {
Expand Down Expand Up @@ -211,7 +234,8 @@ func (d BigDec) GTE(d2 BigDec) bool { return (d.i).Cmp(d2.i) >= 0 } /
func (d BigDec) LT(d2 BigDec) bool { return (d.i).Cmp(d2.i) < 0 } // less than
func (d BigDec) LTE(d2 BigDec) bool { return (d.i).Cmp(d2.i) <= 0 } // less than or equal
func (d BigDec) Neg() BigDec { return BigDec{new(big.Int).Neg(d.i)} } // reverse the decimal sign
func (d BigDec) Abs() BigDec { return BigDec{new(big.Int).Abs(d.i)} } // absolute value
// nolint: stylecheck
func (d BigDec) Abs() BigDec { return BigDec{new(big.Int).Abs(d.i)} } // absolute value

// BigInt returns a copy of the underlying big.Int.
func (d BigDec) BigInt() *big.Int {
Expand Down Expand Up @@ -836,3 +860,45 @@ func DecApproxEq(t *testing.T, d1 BigDec, d2 BigDec, tol BigDec) (*testing.T, bo
diff := d1.Sub(d2).Abs()
return t, diff.LTE(tol), "expected |d1 - d2| <:\t%v\ngot |d1 - d2| = \t\t%v", tol.String(), diff.String()
}

// ApproxLog2 returns the approximation of log_2 {x}.
// Rounds down by truncating and right shifting during
// calculations.
func (x BigDec) ApproxLog2() BigDec {
if x.LT(OneDec()) {
panic(fmt.Sprintf("only supporting values >= 1, given (%s)", x))
}

// Normalize x to be 1 <= x < 2

// y is the exponent that results in a whole multiple of 2.
y := int64(0)

// invariant: x >= 1
Copy link
Contributor

Choose a reason for hiding this comment

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

this is just a wording comment, but doesn't invariant imply that x>=1 is true throughout the iteration? I think you mean that after the iteration x>=1. Are we using these invariants for formal verification somewhere?

Copy link
Member Author

@p0mvn p0mvn Oct 24, 2022

Choose a reason for hiding this comment

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

I think I was misusing the term to imply that we are aiming to normalize the value to be >= 1 after the iteration is complete. Going to change this wording

// while x < 1
for x.LT(OneDec()) {
x.i = x.i.Lsh(x.i, 1)
y = y - 1
}

// invariant: x < 2
// while x >= 2
twoDec := NewBigDec(2)
for x.GTE(twoDec) {
x.i = x.i.Rsh(x.i, 1)
y = y + 1
}

// exponentiate to simplify truncation necessary for
// looking up values in the table.
lookupKey := x.MulInt64(10).TruncateInt()
if lookupKey.GTE(NewInt(20)) || lookupKey.LT(NewInt(10)) {
panic(fmt.Sprintf("invalid lookup key (%s), must be 10 <= lookup key < 2", lookupKey))
}

tableValue, found := log2LookupTable[uint8(lookupKey.Int64())]
if !found {
panic(fmt.Sprintf("no matching value for key (%s) in the lookup table", lookupKey))
}
return NewBigDec(y).Add(tableValue)
}
115 changes: 88 additions & 27 deletions osmomath/decimal_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@ import (
"github.com/stretchr/testify/require"
"github.com/stretchr/testify/suite"
"gopkg.in/yaml.v2"

"github.com/osmosis-labs/osmosis/v12/app/apptesting/osmoassert"
)

type decimalTestSuite struct {
Expand Down Expand Up @@ -152,8 +154,8 @@ func (s *decimalTestSuite) TestDecFloat64() {

func (s *decimalTestSuite) TestSdkDec() {
tests := []struct {
d BigDec
want sdk.Dec
d BigDec
want sdk.Dec
expPanic bool
}{
{NewBigDec(0), sdk.MustNewDecFromStr("0.000000000000000000"), false},
Expand All @@ -177,8 +179,8 @@ func (s *decimalTestSuite) TestSdkDec() {

func (s *decimalTestSuite) TestBigDecFromSdkDec() {
tests := []struct {
d sdk.Dec
want BigDec
d sdk.Dec
want BigDec
expPanic bool
}{
{sdk.MustNewDecFromStr("0.000000000000000000"), NewBigDec(0), false},
Expand Down Expand Up @@ -415,14 +417,14 @@ func (s *decimalTestSuite) TestDecCeil() {
input BigDec
expected BigDec
}{
{MustNewDecFromStr("0.001"), NewBigDec(1)}, // 0.001 => 1.0
{MustNewDecFromStr("-0.001"), ZeroDec()}, // -0.001 => 0.0
{ZeroDec(), ZeroDec()}, // 0.0 => 0.0
{MustNewDecFromStr("0.9"), NewBigDec(1)}, // 0.9 => 1.0
{MustNewDecFromStr("0.001"), NewBigDec(1)}, // 0.001 => 1.0
{MustNewDecFromStr("-0.001"), ZeroDec()}, // -0.001 => 0.0
{ZeroDec(), ZeroDec()}, // 0.0 => 0.0
{MustNewDecFromStr("0.9"), NewBigDec(1)}, // 0.9 => 1.0
{MustNewDecFromStr("4.001"), NewBigDec(5)}, // 4.001 => 5.0
{MustNewDecFromStr("-4.001"), NewBigDec(-4)}, // -4.001 => -4.0
{MustNewDecFromStr("4.7"), NewBigDec(5)}, // 4.7 => 5.0
{MustNewDecFromStr("-4.7"), NewBigDec(-4)}, // -4.7 => -4.0
{MustNewDecFromStr("4.7"), NewBigDec(5)}, // 4.7 => 5.0
{MustNewDecFromStr("-4.7"), NewBigDec(-4)}, // -4.7 => -4.0
}

for i, tc := range testCases {
Expand All @@ -437,11 +439,11 @@ func (s *decimalTestSuite) TestPower() {
power uint64
expected BigDec
}{
{OneDec(), 10, OneDec()}, // 1.0 ^ (10) => 1.0
{NewDecWithPrec(5, 1), 2, NewDecWithPrec(25, 2)}, // 0.5 ^ 2 => 0.25
{NewDecWithPrec(2, 1), 2, NewDecWithPrec(4, 2)}, // 0.2 ^ 2 => 0.04
{NewDecFromInt(NewInt(3)), 3, NewDecFromInt(NewInt(27))}, // 3 ^ 3 => 27
{NewDecFromInt(NewInt(-3)), 4, NewDecFromInt(NewInt(81))}, // -3 ^ 4 = 81
{OneDec(), 10, OneDec()}, // 1.0 ^ (10) => 1.0
{NewDecWithPrec(5, 1), 2, NewDecWithPrec(25, 2)}, // 0.5 ^ 2 => 0.25
{NewDecWithPrec(2, 1), 2, NewDecWithPrec(4, 2)}, // 0.2 ^ 2 => 0.04
{NewDecFromInt(NewInt(3)), 3, NewDecFromInt(NewInt(27))}, // 3 ^ 3 => 27
{NewDecFromInt(NewInt(-3)), 4, NewDecFromInt(NewInt(81))}, // -3 ^ 4 = 81
{MustNewDecFromStr("1.414213562373095048801688724209698079"), 2, NewDecFromInt(NewInt(2))}, // 1.414213562373095048801688724209698079 ^ 2 = 2
}

Expand All @@ -457,14 +459,14 @@ func (s *decimalTestSuite) TestApproxRoot() {
root uint64
expected BigDec
}{
{OneDec(), 10, OneDec()}, // 1.0 ^ (0.1) => 1.0
{NewDecWithPrec(25, 2), 2, NewDecWithPrec(5, 1)}, // 0.25 ^ (0.5) => 0.5
{NewDecWithPrec(4, 2), 2, NewDecWithPrec(2, 1)}, // 0.04 ^ (0.5) => 0.2
{NewDecFromInt(NewInt(27)), 3, NewDecFromInt(NewInt(3))}, // 27 ^ (1/3) => 3
{NewDecFromInt(NewInt(-81)), 4, NewDecFromInt(NewInt(-3))}, // -81 ^ (0.25) => -3
{NewDecFromInt(NewInt(2)), 2, MustNewDecFromStr("1.414213562373095048801688724209698079")}, // 2 ^ (0.5) => 1.414213562373095048801688724209698079
{OneDec(), 10, OneDec()}, // 1.0 ^ (0.1) => 1.0
{NewDecWithPrec(25, 2), 2, NewDecWithPrec(5, 1)}, // 0.25 ^ (0.5) => 0.5
{NewDecWithPrec(4, 2), 2, NewDecWithPrec(2, 1)}, // 0.04 ^ (0.5) => 0.2
{NewDecFromInt(NewInt(27)), 3, NewDecFromInt(NewInt(3))}, // 27 ^ (1/3) => 3
{NewDecFromInt(NewInt(-81)), 4, NewDecFromInt(NewInt(-3))}, // -81 ^ (0.25) => -3
{NewDecFromInt(NewInt(2)), 2, MustNewDecFromStr("1.414213562373095048801688724209698079")}, // 2 ^ (0.5) => 1.414213562373095048801688724209698079
{NewDecWithPrec(1005, 3), 31536000, MustNewDecFromStr("1.000000000158153903837946258002096839")}, // 1.005 ^ (1/31536000) ≈ 1.000000000158153903837946258002096839
{SmallestDec(), 2, NewDecWithPrec(1, 18)}, // 1e-36 ^ (0.5) => 1e-18
{SmallestDec(), 2, NewDecWithPrec(1, 18)}, // 1e-36 ^ (0.5) => 1e-18
{SmallestDec(), 3, MustNewDecFromStr("0.000000000001000000000000000002431786")}, // 1e-36 ^ (1/3) => 1e-12
{NewDecWithPrec(1, 8), 3, MustNewDecFromStr("0.002154434690031883721759293566519280")}, // 1e-8 ^ (1/3) ≈ 0.002154434690031883721759293566519
}
Expand All @@ -485,11 +487,11 @@ func (s *decimalTestSuite) TestApproxSqrt() {
input BigDec
expected BigDec
}{
{OneDec(), OneDec()}, // 1.0 => 1.0
{NewDecWithPrec(25, 2), NewDecWithPrec(5, 1)}, // 0.25 => 0.5
{NewDecWithPrec(4, 2), NewDecWithPrec(2, 1)}, // 0.09 => 0.3
{NewDecFromInt(NewInt(9)), NewDecFromInt(NewInt(3))}, // 9 => 3
{NewDecFromInt(NewInt(-9)), NewDecFromInt(NewInt(-3))}, // -9 => -3
{OneDec(), OneDec()}, // 1.0 => 1.0
{NewDecWithPrec(25, 2), NewDecWithPrec(5, 1)}, // 0.25 => 0.5
{NewDecWithPrec(4, 2), NewDecWithPrec(2, 1)}, // 0.09 => 0.3
{NewDecFromInt(NewInt(9)), NewDecFromInt(NewInt(3))}, // 9 => 3
{NewDecFromInt(NewInt(-9)), NewDecFromInt(NewInt(-3))}, // -9 => -3
{NewDecFromInt(NewInt(2)), MustNewDecFromStr("1.414213562373095048801688724209698079")}, // 2 => 1.414213562373095048801688724209698079
}

Expand Down Expand Up @@ -624,3 +626,62 @@ func BenchmarkMarshalTo(b *testing.B) {
}
}
}

func (s *decimalTestSuite) TestLog2() {

tests := map[string]struct {
initialValue BigDec
expected BigDec

expectedPanic bool
}{
"log_2{0.99}; not supported; panic": {
initialValue: NewDecWithPrec(99, 2),

expectedPanic: true,
},
"log_2{1} = 0": {
initialValue: NewBigDec(1),
expected: NewBigDec(0),
},
"log_2{2} = 1": {
initialValue: NewBigDec(2),
expected: NewBigDec(1),
},
"log_2{512} = 9": {
initialValue: NewBigDec(512),
expected: NewBigDec(9),
},
"log_2{600} = 9": {
initialValue: NewBigDec(580),
// TODO: true value is: 9.179909090014934468590092754117374938
// Need better lookup table.
expected: MustNewDecFromStr("9.137503523749934908329043617236402782"),
},
"log_2{1024} = 10": {
initialValue: NewBigDec(1024),
expected: NewBigDec(10),
},
"log_2{1024.987654321} = 10": {
initialValue: NewDecWithPrec(1024987654321, 9),
// TODO: true value is: 10.001390817654141324352719749259888355
// Need better lookup table
expected: MustNewDecFromStr("10.000000000000000000000000000000000000"),
},
"log_2{912648174127941279170121098210.92821920190204131121} = 99.525973560175362367047484597337715868": {
initialValue: MustNewDecFromStr("912648174127941279170121098210.92821920190204131121"),
// TODO: true value is: 99.525973560175362367047484597337715868
// Need better lookup table
expected: MustNewDecFromStr("99.485426827170241759571649887742440632"),
},
}

for name, tc := range tests {
s.Run(name, func() {
osmoassert.ConditionalPanic(s.T(), tc.expectedPanic, func() {
res := tc.initialValue.ApproxLog2()
s.Require().Equal(tc.expected, res)
})
})
}
}
34 changes: 34 additions & 0 deletions osmomath/log2_bench_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
package osmomath

import (
"testing"
)

func BenchmarkLog2(b *testing.B) {
tests := []struct {
value BigDec
}{
// TODO: Choose selection here more robustly
{
value: MustNewDecFromStr("1.2"),
},
{
value: MustNewDecFromStr("1.234"),
},
{
value: MustNewDecFromStr("1024"),
},
{
value: NewBigDec(2048 * 2048 * 2048 * 2048 * 2048),
},
{
value: MustNewDecFromStr("999999999999999999999999999999999999999999999999999999.9122181273612911"),
},
}

for i := 0; i < b.N; i++ {
for _, test := range tests {
test.value.ApproxLog2()
}
}
}