angle_sc/
trig.rs

1// Copyright (c) 2024-2025 Ken Barker
2
3// Permission is hereby granted, free of charge, to any person obtaining a copy
4// of this software and associated documentation files (the "Software"),
5// to deal in the Software without restriction, including without limitation the
6// rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
7// sell copies of the Software, and to permit persons to whom the Software is
8// furnished to do so, subject to the following conditions:
9
10// The above copyright notice and this permission notice shall be included in
11// all copies or substantial portions of the Software.
12
13// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
14// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
15// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
16// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
17// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
18// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
19// THE SOFTWARE.
20
21//! The `trig` module contains functions for performing accurate trigonometry calculations.
22//!
23//! The accuracy of the `libm::sin` function is poor for angles >= π/4
24//! and the accuracy of the `libm::cos` function is poor for small angles,
25//! i.e., < π/4.
26//! So `sin` π/4 is explicitly set to 1/√2 and `cos` values are calculated
27//! from `sin` values using
28//! [Pythagoras' theorem](https://en.wikipedia.org/wiki/Pythagorean_theorem).
29//!
30//! The `sincos` function accurately calculates the sine and cosine of angles
31//! in `radians` by using
32//! [remquo](https://pubs.opengroup.org/onlinepubs/9699919799/functions/remquo.html)
33//! to reduce an angle into the range: -π/4 <= angle <= π/4;
34//! and its quadrant: along the positive or negative, *x* or *y* axis of the
35//! unit circle.
36//! The `sincos_diff` function reduces the
37//! [round-off error](https://en.wikipedia.org/wiki/Round-off_error)
38//! of the difference of two angles in radians using the
39//! [2Sum](https://en.wikipedia.org/wiki/2Sum) algorithm.
40//!
41//! The `sincosd` function is the `degrees` equivalent of `sincos` and
42//! `sincosd_diff` is the `degrees` equivalent of `sincos_diff`.
43//!
44//! The sines and cosines of angles are represented by the `UnitNegRange`
45//! struct which ensures that they lie in the range:
46//! -1.0 <= value <= 1.0.
47//!
48//! The functions `arctan2` and `arctan2d` are the reciprocal of `sincos` and
49//! `sincosd`, transforming sine and cosines of angles into `radians` or
50//! `degrees` respectively.
51//!
52//! The module contains the other trigonometric functions:
53//! tan, cot, sec and csc as functions taking sin and/or cos and returning
54//! an `Option<f64>` to protect against divide by zero.
55//!
56//! The module also contains functions for:
57//! - [angle sum and difference identities](https://en.wikipedia.org/wiki/List_of_trigonometric_identities#Angle_sum_and_difference_identities);
58//! - [half-angle formulae](https://en.wikipedia.org/wiki/List_of_trigonometric_identities#Half-angle_formulae);
59//! - and the [spherical law of cosines](https://en.wikipedia.org/wiki/Spherical_law_of_cosines).
60
61#![allow(clippy::float_cmp, clippy::suboptimal_flops)]
62
63use crate::{Degrees, Radians, Validate, two_sum};
64use core::cmp::Ordering;
65use core::{f64, ops::Neg};
66
67/// ε * ε, a very small number.
68pub const SQ_EPSILON: f64 = f64::EPSILON * f64::EPSILON;
69
70/// `core::f64::consts::SQRT_3` is currently a nightly-only experimental API,
71/// see <https://doc.rust-lang.org/core/f64/consts/constant.SQRT_3.html>
72#[allow(clippy::excessive_precision, clippy::unreadable_literal)]
73pub const SQRT_3: f64 = 1.732050807568877293527446341505872367_f64;
74
75/// The cosine of 30 degrees: √3/2
76pub const COS_30_DEGREES: f64 = SQRT_3 / 2.0;
77/// The maximum angle in Radians where: `libm::sin(value) == value`
78pub const MAX_LINEAR_SIN_ANGLE: Radians = Radians(9.67e7 * f64::EPSILON);
79/// The maximum angle in Radians where: `swap_sin_cos(libm::sin(value)) == 1.0`
80pub const MAX_COS_ANGLE_IS_ONE: Radians = Radians(3.35e7 * f64::EPSILON);
81
82/// Convert an angle in `Degrees` to `Radians`.
83///
84/// Corrects ±30° to ±π/6.
85#[must_use]
86fn to_radians(angle: Degrees) -> Radians {
87    if angle.0.abs() == 30.0 {
88        Radians(libm::copysign(core::f64::consts::FRAC_PI_6, angle.0))
89    } else {
90        Radians(angle.0.to_radians())
91    }
92}
93
94/// The `UnitNegRange` newtype an f64.
95/// A valid `UnitNegRange` value lies between -1.0 and +1.0 inclusive.
96#[derive(Clone, Copy, Debug, PartialEq, PartialOrd)]
97#[repr(transparent)]
98pub struct UnitNegRange(pub f64);
99
100impl Default for UnitNegRange {
101    fn default() -> Self {
102        Self(0.0)
103    }
104}
105
106impl UnitNegRange {
107    /// Clamp value into the valid range: -1.0 to +1.0 inclusive.
108    ///
109    /// # Examples
110    /// ```
111    /// use angle_sc::trig::UnitNegRange;
112    ///
113    /// assert_eq!(-1.0, UnitNegRange::clamp(-1.0 - f64::EPSILON).0);
114    /// assert_eq!(-1.0, UnitNegRange::clamp(-1.0).0);
115    /// assert_eq!(-0.5, UnitNegRange::clamp(-0.5).0);
116    /// assert_eq!(1.0, UnitNegRange::clamp(1.0).0);
117    /// assert_eq!(1.0, UnitNegRange::clamp(1.0 + f64::EPSILON).0);
118    /// ```
119    #[must_use]
120    pub const fn clamp(value: f64) -> Self {
121        Self(value.clamp(-1.0, 1.0))
122    }
123
124    /// The absolute value of the `UnitNegRange`.
125    #[must_use]
126    pub const fn abs(self) -> Self {
127        Self(self.0.abs())
128    }
129}
130
131impl Validate for UnitNegRange {
132    /// Test whether a `UnitNegRange` is valid.
133    ///
134    /// I.e. whether it lies in the range: -1.0 <= value <= 1.0
135    /// # Examples
136    /// ```
137    /// use angle_sc::trig::UnitNegRange;
138    /// use angle_sc::Validate;
139    ///
140    /// assert!(!UnitNegRange(-1.0 - f64::EPSILON).is_valid());
141    /// assert!(UnitNegRange(-1.0).is_valid());
142    /// assert!(UnitNegRange(1.0).is_valid());
143    /// assert!(!(UnitNegRange(1.0 + f64::EPSILON).is_valid()));
144    /// ```
145    fn is_valid(&self) -> bool {
146        (-1.0..=1.0).contains(&self.0)
147    }
148}
149
150impl Neg for UnitNegRange {
151    type Output = Self;
152
153    /// An implementation of Neg for `UnitNegRange`.
154    ///
155    /// Negates the value.
156    fn neg(self) -> Self {
157        Self(0.0 - self.0)
158    }
159}
160
161/// Calculate a * a - b * b.
162///
163/// Note: calculates (a - b) * (a + b) to minimize round-off error.
164/// * `a`, `b` the values.
165///
166/// returns (a - b) * (a + b)
167#[must_use]
168pub fn sq_a_minus_sq_b(a: UnitNegRange, b: UnitNegRange) -> UnitNegRange {
169    UnitNegRange((a.0 - b.0) * (a.0 + b.0))
170}
171
172/// Calculate 1 - a * a.
173///
174/// Note: calculates (1 - a) * (1 + a) to minimize round-off error.
175/// * `a` the value.
176///
177/// returns (1 - a) * (1 + a)
178#[must_use]
179pub fn one_minus_sq_value(a: UnitNegRange) -> UnitNegRange {
180    sq_a_minus_sq_b(UnitNegRange(1.0), a)
181}
182
183/// Swap the sine into the cosine of an angle and vice versa.
184///
185/// Uses the identity sin<sup>2</sup> + cos<sup>2</sup> = 1.
186/// See:
187/// [Pythagorean identities](https://en.wikipedia.org/wiki/List_of_trigonometric_identities#Pythagorean_identities)
188/// * `a` the sine of the angle.
189///
190/// # Examples
191/// ```
192/// use angle_sc::trig::{UnitNegRange, swap_sin_cos};
193///
194/// assert_eq!(UnitNegRange(0.0), swap_sin_cos(UnitNegRange(-1.0)));
195/// assert_eq!(UnitNegRange(1.0), swap_sin_cos(UnitNegRange(0.0)));
196/// ```
197#[must_use]
198pub fn swap_sin_cos(a: UnitNegRange) -> UnitNegRange {
199    UnitNegRange(libm::sqrt(one_minus_sq_value(a).0))
200}
201
202/// Calculate the cosine of an angle from it's sine and the sign of the cosine.
203///
204/// See: `swap_sin_cos`.
205/// * `a` the sine of the angle.
206/// * `sign` the sign of the cosine of the angle.
207///
208/// return the cosine of the Angle.
209/// # Examples
210/// ```
211/// use angle_sc::trig::{UnitNegRange, cosine_from_sine, COS_30_DEGREES};
212///
213/// assert_eq!(COS_30_DEGREES, cosine_from_sine(UnitNegRange(0.5), 1.0).0);
214/// ```
215#[must_use]
216pub fn cosine_from_sine(a: UnitNegRange, sign: f64) -> UnitNegRange {
217    if a.0.abs() > MAX_COS_ANGLE_IS_ONE.0 {
218        let b = swap_sin_cos(a);
219        if b.0 > 0.0 {
220            UnitNegRange(libm::copysign(b.0, sign))
221        } else {
222            b
223        }
224    } else {
225        UnitNegRange(libm::copysign(1.0, sign))
226    }
227}
228
229/// Calculate the sine of an angle in `Radians`.
230///
231/// Corrects sin ±π/4 to ±1/√2.
232#[must_use]
233pub fn sine(angle: Radians) -> UnitNegRange {
234    let angle_abs = angle.0.abs();
235    if angle_abs == core::f64::consts::FRAC_PI_4 {
236        UnitNegRange(libm::copysign(core::f64::consts::FRAC_1_SQRT_2, angle.0))
237    } else if angle_abs > MAX_LINEAR_SIN_ANGLE.0 {
238        UnitNegRange(libm::sin(angle.0))
239    } else {
240        UnitNegRange(angle.0)
241    }
242}
243
244/// Calculate the cosine of an angle in `Radians` using the sine of the angle.
245///
246/// Corrects cos π/4 to 1/√2.
247#[must_use]
248pub fn cosine(angle: Radians, sin: UnitNegRange) -> UnitNegRange {
249    let angle_abs = angle.0.abs();
250    if angle_abs == core::f64::consts::FRAC_PI_4 {
251        UnitNegRange(libm::copysign(
252            core::f64::consts::FRAC_1_SQRT_2,
253            core::f64::consts::FRAC_PI_2 - angle_abs,
254        ))
255    } else {
256        cosine_from_sine(sin, core::f64::consts::FRAC_PI_2 - angle_abs)
257    }
258}
259
260/// Assign `sin` and `cos` to the `remquo` quadrant: `q`:
261///
262/// - 0: no conversion
263/// - 1: rotate 90° clockwise
264/// - 2: opposite quadrant
265/// - 3: rotate 90° counter-clockwise
266#[must_use]
267fn assign_sin_cos_to_quadrant(
268    sin: UnitNegRange,
269    cos: UnitNegRange,
270    q: i32,
271) -> (UnitNegRange, UnitNegRange) {
272    match q & 3 {
273        1 => (cos, -sin),  // quarter_turn_cw
274        2 => (-sin, -cos), // opposite
275        3 => (-cos, sin),  // quarter_turn_ccw
276        _ => (sin, cos),
277    }
278}
279
280/// Calculate the sine and cosine of an angle from a value in `Radians`.
281///
282/// Note: calculates the cosine of the angle from its sine and overrides both
283/// the sine and cosine for π/4 to their correct values: 1/√2
284///
285/// * `radians` the angle in `Radians`
286///
287/// returns sine and cosine of the angle as `UnitNegRange`s.
288#[must_use]
289pub fn sincos(radians: Radians) -> (UnitNegRange, UnitNegRange) {
290    let rq: (f64, i32) = libm::remquo(radians.0, core::f64::consts::FRAC_PI_2);
291
292    // radians_q is radians in range `-FRAC_PI_4..=FRAC_PI_4`
293    let radians_q = Radians(rq.0);
294    let sin = sine(radians_q);
295    assign_sin_cos_to_quadrant(sin, cosine(radians_q, sin), rq.1)
296}
297
298/// Calculate the sine and cosine of an angle from the difference of a pair of
299/// values in `Radians`.
300///
301/// Note: calculates the cosine of the angle from its sine and overrides the
302/// sine and cosine for π/4 to their correct values: 1/√2
303///
304/// * `a`, `b` the angles in `Radians`
305///
306/// returns sine and cosine of a - b as `UnitNegRange`s.
307#[must_use]
308pub fn sincos_diff(a: Radians, b: Radians) -> (UnitNegRange, UnitNegRange) {
309    let delta = two_sum(a.0, -b.0);
310    let rq: (f64, i32) = libm::remquo(delta.0, core::f64::consts::FRAC_PI_2);
311
312    // radians_q is radians in range `-FRAC_PI_4..=FRAC_PI_4`
313    let radians_q = Radians(rq.0 + delta.1);
314    let sin = sine(radians_q);
315    assign_sin_cos_to_quadrant(sin, cosine(radians_q, sin), rq.1)
316}
317
318/// Accurately calculate an angle in `Radians` from its sine and cosine.
319///
320/// * `sin`, `cos` the sine and cosine of the angle in `UnitNegRange`s.
321///
322/// returns the angle in `Radians`.
323///
324/// # Panics
325///
326/// Panics if `sin` or `cos` are `NaN`.
327#[must_use]
328pub fn arctan2(sin: UnitNegRange, cos: UnitNegRange) -> Radians {
329    let sin_abs = sin.0.abs();
330    let cos_abs = cos.0.abs();
331
332    // calculate radians in the range 0.0..=PI/2
333    let radians_pi_2 = match sin_abs.partial_cmp(&cos_abs).expect("sin or cos is NaN") {
334        Ordering::Equal => core::f64::consts::FRAC_PI_4,
335        Ordering::Less => libm::atan2(sin_abs, cos_abs),
336        Ordering::Greater => core::f64::consts::FRAC_PI_2 - libm::atan2(cos_abs, sin_abs),
337    };
338
339    // calculate radians in the range 0.0..=PI
340    let radians_pi = if cos.0 < 0.0 {
341        core::f64::consts::PI - radians_pi_2
342    } else {
343        radians_pi_2
344    };
345
346    // return radians in the range -π < radians <= π
347    Radians(libm::copysign(radians_pi, sin.0))
348}
349
350/// Calculate the sine and cosine of an angle from a value in `Degrees`.
351///
352/// Note: calculates the cosine of the angle from its sine and overrides the
353/// sine and cosine for ±30° and ±45° to their correct values.
354///
355/// * `degrees` the angle in `Degrees`
356///
357/// returns sine and cosine of the angle as `UnitNegRange`s.
358#[must_use]
359pub fn sincosd(degrees: Degrees) -> (UnitNegRange, UnitNegRange) {
360    let rq: (f64, i32) = libm::remquo(degrees.0, 90.0);
361
362    // radians_q is radians in range `-π/4 <= radians <= π/4`
363    let radians_q = to_radians(Degrees(rq.0));
364    let sin = sine(radians_q);
365    assign_sin_cos_to_quadrant(sin, cosine(radians_q, sin), rq.1)
366}
367
368/// Calculate the sine and cosine of an angle from the difference of a pair of
369/// values in `Degrees`.
370///
371/// Note: calculates the cosine of the angle from its sine and overrides the
372/// sine and cosine for ±30° and ±45° to their correct values.
373///
374/// * `a`, `b` the angles in `Degrees`
375///
376/// returns sine and cosine of a - b as `UnitNegRange`s.
377#[must_use]
378pub fn sincosd_diff(a: Degrees, b: Degrees) -> (UnitNegRange, UnitNegRange) {
379    let delta = two_sum(a.0, -b.0);
380    let rq: (f64, i32) = libm::remquo(delta.0, 90.0);
381
382    // radians_q is radians in range `-π/4 <= radians <= π/4`
383    let radians_q = to_radians(Degrees(rq.0 + delta.1));
384    let sin = sine(radians_q);
385    assign_sin_cos_to_quadrant(sin, cosine(radians_q, sin), rq.1)
386}
387
388/// Accurately calculate a small an angle in `Degrees` from the its sine and cosine.
389///
390/// Converts sin of 0.5 to 30°.
391#[must_use]
392fn arctan2_degrees(sin_abs: f64, cos_abs: f64) -> f64 {
393    if sin_abs == 0.5 {
394        30.0
395    } else {
396        libm::atan2(sin_abs, cos_abs).to_degrees()
397    }
398}
399
400/// Accurately calculate an angle in `Degrees` from its sine and cosine.
401///
402/// * `sin`, `cos` the sine and cosine of the angle in `UnitNegRange`s.
403///
404/// returns the angle in `Degrees`.
405/// # Panics
406///
407/// Panics if `sin` or `cos` are `NaN`.
408#[must_use]
409pub fn arctan2d(sin: UnitNegRange, cos: UnitNegRange) -> Degrees {
410    let sin_abs = sin.0.abs();
411    let cos_abs = cos.0.abs();
412
413    // calculate degrees in the range 0.0..=90.0
414    let degrees_90 = match sin_abs.partial_cmp(&cos_abs).expect("sin or cos is NaN") {
415        Ordering::Equal => 45.0,
416        Ordering::Less => arctan2_degrees(sin_abs, cos_abs),
417        Ordering::Greater => 90.0 - arctan2_degrees(cos_abs, sin_abs),
418    };
419
420    // calculate degrees in the range 0° <= degrees <= 180°
421    let degrees_180 = if cos.0 < 0.0 {
422        180.0 - degrees_90
423    } else {
424        degrees_90
425    };
426
427    // return degrees in the range -180° < degrees <= 180°
428    Degrees(libm::copysign(degrees_180, sin.0))
429}
430
431/// The cosecant of an angle.
432///
433/// * `sin` the sine of the angle.
434///
435/// returns the cosecant or `None` if `sin < SQ_EPSILON`
436#[must_use]
437pub fn csc(sin: UnitNegRange) -> Option<f64> {
438    if sin.0.abs() >= SQ_EPSILON {
439        Some(1.0 / sin.0)
440    } else {
441        None
442    }
443}
444
445/// The secant of an angle.
446///
447/// * `cos` the cosine of the angle.
448///
449/// returns the secant or `None` if `cos < SQ_EPSILON`
450#[must_use]
451pub fn sec(cos: UnitNegRange) -> Option<f64> {
452    if cos.0.abs() >= SQ_EPSILON {
453        Some(1.0 / cos.0)
454    } else {
455        None
456    }
457}
458
459/// The tangent of an angle.
460///
461/// * `cos` the cosine of the angle.
462///
463/// returns the tangent or `None` if `cos < SQ_EPSILON`
464#[must_use]
465pub fn tan(sin: UnitNegRange, cos: UnitNegRange) -> Option<f64> {
466    sec(cos).map(|secant| sin.0 * secant)
467}
468
469/// The cotangent of an angle.
470///
471/// * `sin` the sine of the angle.
472///
473/// returns the cotangent or `None` if `sin < SQ_EPSILON`
474#[must_use]
475pub fn cot(sin: UnitNegRange, cos: UnitNegRange) -> Option<f64> {
476    csc(sin).map(|cosecant| cos.0 * cosecant)
477}
478
479/// Calculate the sine of the difference of two angles: a - b.
480///
481/// See:
482/// [angle sum and difference identities](https://en.wikipedia.org/wiki/List_of_trigonometric_identities#Angle_sum_and_difference_identities).
483/// * `sin_a`, `cos_a` the sine and cosine of angle a.
484/// * `sin_b`, `cos_b` the sine and cosine of angle b.
485///
486/// return sin(a - b)
487#[must_use]
488pub fn sine_diff(
489    sin_a: UnitNegRange,
490    cos_a: UnitNegRange,
491    sin_b: UnitNegRange,
492    cos_b: UnitNegRange,
493) -> UnitNegRange {
494    UnitNegRange::clamp(sin_a.0 * cos_b.0 - sin_b.0 * cos_a.0)
495}
496
497/// Calculate the sine of the sum of two angles: a + b.
498///
499/// See:
500/// [angle sum and difference identities](https://en.wikipedia.org/wiki/List_of_trigonometric_identities#Angle_sum_and_difference_identities).
501/// * `sin_a`, `cos_a` the sine and cosine of angle a.
502/// * `sin_b`, `cos_b` the sine and cosine of angle b.
503///
504/// return sin(a + b)
505#[must_use]
506pub fn sine_sum(
507    sin_a: UnitNegRange,
508    cos_a: UnitNegRange,
509    sin_b: UnitNegRange,
510    cos_b: UnitNegRange,
511) -> UnitNegRange {
512    sine_diff(sin_a, cos_a, -sin_b, cos_b)
513}
514
515/// Calculate the cosine of the difference of two angles: a - b.
516///
517/// See:
518/// [angle sum and difference identities](https://en.wikipedia.org/wiki/List_of_trigonometric_identities#Angle_sum_and_difference_identities).
519/// * `sin_a`, `cos_a` the sine and cosine of angle a.
520/// * `sin_b`, `cos_b` the sine and cosine of angle b.
521///
522/// return cos(a - b)
523#[must_use]
524pub fn cosine_diff(
525    sin_a: UnitNegRange,
526    cos_a: UnitNegRange,
527    sin_b: UnitNegRange,
528    cos_b: UnitNegRange,
529) -> UnitNegRange {
530    UnitNegRange::clamp(cos_a.0 * cos_b.0 + sin_a.0 * sin_b.0)
531}
532
533/// Calculate the cosine of the sum of two angles: a + b.
534///
535/// See:
536/// [angle sum and difference identities](https://en.wikipedia.org/wiki/List_of_trigonometric_identities#Angle_sum_and_difference_identities).
537/// * `sin_a`, `cos_a` the sine and cosine of angle a.
538/// * `sin_b`, `cos_b` the sine and cosine of angle b.
539///
540/// return cos(a + b)
541#[must_use]
542pub fn cosine_sum(
543    sin_a: UnitNegRange,
544    cos_a: UnitNegRange,
545    sin_b: UnitNegRange,
546    cos_b: UnitNegRange,
547) -> UnitNegRange {
548    cosine_diff(sin_a, cos_a, -sin_b, cos_b)
549}
550
551/// Square of the sine of half the Angle.
552///
553/// See: [Half-angle formulae](https://en.wikipedia.org/wiki/List_of_trigonometric_identities#Half-angle_formulae)
554#[must_use]
555pub fn sq_sine_half(cos: UnitNegRange) -> f64 {
556    (1.0 - cos.0) * 0.5
557}
558
559/// Square of the cosine of half the Angle.
560///
561/// See: [Half-angle formulae](https://en.wikipedia.org/wiki/List_of_trigonometric_identities#Half-angle_formulae)
562#[must_use]
563pub fn sq_cosine_half(cos: UnitNegRange) -> f64 {
564    (1.0 + cos.0) * 0.5
565}
566
567/// Calculates the length of the other side in a right angled triangle,
568/// given the length of one side and the hypotenuse.
569///
570/// See: [Pythagorean theorem](https://en.wikipedia.org/wiki/Pythagorean_theorem)
571/// * `length` the length of a side.
572/// * `hypotenuse` the length of the hypotenuse
573///
574/// returns the length of the other side.
575/// zero if length >= hypotenuse or the hypotenuse if length <= 0.
576#[must_use]
577pub fn calculate_adjacent_length(length: f64, hypotenuse: f64) -> f64 {
578    if length <= 0.0 {
579        hypotenuse
580    } else if length >= hypotenuse {
581        0.0
582    } else {
583        libm::sqrt((hypotenuse - length) * (hypotenuse + length))
584    }
585}
586
587/// Calculates the length of the other side in a right angled spherical
588/// triangle, given the length of one side and the hypotenuse.
589///
590/// See: [Spherical law of cosines](https://en.wikipedia.org/wiki/Spherical_law_of_cosines)
591/// * `a` the length of a side.
592/// * `c` the length of the hypotenuse
593///
594/// returns the length of the other side.
595/// zero if a >= c or c if a <= 0.
596#[must_use]
597pub fn spherical_adjacent_length(a: Radians, c: Radians) -> Radians {
598    if a <= Radians(0.0) {
599        c
600    } else if a >= c {
601        Radians(0.0)
602    } else {
603        Radians(libm::acos(libm::cos(c.0) / libm::cos(a.0)))
604    }
605}
606
607/// Calculates the length of the hypotenuse in a right angled spherical
608/// triangle, given the length of both sides.
609///
610/// See: [Spherical law of cosines](https://en.wikipedia.org/wiki/Spherical_law_of_cosines)
611/// * `a`, `b` the lengths of the sides adjacent to the right angle.
612///
613/// returns the length of the hypotenuse.
614#[must_use]
615pub fn spherical_hypotenuse_length(a: Radians, b: Radians) -> Radians {
616    if a <= Radians(0.0) {
617        b
618    } else if b <= Radians(0.0) {
619        a
620    } else {
621        Radians(libm::acos(libm::cos(a.0) * libm::cos(b.0)))
622    }
623}
624
625/// Calculate the length of the adjacent side of a right angled spherical
626/// triangle, given the cosine of the angle and length of the hypotenuse.
627///
628/// See: [Spherical law of cosines](https://en.wikipedia.org/wiki/Spherical_law_of_cosines)
629/// * `cos_angle` the cosine of the adjacent angle.
630/// * `length` the length of the hypotenuse
631///
632/// return the length of the opposite side.
633#[must_use]
634pub fn spherical_cosine_rule(cos_angle: UnitNegRange, length: Radians) -> Radians {
635    Radians(libm::atan(cos_angle.0 * libm::tan(length.0)))
636}
637
638#[cfg(test)]
639mod tests {
640    use super::*;
641    use crate::is_within_tolerance;
642
643    #[test]
644    fn unit_neg_range_traits() {
645        let zero = UnitNegRange::default();
646        assert_eq!(UnitNegRange(0.0), zero);
647        let one = UnitNegRange(1.0);
648
649        let one_clone = one.clone();
650        assert_eq!(one_clone, one);
651
652        let minus_one: UnitNegRange = -one;
653        assert_eq!(minus_one, UnitNegRange(-1.0));
654        assert!(minus_one < one);
655        assert_eq!(one, minus_one.abs());
656
657        print!("UnitNegRange: {:?}", one);
658    }
659
660    #[test]
661    fn unit_neg_range_clamp() {
662        // value < -1
663        assert_eq!(-1.0, UnitNegRange::clamp(-1.0 - f64::EPSILON).0);
664        // value = -1
665        assert_eq!(-1.0, UnitNegRange::clamp(-1.0).0);
666        // value = 1
667        assert_eq!(1.0, UnitNegRange::clamp(1.0).0);
668        // value > 1
669        assert_eq!(1.0, UnitNegRange::clamp(1.0 + f64::EPSILON).0);
670    }
671
672    #[test]
673    fn unit_neg_range_is_valid() {
674        assert!(!UnitNegRange(-1.0 - f64::EPSILON).is_valid());
675        assert!(UnitNegRange(-1.0).is_valid());
676        assert!(UnitNegRange(1.0).is_valid());
677        assert!(!UnitNegRange(1.0 + f64::EPSILON).is_valid());
678    }
679
680    #[test]
681    fn test_trig_functions() {
682        let cos_60 = UnitNegRange(0.5);
683        let sin_60 = swap_sin_cos(cos_60);
684        assert_eq!(COS_30_DEGREES, sin_60.0);
685
686        let sin_120 = sin_60;
687        let cos_120 = cosine_from_sine(sin_120, -1.0);
688
689        let zero = cosine_from_sine(UnitNegRange(1.0), -1.0);
690        assert_eq!(0.0, zero.0);
691        assert!(zero.0.is_sign_positive());
692
693        let recip_sq_epsilon = 1.0 / SQ_EPSILON;
694
695        let sin_msq_epsilon = UnitNegRange(-SQ_EPSILON);
696        assert_eq!(-recip_sq_epsilon, csc(sin_msq_epsilon).unwrap());
697        assert_eq!(-recip_sq_epsilon, sec(sin_msq_epsilon).unwrap());
698
699        let cos_msq_epsilon = swap_sin_cos(sin_msq_epsilon);
700        assert_eq!(1.0, sec(cos_msq_epsilon).unwrap());
701        assert_eq!(1.0, csc(cos_msq_epsilon).unwrap());
702
703        assert_eq!(-SQ_EPSILON, tan(sin_msq_epsilon, cos_msq_epsilon).unwrap());
704        assert_eq!(
705            -recip_sq_epsilon,
706            cot(sin_msq_epsilon, cos_msq_epsilon).unwrap()
707        );
708
709        assert!(is_within_tolerance(
710            sin_120.0,
711            sine_sum(sin_60, cos_60, sin_60, cos_60).0,
712            f64::EPSILON
713        ));
714        assert!(is_within_tolerance(
715            cos_120.0,
716            cosine_sum(sin_60, cos_60, sin_60, cos_60).0,
717            f64::EPSILON
718        ));
719
720        let result = sq_sine_half(cos_120);
721        assert_eq!(sin_60.0, libm::sqrt(result));
722
723        let result = sq_cosine_half(cos_120);
724        assert!(is_within_tolerance(
725            cos_60.0,
726            libm::sqrt(result),
727            f64::EPSILON
728        ));
729    }
730
731    #[test]
732    fn test_small_angle_conversion() {
733        // Test angle == sine(angle) for MAX_LINEAR_SIN_ANGLE
734        assert_eq!(MAX_LINEAR_SIN_ANGLE.0, sine(MAX_LINEAR_SIN_ANGLE).0);
735
736        // Test cos(angle) == cosine(angle) for MAX_COS_ANGLE_IS_ONE
737        let s = sine(MAX_COS_ANGLE_IS_ONE);
738        assert_eq!(
739            libm::cos(MAX_COS_ANGLE_IS_ONE.0),
740            cosine(MAX_COS_ANGLE_IS_ONE, s).0
741        );
742        assert_eq!(1.0, libm::cos(MAX_COS_ANGLE_IS_ONE.0));
743
744        // Test max angle where conventional cos(angle) == 1.0
745        let angle = Radians(4.74e7 * f64::EPSILON);
746        assert_eq!(1.0, libm::cos(angle.0));
747
748        // Note: cosine(angle) < cos(angle) at the given angle
749        // cos(angle) is not accurate for angles close to zero.
750        let s = sine(angle);
751        let result = cosine(angle, s);
752        assert_eq!(1.0 - f64::EPSILON / 2.0, result.0);
753        assert!(result.0 < libm::cos(angle.0));
754    }
755
756    #[test]
757    fn test_radians_conversion() {
758        // Radians is irrational because PI is an irrational number
759        // π/2 != π/3 + π/6
760        // assert_eq!(core::f64::consts::FRAC_PI_2, core::f64::consts::FRAC_PI_3 + core::f64::consts::FRAC_PI_6);
761        assert!(
762            core::f64::consts::FRAC_PI_2
763                != core::f64::consts::FRAC_PI_3 + core::f64::consts::FRAC_PI_6
764        );
765
766        // π/2 + ε = π/3 + π/6 // error is ε
767        assert_eq!(
768            core::f64::consts::FRAC_PI_2 + f64::EPSILON,
769            core::f64::consts::FRAC_PI_3 + core::f64::consts::FRAC_PI_6
770        );
771
772        // π/2 = 2 * π/4
773        assert_eq!(
774            core::f64::consts::FRAC_PI_2,
775            2.0 * core::f64::consts::FRAC_PI_4
776        );
777        // π = 2 * π/2
778        assert_eq!(core::f64::consts::PI, 2.0 * core::f64::consts::FRAC_PI_2);
779
780        // π/4 = 45°
781        assert_eq!(core::f64::consts::FRAC_PI_4, 45.0_f64.to_radians());
782
783        // sine π/4 is off by Epsilon / 2
784        assert_eq!(
785            core::f64::consts::FRAC_1_SQRT_2 - 0.5 * f64::EPSILON,
786            libm::sin(core::f64::consts::FRAC_PI_4)
787        );
788
789        // -π/6 radians round trip
790        let result = sincos(Radians(-core::f64::consts::FRAC_PI_6));
791        assert_eq!(-0.5, result.0.0);
792        assert_eq!(COS_30_DEGREES, result.1.0);
793        assert_eq!(-core::f64::consts::FRAC_PI_6, arctan2(result.0, result.1).0);
794
795        // π/3 radians round trip
796        let result = sincos(Radians(core::f64::consts::FRAC_PI_3));
797        // Not exactly correct because PI is an irrational number
798        // assert_eq!(COS_30_DEGREES, result.0.0);
799        assert!(is_within_tolerance(
800            COS_30_DEGREES,
801            result.0.0,
802            f64::EPSILON
803        ));
804        // assert_eq!(0.5, result.1.0);
805        assert!(is_within_tolerance(0.5, result.1.0, f64::EPSILON));
806        assert_eq!(core::f64::consts::FRAC_PI_3, arctan2(result.0, result.1).0);
807
808        // -π radians round trip to +π radians
809        let result = sincos(Radians(-core::f64::consts::PI));
810        assert_eq!(0.0, result.0.0);
811        assert_eq!(-1.0, result.1.0);
812        assert_eq!(core::f64::consts::PI, arctan2(result.0, result.1).0);
813
814        // π - π/4 radians round trip
815        let result = sincos_diff(
816            Radians(core::f64::consts::PI),
817            Radians(core::f64::consts::FRAC_PI_4),
818        );
819        assert_eq!(core::f64::consts::FRAC_1_SQRT_2, result.0.0);
820        assert_eq!(-core::f64::consts::FRAC_1_SQRT_2, result.1.0);
821        assert_eq!(
822            core::f64::consts::PI - core::f64::consts::FRAC_PI_4,
823            arctan2(result.0, result.1).0
824        );
825
826        // 6*π - π/3 radians round trip
827        let result = sincos_diff(
828            Radians(3.0 * core::f64::consts::TAU),
829            Radians(core::f64::consts::FRAC_PI_3),
830        );
831        // Not exactly correct because π is an irrational number
832        // assert_eq!(-COS_30_DEGREES, result.0.0);
833        assert!(is_within_tolerance(
834            -COS_30_DEGREES,
835            result.0.0,
836            f64::EPSILON
837        ));
838        // assert_eq!(0.5, result.1.0);
839        assert!(is_within_tolerance(0.5, result.1.0, f64::EPSILON));
840        assert_eq!(-core::f64::consts::FRAC_PI_3, arctan2(result.0, result.1).0);
841    }
842
843    #[test]
844    fn test_degrees_conversion() {
845        // Degrees is rational
846        assert_eq!(90.0, 60.0 + 30.0);
847        assert_eq!(90.0, 2.0 * 45.0);
848        assert_eq!(180.0, 2.0 * 90.0);
849
850        // -30 degrees round trip
851        let result = sincosd(Degrees(-30.0));
852        assert_eq!(-0.5, result.0.0);
853        assert_eq!(COS_30_DEGREES, result.1.0);
854        assert_eq!(-30.0, arctan2d(result.0, result.1).0);
855
856        // 60 degrees round trip
857        let result = sincosd(Degrees(60.0));
858        assert_eq!(COS_30_DEGREES, result.0.0);
859        assert_eq!(0.5, result.1.0);
860        assert_eq!(60.0, arctan2d(result.0, result.1).0);
861
862        // -180 degrees round trip to +180 degrees
863        let result = sincosd(Degrees(-180.0));
864        assert_eq!(0.0, result.0.0);
865        assert_eq!(-1.0, result.1.0);
866        assert_eq!(180.0, arctan2d(result.0, result.1).0);
867
868        // 180 - 45 degrees round trip
869        let result = sincosd_diff(Degrees(180.0), Degrees(45.0));
870        assert_eq!(core::f64::consts::FRAC_1_SQRT_2, result.0.0);
871        assert_eq!(-core::f64::consts::FRAC_1_SQRT_2, result.1.0);
872        assert_eq!(180.0 - 45.0, arctan2d(result.0, result.1).0);
873
874        // 1080 - 60 degrees round trip
875        let result = sincosd_diff(Degrees(1080.0), Degrees(60.0));
876        assert_eq!(-COS_30_DEGREES, result.0.0);
877        assert_eq!(0.5, result.1.0);
878        assert_eq!(-60.0, arctan2d(result.0, result.1).0);
879    }
880
881    #[test]
882    fn test_calculate_adjacent_length() {
883        // length == hypotenuse
884        assert_eq!(0.0, calculate_adjacent_length(5.0, 5.0));
885
886        // length == 0.0
887        assert_eq!(5.0, calculate_adjacent_length(0.0, 5.0));
888
889        // length > hypotenuse
890        assert_eq!(0.0, calculate_adjacent_length(6.0, 5.0));
891
892        // 3, 4, 5 triangle
893        assert_eq!(3.0, calculate_adjacent_length(4.0, 5.0));
894    }
895
896    #[test]
897    fn test_spherical_adjacent_length() {
898        // length == hypotenuse
899        assert_eq!(
900            Radians(0.0),
901            spherical_adjacent_length(Radians(5.0_f64.to_radians()), Radians(5.0_f64.to_radians()))
902        );
903
904        // length == 0
905        assert_eq!(
906            Radians(5.0_f64.to_radians()),
907            spherical_adjacent_length(Radians(0.0), Radians(5.0_f64.to_radians()))
908        );
909
910        // length > hypotenuse
911        assert_eq!(
912            Radians(0.0),
913            spherical_adjacent_length(Radians(6.0_f64.to_radians()), Radians(5.0_f64.to_radians()))
914        );
915
916        // 3, 4, 5 triangle
917        let result =
918            spherical_adjacent_length(Radians(4.0_f64.to_radians()), Radians(5.0_f64.to_radians()));
919        assert!(is_within_tolerance(3.0_f64.to_radians(), result.0, 1.0e-4));
920    }
921
922    #[test]
923    fn test_spherical_hypotenuse_length() {
924        let zero = Radians(0.0);
925        let three = Radians(3.0_f64.to_radians());
926        let four = Radians(4.0_f64.to_radians());
927
928        // Negative length a
929        assert_eq!(three, spherical_hypotenuse_length(-four, three));
930        // Negative length b
931        assert_eq!(four, spherical_hypotenuse_length(four, -three));
932
933        // Zero length a
934        assert_eq!(three, spherical_hypotenuse_length(zero, three));
935        // Zero length b
936        assert_eq!(four, spherical_hypotenuse_length(four, zero));
937        // Zero length a & b
938        assert_eq!(zero, spherical_hypotenuse_length(zero, zero));
939
940        // 3, 4, 5 triangles, note 5 degrees is 0.08726646259971647 radians
941        let result = Radians(0.087240926337265545);
942        assert_eq!(result, spherical_hypotenuse_length(four, three));
943        assert_eq!(result, spherical_hypotenuse_length(three, four));
944    }
945
946    #[test]
947    fn test_spherical_cosine_rule() {
948        let result = spherical_cosine_rule(UnitNegRange(0.0), Radians(1.0));
949        assert_eq!(0.0, result.0);
950
951        let result = spherical_cosine_rule(UnitNegRange(0.8660254037844386), Radians(0.5));
952        assert_eq!(0.44190663576327144, result.0);
953
954        let result = spherical_cosine_rule(UnitNegRange(0.5), Radians(1.0));
955        assert_eq!(0.66161993185017653, result.0);
956
957        let result = spherical_cosine_rule(UnitNegRange(1.0), Radians(1.0));
958        assert_eq!(1.0, result.0);
959    }
960}