Skip to content

Commit 67db8e2

Browse files
committed
fix atan2 so unresolved sign is not treated as proven zero.
1 parent 5c15f04 commit 67db8e2

3 files changed

Lines changed: 152 additions & 24 deletions

File tree

src/computable/node.rs

Lines changed: 64 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ use std::{
2424
};
2525

2626
pub type Precision = i32;
27+
const ATAN2_SIGN_REFINEMENT_FLOOR: Precision = -4096;
2728

2829
#[derive(Clone, Debug, PartialEq, Default)]
2930
pub(crate) enum Cache {
@@ -419,6 +420,14 @@ fn public_sign(sign: Sign) -> RealSign {
419420
}
420421
}
421422

423+
fn private_sign(sign: RealSign) -> Sign {
424+
match sign {
425+
RealSign::Negative => Sign::Minus,
426+
RealSign::Zero => Sign::NoSign,
427+
RealSign::Positive => Sign::Plus,
428+
}
429+
}
430+
422431
use std::sync::Arc;
423432
use std::sync::atomic::AtomicBool;
424433

@@ -3217,28 +3226,71 @@ impl Computable {
32173226
/// - axes return exact constants: `pi/2`, `-pi/2`, `pi`, or zero.
32183227
/// - the origin `(0, 0)` returns zero, matching `f64::atan2`.
32193228
pub fn atan2(self, x: Computable) -> Computable {
3220-
let y_sign = self.sign();
3221-
let x_sign = x.sign();
3229+
let y_sign = self.structural_facts().sign.map(private_sign);
3230+
let x_sign = x.structural_facts().sign.map(private_sign);
32223231
match (y_sign, x_sign) {
3223-
(Sign::NoSign, Sign::NoSign) | (Sign::NoSign, Sign::Plus) => {
3232+
(Some(Sign::NoSign), Some(Sign::NoSign)) | (Some(Sign::NoSign), Some(Sign::Plus)) => {
32243233
crate::trace_dispatch!("computable", "atan2", "axis-zero-y");
32253234
return Self::zero();
32263235
}
3227-
(Sign::NoSign, Sign::Minus) => {
3236+
(Some(Sign::NoSign), Some(Sign::Minus)) => {
32283237
crate::trace_dispatch!("computable", "atan2", "axis-negative-x");
32293238
return Self::pi();
32303239
}
3231-
(Sign::Plus, Sign::NoSign) => {
3240+
(Some(Sign::Plus), Some(Sign::NoSign)) => {
32323241
crate::trace_dispatch!("computable", "atan2", "axis-positive-y");
32333242
return Self::pi().shift_right(1);
32343243
}
3235-
(Sign::Minus, Sign::NoSign) => {
3244+
(Some(Sign::Minus), Some(Sign::NoSign)) => {
32363245
crate::trace_dispatch!("computable", "atan2", "axis-negative-y");
32373246
return Self::pi().shift_right(1).negate();
32383247
}
32393248
_ => {}
32403249
}
3241-
let base = self.multiply(x.clone().inverse()).atan();
3250+
match (
3251+
y_sign.unwrap_or_else(|| self.sign()),
3252+
x_sign.unwrap_or_else(|| x.sign()),
3253+
) {
3254+
(Sign::NoSign, Sign::Plus) => {
3255+
crate::trace_dispatch!("computable", "atan2", "quadrant-right");
3256+
return self.multiply(x.inverse()).atan();
3257+
}
3258+
(Sign::NoSign, Sign::NoSign) => {
3259+
crate::trace_dispatch!("computable", "atan2", "unresolved-origin");
3260+
return Self::zero();
3261+
}
3262+
(Sign::NoSign, Sign::Minus) => {
3263+
let y_sign = self
3264+
.sign_until(ATAN2_SIGN_REFINEMENT_FLOOR)
3265+
.map(private_sign);
3266+
return match y_sign {
3267+
Some(Sign::Minus) => {
3268+
crate::trace_dispatch!("computable", "atan2", "quadrant-lower-left");
3269+
self.multiply(x.inverse()).atan().add(Self::pi().negate())
3270+
}
3271+
Some(Sign::Plus) => {
3272+
crate::trace_dispatch!("computable", "atan2", "quadrant-upper-left");
3273+
self.multiply(x.inverse()).atan().add(Self::pi())
3274+
}
3275+
_ => {
3276+
crate::trace_dispatch!("computable", "atan2", "axis-negative-x");
3277+
Self::pi()
3278+
}
3279+
};
3280+
}
3281+
(Sign::Plus, Sign::NoSign) => {
3282+
crate::trace_dispatch!("computable", "atan2", "half-angle-positive-y");
3283+
return Self::atan2_half_angle(self, x);
3284+
}
3285+
(Sign::Minus, Sign::NoSign) => {
3286+
crate::trace_dispatch!("computable", "atan2", "half-angle-negative-y");
3287+
return Self::atan2_half_angle(self, x);
3288+
}
3289+
_ => {}
3290+
}
3291+
let x_sign = x_sign.unwrap_or_else(|| x.sign());
3292+
let y_sign = y_sign.unwrap_or_else(|| self.sign());
3293+
let base = self.multiply(x.inverse()).atan();
32423294
if x_sign == Sign::Plus {
32433295
crate::trace_dispatch!("computable", "atan2", "quadrant-right");
32443296
base
@@ -3251,6 +3303,11 @@ impl Computable {
32513303
}
32523304
}
32533305

3306+
fn atan2_half_angle(y: Computable, x: Computable) -> Computable {
3307+
let radius = x.clone().square().add(y.clone().square()).sqrt();
3308+
y.multiply(radius.add(x).inverse()).atan().shift_left(1)
3309+
}
3310+
32543311
/// Inverse sine of this number.
32553312
pub fn asin(self) -> Computable {
32563313
if let Some(rational) = self.exact_rational() {

src/real/arithmetic.rs

Lines changed: 25 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -4032,37 +4032,37 @@ impl Real {
40324032
// more expensive than reading already-derivable structural facts. Only
40334033
// descend to the refinement path when structural inspection cannot
40344034
// decide for one of the inputs.
4035-
let y_sign = match self.structural_facts().sign {
4036-
Some(RealSign::Zero) => Sign::NoSign,
4037-
Some(RealSign::Positive) => Sign::Plus,
4038-
Some(RealSign::Negative) => Sign::Minus,
4039-
None => self.best_sign(),
4040-
};
4041-
let x_sign = match x.structural_facts().sign {
4042-
Some(RealSign::Zero) => Sign::NoSign,
4043-
Some(RealSign::Positive) => Sign::Plus,
4044-
Some(RealSign::Negative) => Sign::Minus,
4045-
None => x.best_sign(),
4046-
};
4035+
let y_sign = self.structural_facts().sign.map(num_sign_from_real);
4036+
let x_sign = x.structural_facts().sign.map(num_sign_from_real);
40474037
match (y_sign, x_sign) {
4048-
(Sign::NoSign, Sign::NoSign) | (Sign::NoSign, Sign::Plus) => {
4038+
(Some(Sign::NoSign), Some(Sign::NoSign)) | (Some(Sign::NoSign), Some(Sign::Plus)) => {
40494039
crate::trace_dispatch!("real", "atan2", "axis-zero-y");
40504040
return Self::zero();
40514041
}
4052-
(Sign::NoSign, Sign::Minus) => {
4042+
(Some(Sign::NoSign), Some(Sign::Minus)) => {
40534043
crate::trace_dispatch!("real", "atan2", "axis-negative-x");
40544044
return Self::pi();
40554045
}
4056-
(Sign::Plus, Sign::NoSign) => {
4046+
(Some(Sign::Plus), Some(Sign::NoSign)) => {
40574047
crate::trace_dispatch!("real", "atan2", "axis-positive-y");
40584048
return Self::pi_fraction(1, 2);
40594049
}
4060-
(Sign::Minus, Sign::NoSign) => {
4050+
(Some(Sign::Minus), Some(Sign::NoSign)) => {
40614051
crate::trace_dispatch!("real", "atan2", "axis-negative-y");
40624052
return Self::pi_fraction(-1, 2);
40634053
}
40644054
_ => {}
40654055
}
4056+
let y_sign = y_sign.unwrap_or_else(|| self.best_sign());
4057+
let x_sign = x_sign.unwrap_or_else(|| x.best_sign());
4058+
if y_sign == Sign::NoSign && x_sign != Sign::Plus {
4059+
crate::trace_dispatch!("real", "atan2", "generic-computable");
4060+
return Self::irrational_from_computable(self.fold().atan2(x.fold()));
4061+
}
4062+
if x_sign == Sign::NoSign {
4063+
crate::trace_dispatch!("real", "atan2", "generic-computable");
4064+
return Self::irrational_from_computable(self.fold().atan2(x.fold()));
4065+
}
40664066
let ratio = (self / &x).expect("nonzero x rules out divide-by-zero");
40674067
let base = ratio.atan().expect("Real::atan is total");
40684068
if x_sign == Sign::Plus {
@@ -4140,7 +4140,7 @@ impl Real {
41404140
crate::trace_dispatch!("real", "tanh", "generic-exp-identity");
41414141
let positive = self.clone().exp()?;
41424142
let negative = self.neg().exp()?;
4143-
(positive.clone() - negative.clone()) / (positive + negative)
4143+
(&positive - &negative) / (positive + negative)
41444144
}
41454145

41464146
/// The inverse hyperbolic sine of this Real.
@@ -4711,6 +4711,14 @@ fn real_sign_from_num(sign: Sign) -> RealSign {
47114711
}
47124712
}
47134713

4714+
fn num_sign_from_real(sign: RealSign) -> Sign {
4715+
match sign {
4716+
RealSign::Negative => Sign::Minus,
4717+
RealSign::Zero => Sign::NoSign,
4718+
RealSign::Positive => Sign::Plus,
4719+
}
4720+
}
4721+
47144722
fn multiply_public_sign(left: Option<RealSign>, right: Option<RealSign>) -> Option<RealSign> {
47154723
match (left?, right?) {
47164724
(RealSign::Zero, _) | (_, RealSign::Zero) => Some(RealSign::Zero),

src/real/tests.rs

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1913,6 +1913,24 @@ mod tests {
19131913
}
19141914
}
19151915

1916+
#[test]
1917+
fn atan2_unresolved_positive_y_does_not_collapse_to_axis() {
1918+
let tiny = Real::new(
1919+
Rational::from_bigint_fraction(
1920+
num::BigInt::from(1_u8),
1921+
num::BigUint::from(1_u8) << 2500,
1922+
)
1923+
.unwrap(),
1924+
);
1925+
let y = (Real::pi() + tiny.clone()) - Real::pi();
1926+
assert_eq!(y.structural_facts().sign, None);
1927+
1928+
let got = y.atan2(Real::one());
1929+
let expected = tiny.atan2(Real::one());
1930+
assert_ne!(got, Real::zero());
1931+
assert_eq!(got.to_f64_approx(), expected.to_f64_approx());
1932+
}
1933+
19161934
#[test]
19171935
fn atan2_is_consistent_under_uniform_positive_scaling() {
19181936
// atan2(ky, kx) = atan2(y, x) for k > 0. Pick coords whose |y/x|
@@ -1990,6 +2008,51 @@ mod tests {
19902008
}
19912009
}
19922010

2011+
#[test]
2012+
fn computable_atan2_unresolved_positive_y_does_not_collapse_to_axis() {
2013+
use crate::Computable;
2014+
use num::Zero;
2015+
2016+
let tiny = Rational::from_bigint_fraction(
2017+
num::BigInt::from(1_u8),
2018+
num::BigUint::from(1_u8) << 2500,
2019+
)
2020+
.unwrap();
2021+
let y = Computable::pi()
2022+
.add(Computable::rational(tiny.clone()))
2023+
.add(Computable::pi().negate());
2024+
assert_eq!(y.sign(), num::bigint::Sign::NoSign);
2025+
2026+
let got = y.atan2(Computable::one()).approx(-2600);
2027+
let expected = Computable::rational(tiny)
2028+
.atan2(Computable::one())
2029+
.approx(-2600);
2030+
assert!(!got.is_zero());
2031+
assert_eq!(got, expected);
2032+
}
2033+
2034+
#[test]
2035+
fn computable_atan2_unresolved_negative_y_on_negative_x_keeps_lower_branch() {
2036+
use crate::Computable;
2037+
2038+
let tiny = Rational::from_bigint_fraction(
2039+
num::BigInt::from(1_u8),
2040+
num::BigUint::from(1_u8) << 2500,
2041+
)
2042+
.unwrap();
2043+
let y = Computable::pi()
2044+
.add(Computable::rational(tiny.clone()).negate())
2045+
.add(Computable::pi().negate());
2046+
assert_eq!(y.sign(), num::bigint::Sign::NoSign);
2047+
2048+
let got = y.atan2(Computable::one().negate()).approx(-2600);
2049+
let expected = Computable::rational(tiny)
2050+
.negate()
2051+
.atan2(Computable::one().negate())
2052+
.approx(-2600);
2053+
assert_eq!(got, expected);
2054+
}
2055+
19932056
#[test]
19942057
fn dot2_refs_all_zero_returns_zero() {
19952058
let left = [Real::zero(), Real::zero()];

0 commit comments

Comments
 (0)