Skip to main content

relp_num/traits/
absorb.rs

1//! # Acting on a wide accumulator with a narrow value
2//!
3//! Exact linear programs are stored in narrow number types and computed in wide ones. The
4//! constraint matrix of a network problem holds nothing but `1`, `-1` and `0`; the basis inverse
5//! the simplex method builds from it needs arbitrary precision. Widening every coefficient before
6//! using it throws away exactly the information that makes the narrow type worth having:
7//! multiplying by [`One`](crate::fixed::One) is a clone, adding [`Zero`](crate::fixed::Zero) is
8//! nothing at all, and adding an integer to a fraction leaves its denominator alone.
9//!
10//! [`Absorb`] is the one relation between the two. It is written from the wide side, and that is
11//! the whole point: an implementation names both types concretely, so it can reach the operator
12//! that was written for exactly this pair. A trait written from the narrow side and generic over
13//! the wide type cannot, because inside such a body the wide type is opaque and converting is the
14//! only thing left to do.
15
16use crate::Field;
17
18/// What a wide accumulator does with a narrow value.
19///
20/// The wide type is `Self`, so every implementation sees both types concretely and can be written
21/// in terms of the operators that exist for the pair.
22///
23/// # No defaults
24///
25/// Every method has to be written out. A default body would have to be phrased in terms of
26/// [`from_narrow`](Absorb::from_narrow), which is the slow path: it materialises a wide value and
27/// hands it to the general routine, so a forgotten override would cost orders of magnitude at the
28/// larger widths and cost nothing at the smallest, which is where it would be tested. Requiring
29/// each body makes leaving one out a compile error rather than a benchmark result.
30///
31/// Two macros write the bodies, so the burden is one line per pair in practice:
32/// `absorb_by_ops!` for a pair that has operators, `absorb_by_widening!` for one that does not.
33///
34/// # Extending it
35///
36/// A crate downstream can implement this for its own narrow type against a wide type from here:
37/// `impl<const S: usize> Absorb<MyCoefficient> for Big<S>` is a foreign trait for a foreign `Self`
38/// with a local type parameter, which the orphan rule permits. Such an implementation sees `Big`
39/// concretely and can be as fast as one written here.
40///
41/// A crate that brings its own wide type implements this for it against the narrow types it wants
42/// to read, which is always allowed because `Self` is then local.
43///
44/// # Agreement
45///
46/// The four operating methods have to agree with [`from_narrow`](Absorb::from_narrow):
47/// `w.add_narrow(n)` equals `w += W::from_narrow(n)`, `w.sub_narrow(n)` equals
48/// `w -= W::from_narrow(n)`, `w.mul_narrow(n)` equals `w * W::from_narrow(n)`, and
49/// `w.add_mul_narrow(n, v)` equals `w += v * W::from_narrow(n)`. The tests in this crate check
50/// every implementation against those four statements.
51///
52/// # Example
53///
54/// ```
55/// use relp_num::fixed::One;
56/// use relp_num::{Absorb, RationalBig, RB};
57///
58/// let mut accumulator = RB!(1, 2);
59/// // No `RationalBig` is materialised, and no gcd is taken: the denominator is added into the
60/// // numerator where it stands.
61/// accumulator.add_narrow(&One);
62/// assert_eq!(accumulator, RB!(3, 2));
63///
64/// assert_eq!(RB!(7, 2).mul_narrow(&One), RB!(7, 2));
65/// assert_eq!(<RationalBig as Absorb<One>>::from_narrow(&One), RB!(1));
66/// ```
67pub trait Absorb<N>: Sized {
68    /// The narrow value in the wide type.
69    fn from_narrow(narrow: &N) -> Self;
70
71    /// Add a narrow value to this one.
72    fn add_narrow(&mut self, narrow: &N);
73
74    /// Subtract a narrow value from this one.
75    fn sub_narrow(&mut self, narrow: &N);
76
77    /// Multiply this value by a narrow one.
78    fn mul_narrow(&self, narrow: &N) -> Self;
79
80    /// Add the product of a narrow value and a wide one to this value.
81    ///
82    /// This is the operation an inner product is made of, and the reason it is a method of its own
83    /// rather than a multiplication followed by an addition: for a narrow value that is one, the
84    /// multiplication is not a cheap multiplication but no multiplication, and the intermediate it
85    /// would have produced is not a cheap allocation but no allocation.
86    fn add_mul_narrow(&mut self, narrow: &N, other: &Self);
87}
88
89/// A wide accumulator, together with the narrow values it reads.
90///
91/// Stating `W: AbsorbAll<N>` is stating `W: Field + Absorb<N>`, which is what generic code over a
92/// matrix provider's coefficient type wants: a field to compute in, and the ability to act on it
93/// with what the provider hands over.
94pub trait AbsorbAll<N>: Field + Absorb<N> {}
95impl<N, W: Field + Absorb<N>> AbsorbAll<N> for W {}
96
97#[cfg(test)]
98mod test {
99    use crate::{Absorb, AbsorbAll, Field, FieldRef, Rational8, Rational16, Rational32, Rational64, Rational128, RationalBig, RB};
100    use crate::fixed::{Binary, One, SignedOne, Zero};
101
102    /// Every implementation has to agree with converting and then operating.
103    ///
104    /// This is the contract that makes the whole table trustworthy: the fast bodies are written by
105    /// hand against the operators of a pair, and nothing but this check ties them back to what the
106    /// narrow value means.
107    fn check<T>(value: &T, wide: RationalBig)
108    where
109        RationalBig: Absorb<T>,
110    {
111        assert_eq!(RationalBig::from_narrow(value), wide, "from_narrow");
112
113        let start = RB!(5, 3);
114
115        let mut by_override = start.clone();
116        by_override.add_narrow(value);
117        assert_eq!(by_override, start.clone() + wide.clone(), "add_narrow");
118
119        let mut by_override = start.clone();
120        by_override.sub_narrow(value);
121        assert_eq!(by_override, start.clone() - wide.clone(), "sub_narrow");
122
123        assert_eq!(start.mul_narrow(value), wide.clone() * start.clone(), "mul_narrow");
124        assert_eq!(RB!(0).mul_narrow(value), RB!(0), "mul_narrow by zero");
125
126        // The fused operation, against the two steps it replaces.
127        for other in [RB!(0), RB!(1), RB!(-7, 5), start.clone()] {
128            let mut fused = start.clone();
129            fused.add_mul_narrow(value, &other);
130            assert_eq!(
131                fused,
132                start.clone() + wide.clone() * other.clone(),
133                "add_mul_narrow by {other}",
134            );
135        }
136    }
137
138    #[test]
139    fn markers_agree_with_converting() {
140        check(&One, RB!(1));
141        check(&Zero, RB!(0));
142        check(&Binary::Zero, RB!(0));
143        check(&Binary::One, RB!(1));
144        check(&SignedOne::PlusOne, RB!(1));
145        check(&SignedOne::MinusOne, RB!(-1));
146    }
147
148    #[test]
149    fn options_and_references_agree_with_converting() {
150        check(&Some(One), RB!(1));
151        check(&None::<One>, RB!(0));
152        check(&Some(SignedOne::MinusOne), RB!(-1));
153        check(&&One, RB!(1));
154        check(&Some(&SignedOne::MinusOne), RB!(-1));
155        check(&None::<Rational64>, RB!(0));
156        check(&Some(Rational64::new(3, 4).unwrap()), RB!(3, 4));
157    }
158
159    #[test]
160    fn integers_and_rationals_agree_with_converting() {
161        check(&7_i32, RB!(7));
162        check(&-7_i64, RB!(-7));
163        check(&0_u8, RB!(0));
164        check(&0_i16, RB!(0));
165        check(&Rational8::new(3, 4).unwrap(), RB!(3, 4));
166        check(&Rational16::new(-3, 4).unwrap(), RB!(-3, 4));
167        check(&Rational32::new(3, 4).unwrap(), RB!(3, 4));
168        check(&Rational64::new(3, 4).unwrap(), RB!(3, 4));
169        check(&Rational128::new(3, 4).unwrap(), RB!(3, 4));
170        check(&RB!(-9, 4), RB!(-9, 4));
171    }
172
173    /// The widths that only became reachable once the conversion bound went away.
174    ///
175    /// These used to be covered by an implementation whose `for<'r> From<&'r Self>` bound
176    /// `RationalBig` does not satisfy, so the implementation existed and the trait did not hold,
177    /// which showed up at a use site as an error about a conversion nobody asked for.
178    #[test]
179    fn pointer_widths() {
180        check(&7_usize, RB!(7));
181        check(&7_isize, RB!(7));
182        check(&(-7_isize), RB!(-7));
183    }
184
185    /// Generic code over a field, stated the way it has to be stated with [`FieldRef`].
186    ///
187    /// The bound on the reference is a higher ranked one on a type that is not `Self`, so it is a
188    /// requirement on the caller rather than something a `F: Field` bound implies. Moving it onto
189    /// the definition of `Field` does not help: it stays a requirement, and one that then has to be
190    /// discharged at every use of `Field` instead of every use of the reference operations.
191    fn with_field_ref<F: Field>(a: &F, b: &F) -> F
192    where
193        for<'r> &'r F: FieldRef<F>,
194    {
195        &(a * b) + a
196    }
197
198    /// The same function over [`AbsorbAll`], which is one bound and no higher ranked anything.
199    ///
200    /// `Absorb<F> for F` is the reference arithmetic: every method already takes its operands by
201    /// reference, so there is no reference type left to put a bound on.
202    fn with_absorb<F: AbsorbAll<F>>(a: &F, b: &F) -> F {
203        let mut result = a.mul_narrow(b);
204        result.add_narrow(a);
205        result
206    }
207
208    #[test]
209    fn one_bound_replaces_the_higher_ranked_pair() {
210        let a = RB!(3, 2);
211        let b = RB!(5, 7);
212        assert_eq!(with_field_ref(&a, &b), with_absorb(&a, &b));
213        assert_eq!(with_absorb(&a, &b), RB!(3, 2) * RB!(5, 7) + RB!(3, 2));
214    }
215
216    /// Mixed narrow and wide arithmetic behind the same single bound.
217    ///
218    /// This is the shape a sparse inner product has, and the bound is stated once on the
219    /// accumulator rather than once per narrow type the caller might bring.
220    fn inner_product<W: AbsorbAll<N>, N>(column: &[(usize, N)], pi: &[W]) -> W {
221        let mut total = W::zero();
222        for (index, value) in column {
223            total.add_mul_narrow(value, &pi[*index]);
224        }
225        total
226    }
227
228    #[test]
229    fn inner_product_over_any_narrow_type() {
230        let pi = [RB!(5), RB!(4), RB!(-1, 2)];
231
232        // A marker column: no multiplication happens at all.
233        assert_eq!(inner_product(&[(0, One), (2, One)], &pi), RB!(9, 2));
234        assert_eq!(
235            inner_product(&[(0, SignedOne::MinusOne), (1, SignedOne::PlusOne)], &pi),
236            RB!(-1),
237        );
238        // A small rational column.
239        let column = [(0, Rational64::new(1, 5).unwrap()), (1, Rational64::new(1, 2).unwrap())];
240        assert_eq!(inner_product(&column, &pi), RB!(3));
241        // An integer column.
242        assert_eq!(inner_product(&[(1, 3_i64)], &pi), RB!(12));
243        // A column of the wide type itself.
244        assert_eq!(inner_product(&[(0, RB!(2))], &pi), RB!(10));
245    }
246
247    /// The trait is usable through a reference and an `Option`, which is how a sparse column of a
248    /// matrix provider arrives.
249    #[test]
250    fn references_and_absent_values() {
251        let pi = [RB!(5), RB!(4)];
252        let column: Vec<(usize, Option<One>)> = vec![(0, Some(One)), (1, None)];
253        assert_eq!(inner_product(&column, &pi), RB!(5));
254
255        let stored = [One, One];
256        let column: Vec<(usize, &One)> = vec![(0, &stored[0]), (1, &stored[1])];
257        assert_eq!(inner_product(&column, &pi), RB!(9));
258    }
259}