perplex_num/
polar.rs

1//! # Hyperbolic Polar Module
2//!
3//! This module provides the functionality to work with perplex numbers in polar form, which is particularly useful in the context of hyperbolic geometry.
4//! It includes methods for converting between the standard `Perplex` representation and the `HyperbolicPolar` form, as well as operations like exponentiation within the hyperbolic plane.
5//! The hyperbolic polar form encodes a perplex number `z` as a triple of two real numbers `rho` and `theta`, as well as one out of four perplex numbers `klein`, such that `z= klein rho (cosh(theta) + h sinh(theta))`.
6//! `Klein` is defined by the sector of the hyperbolic plane in which the perplex number is in. Formulas are taken from Tab. 1 and Appendix B in [Hyperbolic trigonometry in two-dimensional space-time geometry](https://doi.org/10.1393/ncb/i2003-10012-9).
7//!
8//! ## Usage
9//!
10//! Here is an example of how to use the `HyperbolicPolar` struct to convert a `Perplex` number
11//! into its polar form and back:
12//!
13//! ```
14//! use num_traits::Pow;
15//! use perplex_num::{HyperbolicSector, HyperbolicPolar, Perplex};
16//! let z = Perplex { t: 1.0, x: 0.5 };
17//! // Convert the Perplex number to HyperbolicPolar form
18//! let polar_form: HyperbolicPolar<f64> = z.into();
19//! // Perform operations in polar form...
20//! // For example, raise to a power
21//! let polar_powered = polar_form.pow(2);
22//! // Convert back to Perplex form
23//! let z_powered: Perplex<f64> = polar_powered.into();
24//! approx::assert_abs_diff_eq!(z_powered, Perplex { t: 1.25, x: 1.0 }, epsilon=0.0000000001);
25//! ```
26
27use super::Perplex;
28use num_traits::{Float, Num, One, Pow};
29
30/// Represents the sector of the hyperbolic plane a perplex number is in.
31///
32/// The hyperbolic plane is divided into four sectors by the intersection of two diagonals,
33/// where `t = x` and `t = -x`. This enum also includes the `Diagonal` variant to represent
34/// light-like perplex numbers where the time and space components are equal in magnitude.
35#[derive(Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Hash, Debug, Default)]
36pub enum HyperbolicSector<T> {
37    /// The sector where the time component is greater than the space component in absolute value.
38    #[default]
39    Right,
40    /// The sector where the space component is greater than the time component in absolute value.
41    Up,
42    /// The mirror image of the Right sector, where the negative time component is greater in magnitude.
43    Left,
44    /// The mirror image of the Up sector, where the negative space component is greater in magnitude.
45    Down,
46    /// Represents a light-like perplex number on the diagonal where `t` and `x` are equal.
47    /// The value `T` encodes which diagonal line is used based on its sign.
48    Diagonal(T),
49}
50
51impl<T: Copy + Float> From<Perplex<T>> for HyperbolicSector<T> {
52    /// Converts a perplex number into its corresponding hyperbolic sector.
53    ///
54    /// Light-like numbers are converted to the `Diagonal` variant, while others are
55    /// categorized based on the magnitude and sign of their time and space components.
56    #[inline]
57    fn from(z: Perplex<T>) -> Self {
58        let Perplex { t, x } = z;
59        let (t_abs, x_abs) = (t.abs(), x.abs());
60        if t_abs == x_abs {
61            Self::Diagonal(t)
62        } else if t_abs > x_abs {
63            if t > T::zero() {
64                Self::Right
65            } else {
66                Self::Left
67            }
68        } else if x > T::zero() {
69            Self::Up
70        } else {
71            Self::Down
72        }
73    }
74}
75
76/// Represents a perplex number in hyperbolic polar form.
77///
78/// This struct is used to convert a perplex number to and from hyperbolic polar form,
79/// which is useful for operations that are more naturally expressed in this form.
80/// The conversion formulas are based on hyperbolic trigonometry principles.
81#[derive(Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Hash, Debug)]
82pub struct HyperbolicPolar<T> {
83    /// The modulus of the perplex number, representing the hyperbolic radius.
84    pub rho: T,
85    /// The argument of the perplex number, representing the hyperbolic angle.
86    pub theta: T,
87    /// The sector of the hyperbolic plane the number is in.
88    pub sector: HyperbolicSector<T>,
89}
90
91impl<T: Copy + Num> Default for HyperbolicPolar<T> {
92    /// Provides a default value for `HyperbolicPolar`, which is in the Right sector
93    /// with a modulus of one and an angle of zero, i.e., the neutral element of multiplication `Perplex::default()`.
94    #[inline]
95    fn default() -> Self {
96        Self {
97            rho: T::one(),
98            theta: T::zero(),
99            sector: HyperbolicSector::Right,
100        }
101    }
102}
103
104impl<T: Copy + Float> From<Perplex<T>> for HyperbolicPolar<T> {
105    /// Converts a perplex number to hyperbolic polar form.
106    ///
107    /// The conversion takes into account the sector of the hyperbolic plane the number
108    /// is in and uses the appropriate hyperbolic trigonometric functions.
109    #[inline]
110    fn from(z: Perplex<T>) -> Self {
111        Self {
112            rho: z.norm(),
113            theta: z.arg(),
114            sector: HyperbolicSector::from(z),
115        }
116    }
117}
118
119impl<T: Copy + Float> From<HyperbolicPolar<T>> for Perplex<T> {
120    /// Converts a hyperbolic polar representation back to a perplex number.
121    ///
122    /// This method applies the inverse of the hyperbolic polar conversion, reconstructing
123    /// the original perplex number from its modulus, argument, and sector.
124    #[inline]
125    fn from(polar: HyperbolicPolar<T>) -> Self {
126        let HyperbolicPolar { rho, theta, sector } = polar;
127        match sector {
128            HyperbolicSector::Right => Self::new(rho * theta.cosh(), rho * theta.sinh()),
129            HyperbolicSector::Up => Self::new(rho * theta.sinh(), rho * theta.cosh()),
130            HyperbolicSector::Left => Self::new(-rho * theta.cosh(), -rho * theta.sinh()),
131            HyperbolicSector::Down => Self::new(-rho * theta.sinh(), -rho * theta.cosh()),
132            HyperbolicSector::Diagonal(t) => {
133                if theta == T::infinity() {
134                    Self::new(t, t)
135                } else {
136                    // theta == T::neg_infinity()
137                    Self::new(t, -t)
138                }
139            }
140        }
141    }
142}
143
144impl<T: Copy + Float> Perplex<T> {
145    /// Creates a new `Perplex` number `z`  with a given hyperbolic angle `theta` such that `z= exp(h theta)`.
146    ///
147    /// It is used to create a `Perplex` number with a given phase, using hyperbolic cosine and sine.
148    #[inline]
149    pub fn cis(theta: T) -> Self {
150        Self::new(theta.cosh(), theta.sinh())
151    }
152
153    /// Calculates the hyperbolic argument of `self`.
154    ///
155    /// The argument is the angle in the hyperbolic plane from the positive time axis to the line
156    /// connecting the origin to `self`. It is defined by a piecewise function, with special cases
157    /// for light-like perplex numbers, whereby lines x=t and x=-t are mapped to ∞ and -∞, respectively, according to Sec. 4.1 in [New characterizations of the ring of the split-complex numbers and the field C of complex numbers and their comparative analyses](https://doi.org/10.48550/arXiv.2305.04586).
158    /// The formula is taken from Eq. 4.1.6 in Sec 4.1.1 `Hyperbolic Exponential Function and Hyperbolic Polar Transformation` in [The Mathematics of Minkowski Space-Time](https://doi.org/10.1007/978-3-7643-8614-6).
159    #[inline]
160    pub fn arg(self) -> T {
161        let Self { t, x } = self;
162        let (t_abs, x_abs) = (t.abs(), x.abs());
163        if t_abs == x_abs {
164            // self.is_light_like()
165            if t == x {
166                // line x = t
167                T::infinity()
168            } else {
169                // line x = -t
170                T::neg_infinity()
171            }
172        } else if t_abs > x_abs {
173            (x / t).atanh()
174        } else {
175            (t / x).atanh()
176        }
177    }
178
179    /// Calculate the Klein index of `self` for space- or time-like numbers. Returns `None` for light-like numbers.
180    ///
181    /// The Klein index is determined by the sector of the hyperbolic plane in which `self` resides.
182    /// Formula is taken from Tab. 1 and Appendix B in [Hyperbolic trigonometry in two-dimensional space-time geometry](https://doi.org/10.1393/ncb/i2003-10012-9).
183    #[inline]
184    pub fn klein(self) -> Option<Self> {
185        let Self { t, x } = self;
186        let (t_abs, x_abs) = (t.abs(), x.abs());
187        if t_abs == x_abs {
188            // light-like
189            None
190        } else if t_abs > x_abs {
191            if t > T::zero() {
192                // Right-Sector
193                Some(Self::one())
194            } else {
195                // Left-Sector
196                Some(-Self::one())
197            }
198        } else if x > T::zero() {
199            // Up-Sector
200            Some(Self::h())
201        } else {
202            // Down-Sector
203            Some(-Self::h())
204        }
205    }
206
207    /// Retrieves the hyperbolic sector of the perplex number.
208    ///
209    /// # Examples
210    ///
211    /// ```
212    /// use perplex_num::{Perplex, HyperbolicSector};
213    ///
214    /// let perplex = Perplex::new(1.0, 0.5);
215    /// assert_eq!(perplex.sector(), HyperbolicSector::Right);
216    /// ```
217    #[inline]
218    pub fn sector(&self) -> HyperbolicSector<T> {
219        (*self).into()
220    }
221
222    /// Retrieves the hyperbolic polar form from a perplex number.
223    ///
224    /// # Examples
225    ///
226    /// ```
227    /// use perplex_num::{Perplex, HyperbolicPolar};
228    ///
229    /// let perplex = Perplex::new(1.0, 0.5);
230    /// let polar = perplex.polar();
231    /// assert_eq!(polar.rho, perplex.norm());
232    /// assert_eq!(polar.theta, perplex.arg());
233    /// ```
234    #[inline]
235    pub fn polar(&self) -> HyperbolicPolar<T> {
236        (*self).into()
237    }
238}
239
240impl<T: Copy + Float> Pow<u32> for HyperbolicPolar<T> {
241    /// Raises `self` to the power of unsigned `exp`.
242    ///
243    /// This method is based on an extended version of Formula 4.6 in [New characterizations of the ring of the split-complex numbers and the field C of complex numbers and their comparative analyses](https://doi.org/10.48550/arXiv.2305.04586), ensuring consistency across the plane.
244    type Output = Self;
245    #[inline]
246    fn pow(self, exp: u32) -> Self::Output {
247        match exp {
248            0 => Self::default(),
249            1 => self,
250            _ => {
251                let n = exp as i32;
252                let Self { rho, theta, sector } = self;
253                if let HyperbolicSector::Diagonal(t) = sector {
254                    let t_new = t * (t + t).powi(n - 1); // t^n * 2^{n-1}
255                    HyperbolicPolar {
256                        rho,
257                        theta,
258                        sector: HyperbolicSector::Diagonal(t_new),
259                    }
260                } else {
261                    let new_sector = if n % 2 == 0 {
262                        HyperbolicSector::Right // since -1^2 = 1 and h^2=1
263                    } else {
264                        sector
265                    };
266                    HyperbolicPolar {
267                        rho: rho.powi(n), // Formula 4.6
268                        theta: T::from(n).unwrap() * theta,
269                        sector: new_sector,
270                    }
271                }
272            }
273        }
274    }
275}
276
277#[cfg(test)]
278mod tests {
279    use super::*;
280    use approx::assert_abs_diff_eq;
281    use num_traits::*;
282    #[test]
283    fn test_polar() {
284        assert_abs_diff_eq!(
285            Perplex::<f64>::default(),
286            Perplex::from(HyperbolicPolar::default()),
287            epsilon = 0.0001
288        );
289
290        let z = Perplex::new(1.0, 1.0); // Diagonal x=t
291        assert!(z.is_light_like(), "1 + h is light-like!");
292        assert_eq!(z.arg(), f64::infinity(), "Argument of 1 + h is infinity!");
293        assert!(
294            z.klein().is_none(),
295            "Klein is not defined for light-like numbers!"
296        );
297        assert_eq!(
298            HyperbolicPolar::from(z),
299            HyperbolicPolar {
300                rho: 0.0,
301                theta: f64::infinity(),
302                sector: HyperbolicSector::Diagonal(1.0)
303            },
304            "Polar form of 1 + h!"
305        );
306        assert_abs_diff_eq!(z, Perplex::from(HyperbolicPolar::from(z)), epsilon = 0.0001);
307
308        let z = Perplex::new(1.0, -1.0); // Diagonal x=-t
309        assert!(z.is_light_like(), "1 - h is light-like!");
310        assert_eq!(
311            z.arg(),
312            f64::neg_infinity(),
313            "Argument of 1 - h is  negative infinity!"
314        );
315        assert!(
316            z.klein().is_none(),
317            "Klein is not defined for light-like numbers!"
318        );
319        assert_eq!(
320            HyperbolicPolar::from(z),
321            HyperbolicPolar {
322                rho: 0.0,
323                theta: f64::neg_infinity(),
324                sector: HyperbolicSector::Diagonal(1.0)
325            },
326            "Polar form of 1 - h!"
327        );
328        assert_abs_diff_eq!(z, Perplex::from(HyperbolicPolar::from(z)), epsilon = 0.0001);
329
330        let z = Perplex::new(2.0, 1.0); // Right-Sector
331        assert!(z.is_time_like(), "2 + h is time-like!");
332        assert_ne!(z.arg(), 0.0, "2 + h has a non-zero argument!");
333        assert_eq!(
334            z.klein().unwrap(),
335            Perplex::new(1.0, 0.0),
336            "2 + h is in the right-sector!"
337        );
338        assert_abs_diff_eq!(z, Perplex::from(HyperbolicPolar::from(z)), epsilon = 0.0001);
339
340        let z = Perplex::new(-2.0, 1.0); // Left-Sector
341        assert!(z.is_time_like(), "-2 + h is time-like!");
342        assert_ne!(z.arg(), 0.0, "-2 + h has a non-zero argument!");
343        assert_eq!(
344            z.klein().unwrap(),
345            Perplex::new(-1.0, 0.0),
346            "-2 + h is in the left-sector!"
347        );
348        assert_abs_diff_eq!(z, Perplex::from(HyperbolicPolar::from(z)), epsilon = 0.0001);
349
350        let z = Perplex::new(1.0, 2.0); // Up-Sector
351        assert!(z.is_space_like(), "1 + 2h is space-like!");
352        assert_ne!(z.arg(), 0.0, "1 + 2h has a non-zero argument!");
353        assert_eq!(
354            z.klein().unwrap(),
355            Perplex::new(0.0, 1.0),
356            "1 + 2h is in the up-sector!"
357        );
358        assert_abs_diff_eq!(z, Perplex::from(HyperbolicPolar::from(z)), epsilon = 0.0001);
359
360        let z = Perplex::new(1.0, -2.0); // Down-Sector
361        assert!(z.is_space_like(), "1 - 2h is space-like!");
362        assert_ne!(z.arg(), 0.0, "1 - 2h has a non-zero argument!");
363        assert_eq!(
364            z.klein().unwrap(),
365            Perplex::new(0.0, -1.0),
366            "1 - 2h is in the down-sector!"
367        );
368        assert_abs_diff_eq!(z, Perplex::from(HyperbolicPolar::from(z)), epsilon = 0.0001);
369
370        let z = Perplex::cis(f64::PI() / 2.0);
371        assert_abs_diff_eq!(z, Perplex::from(HyperbolicPolar::from(z)), epsilon = 0.0001);
372    }
373
374    fn polar_mul_test_loop(z: Perplex<f64>) {
375        let polar = HyperbolicPolar::from(z);
376        assert_abs_diff_eq!(Perplex::default(), Perplex::from(polar.pow(0)));
377        assert_abs_diff_eq!(z, Perplex::from(polar.pow(1)), epsilon = 0.0001);
378        assert_abs_diff_eq!(z * z, Perplex::from(polar.pow(2)), epsilon = 0.0001);
379        assert_abs_diff_eq!(z * z * z, Perplex::from(polar.pow(3)), epsilon = 0.0001);
380        assert_abs_diff_eq!(z * z * z * z, Perplex::from(polar.pow(4)), epsilon = 0.0001);
381    }
382
383    #[test]
384    fn test_polar_multiplication() {
385        let z = Perplex::new(1.0, 1.0); // Diagonal x=t
386        polar_mul_test_loop(z);
387        let z = Perplex::new(1.0, -1.0); // Diagonal x=-t
388        polar_mul_test_loop(z);
389        let z = Perplex::new(2.0, 1.0); // Right-Sector
390        polar_mul_test_loop(z);
391        polar_mul_test_loop(z.inv().unwrap());
392        let z = Perplex::new(-2.0, 1.0); // Left-Sector
393        polar_mul_test_loop(z);
394        polar_mul_test_loop(z.inv().unwrap());
395        let z = Perplex::new(1.0, 2.0); // Up-Sector
396        polar_mul_test_loop(z);
397        polar_mul_test_loop(z.inv().unwrap());
398        let z = Perplex::new(1.0, -2.0); // Down-Sector
399        polar_mul_test_loop(z);
400        polar_mul_test_loop(z.inv().unwrap());
401        let z = Perplex::cis(f64::PI() / 2.0);
402        polar_mul_test_loop(z);
403        polar_mul_test_loop(z.inv().unwrap());
404    }
405    #[test]
406    fn test_polar_sector() {
407        let perplex = Perplex::new(1.0, 0.5);
408        assert_eq!(perplex.sector(), HyperbolicSector::Right);
409        let polar = perplex.polar();
410        assert_eq!(polar.rho, f64::sqrt(0.75));
411        assert_eq!(polar.theta, perplex.arg());
412        assert_eq!(polar.sector, HyperbolicSector::Right);
413    }
414}