Skip to main content

angle_sc/
trig.rs

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