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