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

Move binary search from osmomath to osmoutils (backport #3763) #3765

Merged
merged 2 commits into from
Dec 16, 2022
Merged
Show file tree
Hide file tree
Changes from all 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
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

* Correctly apply DragonBerry IBC patch

### API breaks

* [#3763](https://github.com/osmosis-labs/osmosis/pull/3763) Move binary search and error tolerance code from `osmoutils` into `osmomath`

### Bug fixes

* [#3608](https://github.com/osmosis-labs/osmosis/pull/3608) Make it possible to state export from any directory.
Expand All @@ -77,6 +81,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
* [#3634](https://github.com/osmosis-labs/osmosis/pull/3634) (Makefile) Ensure correct golang version in make build and make install. (Thank you @jhernandezb )
* [#3712](https://github.com/osmosis-labs/osmosis/pull/3712) replace `osmomath.BigDec` `Power` with `PowerInteger`


## v13.0.0

This release includes stableswap, and expands the IBC safety & composability functionality of Osmosis. The primary features are:
Expand Down
42 changes: 20 additions & 22 deletions osmoutils/binary_search.go → osmomath/binary_search.go
Original file line number Diff line number Diff line change
@@ -1,11 +1,9 @@
package osmoutils
package osmomath

import (
"errors"

sdk "github.com/cosmos/cosmos-sdk/types"

"github.com/osmosis-labs/osmosis/v13/osmomath"
)

// ErrTolerance is used to define a compare function, which checks if two
Expand All @@ -25,7 +23,7 @@ import (
type ErrTolerance struct {
AdditiveTolerance sdk.Int
MultiplicativeTolerance sdk.Dec
RoundingDir osmomath.RoundingDirection
RoundingDir RoundingDirection
}

// Compare returns if actual is within errTolerance of expected.
Expand All @@ -46,11 +44,11 @@ func (e ErrTolerance) Compare(expected sdk.Int, actual sdk.Int) int {
// so if were supposed to round down, it must be that `expected >= actual`.
// likewise if were supposed to round up, it must be that `expected <= actual`.
// If neither of the above, then rounding direction does not enforce a constraint.
if e.RoundingDir == osmomath.RoundDown {
if e.RoundingDir == RoundDown {
if expected.LT(actual) {
return -1
}
} else if e.RoundingDir == osmomath.RoundUp {
} else if e.RoundingDir == RoundUp {
if expected.GT(actual) {
return 1
}
Expand Down Expand Up @@ -84,16 +82,16 @@ func (e ErrTolerance) Compare(expected sdk.Int, actual sdk.Int) int {
// returns 0 if it is
// returns 1 if not, and expected > actual.
// returns -1 if not, and expected < actual
func (e ErrTolerance) CompareBigDec(expected osmomath.BigDec, actual osmomath.BigDec) int {
func (e ErrTolerance) CompareBigDec(expected BigDec, actual BigDec) int {
// Ensure that even if expected is within tolerance of actual, we don't count it as equal if its in the wrong direction.
// so if were supposed to round down, it must be that `expected >= actual`.
// likewise if were supposed to round up, it must be that `expected <= actual`.
// If neither of the above, then rounding direction does not enforce a constraint.
if e.RoundingDir == osmomath.RoundDown {
if e.RoundingDir == RoundDown {
if expected.LT(actual) {
return -1
}
} else if e.RoundingDir == osmomath.RoundUp {
} else if e.RoundingDir == RoundUp {
if expected.GT(actual) {
return 1
}
Expand All @@ -117,15 +115,15 @@ func (e ErrTolerance) CompareBigDec(expected osmomath.BigDec, actual osmomath.Bi
}
}

if diff.GT(osmomath.BigDecFromSDKDec(e.AdditiveTolerance.ToDec())) {
if diff.GT(BigDecFromSDKDec(e.AdditiveTolerance.ToDec())) {
return comparisonSign
}
}
// Check multiplicative tolerance equations
if !e.MultiplicativeTolerance.IsNil() && !e.MultiplicativeTolerance.IsZero() {
errTerm := diff.Quo(osmomath.MinDec(expected.Abs(), actual.Abs()))
errTerm := diff.Quo(MinDec(expected.Abs(), actual.Abs()))
// fmt.Printf("err term %v\n", errTerm)
if errTerm.GT(osmomath.BigDecFromSDKDec(e.MultiplicativeTolerance)) {
if errTerm.GT(BigDecFromSDKDec(e.MultiplicativeTolerance)) {
return comparisonSign
}
}
Expand Down Expand Up @@ -184,18 +182,18 @@ type SdkDec[D any] interface {
//
// It binary searches on the input range, until it finds an input y s.t. f(y) meets the err tolerance constraints for how close it is to x.
// If we perform more than maxIterations (or equivalently lowerbound = upperbound), we return an error.
func BinarySearchBigDec(f func(input osmomath.BigDec) (osmomath.BigDec, error),
lowerbound osmomath.BigDec,
upperbound osmomath.BigDec,
targetOutput osmomath.BigDec,
func BinarySearchBigDec(f func(input BigDec) (BigDec, error),
lowerbound BigDec,
upperbound BigDec,
targetOutput BigDec,
errTolerance ErrTolerance,
maxIterations int,
) (osmomath.BigDec, error) {
) (BigDec, error) {
// Setup base case of loop
curEstimate := lowerbound.Add(upperbound).Quo(osmomath.NewBigDec(2))
curEstimate := lowerbound.Add(upperbound).Quo(NewBigDec(2))
curOutput, err := f(curEstimate)
if err != nil {
return osmomath.BigDec{}, err
return BigDec{}, err
}
curIteration := 0
for ; curIteration < maxIterations; curIteration += 1 {
Expand All @@ -208,12 +206,12 @@ func BinarySearchBigDec(f func(input osmomath.BigDec) (osmomath.BigDec, error),
} else {
return curEstimate, nil
}
curEstimate = lowerbound.Add(upperbound).Quo(osmomath.NewBigDec(2))
curEstimate = lowerbound.Add(upperbound).Quo(NewBigDec(2))
curOutput, err = f(curEstimate)
if err != nil {
return osmomath.BigDec{}, err
return BigDec{}, err
}
}

return osmomath.BigDec{}, errors.New("hit maximum iterations, did not converge fast enough")
return BigDec{}, errors.New("hit maximum iterations, did not converge fast enough")
}
100 changes: 51 additions & 49 deletions osmoutils/binary_search_test.go → osmomath/binary_search_test.go
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
package osmoutils
package osmomath

import (
"fmt"
Expand All @@ -7,14 +7,12 @@ import (

sdk "github.com/cosmos/cosmos-sdk/types"
"github.com/stretchr/testify/require"

"github.com/osmosis-labs/osmosis/v13/osmomath"
)

var (
withinOne = ErrTolerance{AdditiveTolerance: sdk.OneInt()}
withinFactor8 = ErrTolerance{MultiplicativeTolerance: sdk.NewDec(8)}
zero = osmomath.ZeroDec()
zero = ZeroDec()
)

func TestBinarySearch(t *testing.T) {
Expand Down Expand Up @@ -78,30 +76,34 @@ func TestBinarySearch(t *testing.T) {

// straight line function that returns input. Simplest to binary search on,
// binary search directly reveals one bit of the answer in each iteration with this function.
func lineF(a osmomath.BigDec) (osmomath.BigDec, error) {
func lineF(a BigDec) (BigDec, error) {
return a, nil
}
func cubicF(a osmomath.BigDec) (osmomath.BigDec, error) {
func cubicF(a BigDec) (BigDec, error) {
return a.PowerInteger(3), nil
}

var negCubicFConstant = osmomath.NewBigDec(1 << 62).PowerInteger(3).Neg()
var negCubicFConstant BigDec

func init() {
negCubicFConstant = NewBigDec(1 << 62).PowerInteger(3).Neg()
}

func negCubicF(a osmomath.BigDec) (osmomath.BigDec, error) {
func negCubicF(a BigDec) (BigDec, error) {
return a.PowerInteger(3).Add(negCubicFConstant), nil
}

type searchFn func(osmomath.BigDec) (osmomath.BigDec, error)
type searchFn func(BigDec) (BigDec, error)

type binarySearchTestCase struct {
f searchFn
lowerbound osmomath.BigDec
upperbound osmomath.BigDec
targetOutput osmomath.BigDec
lowerbound BigDec
upperbound BigDec
targetOutput BigDec
errTolerance ErrTolerance
maxIterations int

expectedSolvedInput osmomath.BigDec
expectedSolvedInput BigDec
expectErr bool
// This binary searches inputs to a monotonic increasing function F
// We stop when the answer is within error bounds stated by errTolerance
Expand All @@ -117,7 +119,7 @@ type binarySearchTestCase struct {
func TestBinarySearchLineIterationCounts(t *testing.T) {
tests := map[string]binarySearchTestCase{}

generateExactTestCases := func(lowerbound, upperbound osmomath.BigDec,
generateExactTestCases := func(lowerbound, upperbound BigDec,
errTolerance ErrTolerance, maxNumIters int) {
tcSetName := fmt.Sprintf("simple linear case: lower %s, upper %s", lowerbound.String(), upperbound.String())
// first pass get it working with no err tolerance or rounding direction
Expand All @@ -142,9 +144,9 @@ func TestBinarySearchLineIterationCounts(t *testing.T) {
}
}

generateExactTestCases(osmomath.ZeroDec(), osmomath.NewBigDec(1<<20), withinOne, 20)
generateExactTestCases(ZeroDec(), NewBigDec(1<<20), withinOne, 20)
// we can go further than 50, if we could specify non-integer additive err tolerance. TODO: Add this.
generateExactTestCases(osmomath.NewBigDec(1<<20), osmomath.NewBigDec(1<<50), withinOne, 50)
generateExactTestCases(NewBigDec(1<<20), NewBigDec(1<<50), withinOne, 50)
runBinarySearchTestCases(t, tests, exactlyEqual)
}

Expand All @@ -161,10 +163,10 @@ func TestIterationDepthRandValue(t *testing.T) {
errTolerance ErrTolerance, maxNumIters int, errToleranceName string) {
targetF := fnMap[fnName]
targetX := int64(rand.Intn(int(upperbound-lowerbound-1))) + lowerbound + 1
target, _ := targetF(osmomath.NewBigDec(targetX))
target, _ := targetF(NewBigDec(targetX))
testCase := binarySearchTestCase{
f: lineF,
lowerbound: osmomath.NewBigDec(lowerbound), upperbound: osmomath.NewBigDec(upperbound),
lowerbound: NewBigDec(lowerbound), upperbound: NewBigDec(upperbound),
targetOutput: target, expectedSolvedInput: target,
errTolerance: errTolerance,
maxIterations: maxNumIters,
Expand Down Expand Up @@ -194,7 +196,7 @@ const (
equalWithinOne equalityMode = iota
)

func withRoundingDir(e ErrTolerance, r osmomath.RoundingDirection) ErrTolerance {
func withRoundingDir(e ErrTolerance, r RoundingDirection) ErrTolerance {
return ErrTolerance{
AdditiveTolerance: e.AdditiveTolerance,
MultiplicativeTolerance: e.MultiplicativeTolerance,
Expand All @@ -213,11 +215,11 @@ func runBinarySearchTestCases(t *testing.T, tests map[string]binarySearchTestCas
} else {
require.NoError(t, err)
if equality == exactlyEqual {
require.True(osmomath.DecEq(t, tc.expectedSolvedInput, actualSolvedInput))
require.True(DecEq(t, tc.expectedSolvedInput, actualSolvedInput))
} else if equality == errToleranceEqual {
require.True(t, tc.errTolerance.CompareBigDec(tc.expectedSolvedInput, actualSolvedInput) == 0)
} else {
_, valid, msg, dec1, dec2 := osmomath.DecApproxEq(t, tc.expectedSolvedInput, actualSolvedInput, osmomath.OneDec())
_, valid, msg, dec1, dec2 := DecApproxEq(t, tc.expectedSolvedInput, actualSolvedInput, OneDec())
require.True(t, valid, msg+" \n d1 = %s, d2 = %s", dec1, dec2,
tc.expectedSolvedInput, actualSolvedInput)
}
Expand All @@ -230,8 +232,8 @@ func TestBinarySearchBigDec(t *testing.T) {
testErrToleranceAdditive := ErrTolerance{AdditiveTolerance: sdk.NewInt(1 << 30)}
errToleranceBoth := ErrTolerance{AdditiveTolerance: sdk.NewInt(1 << 30), MultiplicativeTolerance: sdk.NewDec(1 << 3)}

twoTo50 := osmomath.NewBigDec(1 << 50)
twoTo25PlusOne := osmomath.NewBigDec(1 + (1 << 25))
twoTo50 := NewBigDec(1 << 50)
twoTo25PlusOne := NewBigDec(1 + (1 << 25))
twoTo25PlusOneCubed := twoTo25PlusOne.PowerInteger(3)

tests := map[string]binarySearchTestCase{
Expand All @@ -245,34 +247,34 @@ func TestBinarySearchBigDec(t *testing.T) {
"cubic f, within 2^30, target 2^33 - 2^29": {
cubicF,
zero, twoTo50,
osmomath.NewBigDec((1 << 33) - (1 << 29)),
testErrToleranceAdditive, 51, osmomath.NewBigDec(1 << 11), false},
NewBigDec((1 << 33) - (1 << 29)),
testErrToleranceAdditive, 51, NewBigDec(1 << 11), false},
// basically same as above, but due to needing to roundup, we converge at a value > 2^11.
// We try (1<<11 + 1<<10)^3 which is way too large.
// notice by trial, that (1 << 11 + 1<<7)^3 - target > 2^30, but that
// (1 << 11 + 1<<6)^3 - target < 2^30, so that is the answer.
"cubic f, within 2^30, roundup, target 2^33 + 2^29": {
cubicF,
zero, twoTo50,
osmomath.NewBigDec((1 << 33) + (1 << 29)),
withRoundingDir(testErrToleranceAdditive, osmomath.RoundUp),
51, osmomath.NewBigDec(1<<11 + 1<<6), false},
NewBigDec((1 << 33) + (1 << 29)),
withRoundingDir(testErrToleranceAdditive, RoundUp),
51, NewBigDec(1<<11 + 1<<6), false},
"cubic f, large multiplicative err tolerance, converges": {
cubicF,
zero, twoTo50,
osmomath.NewBigDec(1 << 30), withinFactor8, 51, osmomath.NewBigDec(1 << 11), false},
NewBigDec(1 << 30), withinFactor8, 51, NewBigDec(1 << 11), false},
"cubic f, both err tolerances, converges": {
cubicF,
zero, twoTo50,
osmomath.NewBigDec((1 << 33) - (1 << 29)),
errToleranceBoth, 51, osmomath.NewBigDec(1 << 11), false},
NewBigDec((1 << 33) - (1 << 29)),
errToleranceBoth, 51, NewBigDec(1 << 11), false},
"neg cubic f, no err tolerance, converges": {negCubicF, zero, twoTo50,
twoTo25PlusOneCubed.Add(negCubicFConstant), withinOne, 51, twoTo25PlusOne, false},
// "neg cubic f, large multiplicative err tolerance, converges": {
// negCubicF,
// zero, twoTo50,
// osmomath.NewBigDec(1 << 30).Add(negCubicFConstant),
// withinFactor8, 51, osmomath.NewBigDec(1 << 11), false},
// NewBigDec(1 << 30).Add(negCubicFConstant),
// withinFactor8, 51, NewBigDec(1 << 11), false},
}

runBinarySearchTestCases(t, tests, equalWithinOne)
Expand All @@ -281,35 +283,35 @@ func TestBinarySearchBigDec(t *testing.T) {
func TestBinarySearchRoundingBehavior(t *testing.T) {
withinTwoTo30 := ErrTolerance{AdditiveTolerance: sdk.NewInt(1 << 30)}

twoTo50 := osmomath.NewBigDec(1 << 50)
// twoTo25PlusOne := osmomath.NewBigDec(1 + (1 << 25))
twoTo50 := NewBigDec(1 << 50)
// twoTo25PlusOne := NewBigDec(1 + (1 << 25))
// twoTo25PlusOneCubed := twoTo25PlusOne.Power(3)

tests := map[string]binarySearchTestCase{
"lineF, roundup within 2^30, target 2^32 + 2^30 + 1, expected=2^32 + 2^31": {f: lineF,
lowerbound: zero, upperbound: twoTo50,
targetOutput: osmomath.NewBigDec((1 << 32) + (1 << 30) + 1),
errTolerance: withRoundingDir(withinTwoTo30, osmomath.RoundUp),
targetOutput: NewBigDec((1 << 32) + (1 << 30) + 1),
errTolerance: withRoundingDir(withinTwoTo30, RoundUp),
maxIterations: 51,
expectedSolvedInput: osmomath.NewBigDec(1<<32 + 1<<31)},
expectedSolvedInput: NewBigDec(1<<32 + 1<<31)},
"lineF, roundup within 2^30, target 2^32 + 2^30 - 1, expected=2^32 + 2^30": {f: lineF,
lowerbound: zero, upperbound: twoTo50,
targetOutput: osmomath.NewBigDec((1 << 32) + (1 << 30) - 1),
errTolerance: withRoundingDir(withinTwoTo30, osmomath.RoundUp),
targetOutput: NewBigDec((1 << 32) + (1 << 30) - 1),
errTolerance: withRoundingDir(withinTwoTo30, RoundUp),
maxIterations: 51,
expectedSolvedInput: osmomath.NewBigDec(1<<32 + 1<<30)},
expectedSolvedInput: NewBigDec(1<<32 + 1<<30)},
"lineF, rounddown within 2^30, target 2^32 + 2^30 + 1, expected=2^32 + 2^31": {f: lineF,
lowerbound: zero, upperbound: twoTo50,
targetOutput: osmomath.NewBigDec((1 << 32) + (1 << 30) + 1),
errTolerance: withRoundingDir(withinTwoTo30, osmomath.RoundDown),
targetOutput: NewBigDec((1 << 32) + (1 << 30) + 1),
errTolerance: withRoundingDir(withinTwoTo30, RoundDown),
maxIterations: 51,
expectedSolvedInput: osmomath.NewBigDec(1<<32 + 1<<30)},
expectedSolvedInput: NewBigDec(1<<32 + 1<<30)},
"lineF, rounddown within 2^30, target 2^32 + 2^30 - 1, expected=2^32 + 2^30": {f: lineF,
lowerbound: zero, upperbound: twoTo50,
targetOutput: osmomath.NewBigDec((1 << 32) + (1 << 30) - 1),
errTolerance: withRoundingDir(withinTwoTo30, osmomath.RoundDown),
targetOutput: NewBigDec((1 << 32) + (1 << 30) - 1),
errTolerance: withRoundingDir(withinTwoTo30, RoundDown),
maxIterations: 51,
expectedSolvedInput: osmomath.NewBigDec(1 << 32)},
expectedSolvedInput: NewBigDec(1 << 32)},
}

runBinarySearchTestCases(t,
Expand Down Expand Up @@ -358,11 +360,11 @@ func TestErrTolerance_Compare(t *testing.T) {
if gotIntRev != -tt.expectedCompareResult {
t.Errorf("ErrTolerance.Compare() = %v, want %v", gotIntRev, -tt.expectedCompareResult)
}
gotBigDec := tt.tol.CompareBigDec(osmomath.NewBigDec(tt.intInput), osmomath.NewBigDec(tt.intReference))
gotBigDec := tt.tol.CompareBigDec(NewBigDec(tt.intInput), NewBigDec(tt.intReference))
if gotBigDec != tt.expectedCompareResult {
t.Errorf("ErrTolerance.CompareBigDec() = %v, want %v", gotBigDec, tt.expectedCompareResult)
}
gotBigDecRev := tt.tol.CompareBigDec(osmomath.NewBigDec(tt.intReference), osmomath.NewBigDec(tt.intInput))
gotBigDecRev := tt.tol.CompareBigDec(NewBigDec(tt.intReference), NewBigDec(tt.intInput))
if gotBigDecRev != -tt.expectedCompareResult {
t.Errorf("ErrTolerance.CompareBigDec() = %v, want %v", gotBigDecRev, -tt.expectedCompareResult)
}
Expand Down
4 changes: 0 additions & 4 deletions osmomath/math.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,10 +10,6 @@ import (
// TODO: Analyze choice here.
var powPrecision, _ = sdk.NewDecFromStr("0.00000001")

// Singletons.
// nolint: deadcode, unused
var zero sdk.Dec = sdk.ZeroDec()

var (
one_half sdk.Dec = sdk.MustNewDecFromStr("0.5")
one sdk.Dec = sdk.OneDec()
Expand Down
Loading