Skip to main content

yui_core/conc/lc/
lc.rs

1//! Linear combination: a finite formal sum `Σ rᵢ · xᵢ` with `xᵢ` keys and
2//! `rᵢ` coefficients in a ring `R`.
3//!
4//! Implements the [free `R`-module](crate::RMod) over the key set, i.e. the
5//! polynomial ring viewpoint without any multiplicative structure on keys.
6//!
7//! Internally stored via [`super::lc_data::LcData`], which specializes the
8//! empty and single-term cases (the common shape in the cobordism algebra
9//! hot path of `yui-kh`) so that those paths avoid the per-entry hashmap
10//! allocation. The struct [`Lc`] additionally caches an `R::zero()` so that
11//! [`Lc::coeff`] can return a stable `&R` for missing keys.
12//!
13//! See: <https://en.wikipedia.org/wiki/Linear_combination>,
14//! <https://en.wikipedia.org/wiki/Free_module>
15
16use std::collections::HashMap;
17use std::fmt::{Display, Debug};
18use std::ops::{Add, AddAssign, Neg, Sub, SubAssign, Mul, MulAssign};
19use itertools::Itertools;
20use num_traits::Zero;
21use auto_impl_ops::auto_ops;
22use crate::abst::{MathType, AddMon, AddMonOps, AddGrp, AddGrpOps, Ring, RingOps, RMod, RModOps};
23
24use super::lc_key::*;
25use super::lc_data::{LcData, LcDataIter, LcDataIntoIter};
26
27/// A linear combination `Σ rᵢ · xᵢ` with keys `X: LcKey` and coefficients in a
28/// ring `R`. Stored via a private `LcData`, which specializes the empty and
29/// single-term cases to avoid hashmap allocation.
30#[derive(PartialEq, Eq, Clone, Default, Debug)]
31#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
32#[cfg_attr(feature = "serde", serde(transparent))]
33pub struct Lc<X, R>
34where
35    X: LcKey,
36    R: Ring, for<'x> &'x R: RingOps<R>
37{
38    data: LcData<X, R>,
39    #[cfg_attr(feature = "serde", serde(skip))]
40    r_zero: R
41}
42
43impl<X, R> Lc<X, R>
44where
45    X: LcKey,
46    R: Ring, for<'x> &'x R: RingOps<R>
47{
48    pub fn new() -> Self {
49        Self { data: LcData::Zero, r_zero: R::zero() }
50    }
51
52    pub fn nterms(&self) -> usize {
53        self.data.len()
54    }
55
56    pub fn any_term(&self) -> Option<(&X, &R)> {
57        self.iter().next()
58    }
59
60    pub fn keys(&self) -> impl Iterator<Item = &X> {
61        self.iter().map(|(k, _)| k)
62    }
63
64    pub fn is_singleton(&self) -> bool {
65        self.nterms() == 1 &&
66        self.iter().next().unwrap().1.is_one()
67    }
68
69    pub fn as_singleton(&self) -> Option<X> {
70        if !self.is_singleton() {
71            None?
72        }
73        self.iter().next().map(|(x, _)| x.clone())
74    }
75
76    pub fn coeff(&self, x: &X) -> &R {
77        self.data.get(x).unwrap_or(&self.r_zero)
78    }
79
80    pub fn iter(&self) -> LcDataIter<'_, X, R> {
81        self.data.iter()
82    }
83
84    pub fn map<Y, S, F>(self, f: F) -> Lc<Y, S>
85    where
86        Y: LcKey,
87        S: Ring, for<'x> &'x S: RingOps<S>,
88        F: Fn(X, R) -> (Y, S)
89    {
90        self.into_iter().map(|(x, r)| f(x, r)).collect()
91    }
92
93    pub fn map_coeffs<S, F>(self, f: F) -> Lc<X, S>
94    where
95        S: Ring, for<'x> &'x S: RingOps<S>,
96        F: Fn(R) -> S
97    {
98        self.map(|x, r| (x, f(r)))
99    }
100
101    pub fn map_keys<Y, F>(self, f: F) -> Lc<Y, R>
102    where
103        Y: LcKey,
104        F: Fn(X) -> Y
105    {
106        self.map(|x, r| (f(x), r))
107    }
108
109    pub fn map_ref<Y, S, F>(&self, f: F) -> Lc<Y, S>
110    where
111        Y: LcKey,
112        S: Ring, for<'x> &'x S: RingOps<S>,
113        F: Fn(&X, &R) -> (Y, S)
114    {
115        self.iter().map(|(x, r)| f(x, r)).collect()
116    }
117
118    pub fn filter<F>(self, f: F) -> Self
119    where F: Fn(&X) -> bool {
120        self.into_iter().filter(|(x, _)| f(x)).collect()
121    }
122
123    pub fn filtered<F>(&self, f: F) -> Self
124    where F: Fn(&X) -> bool {
125        self.iter().filter_map(|(x, a)|
126            if f(x) {
127                Some((x.clone(), a.clone()))
128            } else {
129                None
130            }
131        ).collect()
132    }
133
134    /// Add all pairs at once. Terms may cancel along the way, so the reduced form is
135    /// restored once at the end — prefer this over repeated `add_pair` in a hot loop.
136    pub fn add_pairs<I>(&mut self, pairs: I)
137    where I: IntoIterator<Item = (X, R)> {
138        for (x, r) in pairs {
139            self.data.add_pair_unreduced(x, r);
140        }
141        self.data.reduce();
142    }
143
144    /// Same, taking each key by reference — the coefficient is generally the cheaper
145    /// of the two to clone (compare a `Cob` key in `yui-kh`), so it is passed by value.
146    pub fn add_pairs_ref<'a, I>(&mut self, pairs: I)
147    where I: IntoIterator<Item = (&'a X, R)>, X: 'a {
148        for (x, r) in pairs {
149            self.data.add_pair_ref_unreduced(x, r);
150        }
151        self.data.reduce();
152    }
153
154    pub fn add_pair(&mut self, rhs: (X, R)) {
155        self.add_pairs([rhs]);
156    }
157
158    pub fn add_pair_ref(&mut self, rhs: (&X, R)) {
159        self.add_pairs_ref([rhs]);
160    }
161
162    pub fn apply<F, Y: LcKey>(&self, f: F) -> Lc<Y, R>
163    where F: Fn(&X) -> Lc<Y, R> {
164        self.iter().flat_map(|(x, r)| {
165            f(x).into_iter().map(move |(y, s)| {
166                (y, r * &s)
167            })
168        }).collect()
169    }
170
171    pub fn apply_bilin<Y, Z, F>(&self, other: &Lc<Y, R>, x_map: F) -> Lc<Z, R>
172    where Y: LcKey, Z: LcKey, F: Fn(&X, &Y) -> Z {
173        match (&self.data, &other.data) {
174            (LcData::Zero, _) | (_, LcData::Zero) => Lc::zero(),
175            (LcData::Single(x, r), _) =>
176                other.map_ref(|y, s| (x_map(x, y), r * s)),
177            (_, LcData::Single(y, s)) =>
178                self.map_ref(|x, r| (x_map(x, y), r * s)),
179            (LcData::Many(_), LcData::Many(_)) => {
180                let x_map = &x_map;
181                let mut res = Lc::zero();
182                res.add_pairs(self.iter().flat_map(|(x, r)|
183                    other.iter().map(move |(y, s)| (x_map(x, y), r * s))
184                ));
185                res
186            }
187        }
188    }
189
190    pub fn sort_terms_by<F>(&self, cmp: F) -> impl Iterator<Item = (&X, &R)>
191    where F: Fn(&X, &X) -> std::cmp::Ordering {
192        self.iter().sorted_by(|(x, _), (y, _)| cmp(x, y))
193    }
194
195    pub fn to_string_by<F>(&self, cmp: F, descending: bool) -> String
196    where F: Fn(&X, &X) -> std::cmp::Ordering {
197        use crate::util::format::lc;
198        if descending {
199            lc( self.sort_terms_by(|x, y| cmp(x, y).reverse()) )
200        } else {
201            lc( self.sort_terms_by(cmp) )
202        }
203    }
204
205    pub fn is_homogeneous<T, F>(&self, f: F) -> bool
206    where T: PartialEq, F: Fn(&X) -> T {
207        self.keys().map(f).all_equal()
208    }
209
210    pub fn homogeneous_value<T, F>(&self, f: F) -> Option<T>
211    where T: PartialEq, F: Fn(&X) -> T {
212        let mut iter = self.keys();
213        let first = f(iter.next()?);
214        if iter.all(|k| f(k) == first) { Some(first) } else { None }
215    }
216}
217
218impl<X, R> From<X> for Lc<X, R>
219where
220    X: LcKey,
221    R: Ring, for<'x> &'x R: RingOps<R>
222{
223    fn from(x: X) -> Self {
224        Self::from((x, R::one()))
225    }
226}
227
228impl<X, R> From<(X, R)> for Lc<X, R>
229where
230    X: LcKey,
231    R: Ring, for<'x> &'x R: RingOps<R>
232{
233    fn from(value: (X, R)) -> Self {
234        Self::from_iter([value])
235    }
236}
237
238impl<X, R> From<HashMap<X, R>> for Lc<X, R>
239where
240    X: LcKey,
241    R: Ring, for<'x> &'x R: RingOps<R>
242{
243    fn from(value: HashMap<X, R>) -> Self {
244        Self::from_iter(value)
245    }
246}
247
248impl<X, R> FromIterator<(X, R)> for Lc<X, R>
249where
250    X: LcKey,
251    R: Ring, for<'x> &'x R: RingOps<R>
252{
253    fn from_iter<T: IntoIterator<Item = (X, R)>>(iter: T) -> Self {
254        let mut res = Self::new();
255        res.add_pairs(iter);
256        res
257    }
258}
259
260impl<X, R> IntoIterator for Lc<X, R>
261where
262    X: LcKey,
263    R: Ring, for<'x> &'x R: RingOps<R>
264{
265    type Item = (X, R);
266    type IntoIter = LcDataIntoIter<X, R>;
267
268    fn into_iter(self) -> Self::IntoIter {
269        self.data.into_iter()
270    }
271}
272
273impl<X, R> Display for Lc<X, R>
274where
275    X: LcKey,
276    R: Ring, for<'x> &'x R: RingOps<R>
277{
278    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
279        f.write_str(&self.to_string_by(X::cmp, false))
280    }
281}
282
283impl<X, R> Zero for Lc<X, R>
284where
285    X: LcKey,
286    R: Ring, for<'x> &'x R: RingOps<R>
287{
288    fn zero() -> Self {
289        Self::new()
290    }
291
292    fn is_zero(&self) -> bool {
293        self.data.is_empty()
294    }
295}
296
297impl<X, R> Neg for Lc<X, R>
298where
299    X: LcKey,
300    R: Ring, for<'x> &'x R: RingOps<R>
301{
302    type Output = Self;
303
304    fn neg(self) -> Self::Output {
305        self.map_coeffs(|r| -r)
306    }
307}
308
309impl<X, R> Neg for &Lc<X, R>
310where
311    X: LcKey,
312    R: Ring, for<'x> &'x R: RingOps<R>
313{
314    type Output = Lc<X, R>;
315
316    fn neg(self) -> Self::Output {
317        self.map_ref(|x, r| (x.clone(), -r))
318    }
319}
320
321// Neither form clones a key that is already present. The split `auto_ops` arg-sets
322// generate the four `Add` variants by rhs-ownership so the two impls don't collide.
323// note: the arg-set form `auto_ops(val_val, ref_val)` is an undocumented API of `auto_impl_ops`.
324#[auto_ops(val_val, ref_val)]
325impl<X, R> AddAssign<Lc<X, R>> for Lc<X, R>
326where
327    X: LcKey,
328    R: Ring, for<'x> &'x R: RingOps<R>
329{
330    fn add_assign(&mut self, rhs: Self) {
331        self.add_pairs(rhs.data);
332    }
333}
334
335#[auto_ops(val_ref, ref_ref)]
336impl<X, R> AddAssign<&Lc<X, R>> for Lc<X, R>
337where
338    X: LcKey,
339    R: Ring, for<'x> &'x R: RingOps<R>
340{
341    fn add_assign(&mut self, rhs: &Self) {
342        self.add_pairs_ref(rhs.data.iter().map(|(x, r)| (x, r.clone())));
343    }
344}
345
346#[auto_ops(val_val, ref_val)]
347impl<X, R> SubAssign<Lc<X, R>> for Lc<X, R>
348where
349    X: LcKey,
350    R: Ring, for<'x> &'x R: RingOps<R>
351{
352    fn sub_assign(&mut self, rhs: Self) {
353        self.add_pairs(rhs.data.into_iter().map(|(x, r)| (x, -r)));
354    }
355}
356
357#[auto_ops(val_ref, ref_ref)]
358impl<X, R> SubAssign<&Lc<X, R>> for Lc<X, R>
359where
360    X: LcKey,
361    R: Ring, for<'x> &'x R: RingOps<R>
362{
363    fn sub_assign(&mut self, rhs: &Self) {
364        self.add_pairs_ref(rhs.data.iter().map(|(x, r)| (x, -r)));
365    }
366}
367
368#[auto_ops]
369impl<X, R> MulAssign<&R> for Lc<X, R>
370where
371    X: LcKey,
372    R: Ring, for<'x> &'x R: RingOps<R>
373{
374    fn mul_assign(&mut self, rhs: &R) {
375        if rhs.is_one() {
376            return
377        }
378
379        self.data.map_coeffs_in_place(|r| r * rhs);
380    }
381}
382
383#[auto_ops]
384impl<X, R> Mul for &Lc<X, R>
385where
386    X: LcMulKey,
387    R: Ring, for<'x> &'x R: RingOps<R>
388{
389    type Output = Lc<X, R>;
390
391    fn mul(self, rhs: Self) -> Self::Output {
392        self.apply_bilin(rhs, |x, y| x.mul_ref(y))
393    }
394}
395
396macro_rules! impl_alg_ops {
397    ($trait:ident) => {
398        impl<X, R> $trait<Self> for Lc<X, R>
399        where X: LcKey, R: Ring, for<'x> &'x R: RingOps<R> {}
400
401        impl<X, R> $trait<Lc<X, R>> for &Lc<X, R>
402        where X: LcKey, R: Ring, for<'x> &'x R: RingOps<R> {}
403    };
404}
405
406impl_alg_ops!(AddMonOps);
407impl_alg_ops!(AddGrpOps);
408
409impl<X, R> MathType for Lc<X, R>
410where
411    X: LcKey,
412    R: Ring, for<'x> &'x R: RingOps<R>
413{
414    fn math_symbol() -> String {
415        format!("{}<{}>", R::math_symbol(), X::math_symbol())
416    }
417}
418
419impl<X, R> AddMon for Lc<X, R>
420where
421    X: LcKey,
422    R: Ring, for<'x> &'x R: RingOps<R>
423{}
424
425impl<X, R> AddGrp for Lc<X, R>
426where
427    X: LcKey,
428    R: Ring, for<'x> &'x R: RingOps<R>
429{}
430
431
432impl<X, R> RModOps<R, Self> for Lc<X, R>
433where
434    X: LcKey,
435    R: Ring, for<'x> &'x R: RingOps<R>
436{}
437
438impl<X, R> RModOps<R, Lc<X, R>> for &Lc<X, R>
439where
440    X: LcKey,
441    R: Ring, for<'x> &'x R: RingOps<R>
442{}
443
444impl<X, R> RMod for Lc<X, R>
445where
446    X: LcKey,
447    R: Ring, for<'x> &'x R: RingOps<R>
448{
449    type R = R;
450}
451
452#[cfg(test)]
453mod tests {
454    use num_traits::Zero;
455    use maplit::hashmap;
456    use crate::abst::{MathType, AddMon};
457    use crate::lc::{AsKey, Lc};
458
459    type X = AsKey<i32>;
460    fn e(i: i32) -> X {
461        X::from(i)
462    }
463
464    #[test]
465    fn math_symbol() {
466        type L = Lc<X, i32>;
467        let symbol = L::math_symbol();
468        assert_eq!(symbol, "Z<Free<i32>>");
469    }
470
471    #[test]
472    fn fmt() {
473        type L = Lc<X, i32>;
474
475        let z = L::from(hashmap!{ e(1) => 1 });
476        assert_eq!(z.to_string(), "<1>");
477
478        let z = L::from(hashmap!{ e(1) => -1 });
479        assert_eq!(z.to_string(), "-<1>");
480
481        let z = L::from(hashmap!{ e(1) => 2 });
482        assert_eq!(z.to_string(), "2<1>");
483
484        let z = L::from(hashmap!{ e(1) => 1, e(2) => 1 });
485        assert_eq!(z.to_string(), "<1> + <2>");
486
487        let z = L::from(hashmap!{ e(1) => -1, e(2) => -1 });
488        assert_eq!(z.to_string(), "-<1> - <2>");
489
490        let z = L::from(hashmap!{ e(1) => 2, e(2) => 3 });
491        assert_eq!(z.to_string(), "2<1> + 3<2>");
492
493        let z = L::from(hashmap!{ e(1) => -2, e(2) => -3 });
494        assert_eq!(z.to_string(), "-2<1> - 3<2>");
495    }
496
497    #[test]
498    fn default() {
499        type L = Lc<X, i32>;
500        let z = L::default();
501        assert!(z.data.is_empty());
502    }
503
504    #[test]
505    fn from_singleton() {
506        type L = Lc<X, i32>;
507        let x = e(0);
508        let z = L::from(x);
509        assert_eq!(z, L::from(hashmap!{ e(0) => 1 }));
510    }
511
512    #[test]
513    fn from_pair() {
514        type L = Lc<X, i32>;
515        let x = e(0);
516        let z = L::from((x, 2));
517        assert_eq!(z, L::from(hashmap!{ e(0) => 2 }));
518    }
519
520    #[test]
521    fn from_iter() {
522        type L = Lc<X, i32>;
523        let z = L::from_iter([(e(0), 1), (e(1), 0), (e(2), 2)]);
524
525        assert!(!z.is_zero());
526        assert_eq!(z.nterms(), 2);
527        assert_eq!(z.coeff(&e(0)), &1);
528        assert_eq!(z.coeff(&e(2)), &2);
529    }
530
531    #[test]
532    fn into_singleton() {
533        type L = Lc<X, i32>;
534        let z = L::from(e(0));
535
536        assert!(z.is_singleton());
537        assert_eq!(z.as_singleton(), Some(e(0)));
538
539        let z = L::from((e(0), 2));
540        assert!(!z.is_singleton());
541        assert_eq!(z.as_singleton(), None);
542
543        let z = L::from_iter([(e(0), 1), (e(1), 1)]);
544        assert!(!z.is_singleton());
545        assert_eq!(z.as_singleton(), None);
546    }
547
548    #[test]
549    fn eq() {
550        type L = Lc<X, i32>;
551        let z1 = L::from(hashmap!{ e(1) => 1, e(2) => 2 });
552        let z2 = L::from(hashmap!{ e(2) => 2, e(1) => 1 });
553        let z3 = L::from(hashmap!{ e(1) => 1 });
554
555        assert_eq!(z1, z2);
556        assert_ne!(z1, z3);
557    }
558
559    #[test]
560    fn zero() {
561        type L = Lc<X, i32>;
562        let z = L::zero();
563
564        assert!(z.data.is_empty());
565        assert!(z.is_zero());
566
567        let z = L::from(hashmap!{ e(1) => 1 });
568
569        assert!(!z.data.is_empty());
570        assert!(!z.is_zero());
571    }
572
573    #[test]
574    fn add_pair_reduced() {
575        type L = Lc<X, i32>;
576
577        // cancelled terms must be gone the moment `add_pair` returns
578        let mut z = L::from(hashmap!{ e(1) => 1, e(2) => 2, e(3) => 1 });
579        z.add_pair((e(1), -1));
580        assert_eq!(z, L::from(hashmap!{ e(2) => 2, e(3) => 1 }));
581        assert_eq!(z.nterms(), 2);
582
583        z.add_pair((e(2), -1));
584        z.add_pair((e(3), -1));
585        assert_eq!(z, L::from(hashmap!{ e(2) => 1 }));
586        assert_eq!(z.nterms(), 1);
587    }
588
589    #[test]
590    fn add_pairs_reduced() {
591        type L = Lc<X, i32>;
592
593        let mut z = L::from(hashmap!{ e(1) => 1, e(2) => 2, e(3) => 1 });
594        z.add_pairs([(e(1), -1), (e(2), -1), (e(3), -1)]);
595
596        assert_eq!(z, L::from(hashmap!{ e(2) => 1 }));
597        assert_eq!(z.nterms(), 1);
598
599        z.add_pairs([(e(2), -1)]);
600        assert!(z.is_zero());
601        assert_eq!(z.nterms(), 0);
602    }
603
604    #[test]
605    fn add() {
606        type L = Lc<X, i32>;
607        let z1 = L::from(hashmap!{ e(1) => 1, e(2) => 2 });
608        let z2 = L::from(hashmap!{ e(2) => 20, e(3) => 30 });
609        let w = z1 + z2;
610
611        assert_eq!(w, L::from(hashmap!{ e(1) => 1, e(2) => 22, e(3) => 30 }));
612    }
613
614    #[test]
615    fn add_ref() {
616        type L = Lc<X, i32>;
617        let z1 = L::from(hashmap!{ e(1) => 1, e(2) => 2 });
618        let z2 = L::from(hashmap!{ e(2) => 20, e(3) => 30 });
619        let w = &z1 + &z2;
620
621        assert_eq!(w, L::from(hashmap!{ e(1) => 1, e(2) => 22, e(3) => 30 }));
622    }
623
624    #[test]
625    fn add_assign() {
626        type L = Lc<X, i32>;
627        let mut z1 = L::from(hashmap!{ e(1) => 1, e(2) => 2 });
628        let z2 = L::from(hashmap!{ e(2) => 20, e(3) => 30 });
629        z1 += z2;
630
631        assert_eq!(z1, L::from(hashmap!{ e(1) => 1, e(2) => 22, e(3) => 30 }));
632    }
633
634    #[test]
635    fn add_assign_ref() {
636        type L = Lc<X, i32>;
637        let mut z1 = L::from(hashmap!{ e(1) => 1, e(2) => 2 });
638        let z2 = L::from(hashmap!{ e(2) => 20, e(3) => 30 });
639        z1 += &z2;
640
641        assert_eq!(z1, L::from(hashmap!{ e(1) => 1, e(2) => 22, e(3) => 30 }));
642    }
643
644    #[test]
645    fn sum() {
646        type L = Lc<X, i32>;
647        let z1 = L::from(hashmap!{ e(1) => 1, e(2) => 2 });
648        let z2 = L::from(hashmap!{ e(2) => 20, e(3) => 30 });
649        let z3 = L::from(hashmap!{ e(3) => 300, e(4) => 400 });
650        let w  = L::sum([z1, z2, z3]);
651
652        assert_eq!(w, L::from(hashmap!{ e(1) => 1, e(2) => 22, e(3) => 330, e(4) => 400 }));
653    }
654
655    #[test]
656    fn sum_ref() {
657        type L = Lc<X, i32>;
658        let z1 = L::from(hashmap!{ e(1) => 1, e(2) => 2 });
659        let z2 = L::from(hashmap!{ e(2) => 20, e(3) => 30 });
660        let z3 = L::from(hashmap!{ e(3) => 300, e(4) => 400 });
661        let w  = L::sum([&z1, &z2, &z3]);
662
663        assert_eq!(w, L::from(hashmap!{ e(1) => 1, e(2) => 22, e(3) => 330, e(4) => 400 }));
664    }
665
666    #[test]
667    fn neg() {
668        type L = Lc<X, i32>;
669        let z = L::from(hashmap!{ e(1) => 1, e(2) => 2 });
670        assert_eq!(-z, L::from(hashmap!{ e(1) => -1, e(2) => -2 }));
671    }
672
673    #[test]
674    fn neg_ref() {
675        type L = Lc<X, i32>;
676        let z = L::from(hashmap!{ e(1) => 1, e(2) => 2 });
677        assert_eq!(-(&z), L::from(hashmap!{ e(1) => -1, e(2) => -2 }));
678    }
679
680    #[test]
681    fn sub() {
682        type L = Lc<X, i32>;
683        let z1 = L::from(hashmap!{ e(1) => 1, e(2) => 2 });
684        let z2 = L::from(hashmap!{ e(2) => 20, e(3) => 30 });
685        let w = z1 - z2;
686
687        assert_eq!(w, L::from(hashmap!{ e(1) => 1, e(2) => -18, e(3) => -30 }));
688    }
689
690    #[test]
691    fn sub_ref() {
692        type L = Lc<X, i32>;
693        let z1 = L::from(hashmap!{ e(1) => 1, e(2) => 2 });
694        let z2 = L::from(hashmap!{ e(2) => 20, e(3) => 30 });
695        let w = &z1 - &z2;
696
697        assert_eq!(w, L::from(hashmap!{ e(1) => 1, e(2) => -18, e(3) => -30 }));
698    }
699
700    #[test]
701    fn sub_assign() {
702        type L = Lc<X, i32>;
703        let mut z1 = L::from(hashmap!{ e(1) => 1, e(2) => 2 });
704        let z2 = L::from(hashmap!{ e(2) => 20, e(3) => 30 });
705        z1 -= z2;
706
707        assert_eq!(z1, L::from(hashmap!{ e(1) => 1, e(2) => -18, e(3) => -30 }));
708    }
709
710    #[test]
711    fn sub_assign_ref() {
712        type L = Lc<X, i32>;
713        let mut z1 = L::from(hashmap!{ e(1) => 1, e(2) => 2 });
714        let z2 = L::from(hashmap!{ e(2) => 20, e(3) => 30 });
715        z1 -= &z2;
716
717        assert_eq!(z1, L::from(hashmap!{ e(1) => 1, e(2) => -18, e(3) => -30 }));
718    }
719
720    // The owned/borrowed `+=`/`-=` split is wired through undocumented `auto_ops` args, so
721    // cross-check every generated operator form (val/ref × val/ref) and both assign forms against
722    // a HashMap ground truth over Zero/Single/Many cases incl. full cancellation.
723    #[test]
724    fn op_forms_consistent() {
725        use std::collections::HashMap;
726        type L = Lc<X, i32>;
727
728        let lc = |pairs: &[(i32, i32)]| -> L {
729            L::from_iter(pairs.iter().map(|&(k, c)| (e(k), c)))
730        };
731        let reference = |a: &[(i32, i32)], b: &[(i32, i32)], sign: i32| -> L {
732            let mut m: HashMap<i32, i32> = HashMap::new();
733            for &(k, c) in a { *m.entry(k).or_default() += c; }
734            for &(k, c) in b { *m.entry(k).or_default() += sign * c; }
735            L::from_iter(m.into_iter().filter(|&(_, c)| c != 0).map(|(k, c)| (e(k), c)))
736        };
737
738        let cases: &[&[(i32, i32)]] = &[
739            &[],
740            &[(1, 5)],
741            &[(1, -5)],
742            &[(1, 1), (2, 2)],
743            &[(2, 20), (3, 30)],
744            &[(1, 3), (2, -2), (3, 7)],
745            &[(1, -3), (2, 2), (3, -7)],   // negation of the previous → cancels to zero on add
746            &[(1, 1), (2, 1), (3, 1), (4, 1), (5, 1)],
747        ];
748
749        for a in cases {
750            for b in cases {
751                let (la, lb) = (lc(a), lc(b));
752                let exp_add = reference(a, b, 1);
753                let exp_sub = reference(a, b, -1);
754
755                assert_eq!(la.clone() + lb.clone(), exp_add, "Add val_val {a:?} {b:?}");
756                assert_eq!(la.clone() + &lb,        exp_add, "Add val_ref {a:?} {b:?}");
757                assert_eq!(&la + lb.clone(),        exp_add, "Add ref_val {a:?} {b:?}");
758                assert_eq!(&la + &lb,               exp_add, "Add ref_ref {a:?} {b:?}");
759                { let mut t = la.clone(); t += lb.clone(); assert_eq!(t, exp_add, "+= val {a:?} {b:?}"); }
760                { let mut t = la.clone(); t += &lb;        assert_eq!(t, exp_add, "+= ref {a:?} {b:?}"); }
761
762                assert_eq!(la.clone() - lb.clone(), exp_sub, "Sub val_val {a:?} {b:?}");
763                assert_eq!(la.clone() - &lb,        exp_sub, "Sub val_ref {a:?} {b:?}");
764                assert_eq!(&la - lb.clone(),        exp_sub, "Sub ref_val {a:?} {b:?}");
765                assert_eq!(&la - &lb,               exp_sub, "Sub ref_ref {a:?} {b:?}");
766                { let mut t = la.clone(); t -= lb.clone(); assert_eq!(t, exp_sub, "-= val {a:?} {b:?}"); }
767                { let mut t = la.clone(); t -= &lb;        assert_eq!(t, exp_sub, "-= ref {a:?} {b:?}"); }
768
769                // Borrowed operands must be untouched by the ref-rhs / ref-lhs forms.
770                let _ = &la + &lb;
771                let _ = &la - &lb;
772                assert_eq!(la, lc(a), "lhs mutated {a:?}");
773                assert_eq!(lb, lc(b), "rhs mutated {b:?}");
774            }
775        }
776    }
777
778    #[test]
779    fn mul() {
780        type L = Lc<X, i32>;
781        let z = L::from(hashmap!{ e(1) => 1, e(2) => 2 });
782        let r = 2;
783        let w = z * r;
784
785        assert_eq!(w, L::from(hashmap!{ e(1) => 2, e(2) => 4 }));
786    }
787
788    #[test]
789    fn mul_ref() {
790        type L = Lc<X, i32>;
791        let z = L::from(hashmap!{ e(1) => 1, e(2) => 2 });
792        let r = 2;
793        let w = z * r;
794
795        assert_eq!(w, L::from(hashmap!{ e(1) => 2, e(2) => 4 }));
796    }
797
798    #[test]
799    fn mul_assign() {
800        type L = Lc<X, i32>;
801        let mut z = L::from(hashmap!{ e(1) => 1, e(2) => 2 });
802        let r = 2;
803        z *= r;
804
805        assert_eq!(z, L::from(hashmap!{ e(1) => 2, e(2) => 4 }));
806    }
807
808    #[test]
809    fn mul_assign_ref() {
810        type L = Lc<X, i32>;
811        let mut z = L::from(hashmap!{ e(1) => 1, e(2) => 2 });
812        let r = 2;
813        z *= &r;
814
815        assert_eq!(z, L::from(hashmap!{ e(1) => 2, e(2) => 4 }));
816    }
817
818    #[test]
819    fn map_coeffs() {
820        type L = Lc<X, i32>;
821        let z = L::from(hashmap!{ e(1) => 1, e(2) => 2 });
822        let w = z.map_coeffs(|a| a * 10);
823
824        assert_eq!(w, L::from(hashmap!{ e(1) => 10, e(2) => 20 }));
825    }
826
827    #[test]
828    fn map_keys() {
829        type L = Lc<X, i32>;
830        let z = L::from(hashmap!{ e(1) => 1, e(2) => 2 });
831        let w = z.map_keys(|x| e(x.0 * 10));
832
833        assert_eq!(w, L::from(hashmap!{ e(10) => 1, e(20) => 2 }));
834    }
835
836    #[test]
837    fn filter_keys() {
838        type L = Lc<X, i32>;
839        let z = L::from_iter( (1..10).map(|i| (e(i), i * 10)) );
840        let w = z.filtered(|x| x.0 % 3 == 0 );
841        assert_eq!(w, L::from(hashmap!{ e(3) => 30, e(6) => 60, e(9) => 90}))
842    }
843
844    #[test]
845    #[cfg(feature = "serde")]
846    fn serialize() {
847        type L = Lc<X, i32>;
848        let z = L::from(hashmap!{ e(1) => 1, e(2) => 2 });
849        let ser = serde_json::to_string(&z).unwrap();
850        let deser = serde_json::from_str::<L>(&ser).unwrap();
851        assert_eq!(z, deser);
852    }
853}