Skip to content

Commit

Permalink
Move binary search from osmomath to osmoutils (#3763)
Browse files Browse the repository at this point in the history
* Move binary search from osmomath to osmoutils

* Add changelog
  • Loading branch information
ValarDragon authored Dec 16, 2022
1 parent f4d8377 commit 3095a7d
Show file tree
Hide file tree
Showing 10 changed files with 86 additions and 89 deletions.
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,10 @@ 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`.
* [#3693](https://github.com/osmosis-labs/osmosis/pull/3693) Add `EstimateSwapExactAmountOut` query to stargate whitelist

### 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 @@ -62,6 +66,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
* [#3712](https://github.com/osmosis-labs/osmosis/pull/3712) replace `osmomath.BigDec` `Power` with `PowerInteger`
* [#3711](https://github.com/osmosis-labs/osmosis/pull/3711) Use Dec instead of Int for additive `ErrTolerace` in `osmoutils`.


## 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.Dec
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)) {
if diff.GT(BigDecFromSDKDec(e.AdditiveTolerance)) {
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.OneDec()}
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.NewDec(1 << 30)}
errToleranceBoth := ErrTolerance{AdditiveTolerance: sdk.NewDec(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.NewDec(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

0 comments on commit 3095a7d

Please sign in to comment.