use proptest::prelude::*;
use renew_fixed::{Angle, Fixed, Vec2};
const ONE: f64 = 65536.0;
const WORST_ULP: f64 = 1.05;
const FULL_TURN: f64 = 4_294_967_296.0;
fn radians(angle: Angle) -> f64 {
f64::from(angle.to_bits()) / FULL_TURN * core::f64::consts::TAU
}
fn error_ulp(got: Fixed, want: f64) -> f64 {
let Ok(raw) = i32::try_from(got.to_bits()) else {
return f64::INFINITY;
};
(f64::from(raw) - want * ONE).abs()
}
proptest! {
#![proptest_config(ProptestConfig { cases: 8192, ..ProptestConfig::default() })]
#[test]
fn sine_and_cosine_are_within_the_stated_error(bits in any::<u32>()) {
let angle = Angle::from_bits(bits);
let theta = radians(angle);
prop_assert!(
error_ulp(angle.sin(), theta.sin()) <= WORST_ULP,
"sin({bits}) off by more than {WORST_ULP} ulp"
);
prop_assert!(
error_ulp(angle.cos(), theta.cos()) <= WORST_ULP,
"cos({bits}) off by more than {WORST_ULP} ulp"
);
}
#[test]
fn sine_squared_plus_cosine_squared_is_one(bits in any::<u32>()) {
let angle = Angle::from_bits(bits);
let (sin, cos) = angle.sin_cos();
let sum = sin.saturating_mul(sin) + cos.saturating_mul(cos);
let error = (sum.to_bits() - Fixed::ONE.to_bits()).abs();
prop_assert!(error <= 8, "sin²+cos² off by {error} raw units");
}
#[test]
fn a_full_turn_is_exactly_the_identity(bits in any::<u32>()) {
let angle = Angle::from_bits(bits);
let round_trip = angle + Angle::QUARTER + Angle::QUARTER + Angle::QUARTER + Angle::QUARTER;
prop_assert_eq!(round_trip, angle);
prop_assert_eq!(angle + Angle::HALF + Angle::HALF, angle);
prop_assert_eq!(angle - angle, Angle::ZERO);
prop_assert_eq!(angle + (-angle), Angle::ZERO);
}
#[test]
fn the_quadrant_symmetries_hold(bits in any::<u32>()) {
let a = Angle::from_bits(bits);
let sin = a.sin();
let cos = a.cos();
prop_assert_eq!((a + Angle::HALF).sin(), -sin);
prop_assert_eq!((a + Angle::HALF).cos(), -cos);
prop_assert_eq!((a + Angle::QUARTER).sin(), cos);
prop_assert_eq!((-a).sin(), -sin);
prop_assert_eq!((-a).cos(), cos);
}
#[test]
fn rotating_a_vector_preserves_its_length(
x in -1000i64 * 65536..1000 * 65536,
y in -1000i64 * 65536..1000 * 65536,
bits in any::<u32>(),
) {
let v = Vec2::new(Fixed::from_bits(x), Fixed::from_bits(y));
let rotated = v.rotate(Angle::from_bits(bits));
let before = v.length().to_bits();
let after = rotated.length().to_bits();
let tolerance = 8 + (before >> 12);
prop_assert!(
(after - before).abs() <= tolerance,
"rotating changed length from {before} to {after}, past {tolerance}"
);
}
#[test]
fn rotating_back_returns_the_vector(
x in -1000i64 * 65536..1000 * 65536,
y in -1000i64 * 65536..1000 * 65536,
bits in any::<u32>(),
) {
let v = Vec2::new(Fixed::from_bits(x), Fixed::from_bits(y));
let angle = Angle::from_bits(bits);
let back = v.rotate(angle).rotate(-angle);
let drift = (back - v).length().to_bits();
let tolerance = 16 + (v.length().to_bits() >> 11);
prop_assert!(drift <= tolerance, "round trip drifted {drift}, past {tolerance}");
}
}
#[test]
fn the_cardinal_angles_are_exact() {
assert_eq!(Angle::ZERO.sin(), Fixed::ZERO);
assert_eq!(Angle::ZERO.cos(), Fixed::ONE);
assert_eq!(Angle::QUARTER.sin(), Fixed::ONE);
assert_eq!(Angle::QUARTER.cos(), Fixed::ZERO);
assert_eq!(Angle::HALF.sin(), Fixed::ZERO);
assert_eq!(Angle::HALF.cos(), -Fixed::ONE);
assert_eq!(Angle::THREE_QUARTERS.sin(), -Fixed::ONE);
assert_eq!(Angle::THREE_QUARTERS.cos(), Fixed::ZERO);
assert_eq!(Angle::from_degrees(0), Angle::ZERO);
assert_eq!(Angle::from_degrees(90), Angle::QUARTER);
assert_eq!(Angle::from_degrees(180), Angle::HALF);
assert_eq!(Angle::from_degrees(270), Angle::THREE_QUARTERS);
assert_eq!(Angle::from_degrees(360), Angle::ZERO);
assert_eq!(Angle::from_degrees(450), Angle::QUARTER);
assert_eq!(Angle::from_degrees(-90), Angle::THREE_QUARTERS);
assert_eq!(Angle::from_turn_ratio(1, 4), Angle::QUARTER);
assert_eq!(Angle::from_turn_ratio(1, 2), Angle::HALF);
assert_eq!(Vec2::X.rotate(Angle::QUARTER), Vec2::Y);
assert_eq!(Vec2::Y.rotate(Angle::QUARTER), -Vec2::X);
assert_eq!(Vec2::X.rotate(Angle::HALF), -Vec2::X);
}
#[test]
fn the_table_endpoints_are_exact() {
assert_eq!(Angle::from_bits(0).sin(), Fixed::ZERO);
assert_eq!(Angle::from_bits(1 << 30).sin(), Fixed::ONE);
let nearly = Angle::from_bits((1 << 30) - 1).sin();
assert_eq!(nearly, Fixed::ONE, "the nearest representable value is one");
let mut steps = 0u64;
let mut bits = 0u32;
while steps < 200_000 {
let angle = Angle::from_bits(bits);
assert!(angle.sin() <= Fixed::ONE, "sin exceeded one at {bits}");
assert!(
angle.sin() >= -Fixed::ONE,
"sin went below minus one at {bits}"
);
assert!(angle.cos() <= Fixed::ONE, "cos exceeded one at {bits}");
assert!(
angle.cos() >= -Fixed::ONE,
"cos went below minus one at {bits}"
);
bits = bits.wrapping_add(21_473);
steps += 1;
}
}