use crate::scaling::TSymScalingMethod;
use crate::sparse_sym_iface::{EMatrixFormat, FactorPattern, SparseSymLinearSolverInterface};
use crate::status::ESymSolverStatus;
use crate::sym_solver::SymLinearSolver;
use pounce_common::types::{Index, Number};
use pounce_linalg::triplet_convert::{TriFull, TripletToCsrConverter};
pub struct TSymLinearSolver {
backend: Box<dyn SparseSymLinearSolverInterface>,
scaling_method: Option<Box<dyn TSymScalingMethod>>,
matrix_format: EMatrixFormat,
converter: Option<TripletToCsrConverter>,
initialized: bool,
have_structure: bool,
use_scaling: bool,
just_switched_on_scaling: bool,
linear_scaling_on_demand: bool,
dim: Index,
nonzeros_triplet: Index,
nonzeros_compressed: Index,
airn: Vec<Index>,
ajcn: Vec<Index>,
scaling_factors: Vec<Number>,
}
impl std::fmt::Debug for TSymLinearSolver {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("TSymLinearSolver")
.field("matrix_format", &self.matrix_format)
.field("dim", &self.dim)
.field("nonzeros_triplet", &self.nonzeros_triplet)
.field("nonzeros_compressed", &self.nonzeros_compressed)
.field("use_scaling", &self.use_scaling)
.field("initialized", &self.initialized)
.finish_non_exhaustive()
}
}
impl TSymLinearSolver {
pub fn new(
backend: Box<dyn SparseSymLinearSolverInterface>,
scaling_method: Option<Box<dyn TSymScalingMethod>>,
linear_scaling_on_demand: bool,
) -> Self {
let matrix_format = backend.matrix_format();
let converter = match matrix_format {
EMatrixFormat::TripletFormat => None,
EMatrixFormat::CsrFormat0Offset => {
Some(TripletToCsrConverter::new(0, TriFull::Triangular))
}
EMatrixFormat::CsrFormat1Offset => {
Some(TripletToCsrConverter::new(1, TriFull::Triangular))
}
EMatrixFormat::CsrFullFormat0Offset => {
Some(TripletToCsrConverter::new(0, TriFull::Full))
}
EMatrixFormat::CsrFullFormat1Offset => {
Some(TripletToCsrConverter::new(1, TriFull::Full))
}
};
let use_scaling = scaling_method.is_some() && !linear_scaling_on_demand;
Self {
backend,
scaling_method,
matrix_format,
converter,
initialized: false,
have_structure: false,
use_scaling,
just_switched_on_scaling: false,
linear_scaling_on_demand,
dim: 0,
nonzeros_triplet: 0,
nonzeros_compressed: 0,
airn: Vec::new(),
ajcn: Vec::new(),
scaling_factors: Vec::new(),
}
}
pub fn initialize_structure(
&mut self,
dim: Index,
airn: &[Index],
ajcn: &[Index],
) -> ESymSolverStatus {
assert_eq!(airn.len(), ajcn.len());
let nz = airn.len() as Index;
self.dim = dim;
self.nonzeros_triplet = nz;
self.airn = airn.to_vec();
self.ajcn = ajcn.to_vec();
let (ia, ja, nonzeros) = match self.converter.as_mut() {
None => (&self.airn[..], &self.ajcn[..], self.nonzeros_triplet),
Some(conv) => {
let nonzeros_compressed = conv.initialize(self.dim, &self.airn, &self.ajcn);
self.nonzeros_compressed = nonzeros_compressed;
(conv.ia(), conv.ja(), nonzeros_compressed)
}
};
let status = self.backend.initialize_structure(dim, nonzeros, ia, ja);
if status != ESymSolverStatus::Success {
return status;
}
if self.scaling_method.is_some() {
self.scaling_factors = vec![0.0; dim as usize];
}
self.have_structure = true;
self.initialized = true;
status
}
#[allow(clippy::too_many_arguments)]
pub fn multi_solve(
&mut self,
vals: &[Number],
new_matrix: bool,
nrhs: Index,
rhs_vals: &mut [Number],
check_neg_evals: bool,
number_of_neg_evals: Index,
) -> ESymSolverStatus {
debug_assert!(self.initialized);
debug_assert_eq!(vals.len(), self.nonzeros_triplet as usize);
debug_assert_eq!(rhs_vals.len(), (self.dim * nrhs) as usize);
{
use std::sync::atomic::{AtomicBool, AtomicUsize, Ordering};
static CALL_COUNT: AtomicUsize = AtomicUsize::new(0);
static WARNED: AtomicBool = AtomicBool::new(false);
let n_call = CALL_COUNT.fetch_add(1, Ordering::SeqCst);
let skip: usize = std::env::var("POUNCE_DBG_KKT_DUMP_SKIP")
.ok()
.and_then(|s| s.parse().ok())
.unwrap_or(0);
if n_call < skip {
} else if let Ok(path) = std::env::var("POUNCE_DBG_KKT_DUMP") {
if !WARNED.swap(true, Ordering::SeqCst) {
tracing::warn!(
target: "pounce::linsol",
"POUNCE_DBG_KKT_DUMP is deprecated; prefer `--dump kkt:<iter-spec>` (see pounce --help)"
);
}
use std::io::Write;
if let Ok(mut f) = std::fs::File::create(&path) {
let dim = self.dim as u64;
let nnz = self.nonzeros_triplet as u64;
let nrhs64 = nrhs as u64;
let _ = f.write_all(&dim.to_le_bytes());
let _ = f.write_all(&nnz.to_le_bytes());
let _ = f.write_all(&nrhs64.to_le_bytes());
for &i in &self.airn {
let _ = f.write_all(&(i as i64).to_le_bytes());
}
for &j in &self.ajcn {
let _ = f.write_all(&(j as i64).to_le_bytes());
}
for &v in vals {
let _ = f.write_all(&v.to_le_bytes());
}
for &v in &*rhs_vals {
let _ = f.write_all(&v.to_le_bytes());
}
let _ = f.flush();
}
unsafe {
std::env::remove_var("POUNCE_DBG_KKT_DUMP");
}
}
}
let mut new_matrix = new_matrix;
if new_matrix || self.just_switched_on_scaling {
self.give_matrix_to_solver(true, vals);
new_matrix = true;
}
if self.use_scaling {
for irhs in 0..nrhs as usize {
let base = irhs * self.dim as usize;
for i in 0..self.dim as usize {
rhs_vals[base + i] *= self.scaling_factors[i];
}
}
}
let status = loop {
let (ia_ptr, ia_len, ja_ptr, ja_len) = match self.converter.as_ref() {
None => (
self.airn.as_ptr(),
self.airn.len(),
self.ajcn.as_ptr(),
self.ajcn.len(),
),
Some(c) => (c.ia().as_ptr(), c.ia().len(), c.ja().as_ptr(), c.ja().len()),
};
let (ia, ja) = unsafe {
(
std::slice::from_raw_parts(ia_ptr, ia_len),
std::slice::from_raw_parts(ja_ptr, ja_len),
)
};
let s = self.backend.multi_solve(
new_matrix,
ia,
ja,
nrhs,
rhs_vals,
check_neg_evals,
number_of_neg_evals,
);
if s == ESymSolverStatus::CallAgain {
self.give_matrix_to_solver(false, vals);
continue;
}
break s;
};
if status == ESymSolverStatus::Success && self.use_scaling {
for irhs in 0..nrhs as usize {
let base = irhs * self.dim as usize;
for i in 0..self.dim as usize {
rhs_vals[base + i] *= self.scaling_factors[i];
}
}
}
status
}
fn give_matrix_to_solver(&mut self, new_matrix: bool, vals: &[Number]) {
if self.matrix_format == EMatrixFormat::TripletFormat && !self.use_scaling {
let pa = self.backend.values_array_mut();
pa[..self.nonzeros_triplet as usize]
.copy_from_slice(&vals[..self.nonzeros_triplet as usize]);
return;
}
let mut atriplet: Vec<Number> = vals[..self.nonzeros_triplet as usize].to_vec();
if self.use_scaling {
if new_matrix || self.just_switched_on_scaling {
let Some(method) = self.scaling_method.as_mut() else {
unreachable!("use_scaling without a scaling method")
};
let ok = method.compute_sym_t_scaling_factors(
self.dim,
self.nonzeros_triplet,
&self.airn,
&self.ajcn,
&atriplet,
&mut self.scaling_factors,
);
assert!(ok, "scaling method failed");
self.just_switched_on_scaling = false;
}
for (i, a) in atriplet
.iter_mut()
.enumerate()
.take(self.nonzeros_triplet as usize)
{
let r = (self.airn[i] - 1) as usize;
let c = (self.ajcn[i] - 1) as usize;
*a *= self.scaling_factors[r] * self.scaling_factors[c];
}
}
if self.matrix_format == EMatrixFormat::TripletFormat {
let pa = self.backend.values_array_mut();
pa[..self.nonzeros_triplet as usize].copy_from_slice(&atriplet);
} else {
let Some(conv) = self.converter.as_ref() else {
unreachable!("non-triplet matrix_format requires a converter");
};
let pa = self.backend.values_array_mut();
conv.convert_values(&atriplet, &mut pa[..self.nonzeros_compressed as usize]);
}
}
pub fn factor_pattern(&self, want_values: bool) -> Option<FactorPattern> {
self.backend.factor_pattern(want_values)
}
}
impl SymLinearSolver for TSymLinearSolver {
fn number_of_neg_evals(&self) -> Index {
self.backend.number_of_neg_evals()
}
fn increase_quality(&mut self) -> bool {
if self.scaling_method.is_some() && !self.use_scaling && self.linear_scaling_on_demand {
self.use_scaling = true;
self.just_switched_on_scaling = true;
return true;
}
self.backend.increase_quality()
}
fn provides_inertia(&self) -> bool {
self.backend.provides_inertia()
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::scaling::IdentityScalingMethod;
#[derive(Default)]
struct MockBackend {
dim: Index,
nz: Index,
a: Vec<Number>,
last_solve_was_new_matrix: bool,
last_solve_was_scaled_a: Option<Vec<Number>>,
canned_solution: Vec<Number>,
neg_evals: Index,
increase_quality_calls: u32,
max_increase_quality_calls: u32,
}
impl SparseSymLinearSolverInterface for MockBackend {
fn initialize_structure(
&mut self,
dim: Index,
nz: Index,
_ia: &[Index],
_ja: &[Index],
) -> ESymSolverStatus {
self.dim = dim;
self.nz = nz;
self.a = vec![0.0; nz as usize];
ESymSolverStatus::Success
}
fn values_array_mut(&mut self) -> &mut [Number] {
&mut self.a
}
fn multi_solve(
&mut self,
new_matrix: bool,
_ia: &[Index],
_ja: &[Index],
nrhs: Index,
rhs_vals: &mut [Number],
_check: bool,
_nev: Index,
) -> ESymSolverStatus {
self.last_solve_was_new_matrix = new_matrix;
self.last_solve_was_scaled_a = Some(self.a.clone());
assert_eq!(rhs_vals.len(), (self.dim * nrhs) as usize);
for irhs in 0..nrhs as usize {
let base = irhs * self.dim as usize;
rhs_vals[base..base + self.dim as usize].copy_from_slice(&self.canned_solution);
}
ESymSolverStatus::Success
}
fn number_of_neg_evals(&self) -> Index {
self.neg_evals
}
fn increase_quality(&mut self) -> bool {
self.increase_quality_calls += 1;
self.increase_quality_calls <= self.max_increase_quality_calls
}
fn provides_inertia(&self) -> bool {
true
}
fn matrix_format(&self) -> EMatrixFormat {
EMatrixFormat::TripletFormat
}
}
fn make_2x2_indef_pattern() -> ([Index; 3], [Index; 3]) {
([1, 2, 2], [1, 1, 2])
}
#[test]
fn unscaled_triplet_solve_passes_values_through() {
let backend = MockBackend {
canned_solution: vec![10.0, 20.0],
..Default::default()
};
let mut solver = TSymLinearSolver::new(Box::new(backend), None, false);
let (irn, jcn) = make_2x2_indef_pattern();
assert_eq!(
solver.initialize_structure(2, &irn, &jcn),
ESymSolverStatus::Success
);
let vals = [2.0, 1.0, 3.0];
let mut rhs = [3.0, 4.0];
assert_eq!(
solver.multi_solve(&vals, true, 1, &mut rhs, false, 0),
ESymSolverStatus::Success
);
assert_eq!(rhs, [10.0, 20.0]);
assert!(solver.provides_inertia());
}
#[test]
fn identity_scaling_does_not_change_values() {
let backend = MockBackend {
canned_solution: vec![1.0, 1.0],
..Default::default()
};
let mut solver = TSymLinearSolver::new(
Box::new(backend),
Some(Box::new(IdentityScalingMethod)),
false,
);
let (irn, jcn) = make_2x2_indef_pattern();
solver.initialize_structure(2, &irn, &jcn);
let vals = [2.0, 1.0, 3.0];
let mut rhs = [4.0, 5.0];
assert_eq!(
solver.multi_solve(&vals, true, 1, &mut rhs, false, 0),
ESymSolverStatus::Success
);
assert_eq!(rhs, [1.0, 1.0]);
}
#[test]
fn nontrivial_scaling_premultiplies_matrix_and_postmultiplies_solution() {
struct DiagTwoThree;
impl TSymScalingMethod for DiagTwoThree {
fn compute_sym_t_scaling_factors(
&mut self,
_n: Index,
_nnz: Index,
_airn: &[Index],
_ajcn: &[Index],
_a: &[Number],
scaling_factors: &mut [Number],
) -> bool {
scaling_factors[0] = 2.0;
scaling_factors[1] = 3.0;
true
}
}
let backend = MockBackend {
canned_solution: vec![7.0, 11.0],
..Default::default()
};
let mut solver =
TSymLinearSolver::new(Box::new(backend), Some(Box::new(DiagTwoThree)), false);
let (irn, jcn) = make_2x2_indef_pattern();
solver.initialize_structure(2, &irn, &jcn);
let vals = [2.0, 1.0, 3.0];
let mut rhs = [4.0, 5.0];
assert_eq!(
solver.multi_solve(&vals, true, 1, &mut rhs, false, 0),
ESymSolverStatus::Success
);
assert_eq!(rhs, [2.0 * 7.0, 3.0 * 11.0]);
}
#[test]
fn increase_quality_switches_on_scaling_first() {
let backend = MockBackend {
canned_solution: vec![0.0, 0.0],
max_increase_quality_calls: 5,
..Default::default()
};
let mut solver = TSymLinearSolver::new(
Box::new(backend),
Some(Box::new(IdentityScalingMethod)),
true, );
assert!(solver.increase_quality());
assert!(solver.increase_quality());
}
#[test]
fn increase_quality_without_scaling_goes_straight_to_backend() {
let backend = MockBackend {
max_increase_quality_calls: 1,
..Default::default()
};
let mut solver = TSymLinearSolver::new(Box::new(backend), None, false);
assert!(solver.increase_quality());
assert!(!solver.increase_quality());
}
}