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