Skip to main content

karpal_algebra/
module.rs

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