mango-v4/programs/mango-v4/src/i80f48.rs

309 lines
9.9 KiB
Rust

use fixed::types::I80F48;
pub trait ClampToInt {
fn clamp_to_i64(&self) -> i64;
fn clamp_to_u64(&self) -> u64;
}
impl ClampToInt for I80F48 {
fn clamp_to_i64(&self) -> i64 {
if *self <= i64::MIN {
i64::MIN
} else if *self >= i64::MAX {
i64::MAX
} else {
self.to_num::<i64>()
}
}
fn clamp_to_u64(&self) -> u64 {
if *self <= 0 {
0
} else if *self >= u64::MAX {
u64::MAX
} else {
self.to_num::<u64>()
}
}
}
impl ClampToInt for f64 {
fn clamp_to_i64(&self) -> i64 {
if *self <= i64::MIN as f64 {
i64::MIN
} else if *self >= i64::MAX as f64 {
i64::MAX
} else {
*self as i64
}
}
fn clamp_to_u64(&self) -> u64 {
if *self <= 0.0 {
0
} else if *self >= u64::MAX as f64 {
u64::MAX
} else {
*self as u64
}
}
}
impl ClampToInt for u64 {
fn clamp_to_i64(&self) -> i64 {
if *self >= i64::MAX as u64 {
i64::MAX
} else {
*self as i64
}
}
fn clamp_to_u64(&self) -> u64 {
*self
}
}
pub trait LowPrecisionDivision {
fn checked_div_30bit_precision(&self, rhs: I80F48) -> Option<I80F48>;
fn checked_div_f64_precision(&self, rhs: I80F48) -> Option<I80F48>;
}
impl LowPrecisionDivision for I80F48 {
/// Divide by taking the top 64 bits of self, and top 32 bits of rhs. Then divide
/// those as u64 and shift everything back. Leads to a division result that has the
/// first 30 bits correct.
fn checked_div_30bit_precision(&self, rhs: I80F48) -> Option<I80F48> {
let a = self;
let b = rhs;
if b.is_zero() {
return None;
}
let a_neg = a.is_negative();
let b_neg = b.is_negative();
let r_neg = a_neg ^ b_neg;
let an_bits = if a_neg { -a.to_bits() } else { a.to_bits() } as u128;
let bn_bits = if b_neg { -b.to_bits() } else { b.to_bits() } as u128;
let an_zeros = an_bits.leading_zeros();
let bn_zeros = bn_bits.leading_zeros();
// shift (positive means to the left) and
// ar has the high bit be 1
let (an_shift, ar) = if an_zeros >= 64 {
let s = an_zeros - 64;
(s as i32, (an_bits << s) as u64)
} else {
let s = 64 - an_zeros;
(-(s as i32), (an_bits >> s) as u64)
};
// br has the first u32 be zero
let (bn_shift, br) = if bn_zeros >= 96 {
(0, bn_bits as u64)
} else {
let s = 96 - bn_zeros;
(-(s as i32), (bn_bits >> s) as u64)
};
let rr = ar / br;
let s = 48 + bn_shift - an_shift;
let rr_zeros = rr.leading_zeros() as i32;
let r = if 63 + rr_zeros < s {
return None; // overflow
} else if s >= 0 {
(rr as u128) << s
} else {
(rr as u128) >> (-s)
};
Some(I80F48::from_bits(if r_neg {
-(r as i128)
} else {
r as i128
}))
}
/// Convert to f64 and divide those.
fn checked_div_f64_precision(&self, rhs: I80F48) -> Option<I80F48> {
I80F48::checked_from_num(self.to_num::<f64>() / rhs.to_num::<f64>())
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
pub fn test_i80f48_mul_rounding() {
// It's not desired, but I80F48 seems to round to -inf
let price = I80F48::from_num(0.04);
let x = I80F48::from_bits(96590783907000000);
assert_eq!((x * price).to_string(), "13.726375969298193");
assert_eq!(((-x) * price).to_string(), "-13.726375969298196");
}
/// Random I80F48, uniformly distributed over number of value bits.
fn rand_i80f48() -> I80F48 {
use rand::Rng;
let mut rng = rand::thread_rng();
let bits = rng.gen_range(0..128);
if bits == 0 {
return I80F48::ZERO;
}
let val = rng.gen_range(2u128.pow(bits - 1)..2u128.pow(bits));
I80F48::from_bits(if rng.gen::<bool>() {
-(val as i128)
} else {
val as i128
})
}
/// Check that I80F48 division is correct
#[test]
pub fn test_i80f48_div() {
for _ in 0..10000 {
let a = rand_i80f48();
let b = rand_i80f48();
let actual = a.checked_div(b);
let expected = (|| {
use num::BigInt;
if b.is_zero() {
return None;
}
let a = BigInt::from(a.to_bits()) * BigInt::from(2).pow(128);
let b = BigInt::from(b.to_bits());
let r: BigInt = (a / b) >> 80;
if r > BigInt::from(i128::MAX) || r < BigInt::from(i128::MIN) {
return None;
}
let (sign, digits) = r.to_u64_digits();
assert!(digits.len() < 3);
let rv = match digits.len() {
0 => 0u128,
1 => digits[0] as u128,
2 => digits[0] as u128 + (digits[1] as u128) * (1 << 64),
_ => 0,
};
Some(I80F48::from_bits(match sign {
num::bigint::Sign::Minus => -(rv as i128),
_ => rv as i128,
}))
})();
assert_eq!(actual.is_some(), expected.is_some());
if actual.is_some() && expected.is_some() {
let actual = actual.unwrap();
let expected = expected.unwrap();
if actual.is_positive() {
assert_eq!(actual, expected);
} else {
// BigInt shr rounds differently than I80F48 div
assert!(actual == expected || actual == expected + I80F48::DELTA);
}
}
}
}
#[test]
pub fn test_i80f48_div_30bit_sanity() {
let one = I80F48::ONE;
let two = I80F48::from(2);
let half = I80F48::from_num(0.5);
assert_eq!(one.checked_div_30bit_precision(one), Some(one));
assert_eq!(one.checked_div_30bit_precision(-one), Some(-one));
assert_eq!((-one).checked_div_30bit_precision(one), Some(-one));
assert_eq!((-one).checked_div_30bit_precision(-one), Some(one));
assert_eq!(half.checked_div_30bit_precision(half), Some(one));
assert_eq!(two.checked_div_30bit_precision(two), Some(one));
assert_eq!(one.checked_div_30bit_precision(two), Some(half));
assert_eq!(one.checked_div_30bit_precision(half), Some(two));
assert_eq!(two.checked_div_30bit_precision(half), Some(I80F48::from(4)));
assert_eq!(
one.checked_div_30bit_precision(I80F48::from_bits(1)),
Some(I80F48::from_bits(1 << 96))
);
for i in 0..127 {
for j in 0..127 {
println!("i {i}, j {j}");
let a = I80F48::from_bits(1 << i);
let b = I80F48::from_bits(1 << j);
let s = i + 48 - j;
let r = if s >= 0 && s < 127 {
Some(I80F48::from_bits(1 << s))
} else if s < 0 {
Some(I80F48::ZERO)
} else {
None
};
assert_eq!(a.checked_div_30bit_precision(b), r);
}
}
}
#[test]
pub fn test_i80f48_div_30bit_random() {
for _ in 0..10000 {
let a = rand_i80f48();
let b = rand_i80f48();
let actual = a.checked_div_30bit_precision(b);
let expected = a.checked_div(b);
assert_eq!(actual.is_some(), expected.is_some());
if actual.is_some() && expected.is_some() {
let actual = actual.unwrap();
let expected = expected.unwrap();
let precision_bits = {
let zexp = expected.abs().to_bits().leading_zeros();
let err = (actual - expected).abs();
let zerr = err.to_bits().leading_zeros();
zerr as i32 - zexp as i32
};
let sign_ok = actual.is_negative() == expected.is_negative();
// either at least 30 bits of precision, or all but the last bit correct (last bit may round differently)
assert!(
(precision_bits >= 30 && sign_ok) || (actual - expected).abs() <= I80F48::DELTA
);
}
}
}
#[test]
pub fn test_i80f48_div_f64_random() {
for _ in 0..10000 {
let a = rand_i80f48();
let b = rand_i80f48();
let actual = a.checked_div_f64_precision(b);
let expected = a.checked_div(b);
assert_eq!(actual.is_some(), expected.is_some());
if actual.is_some() && expected.is_some() {
let actual = actual.unwrap();
let expected = expected.unwrap();
let precision_bits = {
let zexp = expected.abs().to_bits().leading_zeros();
let err = (actual - expected).abs();
let zerr = err.to_bits().leading_zeros();
zerr as i32 - zexp as i32
};
let sign_ok = actual.is_negative() == expected.is_negative();
// either at least 50 bits of precision, or all but the last bit correct (last bit may round differently)
assert!(
(precision_bits >= 50 && sign_ok) || (actual - expected).abs() <= I80F48::DELTA
);
}
}
}
// regression test for https://gitlab.com/tspiteri/fixed/-/issues/57
// see https://github.com/blockworks-foundation/fixed/issues/1
#[test]
fn bug_fixed_comparison_u64() {
let a: u64 = 66000;
let b: u64 = 1000;
assert!(I80F48::from(a) > b); // fails!
}
}