use faer::Mat;
use rand::{Rng, RngExt};
#[derive(Debug, Clone)]
pub struct TortureTestConfig {
pub num_instances: usize,
pub delay_probability: f64,
pub singular_probability: f64,
pub dblk_singular_probability: f64,
pub matrix_sizes: Vec<(usize, usize)>,
pub backward_error_threshold: f64,
pub seed: u64,
}
impl Default for TortureTestConfig {
fn default() -> Self {
Self {
num_instances: 500,
delay_probability: 0.70,
singular_probability: 0.20,
dblk_singular_probability: 0.10,
matrix_sizes: vec![(32, 32), (64, 64), (128, 128), (128, 48)],
backward_error_threshold: 5e-11,
seed: 12345,
}
}
}
pub fn cause_delays(matrix: &mut Mat<f64>, block_size: usize, rng: &mut impl Rng) {
let n = matrix.nrows();
if n < 8 {
return;
}
let num_rows = n / 8;
let num_entries = n / 8;
let mut scaled_rows = Vec::with_capacity(num_rows);
for i in 0..num_rows {
let row = if i == 0 && n > block_size {
block_size + rng.random_range(0..n - block_size)
} else {
rng.random_range(0..n)
};
scaled_rows.push(row);
for j in 0..n {
matrix[(row, j)] *= 1000.0;
matrix[(j, row)] *= 1000.0;
}
matrix[(row, row)] /= 1000.0;
}
for _ in 0..num_entries {
let i = rng.random_range(0..n);
let j = rng.random_range(0..n);
matrix[(i, j)] *= 1000.0;
matrix[(j, i)] *= 1000.0;
if i == j {
matrix[(i, j)] /= 1000.0;
}
}
}
pub fn make_singular(matrix: &mut Mat<f64>, col1: usize, col2: usize) {
assert_ne!(col1, col2, "col1 and col2 must be different");
let n = matrix.nrows();
assert!(col1 < n && col2 < n, "column indices out of bounds");
let scale = 2.0;
let diag_col1 = matrix[(col1, col1)];
let col1_vals: Vec<f64> = (0..n).map(|i| matrix[(i, col1)]).collect();
for i in 0..n {
if i != col2 {
matrix[(i, col2)] = scale * col1_vals[i];
matrix[(col2, i)] = scale * col1_vals[i];
}
}
matrix[(col2, col1)] = scale * diag_col1;
matrix[(col1, col2)] = scale * diag_col1;
matrix[(col2, col2)] = scale * scale * diag_col1;
}
pub fn make_dblk_singular(matrix: &mut Mat<f64>, block_row: usize, block_size: usize) {
assert!(block_size >= 2, "block_size must be >= 2");
let n = matrix.nrows();
assert!(
block_row + block_size <= n,
"block extends beyond matrix dimensions"
);
let col1 = block_row;
let col2 = block_row + block_size - 1;
make_singular(matrix, col1, col2);
}
pub fn generate_dense_symmetric_indefinite(n: usize, rng: &mut impl Rng) -> Mat<f64> {
let mut a = Mat::zeros(n, n);
for j in 0..n {
for i in j..n {
let v: f64 = rng.random_range(-1.0..1.0);
a[(i, j)] = v;
a[(j, i)] = v;
}
}
for i in 0..n {
let row_sum: f64 = (0..n).filter(|&j| j != i).map(|j| a[(i, j)].abs()).sum();
let margin = 1.0 + rng.random::<f64>();
if i < n / 2 {
a[(i, i)] = row_sum + margin;
} else {
a[(i, i)] = -(row_sum + margin);
}
}
a
}
pub fn generate_dense_symmetric_pd(n: usize, rng: &mut impl Rng) -> Mat<f64> {
let mut a = Mat::zeros(n, n);
for j in 0..n {
for i in j..n {
let v: f64 = rng.random_range(-1.0..1.0);
a[(i, j)] = v;
a[(j, i)] = v;
}
}
for i in 0..n {
let row_sum: f64 = (0..n).filter(|&j| j != i).map(|j| a[(i, j)].abs()).sum();
let margin = 1.0 + rng.random::<f64>();
a[(i, i)] = row_sum + margin;
}
a
}
#[cfg(test)]
mod tests {
use super::*;
use rand::SeedableRng;
use rand::rngs::StdRng;
fn seeded_rng() -> StdRng {
StdRng::seed_from_u64(42)
}
fn is_symmetric(m: &Mat<f64>) -> bool {
let n = m.nrows();
for i in 0..n {
for j in 0..n {
if (m[(i, j)] - m[(j, i)]).abs() > 1e-15 {
return false;
}
}
}
true
}
#[test]
fn cause_delays_preserves_symmetry() {
let mut rng = seeded_rng();
let mut m = generate_dense_symmetric_indefinite(64, &mut rng);
assert!(is_symmetric(&m));
cause_delays(&mut m, 32, &mut rng);
assert!(is_symmetric(&m), "cause_delays broke symmetry");
}
#[test]
fn cause_delays_scales_rows() {
let mut rng = seeded_rng();
let n = 64;
let original = generate_dense_symmetric_indefinite(n, &mut rng);
let mut perturbed = original.clone();
cause_delays(&mut perturbed, 32, &mut rng);
let mut modified_rows = 0;
for i in 0..n {
let orig_norm: f64 = (0..n).map(|j| original[(i, j)].powi(2)).sum::<f64>().sqrt();
let pert_norm: f64 = (0..n)
.map(|j| perturbed[(i, j)].powi(2))
.sum::<f64>()
.sqrt();
if (pert_norm - orig_norm).abs() > 1e-10 {
modified_rows += 1;
}
}
assert!(
modified_rows >= n / 8,
"expected at least {} modified rows, got {}",
n / 8,
modified_rows
);
}
#[test]
fn cause_delays_noop_for_small_matrix() {
let mut rng = seeded_rng();
let mut m = generate_dense_symmetric_indefinite(4, &mut rng);
let original = m.clone();
cause_delays(&mut m, 32, &mut rng);
for i in 0..4 {
for j in 0..4 {
assert_eq!(m[(i, j)], original[(i, j)]);
}
}
}
#[test]
fn make_singular_creates_rank_deficiency() {
let mut rng = seeded_rng();
let n = 16;
let mut m = generate_dense_symmetric_indefinite(n, &mut rng);
make_singular(&mut m, 0, 1);
assert!(is_symmetric(&m), "make_singular broke symmetry");
for i in 0..n {
assert!(
(m[(i, 1)] - 2.0 * m[(i, 0)]).abs() < 1e-12,
"col1 and col2 not linearly dependent at row {}: m[{},1]={}, 2*m[{},0]={}",
i,
i,
m[(i, 1)],
i,
2.0 * m[(i, 0)]
);
}
}
#[test]
fn make_singular_preserves_symmetry() {
let mut rng = seeded_rng();
let mut m = generate_dense_symmetric_indefinite(32, &mut rng);
make_singular(&mut m, 5, 10);
assert!(is_symmetric(&m), "make_singular broke symmetry");
}
#[test]
fn make_dblk_singular_targets_block() {
let mut rng = seeded_rng();
let n = 64;
let mut m = generate_dense_symmetric_indefinite(n, &mut rng);
make_dblk_singular(&mut m, 16, 8);
assert!(is_symmetric(&m), "make_dblk_singular broke symmetry");
let col1 = 16;
let col2 = 23; for i in 0..n {
assert!(
(m[(i, col2)] - 2.0 * m[(i, col1)]).abs() < 1e-12,
"block columns not linearly dependent at row {}",
i
);
}
}
#[test]
#[should_panic(expected = "col1 and col2 must be different")]
fn make_singular_same_col_panics() {
let mut rng = seeded_rng();
let mut m = generate_dense_symmetric_indefinite(8, &mut rng);
make_singular(&mut m, 3, 3);
}
#[test]
#[should_panic(expected = "block_size must be >= 2")]
fn make_dblk_singular_size_1_panics() {
let mut rng = seeded_rng();
let mut m = generate_dense_symmetric_indefinite(8, &mut rng);
make_dblk_singular(&mut m, 0, 1);
}
#[test]
fn generate_dense_symmetric_indefinite_is_symmetric() {
let mut rng = seeded_rng();
let m = generate_dense_symmetric_indefinite(50, &mut rng);
assert!(is_symmetric(&m));
}
#[test]
fn generate_dense_symmetric_indefinite_has_mixed_signs() {
let mut rng = seeded_rng();
let m = generate_dense_symmetric_indefinite(50, &mut rng);
let mut has_pos = false;
let mut has_neg = false;
for i in 0..50 {
if m[(i, i)] > 0.0 {
has_pos = true;
}
if m[(i, i)] < 0.0 {
has_neg = true;
}
}
assert!(has_pos && has_neg, "should have mixed diagonal signs");
}
#[test]
fn generate_dense_symmetric_pd_is_symmetric() {
let mut rng = seeded_rng();
let m = generate_dense_symmetric_pd(50, &mut rng);
assert!(is_symmetric(&m));
}
#[test]
fn generate_dense_symmetric_pd_positive_diag() {
let mut rng = seeded_rng();
let m = generate_dense_symmetric_pd(50, &mut rng);
for i in 0..50 {
assert!(m[(i, i)] > 0.0, "diagonal entry {} should be positive", i);
}
}
}