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