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