Skip to main content

karpal_algebra/
module.rs

1use crate::abelian::AbelianGroup;
2use crate::ring::Ring;
3
4/// A module over a ring R — an abelian group with scalar multiplication.
5///
6/// Laws:
7/// - `a.scale(R::one()) == a`
8/// - `a.scale(r).scale(s) == a.scale(r.mul(s))`
9/// - `a.combine(b).scale(r) == a.scale(r).combine(b.scale(r))`
10/// - `a.scale(r.add(s)) == a.scale(r).combine(a.scale(s))`
11pub trait Module<R: Ring>: AbelianGroup {
12    fn scale(self, scalar: R) -> Self;
13}
14
15impl Module<f32> for f32 {
16    fn scale(self, scalar: f32) -> Self {
17        self * scalar
18    }
19}
20
21impl Module<f64> for f64 {
22    fn scale(self, scalar: f64) -> Self {
23        self * scalar
24    }
25}
26
27impl<F: crate::field::Field + AbelianGroup> Module<F> for (F, F) {
28    fn scale(self, scalar: F) -> Self {
29        (self.0.mul(scalar.clone()), self.1.mul(scalar))
30    }
31}
32
33#[cfg(test)]
34mod tests {
35    use super::*;
36    use crate::semiring::Semiring;
37    use karpal_core::Semigroup;
38
39    #[test]
40    fn f64_scale() {
41        assert!((3.0f64.scale(2.0) - 6.0).abs() < 1e-10);
42    }
43
44    #[test]
45    fn f64_scale_one() {
46        assert!((5.0f64.scale(f64::one()) - 5.0).abs() < 1e-10);
47    }
48
49    #[test]
50    fn tuple_scale() {
51        let v = (1.0f64, 2.0f64).scale(3.0);
52        assert!((v.0 - 3.0).abs() < 1e-10);
53        assert!((v.1 - 6.0).abs() < 1e-10);
54    }
55
56    #[test]
57    fn tuple_scale_distributes_over_add() {
58        let a = (1.0f64, 2.0);
59        let b = (3.0f64, 4.0);
60        let sum_scaled = a.combine(b).scale(2.0);
61        let scaled_sum = a.scale(2.0).combine(b.scale(2.0));
62        assert!((sum_scaled.0 - scaled_sum.0).abs() < 1e-10);
63        assert!((sum_scaled.1 - scaled_sum.1).abs() < 1e-10);
64    }
65}
66
67#[cfg(test)]
68mod law_tests {
69    use super::*;
70    use crate::semiring::Semiring;
71    use karpal_core::Semigroup;
72    use proptest::prelude::*;
73
74    proptest! {
75        #[test]
76        fn scale_one_identity(a in -100.0f64..100.0) {
77            prop_assert!((a.scale(f64::one()) - a).abs() < 1e-10);
78        }
79
80        #[test]
81        fn scale_compatibility(
82            a in -10.0f64..10.0,
83            r in -10.0f64..10.0,
84            s in -10.0f64..10.0
85        ) {
86            let left = a.scale(r).scale(s);
87            let right = a.scale(r.mul(s));
88            prop_assert!((left - right).abs() < 1e-6, "left={}, right={}", left, right);
89        }
90
91        #[test]
92        fn scale_distributes_over_group_add(
93            a in -10.0f64..10.0,
94            b in -10.0f64..10.0,
95            r in -10.0f64..10.0
96        ) {
97            let left = a.combine(b).scale(r);
98            let right = a.scale(r).combine(b.scale(r));
99            prop_assert!((left - right).abs() < 1e-6, "left={}, right={}", left, right);
100        }
101
102        #[test]
103        fn scale_distributes_over_scalar_add(
104            a in -10.0f64..10.0,
105            r in -10.0f64..10.0,
106            s in -10.0f64..10.0
107        ) {
108            let left = a.scale(r.add(s));
109            let right = a.scale(r).combine(a.scale(s));
110            prop_assert!((left - right).abs() < 1e-6, "left={}, right={}", left, right);
111        }
112    }
113}