use core::ops::Range;
use crate::SparseError;
use crate::index::dim_to_u32;
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct BlockPartition {
pub row_blocks: Vec<usize>,
pub col_blocks: Vec<usize>,
}
impl BlockPartition {
pub fn new(row_blocks: Vec<usize>, col_blocks: Vec<usize>) -> Result<Self, SparseError> {
let p = Self {
row_blocks,
col_blocks,
};
p.check_invariants()?;
Ok(p)
}
pub fn from_sizes(row_sizes: &[usize], col_sizes: &[usize]) -> Result<Self, SparseError> {
Self::new(boundaries(row_sizes), boundaries(col_sizes))
}
pub fn check_invariants(&self) -> Result<(), SparseError> {
check_boundaries("row", &self.row_blocks)?;
check_boundaries("column", &self.col_blocks)
}
#[inline]
pub fn n_rows(&self) -> usize {
*self
.row_blocks
.last()
.expect("(B1): non-empty boundary list")
}
#[inline]
pub fn n_cols(&self) -> usize {
*self
.col_blocks
.last()
.expect("(B2): non-empty boundary list")
}
#[inline]
pub fn n_row_blocks(&self) -> usize {
self.row_blocks.len() - 1
}
#[inline]
pub fn n_col_blocks(&self) -> usize {
self.col_blocks.len() - 1
}
#[inline]
pub fn row_block(&self, k: usize) -> Range<usize> {
assert!(k < self.n_row_blocks(), "row block {k} out of range");
self.row_blocks[k]..self.row_blocks[k + 1]
}
#[inline]
pub fn col_block(&self, l: usize) -> Range<usize> {
assert!(l < self.n_col_blocks(), "column block {l} out of range");
self.col_blocks[l]..self.col_blocks[l + 1]
}
pub fn row_block_of(&self, i: usize) -> usize {
assert!(i < self.n_rows(), "row {i} out of range");
block_of(&self.row_blocks, i)
}
pub fn col_block_of(&self, j: usize) -> usize {
assert!(j < self.n_cols(), "column {j} out of range");
block_of(&self.col_blocks, j)
}
}
fn boundaries(sizes: &[usize]) -> Vec<usize> {
let mut b = Vec::with_capacity(sizes.len() + 1);
let mut acc = 0;
b.push(acc);
for &s in sizes {
acc += s;
b.push(acc);
}
b
}
fn check_boundaries(what: &'static str, b: &[usize]) -> Result<(), SparseError> {
let Some((&first, _)) = b.split_first() else {
return Err(SparseError::Malformed(format!(
"{what} block boundaries are empty (expected at least [0])"
)));
};
if first != 0 {
return Err(SparseError::Malformed(format!(
"{what} block boundaries start at {first}, not 0"
)));
}
if let Some(k) = b.windows(2).position(|w| w[0] >= w[1]) {
return Err(SparseError::Malformed(format!(
"{what} block boundaries not strictly increasing at block {k}: {} ≥ {}",
b[k],
b[k + 1]
)));
}
dim_to_u32(what, b[b.len() - 1]).map(|_| ())
}
#[inline]
fn block_of(b: &[usize], i: usize) -> usize {
let k = b.partition_point(|&x| x <= i);
debug_assert!(
k >= 1 && k < b.len(),
"Algorithm B: index {i} outside {b:?}"
);
k - 1
}
#[cfg(test)]
mod tests {
use super::*;
use proptest::prelude::*;
#[test]
fn ex_5_8_5_simplex_and_kkt_partitions() {
let p = BlockPartition::from_sizes(&[4], &[6, 4]).unwrap();
assert_eq!(p.row_blocks, vec![0, 4]);
assert_eq!(p.col_blocks, vec![0, 6, 10]);
assert_eq!((p.n_rows(), p.n_cols()), (4, 10));
assert_eq!((p.n_row_blocks(), p.n_col_blocks()), (1, 2));
assert_eq!(p.row_block(0), 0..4);
assert_eq!(p.col_block(0), 0..6);
assert_eq!(p.col_block(1), 6..10);
for j in 0..10 {
assert_eq!(p.col_block_of(j), usize::from(j >= 6));
assert_eq!(p.row_block_of(j % 4), 0);
}
let k = BlockPartition::new(vec![0, 6, 10], vec![0, 6, 10]).unwrap();
assert_eq!(k, BlockPartition::from_sizes(&[6, 4], &[6, 4]).unwrap());
assert_eq!(k.row_block(1), 6..10);
assert_eq!(k.row_block_of(5), 0);
assert_eq!(k.row_block_of(6), 1);
assert_eq!(k.col_block_of(9), 1);
let e = BlockPartition::new(vec![0], vec![0, 2]).unwrap();
assert_eq!((e.n_rows(), e.n_row_blocks()), (0, 0));
assert_eq!(BlockPartition::from_sizes(&[], &[2]).unwrap(), e);
}
#[test]
fn rejects_bad_boundaries() {
let bad = |r: Vec<usize>, c: Vec<usize>| BlockPartition::new(r, c).unwrap_err();
assert!(matches!(bad(vec![], vec![0]), SparseError::Malformed(_)));
assert!(matches!(
bad(vec![1, 2], vec![0]),
SparseError::Malformed(_)
));
assert!(matches!(
bad(vec![0, 2, 2], vec![0]),
SparseError::Malformed(_)
));
assert!(matches!(
bad(vec![0], vec![0, 3, 1]),
SparseError::Malformed(_)
));
assert!(matches!(
BlockPartition::from_sizes(&[3, 0], &[1]),
Err(SparseError::Malformed(_))
));
#[cfg(target_pointer_width = "64")]
assert!(matches!(
bad(vec![0, 1 << 33], vec![0]),
SparseError::TooLarge { what: "row", .. }
));
let mut p = BlockPartition::from_sizes(&[2], &[2]).unwrap();
p.col_blocks[0] = 1;
assert!(p.check_invariants().is_err());
}
#[test]
#[should_panic(expected = "row 4 out of range")]
fn block_of_rejects_an_index_beyond_the_dimension() {
let p = BlockPartition::from_sizes(&[4], &[1]).unwrap();
let _ = p.row_block_of(4);
}
#[test]
#[should_panic(expected = "column block 1 out of range")]
fn block_range_rejects_a_block_beyond_the_count() {
let p = BlockPartition::from_sizes(&[4], &[1]).unwrap();
let _ = p.col_block(1);
}
proptest! {
#[test]
fn blocks_tile_the_dimension(
rows in proptest::collection::vec(1_usize..6, 0..6),
cols in proptest::collection::vec(1_usize..6, 0..6),
) {
let p = BlockPartition::from_sizes(&rows, &cols).unwrap();
prop_assert!(p.check_invariants().is_ok());
prop_assert_eq!(p.n_row_blocks(), rows.len());
prop_assert_eq!(p.n_col_blocks(), cols.len());
prop_assert_eq!(p.n_rows(), rows.iter().sum::<usize>());
let mut next = 0;
for (k, &s) in rows.iter().enumerate() {
let r = p.row_block(k);
prop_assert_eq!(r.clone(), next..next + s);
for i in r {
prop_assert_eq!(p.row_block_of(i), k);
}
next += s;
}
prop_assert_eq!(next, p.n_rows());
for j in 0..p.n_cols() {
let scan = (0..cols.len()).find(|&l| p.col_block(l).contains(&j)).unwrap();
prop_assert_eq!(p.col_block_of(j), scan);
}
}
}
}