use core::cmp::Ordering;
use core::ops::{Add, Mul, Neg, Sub};
use nebu::field::P;
use nebu::Goldilocks;
pub const FRAC_BITS: u32 = 32;
const SCALE: i128 = 1i128 << FRAC_BITS;
const HALF_P: u64 = P / 2;
#[derive(Clone, Copy, Eq)]
#[repr(transparent)]
pub struct Fx(Goldilocks);
impl Fx {
pub const ZERO: Fx = Fx(Goldilocks::ZERO);
pub const ONE: Fx = Fx(Goldilocks::new(1u64 << FRAC_BITS));
#[inline]
pub fn from_int(n: i64) -> Fx {
Fx(from_signed((n as i128) << FRAC_BITS))
}
#[inline]
pub fn from_ratio(num: i64, den: i64) -> Fx {
if den == 0 {
return Fx::ZERO;
}
Fx(from_signed(round_div(
(num as i128) << FRAC_BITS,
den as i128,
)))
}
pub fn ratio_u128(num: u128, den: u128) -> Fx {
if den == 0 {
return Fx::ZERO;
}
let (mut num, mut den) = (num, den);
while den >= (1u128 << 96) {
num >>= 1;
den >>= 1;
}
let scaled = ((num << FRAC_BITS) + den / 2) / den;
Fx(from_signed(scaled as i128))
}
#[inline]
pub fn raw(self) -> Goldilocks {
self.0.canonicalize()
}
#[inline]
pub fn from_raw(g: Goldilocks) -> Fx {
Fx(g)
}
#[inline]
pub fn is_zero(self) -> bool {
self.0.is_zero()
}
#[inline]
fn signed(self) -> i128 {
to_signed(self.0)
}
#[inline]
pub fn to_f64(self) -> f64 {
self.signed() as f64 / SCALE as f64
}
}
impl Add for Fx {
type Output = Fx;
#[inline]
fn add(self, rhs: Fx) -> Fx {
Fx(self.0 + rhs.0)
}
}
impl Sub for Fx {
type Output = Fx;
#[inline]
fn sub(self, rhs: Fx) -> Fx {
Fx(self.0 - rhs.0)
}
}
impl Neg for Fx {
type Output = Fx;
#[inline]
fn neg(self) -> Fx {
Fx(self.0.field_neg())
}
}
impl Mul for Fx {
type Output = Fx;
#[inline]
fn mul(self, rhs: Fx) -> Fx {
let prod = self.signed() * rhs.signed();
let bias = 1i128 << (FRAC_BITS - 1);
let scaled = if prod >= 0 {
(prod + bias) >> FRAC_BITS
} else {
-(((-prod) + bias) >> FRAC_BITS)
};
Fx(from_signed(scaled))
}
}
impl Fx {
#[inline]
#[allow(clippy::should_implement_trait)] pub fn div(self, rhs: Fx) -> Fx {
self.checked_div(rhs).unwrap_or(Fx::ZERO)
}
#[inline]
pub fn checked_div(self, rhs: Fx) -> Option<Fx> {
let d = rhs.signed();
if d == 0 {
return None;
}
Some(Fx(from_signed(round_div(self.signed() << FRAC_BITS, d))))
}
#[inline]
pub fn recip(self) -> Fx {
Fx::ONE.div(self)
}
#[inline]
pub fn sqrt(self) -> Fx {
let a = self.signed();
if a <= 0 {
return Fx::ZERO;
}
Fx(from_signed(isqrt_u128((a as u128) << FRAC_BITS) as i128))
}
#[inline]
pub fn floor_to_i64(self) -> i64 {
(self.signed() >> FRAC_BITS) as i64
}
pub fn to_i64_scaled(self, frac_bits: u32) -> i64 {
debug_assert!(frac_bits <= FRAC_BITS);
let shift = FRAC_BITS - frac_bits;
if shift == 0 {
return self.signed() as i64;
}
let s = self.signed();
let half = 1i128 << (shift - 1);
(if s >= 0 {
(s + half) >> shift
} else {
-(((-s) + half) >> shift)
}) as i64
}
pub fn exp(self) -> Fx {
let log2e = Fx::from_ratio(14_426_950_409, 10_000_000_000);
let ln2 = Fx::from_ratio(6_931_471_806, 10_000_000_000);
let y = self * log2e;
let i = y.floor_to_i64();
let f = y - Fx::from_int(i); let two_f = exp_series(f * ln2); if i >= 30 {
Fx(from_signed(i128::MAX >> 2))
} else if i >= 0 {
two_f * Fx::from_int(1i64 << i)
} else if i > -(FRAC_BITS as i64) {
two_f.div(Fx::from_int(1i64 << (-i)))
} else {
Fx::ZERO
}
}
}
impl Fx {
pub fn ln(self) -> Fx {
let a = self.signed();
if a <= 0 {
return Fx::ZERO;
}
let msb = 127 - (a as u128).leading_zeros() as i64;
let e = msb - FRAC_BITS as i64;
let m = if e >= 0 {
self.div(Fx::from_int(1i64 << e.min(62)))
} else {
self * Fx::from_int(1i64 << (-e).min(30))
};
let t = (m - Fx::ONE).div(m + Fx::ONE);
let t2 = t * t;
let mut term = t;
let mut sum = t;
let mut k = 3i64;
for _ in 0..14 {
term = term * t2;
sum = sum + term.div(Fx::from_int(k));
k += 2;
}
let ln2 = Fx::from_ratio(6_931_471_806, 10_000_000_000);
Fx::from_int(e) * ln2 + Fx::from_int(2) * sum
}
}
fn exp_series(u: Fx) -> Fx {
let mut term = Fx::ONE;
let mut sum = Fx::ONE;
for n in 1..=12 {
term = term * u.div(Fx::from_int(n));
sum = sum + term;
}
sum
}
impl PartialEq for Fx {
#[inline]
fn eq(&self, other: &Self) -> bool {
self.0 == other.0
}
}
impl Ord for Fx {
#[inline]
fn cmp(&self, other: &Self) -> Ordering {
self.signed().cmp(&other.signed())
}
}
impl PartialOrd for Fx {
#[inline]
fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
Some(self.cmp(other))
}
}
impl core::fmt::Debug for Fx {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
write!(f, "Fx({})", self.to_f64())
}
}
#[inline]
fn to_signed(g: Goldilocks) -> i128 {
let v = g.as_u64();
if v > HALF_P {
v as i128 - P as i128
} else {
v as i128
}
}
#[inline]
fn from_signed(x: i128) -> Goldilocks {
let p = P as i128;
let mut r = x % p;
if r < 0 {
r += p;
}
Goldilocks::new(r as u64)
}
#[inline]
fn round_div(num: i128, den: i128) -> i128 {
let half = den.abs() / 2;
let bias = if (num >= 0) == (den > 0) { half } else { -half };
(num + bias) / den
}
#[inline]
fn isqrt_u128(n: u128) -> u128 {
if n == 0 {
return 0;
}
let mut x = 1u128 << (128 - n.leading_zeros()).div_ceil(2);
loop {
let y = (x + n / x) / 2;
if y >= x {
return x;
}
x = y;
}
}
#[cfg(test)]
mod tests {
use super::*;
const ULP: f64 = 1.0 / (SCALE as f64);
fn close(a: Fx, want: f64) {
let got = a.to_f64();
assert!((got - want).abs() <= 4.0 * ULP, "got {got}, want {want}");
}
#[test]
fn round_trips_encode_decode() {
for &(n, d) in &[(1, 3), (2, 7), (-5, 8), (100, 1), (0, 1), (1, 1_000_000)] {
close(Fx::from_ratio(n, d), n as f64 / d as f64);
}
assert_eq!(Fx::from_int(42).to_f64(), 42.0);
assert_eq!(Fx::from_int(-7).to_f64(), -7.0);
}
#[test]
fn add_sub_neg_exact_when_representable() {
assert_eq!((Fx::from_ratio(1, 4) + Fx::from_ratio(1, 2)).to_f64(), 0.75);
assert_eq!((-Fx::from_int(3)).to_f64(), -3.0);
assert_eq!(
Fx::from_ratio(1, 2) - Fx::from_ratio(3, 4),
Fx::from_ratio(-1, 4)
);
close(Fx::from_ratio(3, 10) - Fx::from_ratio(1, 2), -0.2);
}
#[test]
fn mul_rescales_within_one_ulp() {
assert_eq!((Fx::from_ratio(1, 2) * Fx::from_ratio(1, 2)).to_f64(), 0.25);
close(Fx::from_ratio(1, 10) * Fx::from_ratio(1, 10), 0.01);
close(Fx::from_int(-3) * Fx::from_ratio(1, 4), -0.75);
let x = Fx::from_ratio(7, 13);
assert_eq!((x * Fx::ONE).raw(), x.raw());
}
#[test]
fn div_recip_sqrt() {
assert_eq!(
(Fx::from_ratio(3, 4).div(Fx::from_ratio(1, 4))).to_f64(),
3.0
);
close(Fx::from_int(1).div(Fx::from_int(3)) * Fx::from_int(3), 1.0);
assert_eq!(Fx::from_ratio(1, 4).recip().to_f64(), 4.0);
assert_eq!(Fx::from_ratio(1, 4).sqrt().to_f64(), 0.5);
close(Fx::from_int(2).sqrt(), 2.0_f64.sqrt());
assert_eq!(Fx::from_int(5).div(Fx::ZERO), Fx::ZERO);
assert_eq!(Fx::from_int(5).checked_div(Fx::ZERO), None);
}
#[test]
fn ordering_respects_sign_and_magnitude() {
let mut v = [
Fx::from_ratio(1, 2),
Fx::from_ratio(-1, 5),
Fx::from_int(3),
Fx::ZERO,
Fx::from_ratio(1, 10),
];
v.sort();
let got: Vec<f64> = v.iter().map(|x| x.to_f64()).collect();
assert!(got.windows(2).all(|w| w[0] <= w[1]), "not sorted: {got:?}");
assert!(Fx::from_ratio(-1, 5) < Fx::ZERO);
assert!(Fx::ZERO < Fx::from_ratio(1, 10));
}
#[test]
fn field_element_round_trips() {
let x = Fx::from_ratio(123, 456);
assert_eq!(Fx::from_raw(x.raw()), x);
}
#[test]
fn exp_and_floor() {
assert_eq!(Fx::from_ratio(7, 2).floor_to_i64(), 3);
assert_eq!(Fx::from_ratio(-7, 2).floor_to_i64(), -4); close(Fx::ZERO.exp(), 1.0);
close(Fx::from_int(-1).exp(), (-1.0_f64).exp());
close(Fx::from_int(-5).exp(), (-5.0_f64).exp());
close(Fx::from_int(2).exp(), 2.0_f64.exp());
assert_eq!(Fx::from_int(-40).exp(), Fx::ZERO); }
#[test]
fn ratio_u128_handles_large_ints() {
close(Fx::ratio_u128(1, 3), 1.0 / 3.0);
close(Fx::ratio_u128(3, 4), 0.75);
let big = 1_000_000_000_000_000u128; assert_eq!(Fx::ratio_u128(big, big), Fx::ONE);
close(Fx::ratio_u128(big / 4, big), 0.25);
}
#[test]
fn storage_encoding_round_trips() {
assert_eq!(Fx::from_ratio(1, 2).to_i64_scaled(8), 128);
assert_eq!(Fx::from_ratio(-1, 4).to_i64_scaled(8), -64);
assert_eq!(Fx::from_int(3).to_i64_scaled(16), 3 * 65536);
let x = Fx::from_ratio(7, 11);
let dec = Fx::from_ratio(x.to_i64_scaled(8), 256);
assert!((x.to_f64() - dec.to_f64()).abs() <= 1.0 / 256.0);
}
#[test]
fn ln_matches_reference() {
assert_eq!(Fx::ONE.ln(), Fx::ZERO);
for &(n, d) in &[(2, 1), (1, 2), (10, 1), (271828, 100000), (1, 100)] {
close(Fx::from_ratio(n, d).ln(), (n as f64 / d as f64).ln());
}
close(Fx::from_int(3).ln().exp(), 3.0);
assert_eq!(Fx::ZERO.ln(), Fx::ZERO); }
#[test]
fn associative_add_and_distributive_scale() {
for &(n, d) in &[(1, 3), (7, 9), (-4, 11), (5, 2)] {
let a = Fx::from_ratio(n, d);
let b = Fx::from_ratio(2, 7);
close((a + b) - b, n as f64 / d as f64);
}
}
}