Skip to main content

xingo_sparse/
vector.rs

1//! # Chapter 5.8.3 — The sparse work vector
2//!
3//! ## Purpose
4//! The simplex kernels (FTRAN, BTRAN, pivot row, updates) produce vectors
5//! whose density ranges from a few entries to completely dense, often within
6//! the same solve. [`SparseVec`] serves both regimes, in the spirit of
7//! HiGHS's `HVector`: values live in a *dense work array* of length `size`,
8//! so any entry is read or written in `O(1)`, and an optional *pattern* lists
9//! the positions that may be nonzero, so that sparse operations cost
10//! `O(count)` instead of `O(size)`.
11//!
12//! ## Notation
13//! `y ∈ R^size` is the vector, stored densely in `val`. When the vector is in
14//! *sparse mode*, `P = idx[0..count]` is its pattern; in *dense mode* no
15//! pattern is kept and `count = size` by convention. A packed vector is a
16//! pair of slices `(ι, ν)` meaning `Σ_k ν_k e_{ι_k}`.
17//!
18//! Operations (each with a sparse path, used when the relevant pattern is
19//! known, and a dense path over all `size` positions):
20//!
21//! * `clear`: `y ← 0`;
22//! * `scatter(α, ι, ν)`: `y ← y + α Σ_k ν_k e_{ι_k}` (packed → work);
23//! * `axpy(α, x)`: `y ← y + α x` for another work vector `x`;
24//! * `gather`: work → packed list of the nonzero entries;
25//! * `tidy`: zero out entries with `|y_i| ≤ ε` (a drop tolerance) and drop
26//!   them from `P`.
27//!
28//! ## Algorithm S (Sparse update of the work vector)
29//! To add `δ` to `y_i`:
30//!
31//! S1. [Record.] If the vector is in sparse mode and `mark[i]` is false, set
32//!     `mark[i] ← true` and append `i` to the pattern.
33//! S2. [Update.] Set `val[i] ← val[i] + δ`.
34//!
35//! `axpy` applies S to every `i` with `x_i ≠ 0`, enumerating `i` over the
36//! pattern of `x` if it has one and over `0..size` otherwise; the two
37//! enumerations visit the same set of positions and perform, at each, the
38//! identical floating-point operation, so the sparse and dense paths give
39//! bit-for-bit equal values. `clear` resets only the pattern positions when
40//! `count ≤ 0.3·size` (the HiGHS threshold, a policy constant of this
41//! chapter) and the whole array otherwise.
42//!
43//! The `mark` array makes membership in `P` an `O(1)` test that does not
44//! depend on `val[i] ≠ 0`: an entry that cancels to exactly zero stays in
45//! the pattern and is never duplicated. HiGHS instead keeps cancelled entries
46//! at a tiny sentinel value; the explicit mark costs one byte per position
47//! and keeps exact zeros exact.
48//!
49//! ## Invariants
50//! (V1) `val.len() = mark.len() = size`.
51//! (V2) Sparse mode: the entries of `P` are distinct and `< size`;
52//!      `mark[i] ⇔ i ∈ P`; and `val[i] ≠ 0 ⇒ i ∈ P` (the pattern is a
53//!      superset of the support; it may contain explicit zeros).
54//! (V3) Dense mode: `P` is empty and every `mark[i]` is false.
55//!
56//! Checked by [`SparseVec::check_invariants`].
57//!
58//! ## References
59//! Hall & McKinnon (2005); HiGHS `HVector` (`src/simplex/HVector*`), studied,
60//! not copied; Gilbert & Peierls (1988) for why the pattern matters.
61
62use crate::SparseError;
63use crate::real::{Real, is_exact_zero, is_zero, le_exact};
64
65/// Fraction of `size` above which `clear` zeroes the whole array (a policy
66/// constant, not a numerical tolerance: Chapter 5, §5.C).
67const CLEAR_DENSE_FRACTION: f64 = 0.3;
68
69/// Work vector with a dense value array and an optional nonzero pattern.
70///
71/// See the module documentation for the representation and invariants.
72#[derive(Clone, Debug)]
73pub struct SparseVec<T: Real = f64> {
74    size: u32,
75    idx: Vec<u32>,
76    val: Vec<T>,
77    mark: Vec<bool>,
78    sparse: bool,
79}
80
81impl<T: Real> SparseVec<T> {
82    /// The zero vector of length `size`, in sparse mode with an empty
83    /// pattern.
84    ///
85    /// # Panics
86    /// If `size > u32::MAX`.
87    pub fn new(size: usize) -> Self {
88        let size32 = u32::try_from(size).expect("SparseVec size exceeds u32::MAX");
89        Self {
90            size: size32,
91            idx: Vec::with_capacity(size),
92            val: vec![T::ZERO; size],
93            mark: vec![false; size],
94            sparse: true,
95        }
96    }
97
98    /// A vector equal to `values`, in sparse mode with the exact support as
99    /// pattern (in increasing order).
100    pub fn from_dense(values: &[T]) -> Self {
101        let mut v = Self::new(values.len());
102        v.val.copy_from_slice(values);
103        v.sparse = false;
104        v.rebuild_pattern();
105        v
106    }
107
108    /// Length of the vector.
109    #[inline]
110    pub fn size(&self) -> usize {
111        self.size as usize
112    }
113
114    /// Number of positions that may be nonzero: `|P|` in sparse mode, `size`
115    /// in dense mode.
116    #[inline]
117    pub fn count(&self) -> usize {
118        if self.sparse {
119            self.idx.len()
120        } else {
121            self.size()
122        }
123    }
124
125    /// Fraction `count / size` (zero for an empty vector).
126    pub fn density(&self) -> f64 {
127        if self.size == 0 {
128            return 0.0;
129        }
130        // count ≤ size ≤ u32::MAX, so the conversion is exact.
131        let count = u32::try_from(self.count()).unwrap_or(self.size);
132        f64::from(count) / f64::from(self.size)
133    }
134
135    /// `true` iff the vector is in sparse mode (it keeps a pattern).
136    #[inline]
137    pub fn is_sparse(&self) -> bool {
138        self.sparse
139    }
140
141    /// The pattern `P` in sparse mode (in insertion order), `None` in dense
142    /// mode.
143    #[inline]
144    pub fn pattern(&self) -> Option<&[u32]> {
145        self.sparse.then_some(self.idx.as_slice())
146    }
147
148    /// The dense value array.
149    #[inline]
150    pub fn values(&self) -> &[T] {
151        &self.val
152    }
153
154    /// Mutable access to the dense value array. Since arbitrary writes
155    /// invalidate the pattern, this switches the vector to dense mode; call
156    /// [`SparseVec::rebuild_pattern`] afterwards to return to sparse mode.
157    pub fn values_mut(&mut self) -> &mut [T] {
158        self.make_dense();
159        &mut self.val
160    }
161
162    /// Entry `y_i`.
163    #[inline]
164    pub fn get(&self, i: u32) -> T {
165        self.val[i as usize]
166    }
167
168    /// Algorithm S, step S1: records `i` in the pattern if needed.
169    #[inline]
170    fn touch(&mut self, i: u32) {
171        let iu = i as usize;
172        if self.sparse && !self.mark[iu] {
173            self.mark[iu] = true;
174            self.idx.push(i);
175        }
176    }
177
178    /// Sets `y_i ← v`.
179    #[inline]
180    pub fn set(&mut self, i: u32, v: T) {
181        self.touch(i);
182        self.val[i as usize] = v;
183    }
184
185    /// Algorithm S: `y_i ← y_i + δ`.
186    #[inline]
187    pub fn add(&mut self, i: u32, delta: T) {
188        self.touch(i);
189        self.val[i as usize] += delta;
190    }
191
192    /// Sets `y ← 0` and returns to sparse mode with an empty pattern.
193    /// Costs `O(count)` when `count ≤ 0.3·size`, `O(size)` otherwise.
194    pub fn clear(&mut self) {
195        let partial = self.sparse && le_exact(self.density(), CLEAR_DENSE_FRACTION);
196        if partial {
197            for &i in &self.idx {
198                self.val[i as usize] = T::ZERO;
199            }
200        } else {
201            self.val.fill(T::ZERO);
202        }
203        self.unmark_pattern();
204        self.sparse = true;
205    }
206
207    /// Scatter: `y ← y + α Σ_k ν_k e_{ι_k}` for the packed vector
208    /// `(idx, val)`. Repeated indices accumulate.
209    ///
210    /// # Panics
211    /// If the slices differ in length or an index is `≥ size`.
212    pub fn scatter(&mut self, alpha: T, idx: &[u32], val: &[T]) {
213        assert_eq!(
214            idx.len(),
215            val.len(),
216            "scatter: index and value lengths differ"
217        );
218        for (&i, &v) in idx.iter().zip(val) {
219            self.add(i, alpha * v);
220        }
221    }
222
223    /// `y ← y + α x`. Uses the pattern of `x` when it has one, and a dense
224    /// sweep of `x` otherwise; both paths update exactly the positions with
225    /// `x_i ≠ 0`, so the results are identical. The pattern of `y` is kept
226    /// if `y` is in sparse mode.
227    ///
228    /// # Panics
229    /// If the sizes differ.
230    pub fn axpy(&mut self, alpha: T, x: &SparseVec<T>) {
231        assert_eq!(self.size, x.size, "axpy: size mismatch");
232        if x.sparse {
233            for &i in &x.idx {
234                let xi = x.val[i as usize];
235                if !is_exact_zero(xi) {
236                    self.add(i, alpha * xi);
237                }
238            }
239        } else {
240            for (i, &xi) in (0..x.size).zip(&x.val) {
241                if !is_exact_zero(xi) {
242                    self.add(i, alpha * xi);
243                }
244            }
245        }
246    }
247
248    /// Gather: writes the nonzero entries as a packed vector into `idx` and
249    /// `val` (both cleared first), in pattern order in sparse mode and in
250    /// increasing index order in dense mode.
251    pub fn gather(&self, idx: &mut Vec<u32>, val: &mut Vec<T>) {
252        idx.clear();
253        val.clear();
254        let mut push = |i: u32| {
255            let v = self.val[i as usize];
256            if !is_exact_zero(v) {
257                idx.push(i);
258                val.push(v);
259            }
260        };
261        if self.sparse {
262            self.idx.iter().copied().for_each(&mut push);
263        } else {
264            (0..self.size).for_each(push);
265        }
266    }
267
268    /// Inner product `yᵀ z` with a dense vector `z`.
269    ///
270    /// # Panics
271    /// If `z.len() != size`.
272    pub fn dot(&self, z: &[T]) -> T {
273        assert_eq!(z.len(), self.size(), "dot: size mismatch");
274        if self.sparse {
275            self.idx
276                .iter()
277                .map(|&i| self.val[i as usize] * z[i as usize])
278                .sum()
279        } else {
280            self.val.iter().zip(z).map(|(&a, &b)| a * b).sum()
281        }
282    }
283
284    /// Drops negligible entries: every `y_i` with `|y_i| ≤ drop_tol` is set
285    /// to exactly zero and, in sparse mode, removed from the pattern (the
286    /// solver passes `Tolerances::zero`).
287    pub fn tidy(&mut self, drop_tol: T) {
288        let Self {
289            idx,
290            val,
291            mark,
292            sparse,
293            ..
294        } = self;
295        if *sparse {
296            idx.retain(|&i| {
297                let iu = i as usize;
298                if is_zero(val[iu], drop_tol) {
299                    val[iu] = T::ZERO;
300                    mark[iu] = false;
301                    false
302                } else {
303                    true
304                }
305            });
306        } else {
307            for v in val.iter_mut() {
308                if is_zero(*v, drop_tol) {
309                    *v = T::ZERO;
310                }
311            }
312        }
313    }
314
315    /// The entries `(i, y_i)` of the pattern in sparse mode (in insertion
316    /// order, explicit zeros included), of every position in dense mode.
317    pub fn iter(&self) -> impl Iterator<Item = (u32, T)> + '_ {
318        let all = if self.sparse { 0 } else { self.size };
319        self.idx
320            .iter()
321            .copied()
322            .chain(0..all)
323            .map(move |i| (i, self.val[i as usize]))
324    }
325
326    /// Switches to dense mode, discarding the pattern (`O(count)`).
327    pub fn make_dense(&mut self) {
328        self.unmark_pattern();
329        self.sparse = false;
330    }
331
332    /// Switches to sparse mode with the pattern set to the exact support of
333    /// `y`, in increasing order (`O(size)`).
334    pub fn rebuild_pattern(&mut self) {
335        self.unmark_pattern();
336        for (i, &v) in (0..self.size).zip(&self.val) {
337            if !is_exact_zero(v) {
338                self.mark[i as usize] = true;
339                self.idx.push(i);
340            }
341        }
342        self.sparse = true;
343    }
344
345    /// Empties the pattern and clears its marks, leaving values untouched.
346    fn unmark_pattern(&mut self) {
347        for &i in &self.idx {
348            self.mark[i as usize] = false;
349        }
350        self.idx.clear();
351    }
352
353    /// Checks invariants (V1)–(V3).
354    pub fn check_invariants(&self) -> Result<(), SparseError> {
355        let n = self.size();
356        if self.val.len() != n {
357            return Err(SparseError::Dimension {
358                what: "SparseVec values",
359                expected: n,
360                found: self.val.len(),
361            });
362        }
363        if self.mark.len() != n {
364            return Err(SparseError::Dimension {
365                what: "SparseVec marks",
366                expected: n,
367                found: self.mark.len(),
368            });
369        }
370        let marked = self.mark.iter().filter(|&&b| b).count();
371        if !self.sparse {
372            if !self.idx.is_empty() || marked != 0 {
373                return Err(SparseError::Malformed(
374                    "dense SparseVec keeps a pattern".into(),
375                ));
376            }
377            return Ok(());
378        }
379        for &i in &self.idx {
380            if i as usize >= n {
381                return Err(SparseError::IndexOutOfRange {
382                    what: "SparseVec pattern entry",
383                    index: i as usize,
384                    bound: n,
385                });
386            }
387            if !self.mark[i as usize] {
388                return Err(SparseError::Malformed(format!(
389                    "pattern entry {i} is not marked"
390                )));
391            }
392        }
393        if marked != self.idx.len() {
394            return Err(SparseError::Malformed(format!(
395                "{marked} marks for {} pattern entries (duplicates or stale marks)",
396                self.idx.len()
397            )));
398        }
399        if let Some(i) = (0..n).find(|&i| !self.mark[i] && !is_exact_zero(self.val[i])) {
400            return Err(SparseError::Malformed(format!(
401                "nonzero entry {i} missing from the pattern"
402            )));
403        }
404        Ok(())
405    }
406}
407
408#[cfg(test)]
409mod tests {
410    use super::*;
411    use crate::real::{approx_eq, le};
412    use proptest::prelude::*;
413
414    /// The drop tolerance of the tests (`Tolerances::zero` of the solver).
415    const DROP: f64 = 1e-12;
416
417    #[test]
418    fn ex_1_2_cancellation_keeps_pattern_unique() {
419        let mut y = SparseVec::new(5);
420        y.add(3, 2.0);
421        y.add(3, -2.0); // cancels to an exact zero, stays in the pattern
422        y.add(3, 1.0);
423        assert_eq!(y.pattern(), Some(&[3_u32][..]));
424        assert!(y.check_invariants().is_ok());
425        y.tidy(DROP);
426        assert_eq!(y.count(), 1);
427        y.add(1, 1e-15);
428        y.tidy(DROP);
429        assert_eq!(y.pattern(), Some(&[3_u32][..]));
430        let (mut i, mut v) = (Vec::new(), Vec::new());
431        y.gather(&mut i, &mut v);
432        assert_eq!((i, v), (vec![3], vec![1.0]));
433        y.clear();
434        assert_eq!(y.count(), 0);
435        assert!(y.values().iter().all(|&v| is_exact_zero(v)));
436        assert!(y.check_invariants().is_ok());
437    }
438
439    #[test]
440    fn modes_and_density() {
441        let mut y = SparseVec::from_dense(&[0.0, 2.0, 0.0, 4.0]);
442        assert_eq!(y.pattern(), Some(&[1_u32, 3][..]));
443        assert!(approx_eq(y.density(), 0.5, 0.0));
444        assert!(approx_eq(y.dot(&[1.0, 1.0, 1.0, 1.0]), 6.0, 0.0));
445        y.values_mut()[0] = 1.0;
446        assert!(!y.is_sparse());
447        assert_eq!(y.count(), 4);
448        assert!(y.check_invariants().is_ok());
449        y.rebuild_pattern();
450        assert_eq!(y.pattern(), Some(&[0_u32, 1, 3][..]));
451        assert!(approx_eq(SparseVec::<f64>::new(0).density(), 0.0, 0.0));
452        let it: Vec<(u32, f64)> = y.iter().collect();
453        assert_eq!(it, vec![(0, 1.0), (1, 2.0), (3, 4.0)]);
454        y.make_dense();
455        assert_eq!(y.iter().count(), 4);
456    }
457
458    /// One operation of the equivalence test.
459    #[derive(Clone, Debug)]
460    enum Op {
461        Set(u32, f64),
462        Add(u32, f64),
463        Scatter(f64, Vec<(u32, f64)>),
464        /// `y ← y + α x` with `x` given densely; the flag says whether `x`
465        /// is presented in sparse mode.
466        Axpy(f64, Vec<f64>, bool),
467        Tidy,
468        Clear,
469    }
470
471    /// A value that is often zero, sometimes tiny (exercises `tidy`).
472    fn value() -> impl Strategy<Value = f64> {
473        prop_oneof![
474            3 => Just(0.0),
475            1 => Just(1e-14),
476            6 => -100.0..100.0_f64,
477        ]
478    }
479
480    fn ops() -> impl Strategy<Value = (usize, Vec<Op>)> {
481        (1_usize..30).prop_flat_map(|n| {
482            let n32 = u32::try_from(n).unwrap();
483            let op = prop_oneof![
484                2 => (0..n32, value()).prop_map(|(i, v)| Op::Set(i, v)),
485                2 => (0..n32, value()).prop_map(|(i, v)| Op::Add(i, v)),
486                2 => (value(), proptest::collection::vec((0..n32, value()), 0..8))
487                    .prop_map(|(a, p)| Op::Scatter(a, p)),
488                3 => (value(), proptest::collection::vec(value(), n), any::<bool>())
489                    .prop_map(|(a, x, s)| Op::Axpy(a, x, s)),
490                1 => Just(Op::Tidy),
491                1 => Just(Op::Clear),
492            ];
493            (Just(n), proptest::collection::vec(op, 0..25))
494        })
495    }
496
497    fn apply(y: &mut SparseVec, op: &Op, drop: f64) {
498        match op {
499            Op::Set(i, v) => y.set(*i, *v),
500            Op::Add(i, v) => y.add(*i, *v),
501            Op::Scatter(a, p) => {
502                let (i, v): (Vec<u32>, Vec<f64>) = p.iter().copied().unzip();
503                y.scatter(*a, &i, &v);
504            }
505            Op::Axpy(a, x, sparse) => {
506                let mut xv = SparseVec::from_dense(x);
507                if !sparse {
508                    xv.make_dense();
509                }
510                y.axpy(*a, &xv);
511            }
512            Op::Tidy => y.tidy(drop),
513            Op::Clear => y.clear(),
514        }
515    }
516
517    /// Plain dense oracle for the same operation.
518    fn apply_oracle(y: &mut [f64], op: &Op, drop: f64) {
519        match op {
520            Op::Set(i, v) => y[*i as usize] = *v,
521            Op::Add(i, v) => y[*i as usize] += *v,
522            Op::Scatter(a, p) => {
523                for &(i, v) in p {
524                    y[i as usize] += a * v;
525                }
526            }
527            Op::Axpy(a, x, _) => {
528                for (yi, &xi) in y.iter_mut().zip(x) {
529                    if !is_exact_zero(xi) {
530                        *yi += a * xi;
531                    }
532                }
533            }
534            Op::Tidy => {
535                for v in y.iter_mut() {
536                    if is_zero(*v, drop) {
537                        *v = 0.0;
538                    }
539                }
540            }
541            Op::Clear => y.fill(0.0),
542        }
543    }
544
545    fn gathered(y: &SparseVec) -> Vec<(u32, u64)> {
546        let (mut i, mut v) = (Vec::new(), Vec::new());
547        y.gather(&mut i, &mut v);
548        let mut g: Vec<(u32, u64)> = i.into_iter().zip(v.iter().map(|x| x.to_bits())).collect();
549        g.sort_unstable();
550        g
551    }
552
553    /// Bitwise equality up to the sign of zero.
554    fn same(a: &[f64], b: &[f64]) -> bool {
555        a.iter().zip(b).all(|(&x, &y)| approx_eq(x, y, 0.0))
556    }
557
558    proptest! {
559        /// A random operation sequence gives the same values on a vector kept
560        /// in sparse mode, on one forced to dense mode after every operation,
561        /// and on a plain `Vec<f64>`; the gathered nonzeros coincide and the
562        /// invariants (V1)–(V3) hold throughout.
563        #[test]
564        fn sparse_equals_dense((n, ops) in ops()) {
565            let tol = DROP;
566            let mut ys = SparseVec::new(n);
567            let mut yd = SparseVec::new(n);
568            yd.make_dense();
569            let mut oracle = vec![0.0; n];
570            for op in &ops {
571                apply(&mut ys, op, tol);
572                apply(&mut yd, op, tol);
573                yd.make_dense();
574                apply_oracle(&mut oracle, op, tol);
575                prop_assert!(ys.is_sparse());
576                prop_assert!(ys.check_invariants().is_ok(), "{:?}", ys.check_invariants());
577                prop_assert!(yd.check_invariants().is_ok(), "{:?}", yd.check_invariants());
578                prop_assert!(same(ys.values(), yd.values()));
579                prop_assert!(same(ys.values(), &oracle));
580                prop_assert_eq!(gathered(&ys), gathered(&yd));
581                // The two `dot`s sum bitwise-identical values in different
582                // orders (pattern order vs. index order), so they agree only
583                // up to reordering error, bounded by n·ε·Σ|y_i|; the final
584                // dot may cancel to a small value, so the tolerance must
585                // scale with the ℓ1 mass, not with the result.
586                let ones = vec![1.0; n];
587                let l1: f64 = yd.values().iter().map(|v| v.abs()).sum();
588                prop_assert!(le((ys.dot(&ones) - yd.dot(&ones)).abs(), 1e-12 * l1.max(1.0), 0.0));
589            }
590            // Rebuilding the pattern of the dense copy yields the exact support.
591            yd.rebuild_pattern();
592            prop_assert!(yd.check_invariants().is_ok());
593            prop_assert_eq!(yd.count(), gathered(&ys).len());
594        }
595    }
596}