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>(sin: UnitNegRange<T>, cos: UnitNegRange<T>) -> Degrees<T>
462where
463    T: Float + FloatConst,
464    f64: From<T>,
465{
466    let forty_five = T::from(FORTY_FIVE).expect("Could not convert constant to Float");
467    let ninety = forty_five + forty_five;
468    let one_eighty = ninety + ninety;
469
470    let sin_abs = sin.0.abs();
471    let cos_abs = cos.0.abs();
472
473    // calculate degrees in the range 0.0..=90.0
474    let degrees_90 = match sin_abs.partial_cmp(&cos_abs).expect("sin or cos is NaN") {
475        Ordering::Equal => forty_five,
476        Ordering::Less => arctan2_degrees(sin_abs, cos_abs),
477        Ordering::Greater => ninety - arctan2_degrees(cos_abs, sin_abs),
478    };
479
480    // calculate degrees in the range 0° <= degrees <= 180°
481    let degrees_180 = if cos.0 < T::zero() {
482        one_eighty - degrees_90
483    } else {
484        degrees_90
485    };
486
487    // return degrees in the range -180° < degrees <= 180°
488    Degrees(degrees_180.copysign(sin.0))
489}
490
491/// The cosecant of an angle.
492///
493/// * `sin` the sine of the angle.
494///
495/// returns the cosecant or `None` if `sin < SQ_EPSILON`
496#[must_use]
497pub fn csc<T: Float>(sin: UnitNegRange<T>) -> Option<T> {
498    let sq_epsilon = T::epsilon() * T::epsilon();
499    if sin.0.abs() >= sq_epsilon {
500        Some(T::one() / sin.0)
501    } else {
502        None
503    }
504}
505
506/// The secant of an angle.
507///
508/// * `cos` the cosine of the angle.
509///
510/// returns the secant or `None` if `cos < SQ_EPSILON`
511#[must_use]
512pub fn sec<T: Float>(cos: UnitNegRange<T>) -> Option<T> {
513    let sq_epsilon = T::epsilon() * T::epsilon();
514    if cos.0.abs() >= sq_epsilon {
515        Some(T::one() / cos.0)
516    } else {
517        None
518    }
519}
520
521/// The tangent of an angle.
522///
523/// * `cos` the cosine of the angle.
524///
525/// returns the tangent or `None` if `cos < SQ_EPSILON`
526#[must_use]
527pub fn tan<T: Float>(sin: UnitNegRange<T>, cos: UnitNegRange<T>) -> Option<T> {
528    sec(cos).map(|secant| sin.0 * secant)
529}
530
531/// The cotangent of an angle.
532///
533/// * `sin` the sine of the angle.
534///
535/// returns the cotangent or `None` if `sin < SQ_EPSILON`
536#[must_use]
537pub fn cot<T: Float>(sin: UnitNegRange<T>, cos: UnitNegRange<T>) -> Option<T> {
538    csc(sin).map(|cosecant| cos.0 * cosecant)
539}
540
541/// Calculate the sine of the difference of two angles: a - b.
542///
543/// See:
544/// [angle sum and difference identities](https://en.wikipedia.org/wiki/List_of_trigonometric_identities#Angle_sum_and_difference_identities).
545/// * `sin_a`, `cos_a` the sine and cosine of angle a.
546/// * `sin_b`, `cos_b` the sine and cosine of angle b.
547///
548/// return sin(a - b)
549#[must_use]
550pub fn sine_diff<T: Float>(
551    sin_a: UnitNegRange<T>,
552    cos_a: UnitNegRange<T>,
553    sin_b: UnitNegRange<T>,
554    cos_b: UnitNegRange<T>,
555) -> UnitNegRange<T> {
556    UnitNegRange::clamp(vector2d::perp_product(sin_a.0, cos_a.0, sin_b.0, cos_b.0))
557}
558
559/// Calculate the sine of the sum of two angles: a + b.
560///
561/// See:
562/// [angle sum and difference identities](https://en.wikipedia.org/wiki/List_of_trigonometric_identities#Angle_sum_and_difference_identities).
563/// * `sin_a`, `cos_a` the sine and cosine of angle a.
564/// * `sin_b`, `cos_b` the sine and cosine of angle b.
565///
566/// return sin(a + b)
567#[must_use]
568pub fn sine_sum<T: Float>(
569    sin_a: UnitNegRange<T>,
570    cos_a: UnitNegRange<T>,
571    sin_b: UnitNegRange<T>,
572    cos_b: UnitNegRange<T>,
573) -> UnitNegRange<T> {
574    sine_diff(sin_a, cos_a, -sin_b, cos_b)
575}
576
577/// Calculate the cosine of the difference of two angles: a - b.
578///
579/// See:
580/// [angle sum and difference identities](https://en.wikipedia.org/wiki/List_of_trigonometric_identities#Angle_sum_and_difference_identities).
581/// * `sin_a`, `cos_a` the sine and cosine of angle a.
582/// * `sin_b`, `cos_b` the sine and cosine of angle b.
583///
584/// return cos(a - b)
585#[must_use]
586pub fn cosine_diff<T: Float>(
587    sin_a: UnitNegRange<T>,
588    cos_a: UnitNegRange<T>,
589    sin_b: UnitNegRange<T>,
590    cos_b: UnitNegRange<T>,
591) -> UnitNegRange<T> {
592    UnitNegRange::clamp(vector2d::dot_product(sin_a.0, cos_a.0, sin_b.0, cos_b.0))
593}
594
595/// Calculate the cosine of the sum of two angles: a + b.
596///
597/// See:
598/// [angle sum and difference identities](https://en.wikipedia.org/wiki/List_of_trigonometric_identities#Angle_sum_and_difference_identities).
599/// * `sin_a`, `cos_a` the sine and cosine of angle a.
600/// * `sin_b`, `cos_b` the sine and cosine of angle b.
601///
602/// return cos(a + b)
603#[must_use]
604pub fn cosine_sum<T: Float>(
605    sin_a: UnitNegRange<T>,
606    cos_a: UnitNegRange<T>,
607    sin_b: UnitNegRange<T>,
608    cos_b: UnitNegRange<T>,
609) -> UnitNegRange<T> {
610    cosine_diff(sin_a, cos_a, -sin_b, cos_b)
611}
612
613/// Square of the sine of half the Angle.
614///
615/// See: [Half-angle formulae](https://en.wikipedia.org/wiki/List_of_trigonometric_identities#Half-angle_formulae)
616#[must_use]
617pub fn sq_sine_half<T: Float>(cos: UnitNegRange<T>) -> T {
618    let half = T::one() / (T::one() + T::one());
619    (T::one() - cos.0) * half
620}
621
622/// Square of the cosine of half the Angle.
623///
624/// See: [Half-angle formulae](https://en.wikipedia.org/wiki/List_of_trigonometric_identities#Half-angle_formulae)
625#[must_use]
626pub fn sq_cosine_half<T: Float>(cos: UnitNegRange<T>) -> T {
627    let half = T::one() / (T::one() + T::one());
628    (T::one() + cos.0) * half
629}
630
631/// Calculates the length of the other side in a right angled triangle,
632/// given the length of one side and the hypotenuse.
633///
634/// See: [Pythagorean theorem](https://en.wikipedia.org/wiki/Pythagorean_theorem)
635/// * `length` the length of a side.
636/// * `hypotenuse` the length of the hypotenuse
637///
638/// returns the length of the other side.
639/// zero if length >= hypotenuse or the hypotenuse if length <= 0.
640#[must_use]
641pub fn calculate_adjacent_length<T: Float>(length: T, hypotenuse: T) -> T {
642    if length <= T::zero() {
643        hypotenuse
644    } else if length >= hypotenuse {
645        T::zero()
646    } else {
647        ((hypotenuse - length) * (hypotenuse + length)).sqrt()
648    }
649}
650
651/// Calculates the length of the other side in a right angled spherical
652/// triangle, given the length of one side and the hypotenuse.
653///
654/// See: [Spherical law of cosines](https://en.wikipedia.org/wiki/Spherical_law_of_cosines)
655/// * `a` the length of a side.
656/// * `c` the length of the hypotenuse
657///
658/// returns the length of the other side.
659/// zero if a >= c or c if a <= 0.
660#[must_use]
661pub fn spherical_adjacent_length<T: Float + FloatConst>(
662    a: Radians<T>,
663    c: Radians<T>,
664) -> Radians<T> {
665    if a <= Radians(T::zero()) {
666        c
667    } else if a >= c {
668        Radians(T::zero())
669    } else {
670        Radians((c.0.cos() / a.0.cos()).acos())
671    }
672}
673
674/// Calculates the length of the hypotenuse in a right angled spherical
675/// triangle, given the length of both sides.
676///
677/// See: [Spherical law of cosines](https://en.wikipedia.org/wiki/Spherical_law_of_cosines)
678/// * `a`, `b` the lengths of the sides adjacent to the right angle.
679///
680/// returns the length of the hypotenuse.
681#[must_use]
682pub fn spherical_hypotenuse_length<T: Float + FloatConst>(
683    a: Radians<T>,
684    b: Radians<T>,
685) -> Radians<T> {
686    if a <= Radians(T::zero()) {
687        b
688    } else if b <= Radians(T::zero()) {
689        a
690    } else {
691        Radians((a.0.cos() * b.0.cos()).acos())
692    }
693}
694
695/// Calculate the length of the adjacent side of a right angled spherical
696/// triangle, given the cosine of the angle and length of the hypotenuse.
697///
698/// See: [Spherical law of cosines](https://en.wikipedia.org/wiki/Spherical_law_of_cosines)
699/// * `cos_angle` the cosine of the adjacent angle.
700/// * `length` the length of the hypotenuse
701///
702/// return the length of the opposite side.
703#[must_use]
704pub fn spherical_cosine_rule<T: Float + FloatConst>(
705    cos_angle: UnitNegRange<T>,
706    length: Radians<T>,
707) -> Radians<T> {
708    Radians((cos_angle.0 * length.0.tan()).atan())
709}
710
711#[cfg(test)]
712mod tests {
713    use super::*;
714    use crate::is_within_tolerance;
715
716    #[test]
717    fn unit_neg_range_traits() {
718        let zero = UnitNegRange::default();
719        assert_eq!(UnitNegRange(0.0), zero);
720        let one = UnitNegRange(1.0);
721
722        let one_clone = one.clone();
723        assert_eq!(one_clone, one);
724
725        let minus_one = -one;
726        assert_eq!(minus_one, UnitNegRange(-1.0));
727        assert!(minus_one < one);
728        assert_eq!(one, minus_one.abs());
729
730        print!("UnitNegRange: {:?}", one);
731    }
732
733    #[test]
734    fn unit_neg_range_clamp() {
735        // value < -1
736        assert_eq!(-1.0, UnitNegRange::clamp(-1.0 - f64::EPSILON).0);
737        // value = -1
738        assert_eq!(-1.0, UnitNegRange::clamp(-1.0).0);
739        // value = 1
740        assert_eq!(1.0, UnitNegRange::clamp(1.0).0);
741        // value > 1
742        assert_eq!(1.0, UnitNegRange::clamp(1.0 + f64::EPSILON).0);
743    }
744
745    #[test]
746    fn unit_neg_range_is_valid() {
747        assert!(!UnitNegRange(-1.0 - f64::EPSILON).is_valid());
748        assert!(UnitNegRange(-1.0).is_valid());
749        assert!(UnitNegRange(1.0).is_valid());
750        assert!(!UnitNegRange(1.0 + f64::EPSILON).is_valid());
751    }
752
753    #[test]
754    fn test_trig_functions() {
755        let cos_60 = UnitNegRange(0.5);
756        let sin_60 = swap_sin_cos(cos_60);
757        assert_eq!(COS_30_DEGREES, sin_60.0);
758
759        let sin_120 = sin_60;
760        let cos_120 = cosine_from_sine(sin_120, -1.0);
761
762        let zero = cosine_from_sine(UnitNegRange(1.0), -1.0);
763        assert_eq!(0.0, zero.0);
764        assert!(zero.0.is_sign_positive());
765
766        let recip_sq_epsilon = 1.0 / SQ_EPSILON;
767
768        let sin_msq_epsilon = UnitNegRange(-SQ_EPSILON);
769        assert_eq!(-recip_sq_epsilon, csc(sin_msq_epsilon).unwrap());
770        assert_eq!(-recip_sq_epsilon, sec(sin_msq_epsilon).unwrap());
771
772        let cos_msq_epsilon = swap_sin_cos(sin_msq_epsilon);
773        assert_eq!(1.0, sec(cos_msq_epsilon).unwrap());
774        assert_eq!(1.0, csc(cos_msq_epsilon).unwrap());
775
776        assert_eq!(-SQ_EPSILON, tan(sin_msq_epsilon, cos_msq_epsilon).unwrap());
777        assert_eq!(
778            -recip_sq_epsilon,
779            cot(sin_msq_epsilon, cos_msq_epsilon).unwrap()
780        );
781
782        assert!(is_within_tolerance(
783            sin_120.0,
784            sine_sum(sin_60, cos_60, sin_60, cos_60).0,
785            f64::EPSILON
786        ));
787        assert!(is_within_tolerance(
788            cos_120.0,
789            cosine_sum(sin_60, cos_60, sin_60, cos_60).0,
790            f64::EPSILON
791        ));
792
793        let result = sq_sine_half(cos_120);
794        assert_eq!(sin_60.0, result.sqrt());
795
796        let result = sq_cosine_half(cos_120);
797        assert!(is_within_tolerance(cos_60.0, result.sqrt(), f64::EPSILON));
798    }
799
800    #[test]
801    fn test_small_angle_conversion() {
802        // Test angle == sine(angle) for MAX_LINEAR_SIN_ANGLE
803        assert_eq!(MAX_LINEAR_SIN_ANGLE, sine(Radians(MAX_LINEAR_SIN_ANGLE)).0);
804
805        // Test cos(angle) == cosine(angle) for MAX_COS_ANGLE_IS_ONE
806        let s = sine(Radians(MAX_COS_ANGLE_IS_ONE));
807        assert_eq!(
808            MAX_COS_ANGLE_IS_ONE.cos(),
809            cosine(Radians(MAX_COS_ANGLE_IS_ONE), s).0
810        );
811        assert_eq!(1.0, MAX_COS_ANGLE_IS_ONE.cos());
812
813        // Test max angle where conventional cos(angle) == 1.0
814        let angle = Radians(4.74e7 * f64::EPSILON);
815        assert_eq!(1.0, angle.0.cos());
816
817        // Note: cosine(angle) < cos(angle) at the given angle
818        // cos(angle) is not accurate for angles close to zero.
819        let s = sine(angle);
820        let result = cosine(angle, s);
821        assert_eq!(1.0 - f64::EPSILON / 2.0, result.0);
822        assert!(result.0 < angle.0.cos());
823    }
824
825    #[test]
826    fn test_radians_conversion() {
827        // Radians is irrational because PI is an irrational number
828        // π/2 != π/3 + π/6
829        // assert_eq!(core::f64::consts::FRAC_PI_2, core::f64::consts::FRAC_PI_3 + core::f64::consts::FRAC_PI_6);
830        assert!(
831            core::f64::consts::FRAC_PI_2
832                != core::f64::consts::FRAC_PI_3 + core::f64::consts::FRAC_PI_6
833        );
834
835        // π/2 + ε = π/3 + π/6 // error is ε
836        assert_eq!(
837            core::f64::consts::FRAC_PI_2 + f64::EPSILON,
838            core::f64::consts::FRAC_PI_3 + core::f64::consts::FRAC_PI_6
839        );
840
841        // π/2 = 2 * π/4
842        assert_eq!(
843            core::f64::consts::FRAC_PI_2,
844            2.0 * core::f64::consts::FRAC_PI_4
845        );
846        // π = 2 * π/2
847        assert_eq!(core::f64::consts::PI, 2.0 * core::f64::consts::FRAC_PI_2);
848
849        // π/4 = 45°
850        assert_eq!(core::f64::consts::FRAC_PI_4, 45.0_f64.to_radians());
851
852        // sine π/4 is off by Epsilon / 2
853        assert_eq!(
854            core::f64::consts::FRAC_1_SQRT_2 - 0.5 * f64::EPSILON,
855            core::f64::consts::FRAC_PI_4.sin()
856        );
857
858        // -π/6 radians round trip
859        let result = sincos(Radians(-core::f64::consts::FRAC_PI_6));
860        assert_eq!(-0.5, result.0.0);
861        assert_eq!(COS_30_DEGREES, result.1.0);
862        assert_eq!(-core::f64::consts::FRAC_PI_6, arctan2(result.0, result.1).0);
863
864        // π/3 radians round trip
865        let result = sincos(Radians(core::f64::consts::FRAC_PI_3));
866        // Not exactly correct because PI is an irrational number
867        // assert_eq!(COS_30_DEGREES, result.0.0);
868        assert!(is_within_tolerance(
869            COS_30_DEGREES,
870            result.0.0,
871            f64::EPSILON
872        ));
873        // assert_eq!(0.5, result.1.0);
874        assert!(is_within_tolerance(0.5, result.1.0, f64::EPSILON));
875        assert_eq!(core::f64::consts::FRAC_PI_3, arctan2(result.0, result.1).0);
876
877        // -π radians round trip to +π radians
878        let result = sincos(Radians(-core::f64::consts::PI));
879        assert_eq!(0.0, result.0.0);
880        assert_eq!(-1.0, result.1.0);
881        assert_eq!(core::f64::consts::PI, arctan2(result.0, result.1).0);
882
883        // π - π/4 radians round trip
884        let result = sincos_diff(
885            Radians(core::f64::consts::PI),
886            Radians(core::f64::consts::FRAC_PI_4),
887        );
888        assert_eq!(core::f64::consts::FRAC_1_SQRT_2, result.0.0);
889        assert_eq!(-core::f64::consts::FRAC_1_SQRT_2, result.1.0);
890        assert_eq!(
891            core::f64::consts::PI - core::f64::consts::FRAC_PI_4,
892            arctan2(result.0, result.1).0
893        );
894
895        // 6*π - π/3 radians round trip
896        let result = sincos_diff(
897            Radians(3.0 * core::f64::consts::TAU),
898            Radians(core::f64::consts::FRAC_PI_3),
899        );
900        // Not exactly correct because π is an irrational number
901        // assert_eq!(-COS_30_DEGREES, result.0.0);
902        assert!(is_within_tolerance(
903            -COS_30_DEGREES,
904            result.0.0,
905            f64::EPSILON
906        ));
907        // assert_eq!(0.5, result.1.0);
908        assert!(is_within_tolerance(0.5, result.1.0, f64::EPSILON));
909        assert_eq!(-core::f64::consts::FRAC_PI_3, arctan2(result.0, result.1).0);
910    }
911
912    #[test]
913    fn test_degrees_conversion() {
914        // Degrees is rational
915        assert_eq!(90.0, 60.0 + 30.0);
916        assert_eq!(90.0, 2.0 * 45.0);
917        assert_eq!(180.0, 2.0 * 90.0);
918
919        // -30 degrees round trip
920        let result = sincosd(Degrees(-30.0));
921        assert_eq!(-0.5, result.0.0);
922        assert_eq!(COS_30_DEGREES, result.1.0);
923        assert_eq!(-30.0, arctan2d(result.0, result.1).0);
924
925        // 60 degrees round trip
926        let result = sincosd(Degrees(60.0));
927        assert_eq!(COS_30_DEGREES, result.0.0);
928        assert_eq!(0.5, result.1.0);
929        assert_eq!(60.0, arctan2d(result.0, result.1).0);
930
931        // -180 degrees round trip to +180 degrees
932        let result = sincosd(Degrees(-180.0));
933        assert_eq!(0.0, result.0.0);
934        assert_eq!(-1.0, result.1.0);
935        assert_eq!(180.0, arctan2d(result.0, result.1).0);
936
937        // 180 - 45 degrees round trip
938        let result = sincosd_diff(Degrees(180.0), Degrees(45.0));
939        assert_eq!(core::f64::consts::FRAC_1_SQRT_2, result.0.0);
940        assert_eq!(-core::f64::consts::FRAC_1_SQRT_2, result.1.0);
941        assert_eq!(180.0 - 45.0, arctan2d(result.0, result.1).0);
942
943        // 1080 - 60 degrees round trip
944        let result = sincosd_diff(Degrees(1080.0), Degrees(60.0));
945        assert_eq!(-COS_30_DEGREES, result.0.0);
946        assert_eq!(0.5, result.1.0);
947        assert_eq!(-60.0, arctan2d(result.0, result.1).0);
948    }
949
950    #[test]
951    fn test_calculate_adjacent_length() {
952        // length == hypotenuse
953        assert_eq!(0.0, calculate_adjacent_length(5.0, 5.0));
954
955        // length == 0.0
956        assert_eq!(5.0, calculate_adjacent_length(0.0, 5.0));
957
958        // length > hypotenuse
959        assert_eq!(0.0, calculate_adjacent_length(6.0, 5.0));
960
961        // 3, 4, 5 triangle
962        assert_eq!(3.0, calculate_adjacent_length(4.0, 5.0));
963    }
964
965    #[test]
966    fn test_spherical_adjacent_length() {
967        // length == hypotenuse
968        assert_eq!(
969            Radians(0.0),
970            spherical_adjacent_length(Radians(5.0_f64.to_radians()), Radians(5.0_f64.to_radians()))
971        );
972
973        // length == 0
974        assert_eq!(
975            Radians(5.0_f64.to_radians()),
976            spherical_adjacent_length(Radians(0.0), Radians(5.0_f64.to_radians()))
977        );
978
979        // length > hypotenuse
980        assert_eq!(
981            Radians(0.0),
982            spherical_adjacent_length(Radians(6.0_f64.to_radians()), Radians(5.0_f64.to_radians()))
983        );
984
985        // 3, 4, 5 triangle
986        let result =
987            spherical_adjacent_length(Radians(4.0_f64.to_radians()), Radians(5.0_f64.to_radians()));
988        assert!(is_within_tolerance(3.0_f64.to_radians(), result.0, 1.0e-4));
989    }
990
991    #[test]
992    fn test_spherical_hypotenuse_length() {
993        let zero = Radians(0.0);
994        let three = Radians(3.0_f64.to_radians());
995        let four = Radians(4.0_f64.to_radians());
996
997        // Negative length a
998        assert_eq!(three, spherical_hypotenuse_length(-four, three));
999        // Negative length b
1000        assert_eq!(four, spherical_hypotenuse_length(four, -three));
1001
1002        // Zero length a
1003        assert_eq!(three, spherical_hypotenuse_length(zero, three));
1004        // Zero length b
1005        assert_eq!(four, spherical_hypotenuse_length(four, zero));
1006        // Zero length a & b
1007        assert_eq!(zero, spherical_hypotenuse_length(zero, zero));
1008
1009        // 3, 4, 5 triangles, note 5 degrees is 0.08726646259971647 radians
1010        let result = Radians(0.087240926337265545);
1011        assert_eq!(result, spherical_hypotenuse_length(four, three));
1012        assert_eq!(result, spherical_hypotenuse_length(three, four));
1013    }
1014
1015    #[test]
1016    fn test_spherical_cosine_rule() {
1017        let result = spherical_cosine_rule(UnitNegRange(0.0), Radians(1.0));
1018        assert_eq!(0.0, result.0);
1019
1020        let result = spherical_cosine_rule(UnitNegRange(0.8660254037844386), Radians(0.5));
1021        assert_eq!(0.44190663576327144, result.0);
1022
1023        let result = spherical_cosine_rule(UnitNegRange(0.5), Radians(1.0));
1024        assert_eq!(0.66161993185017653, result.0);
1025
1026        let result = spherical_cosine_rule(UnitNegRange(1.0), Radians(1.0));
1027        assert_eq!(1.0, result.0);
1028    }
1029}