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}