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