use core::cmp::Ordering;
use crate::exact_int::{RationalCoordInt, RoundingInt};
const SCALE_EXP: i32 = 1074;
const MAX_CORRECTIONS: u32 = 8;
fn decompose(x: f64) -> (u64, i32) {
let bits = x.to_bits();
let biased = ((bits >> 52) & 0x7ff) as i32;
let frac = bits & ((1u64 << 52) - 1);
if biased == 0 {
(frac, -1074)
} else {
(frac | (1u64 << 52), biased - 1075)
}
}
fn midpoint(x: f64, y: f64) -> (u64, i32) {
let (mx, ex) = decompose(x);
let (my, ey) = decompose(y);
debug_assert!(
ey == ex || ey == ex + 1,
"adjacent f64 exponents differ by ≤ 1"
);
let sy = u32::try_from(ey - ex).unwrap_or(0);
let sum = mx + (my << sy);
(sum, ex - 1)
}
fn cmp_value_to_dyadic(num: &RationalCoordInt, den: &RationalCoordInt, m: u64, e: i32) -> Ordering {
debug_assert!(!num.is_negative(), "cmp_value_to_dyadic needs a magnitude");
debug_assert!(den.is_positive(), "den > 0 is a RationalCoord2d invariant");
let e_prime = e + SCALE_EXP;
let left_shift = usize::try_from(-e_prime.min(0)).unwrap_or(0);
let right_shift = usize::try_from(e_prime.max(0)).unwrap_or(0);
let left: RoundingInt = num.widen::<100>() << left_shift;
let right: RoundingInt = (RoundingInt::from_u64(m) * den.widen::<100>()) << right_shift;
let diff = left - right;
if diff.is_zero() {
Ordering::Equal
} else if diff.is_negative() {
Ordering::Less
} else {
Ordering::Greater
}
}
fn next_up(x: f64) -> f64 {
f64::from_bits(x.to_bits() + 1)
}
fn next_down(x: f64) -> f64 {
if x == 0.0 {
0.0
} else {
f64::from_bits(x.to_bits() - 1)
}
}
fn estimate(a: &RationalCoordInt, d: &RationalCoordInt) -> f64 {
let ba = a.bit_length();
let bd = d.bit_length();
let sa = ba.saturating_sub(64);
let sd = bd.saturating_sub(64);
let ta = a.bit_window(sa);
let td = d.bit_window(sd);
if td == 0 {
return 0.0;
}
let q = (u128::from(ta) << 64) / u128::from(td);
if q == 0 {
return 0.0;
}
let bq = 128 - i32::try_from(q.leading_zeros()).unwrap_or(128);
let mut exp =
i32::try_from(sa).unwrap_or(i32::MAX / 4) - i32::try_from(sd).unwrap_or(0) - 64 - SCALE_EXP;
let drop = (bq - 53).max(0);
let m = u64::try_from(q >> drop).unwrap_or(u64::MAX);
exp += drop;
ldexp(m, exp)
}
fn ldexp(m: u64, exp: i32) -> f64 {
if m == 0 {
return 0.0;
}
let mut m = m;
let mut exp = exp;
let bits = 64 - i32::try_from(m.leading_zeros()).unwrap_or(64);
if bits > 53 {
let drop = bits - 53;
m >>= drop;
exp += drop;
} else if bits < 53 {
let gain = 53 - bits;
m <<= gain;
exp -= gain;
}
let unbiased = exp + 52;
if unbiased > 1023 {
return f64::INFINITY;
}
if unbiased < -1074 {
return 0.0;
}
if unbiased >= -1022 {
let biased = u64::try_from(unbiased + 1023).unwrap_or(0);
return f64::from_bits((biased << 52) | (m & ((1u64 << 52) - 1)));
}
let shift = (-1074 - exp).max(0);
if shift >= 64 {
return 0.0;
}
f64::from_bits(m >> shift)
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum BracketPosition {
Exact,
BelowMidpoint,
AtMidpoint,
AboveMidpoint,
}
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct F64Bracket {
pub lo: f64,
pub hi: f64,
pub position: BracketPosition,
}
const OVERFLOW_MIDPOINT: (u64, i32) = ((1u64 << 54) - 1, 970);
fn bracket_magnitude(a: &RationalCoordInt, d: &RationalCoordInt) -> F64Bracket {
if a.is_zero() {
return F64Bracket {
lo: 0.0,
hi: 0.0,
position: BracketPosition::Exact,
};
}
let mut c = estimate(a, d);
if !c.is_finite() {
c = f64::MAX;
}
correct_from(c, a, d)
}
fn place_within(c: f64, a: &RationalCoordInt, d: &RationalCoordInt) -> F64Bracket {
let (mc, ec) = decompose(c);
match cmp_value_to_dyadic(a, d, mc, ec) {
Ordering::Equal => F64Bracket {
lo: c,
hi: c,
position: BracketPosition::Exact,
},
Ordering::Less => F64Bracket {
lo: next_down(c),
hi: c,
position: BracketPosition::AboveMidpoint,
},
Ordering::Greater => F64Bracket {
lo: c,
hi: next_up(c),
position: BracketPosition::BelowMidpoint,
},
}
}
fn correct_from(start: f64, a: &RationalCoordInt, d: &RationalCoordInt) -> F64Bracket {
let mut c = start;
for _ in 0..MAX_CORRECTIONS {
let below = next_down(c);
let (lo_m, lo_e) = midpoint(below, c);
match cmp_value_to_dyadic(a, d, lo_m, lo_e) {
Ordering::Less => {
c = below;
continue;
}
Ordering::Equal => {
return F64Bracket {
lo: below,
hi: c,
position: BracketPosition::AtMidpoint,
};
}
Ordering::Greater => {}
}
let above = next_up(c);
if above.is_infinite() {
match cmp_value_to_dyadic(a, d, OVERFLOW_MIDPOINT.0, OVERFLOW_MIDPOINT.1) {
Ordering::Less => return place_within(c, a, d),
Ordering::Equal => {
return F64Bracket {
lo: c,
hi: f64::INFINITY,
position: BracketPosition::AtMidpoint,
};
}
Ordering::Greater => {
return F64Bracket {
lo: c,
hi: f64::INFINITY,
position: BracketPosition::AboveMidpoint,
};
}
}
}
let (hi_m, hi_e) = midpoint(c, above);
match cmp_value_to_dyadic(a, d, hi_m, hi_e) {
Ordering::Greater => c = above,
Ordering::Equal => {
return F64Bracket {
lo: c,
hi: above,
position: BracketPosition::AtMidpoint,
};
}
Ordering::Less => return place_within(c, a, d),
}
}
debug_assert!(
false,
"bracket correction did not converge within {MAX_CORRECTIONS} steps; \
the f64 estimate is further off than its error bound allows"
);
place_within(c, a, d)
}
pub(crate) fn rational_to_bracket(num: &RationalCoordInt, den: &RationalCoordInt) -> F64Bracket {
debug_assert!(den.is_positive(), "den > 0 is a RationalCoord2d invariant");
let b = bracket_magnitude(&num.wrapping_abs(), den);
if !num.is_negative() {
return b;
}
F64Bracket {
lo: -b.hi,
hi: -b.lo,
position: match b.position {
BracketPosition::BelowMidpoint => BracketPosition::AboveMidpoint,
BracketPosition::AboveMidpoint => BracketPosition::BelowMidpoint,
p @ (BracketPosition::Exact | BracketPosition::AtMidpoint) => p,
},
}
}
#[cfg(test)]
mod tests {
use super::{
BracketPosition, F64Bracket, RationalCoordInt, bracket_magnitude, correct_from, decompose,
rational_to_bracket,
};
fn tie_above(c: f64) -> (RationalCoordInt, RationalCoordInt) {
let (mc, ec) = decompose(c);
let shift = usize::try_from(ec + 1074).expect("Ec ≥ −1074 for finite f64");
let num = RationalCoordInt::from_u64(2 * mc + 1) << shift;
(num, RationalCoordInt::from_u64(2))
}
fn at_midpoint(lo: f64, hi: f64) -> F64Bracket {
F64Bracket {
lo,
hi,
position: BracketPosition::AtMidpoint,
}
}
#[test]
fn exact_ties_bracket_as_at_midpoint() {
let bases = [
f64::from_bits(1),
f64::from_bits(3),
f64::from_bits((1u64 << 52) - 1),
f64::from_bits(1u64 << 52),
0.5f64,
1.0f64,
3.0f64,
1_435_363.863_356_009_1f64,
f64::from_bits(0x7FE0_0000_0000_0000),
];
for base in bases {
for c in [base, f64::from_bits(base.to_bits() + 1)] {
let up = f64::from_bits(c.to_bits() + 1);
let (num, den) = tie_above(c);
assert_eq!(
bracket_magnitude(&num, &den),
at_midpoint(c, up),
"tie between {c:e} and {up:e} was not bracketed as AtMidpoint"
);
}
}
}
#[test]
fn values_just_off_a_tie_report_the_correct_half() {
for base in [0.5f64, 1.0, 3.0, 1_435_363.863_356_009_1] {
let up = f64::from_bits(base.to_bits() + 1);
let (tie, den) = tie_above(base);
let below = tie - RationalCoordInt::one();
assert_eq!(
bracket_magnitude(&below, &den),
F64Bracket {
lo: base,
hi: up,
position: BracketPosition::BelowMidpoint
},
"just below the tie between {base:e} and {up:e}"
);
let above = tie + RationalCoordInt::one();
assert_eq!(
bracket_magnitude(&above, &den),
F64Bracket {
lo: base,
hi: up,
position: BracketPosition::AboveMidpoint
},
"just above the tie between {base:e} and {up:e}"
);
}
}
#[test]
fn the_lower_midpoint_branch_brackets_correctly() {
for base in [0.5f64, 1.0, 3.0, 1_435_363.863_356_009_1, f64::from_bits(1)] {
for c in [base, f64::from_bits(base.to_bits() + 1)] {
let up = f64::from_bits(c.to_bits() + 1);
let (num, den) = tie_above(c);
assert_eq!(
correct_from(up, &num, &den),
at_midpoint(c, up),
"lower-branch tie between {c:e} and {up:e}"
);
}
}
}
#[test]
fn a_bad_starting_candidate_reaches_the_same_bracket() {
for base in [0.5f64, 1.0, 1_435_363.863_356_009_1] {
let up = f64::from_bits(base.to_bits() + 1);
let (tie, den) = tie_above(base);
let below = tie - RationalCoordInt::one();
let truth = bracket_magnitude(&below, &den);
assert_eq!(truth.lo.to_bits(), base.to_bits(), "baseline moved");
for start in [base, up, f64::from_bits(base.to_bits() - 1)] {
assert_eq!(
correct_from(start, &below, &den),
truth,
"starting at {start:e} gave a different bracket"
);
}
}
}
#[test]
fn representable_values_bracket_as_exact() {
let cases = [
f64::from_bits(1),
f64::from_bits((1u64 << 52) - 1),
f64::from_bits(1u64 << 52),
0.5f64,
1.0,
f64::MAX,
f64::MIN_POSITIVE,
];
for x in cases {
let (m, e) = decompose(x);
let shift = usize::try_from(e + 1074).expect("e ≥ −1074");
let scaled = RationalCoordInt::from_u64(m) << shift;
assert_eq!(
bracket_magnitude(&scaled, &RationalCoordInt::one()),
F64Bracket {
lo: x,
hi: x,
position: BracketPosition::Exact
},
"{x:e} did not bracket as Exact (m = {m}, e = {e})"
);
}
}
#[test]
fn negative_values_bracket_mirrored() {
let base = 3.0f64;
let up = f64::from_bits(base.to_bits() + 1);
let (tie, den) = tie_above(base);
let just_above = tie + RationalCoordInt::one();
let pos = rational_to_bracket(&just_above, &den);
assert_eq!(pos.position, BracketPosition::AboveMidpoint);
assert_eq!((pos.lo, pos.hi), (base, up));
let neg = rational_to_bracket(&-just_above, &den);
assert_eq!(
neg,
F64Bracket {
lo: -up,
hi: -base,
position: BracketPosition::BelowMidpoint
},
"negating a value in the upper half must land it in the lower half"
);
}
#[test]
fn range_extremes_are_bracketed_per_ieee() {
let two_1024 = RationalCoordInt::one() << (1024 + 1074);
let one = RationalCoordInt::one();
assert_eq!(
bracket_magnitude(&two_1024, &one),
F64Bracket {
lo: f64::MAX,
hi: f64::INFINITY,
position: BracketPosition::AboveMidpoint
}
);
let threshold = RationalCoordInt::from_u64((1u64 << 54) - 1) << (970 + 1074);
assert_eq!(
bracket_magnitude(&threshold, &one),
F64Bracket {
lo: f64::MAX,
hi: f64::INFINITY,
position: BracketPosition::AtMidpoint
}
);
let two = RationalCoordInt::from_u64(2);
assert_eq!(
bracket_magnitude(&one, &two),
F64Bracket {
lo: 0.0,
hi: f64::from_bits(1),
position: BracketPosition::AtMidpoint
}
);
let four = RationalCoordInt::from_u64(4);
assert_eq!(
bracket_magnitude(&one, &four),
F64Bracket {
lo: 0.0,
hi: f64::from_bits(1),
position: BracketPosition::BelowMidpoint
}
);
}
}