-
Notifications
You must be signed in to change notification settings - Fork 119
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
Add fixed-point WithinTolerance
method and greater-than operators
#1135
Conversation
This commit introduces functions to handle the > and >= comparisons for integer types.
Pull Request Test Coverage Report for Build 11055315406Details
💛 - Coveralls |
firstFp: FixedPointFromUint64[N](1000000, 1), | ||
secondFp: FixedPointFromUint64[N](900000, 1), |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Are there any overflow cases to handle?
Seems there should be sufficient space:
log(2**64)/log(1000000*900000) = 1.6
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
WithinTolerance
uses methods from the rfqmath.Arithmetic
interface for calculations, so I think the overflow responsibility should be on those methods. I'm not sure if its possible to get the Mul
calls to overflow.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm not sure if its possible to get the Mul calls to overflow.
So we'll use the BigInt[T]
version everywhere, which for all practical purposes, can't actually overflow.
ContentPriceOracle will deliver quote fixedPointsIs a simplification |
a3e7afc
to
dfa1c7f
Compare
Use case for this new method is replacing |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nice looking PR. Made some comments throughout.
I'm still not fully understanding the use case tho:
This will be used during the handling of sell accept messages and buy accept messages that the node receives from its peer. The bid price or the ask price in those messages is compared to the bid price or the ask price respectively that our oracle provides. If it's within bounds (specified in ppm) we accept the offer.
But iiuc at that point we are already comparing prices converted to BTC? But the WithinTolerance
works with FixedPoint[T]
which I very much associate with taproot assets.
Shouldn't WithinTolerance
work with lnwire.MilliSatoshi
parameters?
rfqmath/fixed_point_test.go
Outdated
@@ -175,4 +338,6 @@ func TestFixedPoint(t *testing.T) { | |||
t.Run("equality", rapid.MakeCheck(testEquality[BigInt])) | |||
|
|||
t.Run("from_uint64", rapid.MakeCheck(testFromUint64[BigInt])) | |||
|
|||
t.Run("within_tolerance", rapid.MakeCheck(testWithinTolerance[BigInt])) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why are we using rapid here if we don't use pseudo-random data for the inputs?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Just following the existing pattern. I might add random data inputs if I can think of something useful.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If you want to use rapid
, then the tests need to be written in a more black-box style. So asserting some invariants/properties based on the desired behavior of the function in the abstract.
Non-blocking though.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think we can retain this current TDD test (moving it outside the scope of this function), and also add this property based test to supplement things:
package rfqmath
import (
"math/big"
"testing"
"github.com/stretchr/testify/require"
"pgregory.net/rapid"
)
func TestWithinToleranceBigInt(t *testing.T) {
rapid.Check(t, func(t *rapid.T) {
// Create a require instance
req := require.New(t)
// Generate random BigInts and scales
a := rapid.Uint64().Draw(t, "a")
b := rapid.Uint64().Draw(t, "b")
scaleA := rapid.Uint8Range(0, 18).Draw(t, "scaleA")
scaleB := rapid.Uint8Range(0, 18).Draw(t, "scaleB")
tolerancePpm := rapid.Uint64Range(0, 1000000).Draw(t, "tolerancePpm")
fpA := FixedPointFromUint64[BigInt](a, scaleA)
fpB := FixedPointFromUint64[BigInt](b, scaleB)
// Test property: WithinTolerance should be reflexive
req.True(fpA.WithinTolerance(fpA, NewBigInt(big.NewInt(int64(tolerancePpm)))),
"A number should be within tolerance of itself")
// Test property: WithinTolerance should be symmetric
resultAB := fpA.WithinTolerance(fpB, NewBigInt(big.NewInt(int64(tolerancePpm))))
resultBA := fpB.WithinTolerance(fpA, NewBigInt(big.NewInt(int64(tolerancePpm))))
req.Equal(resultAB, resultBA,
"WithinTolerance should be symmetric")
// Test property: Zero tolerance should only allow exact equality
zeroTolerance := NewBigInt(big.NewInt(0))
req.Equal(fpA.WithinTolerance(fpB, zeroTolerance), fpA.Equals(fpB),
"Zero tolerance should only allow exact equality")
// Test property: Maximum tolerance (1,000,000 PPM) should always return true
maxTolerance := NewBigInt(big.NewInt(1000000))
req.True(fpA.WithinTolerance(fpB, maxTolerance),
"Maximum tolerance should always return true")
// Test property: Tolerance behavior near boundaries
if !fpA.Equals(fpB) {
largerScale := scaleA
if scaleB > scaleA {
largerScale = scaleB
}
fpAScaled := fpA.ScaleTo(largerScale)
fpBScaled := fpB.ScaleTo(largerScale)
var delta *big.Int
if fpAScaled.Coefficient.Gt(fpBScaled.Coefficient) {
delta = new(big.Int).Sub(fpAScaled.Coefficient.value, fpBScaled.Coefficient.value)
} else {
delta = new(big.Int).Sub(fpBScaled.Coefficient.value, fpAScaled.Coefficient.value)
}
maxCoeff := new(big.Int).Set(fpAScaled.Coefficient.value)
if fpBScaled.Coefficient.Gt(fpAScaled.Coefficient) {
maxCoeff.Set(fpBScaled.Coefficient.value)
}
// Calculate the exact PPM difference
ppmDiff := new(big.Int).Mul(delta, big.NewInt(1000000))
ppmDiff.Div(ppmDiff, maxCoeff)
// Test that WithinTolerance returns true for tolerances greater than or equal to ppmDiff
req.True(fpA.WithinTolerance(fpB, NewBigInt(new(big.Int).Add(ppmDiff, big.NewInt(1)))),
"Should be within tolerance for tolerance > ppmDiff")
// Test that WithinTolerance returns false for tolerances less than ppmDiff
if ppmDiff.Sign() > 0 {
req.False(fpA.WithinTolerance(fpB, NewBigInt(new(big.Int).Sub(ppmDiff, big.NewInt(1)))),
"Should not be within tolerance for tolerance < ppmDiff")
}
}
})
}
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
A variant that breaks things up a bit, and uses float64
to compute the final tolerance values directly:
package rfqmath_test
import (
"math"
"math/big"
"testing"
"pgregory.net/rapid"
"github.com/yourusername/yourmodule/rfqmath"
)
func TestWithinTolerance_EqualValues(t *testing.T) {
rapid.Check(t, func(t *rapid.T) {
// Generate a random coefficient and scale
coef := t.Int64()
scale := uint8(t.Uint32Range(0, 18))
tolerancePpm := t.Uint64()
// Create two identical FixedPoint[BigInt] values
f1 := rfqmath.FixedPoint[rfqmath.BigInt]{
Coefficient: rfqmath.NewBigInt(big.NewInt(coef)),
Scale: scale,
}
f2 := rfqmath.FixedPoint[rfqmath.BigInt]{
Coefficient: rfqmath.NewBigInt(big.NewInt(coef)),
Scale: scale,
}
tolerancePpmBigInt := rfqmath.NewBigInt(big.NewInt(int64(tolerancePpm)))
// The result should always be true when the values are equal
result := f1.WithinTolerance(f2, tolerancePpmBigInt)
if !result {
t.Fatalf("WithinTolerance should be true when values are equal, but got false")
}
})
}
func TestWithinTolerance_ToleranceZero(t *testing.T) {
rapid.Check(t, func(t *rapid.T) {
// Generate random coefficients and scales
coef1 := t.Int64()
coef2 := t.Int64()
scale1 := uint8(t.Uint32Range(0, 18))
scale2 := uint8(t.Uint32Range(0, 18))
f1 := rfqmath.FixedPoint[rfqmath.BigInt]{
Coefficient: rfqmath.NewBigInt(big.NewInt(coef1)),
Scale: scale1,
}
f2 := rfqmath.FixedPoint[rfqmath.BigInt]{
Coefficient: rfqmath.NewBigInt(big.NewInt(coef2)),
Scale: scale2,
}
tolerancePpmBigInt := rfqmath.NewBigInt(big.NewInt(0))
result := f1.WithinTolerance(f2, tolerancePpmBigInt)
// Scale both to the larger scale for comparison
largerScale := scale1
if scale2 > scale1 {
largerScale = scale2
}
scaledF1 := f1.ScaleTo(largerScale)
scaledF2 := f2.ScaleTo(largerScale)
equal := scaledF1.Coefficient.Equals(scaledF2.Coefficient)
if result != equal {
t.Fatalf("WithinTolerance with zero tolerance mismatch: expected %v, got %v", equal, result)
}
})
}
func TestWithinTolerance_Symmetric(t *testing.T) {
rapid.Check(t, func(t *rapid.T) {
// Generate random coefficients, scales, and tolerance
coef1 := t.Int64()
coef2 := t.Int64()
scale1 := uint8(t.Uint32Range(0, 18))
scale2 := uint8(t.Uint32Range(0, 18))
tolerancePpm := t.Uint64()
f1 := rfqmath.FixedPoint[rfqmath.BigInt]{
Coefficient: rfqmath.NewBigInt(big.NewInt(coef1)),
Scale: scale1,
}
f2 := rfqmath.FixedPoint[rfqmath.BigInt]{
Coefficient: rfqmath.NewBigInt(big.NewInt(coef2)),
Scale: scale2,
}
tolerancePpmBigInt := rfqmath.NewBigInt(big.NewInt(int64(tolerancePpm)))
result1 := f1.WithinTolerance(f2, tolerancePpmBigInt)
result2 := f2.WithinTolerance(f1, tolerancePpmBigInt)
if result1 != result2 {
t.Fatalf("WithinTolerance is not symmetric: f1.WithinTolerance(f2)=%v, f2.WithinTolerance(f1)=%v", result1, result2)
}
})
}
func TestWithinTolerance_MaxTolerance(t *testing.T) {
rapid.Check(t, func(t *rapid.T) {
// Generate random coefficients and scales
coef1 := t.Int64()
coef2 := t.Int64()
scale1 := uint8(t.Uint32Range(0, 18))
scale2 := uint8(t.Uint32Range(0, 18))
// Set tolerancePpm to a large value
tolerancePpm := t.Uint64Range(1_000_000, 10_000_000_000)
f1 := rfqmath.FixedPoint[rfqmath.BigInt]{
Coefficient: rfqmath.NewBigInt(big.NewInt(coef1)),
Scale: scale1,
}
f2 := rfqmath.FixedPoint[rfqmath.BigInt]{
Coefficient: rfqmath.NewBigInt(big.NewInt(coef2)),
Scale: scale2,
}
tolerancePpmBigInt := rfqmath.NewBigInt(big.NewInt(int64(tolerancePpm)))
// The result should always be true when tolerancePpm is large
result := f1.WithinTolerance(f2, tolerancePpmBigInt)
if !result {
t.Fatalf("WithinTolerance should be true when tolerancePpm is large, but got false")
}
})
}
func TestWithinTolerance_Property(t *testing.T) {
rapid.Check(t, func(t *rapid.T) {
// Generate random coefficients within a reasonable range
coef1 := t.Int64Range(-1_000_000_000, 1_000_000_000)
coef2 := t.Int64Range(-1_000_000_000, 1_000_000_000)
scale1 := uint8(t.Uint32Range(0, 9))
scale2 := uint8(t.Uint32Range(0, 9))
tolerancePpm := t.Uint64Range(0, 1_000_000)
f1 := rfqmath.FixedPoint[rfqmath.BigInt]{
Coefficient: rfqmath.NewBigInt(big.NewInt(coef1)),
Scale: scale1,
}
f2 := rfqmath.FixedPoint[rfqmath.BigInt]{
Coefficient: rfqmath.NewBigInt(big.NewInt(coef2)),
Scale: scale2,
}
tolerancePpmBigInt := rfqmath.NewBigInt(big.NewInt(int64(tolerancePpm)))
result := f1.WithinTolerance(f2, tolerancePpmBigInt)
// Compute expected result using float64
f1Float := f1.ToFloat64()
f2Float := f2.ToFloat64()
delta := math.Abs(f1Float - f2Float)
maxVal := math.Max(math.Abs(f1Float), math.Abs(f2Float))
tolerance := (float64(tolerancePpm) / 1_000_000) * maxVal
expected := delta <= tolerance
if result != expected {
t.Fatalf("WithinTolerance mismatch:\n"+
"f1 = %v (float: %f),\n"+
"f2 = %v (float: %f),\n"+
"tolerancePpm = %v,\n"+
"delta = %e,\n"+
"tolerance = %e,\n"+
"result = %v,\n"+
"expected = %v",
f1, f1Float, f2, f2Float, tolerancePpm, delta, tolerance, result, expected)
}
})
}
@gijswijs Thanks for taking a look. This PR is in preparation for a follow-up PR. In the follow-up, the price oracle and RFQ wire messages will communicate asset-to-BTC rates instead of BTC prices. The current setup is a workaround where the rate coefficient is effectively typed as a millisatoshi amount. The follow-up PR will address and correct this. Basically we want to compare rates and not prices. And the "prices" we're currently dealing with are rates in disguise. |
dfa1c7f
to
63273a1
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
LGTM 🎽
firstFp: FixedPointFromUint64[N](1000000, 1), | ||
secondFp: FixedPointFromUint64[N](900000, 1), |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm not sure if its possible to get the Mul calls to overflow.
So we'll use the BigInt[T]
version everywhere, which for all practical purposes, can't actually overflow.
rfqmath/fixed_point_test.go
Outdated
@@ -175,4 +338,6 @@ func TestFixedPoint(t *testing.T) { | |||
t.Run("equality", rapid.MakeCheck(testEquality[BigInt])) | |||
|
|||
t.Run("from_uint64", rapid.MakeCheck(testFromUint64[BigInt])) | |||
|
|||
t.Run("within_tolerance", rapid.MakeCheck(testWithinTolerance[BigInt])) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If you want to use rapid
, then the tests need to be written in a more black-box style. So asserting some invariants/properties based on the desired behavior of the function in the abstract.
Non-blocking though.
rfqmath/fixed_point_test.go
Outdated
@@ -175,4 +338,6 @@ func TestFixedPoint(t *testing.T) { | |||
t.Run("equality", rapid.MakeCheck(testEquality[BigInt])) | |||
|
|||
t.Run("from_uint64", rapid.MakeCheck(testFromUint64[BigInt])) | |||
|
|||
t.Run("within_tolerance", rapid.MakeCheck(testWithinTolerance[BigInt])) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think we can retain this current TDD test (moving it outside the scope of this function), and also add this property based test to supplement things:
package rfqmath
import (
"math/big"
"testing"
"github.com/stretchr/testify/require"
"pgregory.net/rapid"
)
func TestWithinToleranceBigInt(t *testing.T) {
rapid.Check(t, func(t *rapid.T) {
// Create a require instance
req := require.New(t)
// Generate random BigInts and scales
a := rapid.Uint64().Draw(t, "a")
b := rapid.Uint64().Draw(t, "b")
scaleA := rapid.Uint8Range(0, 18).Draw(t, "scaleA")
scaleB := rapid.Uint8Range(0, 18).Draw(t, "scaleB")
tolerancePpm := rapid.Uint64Range(0, 1000000).Draw(t, "tolerancePpm")
fpA := FixedPointFromUint64[BigInt](a, scaleA)
fpB := FixedPointFromUint64[BigInt](b, scaleB)
// Test property: WithinTolerance should be reflexive
req.True(fpA.WithinTolerance(fpA, NewBigInt(big.NewInt(int64(tolerancePpm)))),
"A number should be within tolerance of itself")
// Test property: WithinTolerance should be symmetric
resultAB := fpA.WithinTolerance(fpB, NewBigInt(big.NewInt(int64(tolerancePpm))))
resultBA := fpB.WithinTolerance(fpA, NewBigInt(big.NewInt(int64(tolerancePpm))))
req.Equal(resultAB, resultBA,
"WithinTolerance should be symmetric")
// Test property: Zero tolerance should only allow exact equality
zeroTolerance := NewBigInt(big.NewInt(0))
req.Equal(fpA.WithinTolerance(fpB, zeroTolerance), fpA.Equals(fpB),
"Zero tolerance should only allow exact equality")
// Test property: Maximum tolerance (1,000,000 PPM) should always return true
maxTolerance := NewBigInt(big.NewInt(1000000))
req.True(fpA.WithinTolerance(fpB, maxTolerance),
"Maximum tolerance should always return true")
// Test property: Tolerance behavior near boundaries
if !fpA.Equals(fpB) {
largerScale := scaleA
if scaleB > scaleA {
largerScale = scaleB
}
fpAScaled := fpA.ScaleTo(largerScale)
fpBScaled := fpB.ScaleTo(largerScale)
var delta *big.Int
if fpAScaled.Coefficient.Gt(fpBScaled.Coefficient) {
delta = new(big.Int).Sub(fpAScaled.Coefficient.value, fpBScaled.Coefficient.value)
} else {
delta = new(big.Int).Sub(fpBScaled.Coefficient.value, fpAScaled.Coefficient.value)
}
maxCoeff := new(big.Int).Set(fpAScaled.Coefficient.value)
if fpBScaled.Coefficient.Gt(fpAScaled.Coefficient) {
maxCoeff.Set(fpBScaled.Coefficient.value)
}
// Calculate the exact PPM difference
ppmDiff := new(big.Int).Mul(delta, big.NewInt(1000000))
ppmDiff.Div(ppmDiff, maxCoeff)
// Test that WithinTolerance returns true for tolerances greater than or equal to ppmDiff
req.True(fpA.WithinTolerance(fpB, NewBigInt(new(big.Int).Add(ppmDiff, big.NewInt(1)))),
"Should be within tolerance for tolerance > ppmDiff")
// Test that WithinTolerance returns false for tolerances less than ppmDiff
if ppmDiff.Sign() > 0 {
req.False(fpA.WithinTolerance(fpB, NewBigInt(new(big.Int).Sub(ppmDiff, big.NewInt(1)))),
"Should not be within tolerance for tolerance < ppmDiff")
}
}
})
}
rfqmath/fixed_point_test.go
Outdated
@@ -175,4 +338,6 @@ func TestFixedPoint(t *testing.T) { | |||
t.Run("equality", rapid.MakeCheck(testEquality[BigInt])) | |||
|
|||
t.Run("from_uint64", rapid.MakeCheck(testFromUint64[BigInt])) | |||
|
|||
t.Run("within_tolerance", rapid.MakeCheck(testWithinTolerance[BigInt])) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
A variant that breaks things up a bit, and uses float64
to compute the final tolerance values directly:
package rfqmath_test
import (
"math"
"math/big"
"testing"
"pgregory.net/rapid"
"github.com/yourusername/yourmodule/rfqmath"
)
func TestWithinTolerance_EqualValues(t *testing.T) {
rapid.Check(t, func(t *rapid.T) {
// Generate a random coefficient and scale
coef := t.Int64()
scale := uint8(t.Uint32Range(0, 18))
tolerancePpm := t.Uint64()
// Create two identical FixedPoint[BigInt] values
f1 := rfqmath.FixedPoint[rfqmath.BigInt]{
Coefficient: rfqmath.NewBigInt(big.NewInt(coef)),
Scale: scale,
}
f2 := rfqmath.FixedPoint[rfqmath.BigInt]{
Coefficient: rfqmath.NewBigInt(big.NewInt(coef)),
Scale: scale,
}
tolerancePpmBigInt := rfqmath.NewBigInt(big.NewInt(int64(tolerancePpm)))
// The result should always be true when the values are equal
result := f1.WithinTolerance(f2, tolerancePpmBigInt)
if !result {
t.Fatalf("WithinTolerance should be true when values are equal, but got false")
}
})
}
func TestWithinTolerance_ToleranceZero(t *testing.T) {
rapid.Check(t, func(t *rapid.T) {
// Generate random coefficients and scales
coef1 := t.Int64()
coef2 := t.Int64()
scale1 := uint8(t.Uint32Range(0, 18))
scale2 := uint8(t.Uint32Range(0, 18))
f1 := rfqmath.FixedPoint[rfqmath.BigInt]{
Coefficient: rfqmath.NewBigInt(big.NewInt(coef1)),
Scale: scale1,
}
f2 := rfqmath.FixedPoint[rfqmath.BigInt]{
Coefficient: rfqmath.NewBigInt(big.NewInt(coef2)),
Scale: scale2,
}
tolerancePpmBigInt := rfqmath.NewBigInt(big.NewInt(0))
result := f1.WithinTolerance(f2, tolerancePpmBigInt)
// Scale both to the larger scale for comparison
largerScale := scale1
if scale2 > scale1 {
largerScale = scale2
}
scaledF1 := f1.ScaleTo(largerScale)
scaledF2 := f2.ScaleTo(largerScale)
equal := scaledF1.Coefficient.Equals(scaledF2.Coefficient)
if result != equal {
t.Fatalf("WithinTolerance with zero tolerance mismatch: expected %v, got %v", equal, result)
}
})
}
func TestWithinTolerance_Symmetric(t *testing.T) {
rapid.Check(t, func(t *rapid.T) {
// Generate random coefficients, scales, and tolerance
coef1 := t.Int64()
coef2 := t.Int64()
scale1 := uint8(t.Uint32Range(0, 18))
scale2 := uint8(t.Uint32Range(0, 18))
tolerancePpm := t.Uint64()
f1 := rfqmath.FixedPoint[rfqmath.BigInt]{
Coefficient: rfqmath.NewBigInt(big.NewInt(coef1)),
Scale: scale1,
}
f2 := rfqmath.FixedPoint[rfqmath.BigInt]{
Coefficient: rfqmath.NewBigInt(big.NewInt(coef2)),
Scale: scale2,
}
tolerancePpmBigInt := rfqmath.NewBigInt(big.NewInt(int64(tolerancePpm)))
result1 := f1.WithinTolerance(f2, tolerancePpmBigInt)
result2 := f2.WithinTolerance(f1, tolerancePpmBigInt)
if result1 != result2 {
t.Fatalf("WithinTolerance is not symmetric: f1.WithinTolerance(f2)=%v, f2.WithinTolerance(f1)=%v", result1, result2)
}
})
}
func TestWithinTolerance_MaxTolerance(t *testing.T) {
rapid.Check(t, func(t *rapid.T) {
// Generate random coefficients and scales
coef1 := t.Int64()
coef2 := t.Int64()
scale1 := uint8(t.Uint32Range(0, 18))
scale2 := uint8(t.Uint32Range(0, 18))
// Set tolerancePpm to a large value
tolerancePpm := t.Uint64Range(1_000_000, 10_000_000_000)
f1 := rfqmath.FixedPoint[rfqmath.BigInt]{
Coefficient: rfqmath.NewBigInt(big.NewInt(coef1)),
Scale: scale1,
}
f2 := rfqmath.FixedPoint[rfqmath.BigInt]{
Coefficient: rfqmath.NewBigInt(big.NewInt(coef2)),
Scale: scale2,
}
tolerancePpmBigInt := rfqmath.NewBigInt(big.NewInt(int64(tolerancePpm)))
// The result should always be true when tolerancePpm is large
result := f1.WithinTolerance(f2, tolerancePpmBigInt)
if !result {
t.Fatalf("WithinTolerance should be true when tolerancePpm is large, but got false")
}
})
}
func TestWithinTolerance_Property(t *testing.T) {
rapid.Check(t, func(t *rapid.T) {
// Generate random coefficients within a reasonable range
coef1 := t.Int64Range(-1_000_000_000, 1_000_000_000)
coef2 := t.Int64Range(-1_000_000_000, 1_000_000_000)
scale1 := uint8(t.Uint32Range(0, 9))
scale2 := uint8(t.Uint32Range(0, 9))
tolerancePpm := t.Uint64Range(0, 1_000_000)
f1 := rfqmath.FixedPoint[rfqmath.BigInt]{
Coefficient: rfqmath.NewBigInt(big.NewInt(coef1)),
Scale: scale1,
}
f2 := rfqmath.FixedPoint[rfqmath.BigInt]{
Coefficient: rfqmath.NewBigInt(big.NewInt(coef2)),
Scale: scale2,
}
tolerancePpmBigInt := rfqmath.NewBigInt(big.NewInt(int64(tolerancePpm)))
result := f1.WithinTolerance(f2, tolerancePpmBigInt)
// Compute expected result using float64
f1Float := f1.ToFloat64()
f2Float := f2.ToFloat64()
delta := math.Abs(f1Float - f2Float)
maxVal := math.Max(math.Abs(f1Float), math.Abs(f2Float))
tolerance := (float64(tolerancePpm) / 1_000_000) * maxVal
expected := delta <= tolerance
if result != expected {
t.Fatalf("WithinTolerance mismatch:\n"+
"f1 = %v (float: %f),\n"+
"f2 = %v (float: %f),\n"+
"tolerancePpm = %v,\n"+
"delta = %e,\n"+
"tolerance = %e,\n"+
"result = %v,\n"+
"expected = %v",
f1, f1Float, f2, f2Float, tolerancePpm, delta, tolerance, result, expected)
}
})
}
This commit introduces the WithinTolerance method to FixedPoint[T]. The method checks if two fixed-point numbers are within a given tolerance, specified in Parts Per Million (PPM), and returns true if they are.
63273a1
to
b078457
Compare
I've added the property based tests that roas suggested. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
LGTM
}, | ||
secondFp: FixedPoint[N]{ | ||
Coefficient: NewInt[N]().FromUint64( | ||
314_159_265_359, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
👍
This pull request introduces the following changes to the
rfqmath
library:FixedPoint[T].WithinTolerance method
Adds the
WithinTolerance
method to theFixedPoint[T]
type, allowing two fixed-point numbers to be checked for equality within a specified tolerance in Parts Per Million (PPM).Greater-than operators for integer types
Adds support for handling the
>
and>=
comparisons for integer types.These changes are necessary towards closing #871.