Skip to main content

lie_groups/
rplus.rs

1//! ℝ⁺: The Positive Reals (Multiplicative Scaling Group)
2//!
3//! This module implements ℝ⁺, the group of positive real numbers under multiplication.
4//! ℝ⁺ is the symmetry group for **scaling transformations** in applications like:
5//! - Volatility surface analysis (IV scaling)
6//! - Image processing (contrast/brightness)
7//! - Economic growth models (multiplicative factors)
8//!
9//! # Mathematical Background
10//!
11//! ## Definition
12//!
13//! ```text
14//! ℝ⁺ = (0, ∞) with multiplication
15//! ```
16//!
17//! The positive reals form a Lie group under multiplication.
18//!
19//! ## Group Structure
20//!
21//! - **Multiplication**: a · b (standard multiplication)
22//! - **Identity**: 1
23//! - **Inverse**: a⁻¹ = 1/a
24//! - **Abelian**: a · b = b · a
25//!
26//! ## Lie Algebra
27//!
28//! ```text
29//! Lie(ℝ⁺) ≅ ℝ (the real line with addition)
30//! Exponential map: exp(x) = eˣ
31//! Logarithm: log(a) = ln(a)
32//! ```
33//!
34//! ## Topological Properties
35//!
36//! - **Non-compact**: Unlike U(1), ℝ⁺ extends to infinity
37//! - **Simply connected**: π₁(ℝ⁺) = 0 (no winding numbers)
38//! - **Contractible**: Homotopy equivalent to a point
39//!
40//! ## Isomorphism with (ℝ, +)
41//!
42//! The exponential map provides a Lie group isomorphism:
43//! ```text
44//! exp: (ℝ, +) → (ℝ⁺, ×)
45//! log: (ℝ⁺, ×) → (ℝ, +)
46//! ```
47//!
48//! ## Applications
49//!
50//! 1. **Volatility surfaces**: `IV(K,T) = λ · IV_eq(K,T)` models level shifts
51//! 2. **Scale invariance**: Physical systems with no preferred scale
52//! 3. **Log-returns**: `r = log(S_t/S_0)` lives in the Lie algebra
53
54use crate::{LieAlgebra, LieGroup};
55use num_complex::Complex;
56use std::fmt;
57use std::ops::{Add, Mul, MulAssign, Neg, Sub};
58
59// ============================================================================
60// Lie Algebra: ℝ (with addition)
61// ============================================================================
62
63/// Lie algebra of ℝ⁺, isomorphic to (ℝ, +)
64///
65/// Elements represent infinitesimal scaling factors. The exponential map
66/// converts these to finite scalings: exp(x) = eˣ.
67///
68/// # Examples
69///
70/// ```
71/// use lie_groups::rplus::RPlusAlgebra;
72/// use lie_groups::traits::LieAlgebra;
73///
74/// let v = RPlusAlgebra::from_components(&[0.5]);
75/// let w = v.scale(2.0);
76/// assert!((w.value() - 1.0).abs() < 1e-10);
77/// ```
78#[derive(Clone, Copy, Debug, PartialEq)]
79pub struct RPlusAlgebra(pub f64);
80
81impl Add for RPlusAlgebra {
82    type Output = Self;
83    fn add(self, rhs: Self) -> Self {
84        Self(self.0 + rhs.0)
85    }
86}
87
88impl Add<&RPlusAlgebra> for RPlusAlgebra {
89    type Output = RPlusAlgebra;
90    fn add(self, rhs: &RPlusAlgebra) -> RPlusAlgebra {
91        self + *rhs
92    }
93}
94
95impl Add<RPlusAlgebra> for &RPlusAlgebra {
96    type Output = RPlusAlgebra;
97    fn add(self, rhs: RPlusAlgebra) -> RPlusAlgebra {
98        *self + rhs
99    }
100}
101
102impl Add<&RPlusAlgebra> for &RPlusAlgebra {
103    type Output = RPlusAlgebra;
104    fn add(self, rhs: &RPlusAlgebra) -> RPlusAlgebra {
105        *self + *rhs
106    }
107}
108
109impl Sub for RPlusAlgebra {
110    type Output = Self;
111    fn sub(self, rhs: Self) -> Self {
112        Self(self.0 - rhs.0)
113    }
114}
115
116impl Neg for RPlusAlgebra {
117    type Output = Self;
118    fn neg(self) -> Self {
119        Self(-self.0)
120    }
121}
122
123impl Mul<f64> for RPlusAlgebra {
124    type Output = Self;
125    fn mul(self, scalar: f64) -> Self {
126        Self(self.0 * scalar)
127    }
128}
129
130impl Mul<RPlusAlgebra> for f64 {
131    type Output = RPlusAlgebra;
132    fn mul(self, rhs: RPlusAlgebra) -> RPlusAlgebra {
133        rhs * self
134    }
135}
136
137impl RPlusAlgebra {
138    /// Get the real value
139    #[must_use]
140    pub fn value(&self) -> f64 {
141        self.0
142    }
143}
144
145impl LieAlgebra for RPlusAlgebra {
146    const DIM: usize = 1;
147
148    fn zero() -> Self {
149        Self(0.0)
150    }
151
152    fn add(&self, other: &Self) -> Self {
153        Self(self.0 + other.0)
154    }
155
156    fn scale(&self, scalar: f64) -> Self {
157        Self(self.0 * scalar)
158    }
159
160    fn norm(&self) -> f64 {
161        self.0.abs()
162    }
163
164    fn basis_element(i: usize) -> Self {
165        assert_eq!(i, 0, "ℝ⁺ algebra is 1-dimensional");
166        Self(1.0)
167    }
168
169    fn from_components(components: &[f64]) -> Self {
170        assert_eq!(components.len(), 1, "ℝ⁺ algebra has dimension 1");
171        Self(components[0])
172    }
173
174    fn to_components(&self) -> Vec<f64> {
175        vec![self.0]
176    }
177
178    // ℝ⁺ is abelian: [X, Y] = 0 for all X, Y.
179    fn bracket(&self, _other: &Self) -> Self {
180        Self::zero()
181    }
182}
183
184// ============================================================================
185// Lie Group: ℝ⁺ (positive reals under multiplication)
186// ============================================================================
187
188/// An element of ℝ⁺, the multiplicative group of positive reals
189///
190/// Represented as a positive real number x > 0.
191///
192/// # Representation
193///
194/// We store the value directly (not logarithm) for intuitive interpretation.
195/// The logarithm is computed when needed for Lie algebra operations.
196///
197/// # Examples
198///
199/// ```
200/// use lie_groups::{LieGroup, RPlus};
201///
202/// // Create elements
203/// let g = RPlus::from_value(2.0);
204/// let h = RPlus::from_value(3.0);
205///
206/// // Group multiplication
207/// let product = g.compose(&h);
208/// assert!((product.value() - 6.0).abs() < 1e-10);
209///
210/// // Inverse
211/// let g_inv = g.inverse();
212/// assert!((g_inv.value() - 0.5).abs() < 1e-10);
213/// ```
214#[derive(Clone, Copy, Debug, PartialEq)]
215pub struct RPlus {
216    /// The positive real value x > 0
217    value: f64,
218}
219
220impl RPlus {
221    /// Create ℝ⁺ element from a positive real number
222    ///
223    /// # Panics
224    ///
225    /// Panics if `value <= 0`.
226    ///
227    /// # Examples
228    ///
229    /// ```
230    /// use lie_groups::RPlus;
231    ///
232    /// let g = RPlus::from_value(2.5);
233    /// assert!((g.value() - 2.5).abs() < 1e-10);
234    /// ```
235    #[must_use]
236    pub fn from_value(value: f64) -> Self {
237        assert!(value > 0.0, "ℝ⁺ elements must be positive, got {}", value);
238        Self { value }
239    }
240
241    /// Create ℝ⁺ element, clamping to positive range
242    ///
243    /// For robustness when value might be near zero or negative due to
244    /// numerical errors. Clamps to a small positive value.
245    ///
246    /// # Examples
247    ///
248    /// ```
249    /// use lie_groups::RPlus;
250    ///
251    /// let g = RPlus::from_value_clamped(-0.1);
252    /// assert!(g.value() > 0.0);
253    /// ```
254    #[must_use]
255    pub fn from_value_clamped(value: f64) -> Self {
256        Self {
257            value: value.max(1e-10),
258        }
259    }
260
261    /// Get the positive real value
262    #[must_use]
263    pub fn value(&self) -> f64 {
264        self.value
265    }
266
267    /// Create from logarithm (exponential map)
268    ///
269    /// Given x ∈ ℝ (Lie algebra), returns eˣ ∈ ℝ⁺.
270    ///
271    /// # Examples
272    ///
273    /// ```
274    /// use lie_groups::RPlus;
275    ///
276    /// let g = RPlus::from_log(0.0);  // e⁰ = 1
277    /// assert!((g.value() - 1.0).abs() < 1e-10);
278    ///
279    /// let h = RPlus::from_log(1.0);  // e¹ ≈ 2.718
280    /// assert!((h.value() - std::f64::consts::E).abs() < 1e-10);
281    /// ```
282    #[must_use]
283    pub fn from_log(log_value: f64) -> Self {
284        Self {
285            value: log_value.exp(),
286        }
287    }
288
289    /// Scaling perturbation for optimization
290    ///
291    /// Returns a small scaling factor for gradient descent updates.
292    ///
293    /// # Arguments
294    ///
295    /// * `magnitude` - Step size in log-space
296    #[must_use]
297    pub fn scaling(magnitude: f64) -> Self {
298        Self::from_log(magnitude)
299    }
300
301    /// Random ℝ⁺ element (log-normal distribution)
302    ///
303    /// Requires the `rand` feature (enabled by default).
304    /// Samples from log-normal with given mean and std in log-space.
305    ///
306    /// # Panics
307    ///
308    /// Panics if `log_std` is negative or NaN.
309    #[cfg(feature = "rand")]
310    #[must_use]
311    pub fn random<R: rand::Rng>(rng: &mut R, log_mean: f64, log_std: f64) -> Self {
312        use rand::distributions::Distribution;
313        use rand_distr::Normal;
314        let normal =
315            Normal::new(log_mean, log_std).expect("log_std must be non-negative and finite");
316        Self::from_log(normal.sample(rng))
317    }
318}
319
320impl fmt::Display for RPlusAlgebra {
321    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
322        write!(f, "r+({:.4})", self.0)
323    }
324}
325
326/// Group multiplication: g₁ · g₂ (real multiplication)
327impl Mul<&RPlus> for &RPlus {
328    type Output = RPlus;
329    fn mul(self, rhs: &RPlus) -> RPlus {
330        self.compose(rhs)
331    }
332}
333
334impl Mul<&RPlus> for RPlus {
335    type Output = RPlus;
336    fn mul(self, rhs: &RPlus) -> RPlus {
337        self.compose(rhs)
338    }
339}
340
341impl MulAssign<&RPlus> for RPlus {
342    fn mul_assign(&mut self, rhs: &RPlus) {
343        *self = self.compose(rhs);
344    }
345}
346
347impl LieGroup for RPlus {
348    const DIM: usize = 1;
349
350    type Algebra = RPlusAlgebra;
351
352    fn identity() -> Self {
353        Self { value: 1.0 }
354    }
355
356    fn compose(&self, other: &Self) -> Self {
357        Self {
358            value: self.value * other.value,
359        }
360    }
361
362    fn inverse(&self) -> Self {
363        Self {
364            value: 1.0 / self.value,
365        }
366    }
367
368    fn adjoint(&self) -> Self {
369        // Convention: adjoint() returns g⁻¹ for consistency with
370        // unitary matrix groups where g† = g⁻¹.
371        self.inverse()
372    }
373
374    fn adjoint_action(&self, algebra_element: &RPlusAlgebra) -> RPlusAlgebra {
375        // For abelian groups, Ad_g(X) = X
376        *algebra_element
377    }
378
379    fn distance_to_identity(&self) -> f64 {
380        // Distance in log-space: |log(x)|
381        self.value.ln().abs()
382    }
383
384    fn exp(tangent: &RPlusAlgebra) -> Self {
385        // exp: ℝ → ℝ⁺, exp(x) = eˣ
386        Self::from_log(tangent.0)
387    }
388
389    fn log(&self) -> crate::error::LogResult<RPlusAlgebra> {
390        // log: ℝ⁺ → ℝ, log(x) = ln(x)
391        //
392        // Unlike U(1), there's no branch cut ambiguity.
393        // The logarithm is single-valued on ℝ⁺.
394        Ok(RPlusAlgebra(self.value.ln()))
395    }
396
397    fn dim() -> usize {
398        1
399    }
400
401    fn trace(&self) -> Complex<f64> {
402        // "Trace" of 1×1 real matrix [x] = x
403        Complex::new(self.value, 0.0)
404    }
405}
406
407/// Display implementation
408impl std::fmt::Display for RPlus {
409    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
410        write!(f, "ℝ⁺({:.4})", self.value)
411    }
412}
413
414// ============================================================================
415// Mathematical Property Implementations
416// ============================================================================
417
418// Note: ℝ⁺ is NOT compact (extends to infinity), so we don't implement Compact.
419
420/// ℝ⁺ is abelian: a · b = b · a for all positive reals.
421impl crate::Abelian for RPlus {}
422
423// ============================================================================
424// Tests
425// ============================================================================
426
427#[cfg(test)]
428mod tests {
429    use super::*;
430
431    #[test]
432    fn test_identity() {
433        let e = RPlus::identity();
434        assert!((e.value() - 1.0).abs() < 1e-10);
435    }
436
437    #[test]
438    fn test_compose() {
439        let a = RPlus::from_value(2.0);
440        let b = RPlus::from_value(3.0);
441        let product = a.compose(&b);
442        assert!((product.value() - 6.0).abs() < 1e-10);
443    }
444
445    #[test]
446    fn test_inverse() {
447        let a = RPlus::from_value(4.0);
448        let a_inv = a.inverse();
449        assert!((a_inv.value() - 0.25).abs() < 1e-10);
450
451        // a * a⁻¹ = 1
452        let product = a.compose(&a_inv);
453        assert!((product.value() - 1.0).abs() < 1e-10);
454    }
455
456    #[test]
457    fn test_exp_log_roundtrip() {
458        let x = RPlusAlgebra(1.5);
459        let g = RPlus::exp(&x);
460        let x_back = g.log().unwrap();
461        assert!((x_back.value() - x.value()).abs() < 1e-10);
462    }
463
464    #[test]
465    fn test_log_exp_roundtrip() {
466        let g = RPlus::from_value(std::f64::consts::E);
467        let x = g.log().unwrap();
468        let g_back = RPlus::exp(&x);
469        assert!((g_back.value() - g.value()).abs() < 1e-10);
470    }
471
472    #[test]
473    fn test_distance_to_identity() {
474        let e = RPlus::identity();
475        assert!(e.distance_to_identity() < 1e-10);
476
477        let g = RPlus::from_value(std::f64::consts::E);
478        assert!((g.distance_to_identity() - 1.0).abs() < 1e-10);
479
480        // Symmetric: distance(2) = distance(0.5)
481        let a = RPlus::from_value(2.0);
482        let b = RPlus::from_value(0.5);
483        assert!((a.distance_to_identity() - b.distance_to_identity()).abs() < 1e-10);
484    }
485
486    #[test]
487    fn test_algebra_operations() {
488        let x = RPlusAlgebra(1.0);
489        let y = RPlusAlgebra(2.0);
490
491        let sum = x.add(&y);
492        assert!((sum.value() - 3.0).abs() < 1e-10);
493
494        let scaled = x.scale(3.0);
495        assert!((scaled.value() - 3.0).abs() < 1e-10);
496
497        let zero = RPlusAlgebra::zero();
498        assert!(zero.value().abs() < 1e-10);
499    }
500
501    #[test]
502    fn test_abelian_property() {
503        let a = RPlus::from_value(2.0);
504        let b = RPlus::from_value(3.0);
505
506        let ab = a.compose(&b);
507        let ba = b.compose(&a);
508
509        assert!((ab.value() - ba.value()).abs() < 1e-10);
510    }
511
512    #[test]
513    fn test_from_value_clamped() {
514        // Negative values get clamped to positive
515        let g = RPlus::from_value_clamped(-0.5);
516        assert!(g.value() > 0.0);
517        assert!(g.value() >= 1e-10);
518
519        // Zero gets clamped
520        let h = RPlus::from_value_clamped(0.0);
521        assert!(h.value() > 0.0);
522
523        // Positive values pass through unchanged
524        let k = RPlus::from_value_clamped(2.5);
525        assert!((k.value() - 2.5).abs() < 1e-10);
526    }
527
528    #[test]
529    fn test_scaling() {
530        // Scaling with magnitude 0 gives identity
531        let s0 = RPlus::scaling(0.0);
532        assert!((s0.value() - 1.0).abs() < 1e-10);
533
534        // Scaling with positive magnitude gives e^mag
535        let s1 = RPlus::scaling(1.0);
536        assert!((s1.value() - std::f64::consts::E).abs() < 1e-10);
537
538        // Negative magnitude gives shrinking
539        let sm1 = RPlus::scaling(-1.0);
540        assert!((sm1.value() - 1.0 / std::f64::consts::E).abs() < 1e-10);
541    }
542
543    #[test]
544    #[cfg(feature = "rand")]
545    fn test_random() {
546        use rand::SeedableRng;
547        let mut rng = rand::rngs::StdRng::seed_from_u64(42);
548
549        // Random samples should be positive
550        for _ in 0..100 {
551            let g = RPlus::random(&mut rng, 0.0, 1.0);
552            assert!(g.value() > 0.0);
553        }
554
555        // Mean of log should be approximately log_mean
556        let mut log_sum = 0.0;
557        let n = 1000;
558        for _ in 0..n {
559            let g = RPlus::random(&mut rng, 0.5, 0.1);
560            log_sum += g.value().ln();
561        }
562        let log_mean = log_sum / n as f64;
563        assert!(
564            (log_mean - 0.5).abs() < 0.1,
565            "Log mean should be approximately 0.5"
566        );
567    }
568
569    #[test]
570    fn test_adjoint() {
571        // For abelian groups, adjoint = inverse
572        let g = RPlus::from_value(3.0);
573        let adj = g.adjoint();
574        assert!(
575            (adj.value() - 1.0 / 3.0).abs() < 1e-10,
576            "Adjoint should equal inverse"
577        );
578    }
579
580    #[test]
581    fn test_adjoint_action() {
582        // For abelian groups, Ad_g(X) = X
583        let g = RPlus::from_value(5.0);
584        let x = RPlusAlgebra(2.5);
585        let result = g.adjoint_action(&x);
586        assert!((result.value() - x.value()).abs() < 1e-10);
587    }
588
589    #[test]
590    fn test_trace() {
591        let g = RPlus::from_value(7.0);
592        let tr = g.trace();
593        assert!((tr.re - 7.0).abs() < 1e-10);
594        assert!(tr.im.abs() < 1e-10);
595    }
596
597    #[test]
598    fn test_display() {
599        let g = RPlus::from_value(2.5);
600        let s = format!("{}", g);
601        assert!(s.contains("2.5"));
602        assert!(s.contains("ℝ⁺"));
603    }
604
605    #[test]
606    fn test_algebra_dim() {
607        assert_eq!(RPlusAlgebra::dim(), 1);
608    }
609
610    #[test]
611    fn test_algebra_basis_element() {
612        let basis = RPlusAlgebra::basis_element(0);
613        assert!((basis.value() - 1.0).abs() < 1e-10);
614    }
615
616    #[test]
617    #[should_panic(expected = "1-dimensional")]
618    fn test_algebra_basis_element_out_of_bounds() {
619        let _ = RPlusAlgebra::basis_element(1);
620    }
621
622    #[test]
623    fn test_algebra_from_to_components() {
624        let x = RPlusAlgebra::from_components(&[3.5]);
625        assert!((x.value() - 3.5).abs() < 1e-10);
626
627        let comps = x.to_components();
628        assert_eq!(comps.len(), 1);
629        assert!((comps[0] - 3.5).abs() < 1e-10);
630    }
631
632    #[test]
633    #[should_panic(expected = "dimension 1")]
634    fn test_algebra_from_components_wrong_dim() {
635        let _ = RPlusAlgebra::from_components(&[1.0, 2.0]);
636    }
637
638    #[test]
639    fn test_algebra_norm() {
640        let x = RPlusAlgebra(-3.0);
641        assert!((x.norm() - 3.0).abs() < 1e-10);
642
643        let y = RPlusAlgebra(2.5);
644        assert!((y.norm() - 2.5).abs() < 1e-10);
645    }
646
647    #[test]
648    fn test_from_log() {
649        // from_log(0) = e^0 = 1
650        let g = RPlus::from_log(0.0);
651        assert!((g.value() - 1.0).abs() < 1e-10);
652
653        // from_log(1) = e^1 = e
654        let h = RPlus::from_log(1.0);
655        assert!((h.value() - std::f64::consts::E).abs() < 1e-10);
656
657        // from_log(-1) = e^{-1} = 1/e
658        let k = RPlus::from_log(-1.0);
659        assert!((k.value() - 1.0 / std::f64::consts::E).abs() < 1e-10);
660    }
661
662    #[test]
663    fn test_group_dim() {
664        assert_eq!(RPlus::dim(), 1);
665    }
666
667    #[test]
668    #[should_panic(expected = "positive")]
669    fn test_from_value_panics_on_zero() {
670        let _ = RPlus::from_value(0.0);
671    }
672
673    #[test]
674    #[should_panic(expected = "positive")]
675    fn test_from_value_panics_on_negative() {
676        let _ = RPlus::from_value(-1.0);
677    }
678}