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}