use crate::csr::CsrMatrix;
use crate::parallel_amg::strength::StrengthGraph;
use crate::parallel_amg::types::CoarseningResult;
use std::sync::{Arc, Mutex};
struct Lcg {
state: u64,
}
impl Lcg {
fn new(seed: u64) -> Self {
Self {
state: seed ^ 0x123456789abcdef,
}
}
fn next_u64(&mut self) -> u64 {
self.state = self
.state
.wrapping_mul(6364136223846793005)
.wrapping_add(1442695040888963407);
self.state
}
fn next_f64(&mut self) -> f64 {
(self.next_u64() >> 11) as f64 / (1u64 << 53) as f64
}
}
pub fn pmis_coarsening(strength: &StrengthGraph) -> CoarseningResult {
let n = strength.n;
if n == 0 {
return CoarseningResult::from_splitting(Vec::new());
}
let mut status = vec![0u8; n];
let mut rng = Lcg::new(42);
let mut weights: Vec<f64> = (0..n).map(|_| rng.next_f64()).collect();
let mut changed = true;
let mut iter = 0usize;
let max_iter = n + 10;
while changed && iter < max_iter {
changed = false;
iter += 1;
let mut new_c = Vec::new();
for i in 0..n {
if status[i] != 0 {
continue;
}
let mut is_max = true;
for &j in &strength.strong_neighbors[i] {
if status[j] == 0 && weights[j] >= weights[i] && j != i {
is_max = false;
break;
}
}
if is_max {
for &j in &strength.strong_influencers[i] {
if status[j] == 0 && weights[j] >= weights[i] && j != i {
is_max = false;
break;
}
}
}
if is_max {
new_c.push(i);
}
}
for &i in &new_c {
status[i] = 1;
changed = true;
}
for &c in &new_c {
for &j in &strength.strong_neighbors[c] {
if status[j] == 0 {
status[j] = 2;
changed = true;
}
}
for &j in &strength.strong_influencers[c] {
if status[j] == 0 {
status[j] = 2;
changed = true;
}
}
}
for i in 0..n {
if status[i] == 0 {
weights[i] = rng.next_f64();
}
}
}
for s in status.iter_mut() {
if *s == 0 {
*s = 2;
}
}
let cf_splitting: Vec<u8> = status.iter().map(|&s| if s == 1 { 1 } else { 0 }).collect();
CoarseningResult::from_splitting(cf_splitting)
}
pub fn cljp_coarsening(strength: &StrengthGraph, lambda: &[f64]) -> CoarseningResult {
let n = strength.n;
if n == 0 {
return CoarseningResult::from_splitting(Vec::new());
}
let mut status = vec![0u8; n];
let mut lambda_mut: Vec<f64> = lambda.to_vec();
let mut rng = Lcg::new(137);
let mut weights: Vec<f64> = (0..n).map(|i| lambda_mut[i] + rng.next_f64()).collect();
let max_iter = 2 * n + 10;
let mut iter = 0usize;
loop {
let remaining = status.iter().filter(|&&s| s == 0).count();
if remaining == 0 || iter >= max_iter {
break;
}
iter += 1;
let mut new_c = Vec::new();
for i in 0..n {
if status[i] != 0 {
continue;
}
let mut is_local_max = true;
for &j in &strength.strong_neighbors[i] {
if status[j] == 0 && weights[j] > weights[i] {
is_local_max = false;
break;
}
}
if is_local_max {
for &j in &strength.strong_influencers[i] {
if status[j] == 0 && weights[j] > weights[i] {
is_local_max = false;
break;
}
}
}
if is_local_max {
new_c.push(i);
}
}
if new_c.is_empty() {
for i in 0..n {
if status[i] == 0 {
weights[i] = lambda_mut[i] + rng.next_f64();
}
}
continue;
}
for &c in &new_c {
status[c] = 1;
}
let mut new_f = Vec::new();
for &c in &new_c {
for &j in &strength.strong_neighbors[c] {
if status[j] == 0 {
status[j] = 2;
new_f.push(j);
}
}
for &j in &strength.strong_influencers[c] {
if status[j] == 0 {
status[j] = 2;
new_f.push(j);
}
}
}
for &f in &new_f {
for &k in &strength.strong_neighbors[f] {
if status[k] == 0 {
lambda_mut[k] += 0.5;
}
}
}
for i in 0..n {
if status[i] == 0 {
weights[i] = lambda_mut[i] + rng.next_f64();
}
}
}
for s in status.iter_mut() {
if *s == 0 {
*s = 2;
}
}
let cf_splitting: Vec<u8> = status.iter().map(|&s| if s == 1 { 1 } else { 0 }).collect();
CoarseningResult::from_splitting(cf_splitting)
}
pub fn parallel_rs_coarsening(
a: &CsrMatrix<f64>,
strength: &StrengthGraph,
n_threads: usize,
) -> CoarseningResult {
let n = strength.n;
if n == 0 {
return CoarseningResult::from_splitting(Vec::new());
}
let n_threads = n_threads.max(1);
let mut lambda: Vec<f64> = strength
.strong_influencers
.iter()
.map(|v| v.len() as f64)
.collect();
let status = Arc::new(Mutex::new(vec![0u8; n]));
let lambda_arc = Arc::new(Mutex::new(lambda.clone()));
let mut iter = 0usize;
let max_iter = n + 10;
while iter < max_iter {
iter += 1;
let remaining = {
let s = status.lock().unwrap_or_else(|e| e.into_inner());
s.iter().filter(|&&x| x == 0).count()
};
if remaining == 0 {
break;
}
let best_node = {
let s = status.lock().unwrap_or_else(|e| e.into_inner());
let l = lambda_arc.lock().unwrap_or_else(|e| e.into_inner());
let mut best_idx = None;
let mut best_val = -1.0f64;
for i in 0..n {
if s[i] == 0 && l[i] > best_val {
best_val = l[i];
best_idx = Some(i);
}
}
best_idx
};
let c_node = match best_node {
Some(i) => i,
None => break,
};
{
let mut s = status.lock().unwrap_or_else(|e| e.into_inner());
s[c_node] = 1;
}
let strong_nbrs: Vec<usize> = strength.strong_influencers[c_node].clone();
let chunk_size = (strong_nbrs.len() + n_threads - 1).max(1) / n_threads.max(1);
let mut f_nodes_marked: Vec<usize> = Vec::new();
std::thread::scope(|s_scope| {
let mut handles = Vec::new();
let status_ref = Arc::clone(&status);
for chunk in strong_nbrs.chunks(chunk_size.max(1)) {
let chunk_vec: Vec<usize> = chunk.to_vec();
let status_clone = Arc::clone(&status_ref);
let handle = s_scope.spawn(move || {
let mut local_f = Vec::new();
let mut guard = status_clone.lock().unwrap_or_else(|e| e.into_inner());
for &j in &chunk_vec {
if guard[j] == 0 {
guard[j] = 2; local_f.push(j);
}
}
local_f
});
handles.push(handle);
}
for h in handles {
if let Ok(local_f) = h.join() {
f_nodes_marked.extend(local_f);
}
}
});
{
let mut s = status.lock().unwrap_or_else(|e| e.into_inner());
for &j in &strength.strong_neighbors[c_node] {
if s[j] == 0 {
s[j] = 2;
f_nodes_marked.push(j);
}
}
}
{
let s = status.lock().unwrap_or_else(|e| e.into_inner());
let mut l = lambda_arc.lock().unwrap_or_else(|e| e.into_inner());
for &f in &f_nodes_marked {
for &k in &strength.strong_influencers[f] {
if s[k] == 0 {
l[k] += 1.0;
}
}
}
}
{
let l = lambda_arc.lock().unwrap_or_else(|e| e.into_inner());
lambda.copy_from_slice(&l);
}
}
{
let mut s = status.lock().unwrap_or_else(|e| e.into_inner());
for si in s.iter_mut() {
if *si == 0 {
*si = 2;
}
}
}
{
let mut s = status.lock().unwrap_or_else(|e| e.into_inner());
let n_rows = a.shape().0;
for i in 0..n_rows {
if s[i] != 2 {
continue; }
for &j in &strength.strong_influencers[i] {
if s[j] != 2 {
continue; }
let c_nbrs_i: Vec<usize> = strength.strong_influencers[i]
.iter()
.filter(|&&k| s[k] == 1)
.copied()
.collect();
let c_nbrs_j: Vec<usize> = strength.strong_influencers[j]
.iter()
.filter(|&&k| s[k] == 1)
.copied()
.collect();
let has_common_c = c_nbrs_i.iter().any(|k| c_nbrs_j.contains(k));
if !has_common_c {
s[j] = 1;
}
}
}
}
let s = status.lock().unwrap_or_else(|e| e.into_inner());
let cf_splitting: Vec<u8> = s.iter().map(|&x| if x == 1 { 1 } else { 0 }).collect();
CoarseningResult::from_splitting(cf_splitting)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::parallel_amg::strength::{compute_lambda, serial_strength_of_connection};
fn laplacian_1d(n: usize) -> CsrMatrix<f64> {
let mut rows = Vec::new();
let mut cols = Vec::new();
let mut vals = Vec::new();
for i in 0..n {
rows.push(i);
cols.push(i);
vals.push(2.0f64);
}
for i in 0..n - 1 {
rows.push(i);
cols.push(i + 1);
vals.push(-1.0f64);
rows.push(i + 1);
cols.push(i);
vals.push(-1.0f64);
}
CsrMatrix::new(vals, rows, cols, (n, n)).expect("valid Laplacian")
}
#[test]
fn test_pmis_is_independent_set() {
let a = laplacian_1d(12);
let g = serial_strength_of_connection(&a, 0.25);
let result = pmis_coarsening(&g);
for &c1 in &result.c_nodes {
for &c2 in &result.c_nodes {
if c1 != c2 {
assert!(
!g.strong_neighbors[c1].contains(&c2),
"C-nodes {c1} and {c2} are strongly connected — not an independent set"
);
assert!(
!g.strong_neighbors[c2].contains(&c1),
"C-nodes {c2} and {c1} are strongly connected — not an independent set"
);
}
}
}
}
#[test]
fn test_pmis_covers_all() {
let a = laplacian_1d(12);
let g = serial_strength_of_connection(&a, 0.25);
let result = pmis_coarsening(&g);
for &f in &result.f_nodes {
let has_c_neighbor = g.strong_neighbors[f]
.iter()
.any(|&j| result.cf_splitting[j] == 1)
|| g.strong_influencers[f]
.iter()
.any(|&j| result.cf_splitting[j] == 1);
assert!(
has_c_neighbor,
"F-node {f} has no C-node in strong neighborhood"
);
}
}
#[test]
fn test_cljp_valid_splitting() {
let a = laplacian_1d(14);
let g = serial_strength_of_connection(&a, 0.25);
let lambda = compute_lambda(&g);
let result = cljp_coarsening(&g, &lambda);
assert_eq!(result.cf_splitting.len(), 14);
for &s in &result.cf_splitting {
assert!(s == 0 || s == 1, "Invalid splitting value: {s}");
}
assert!(!result.c_nodes.is_empty(), "Must have at least one C-node");
assert!(!result.f_nodes.is_empty(), "Must have at least one F-node");
}
#[test]
fn test_cljp_independent() {
let a = laplacian_1d(14);
let g = serial_strength_of_connection(&a, 0.25);
let lambda = compute_lambda(&g);
let result = cljp_coarsening(&g, &lambda);
for &c1 in &result.c_nodes {
for &c2 in &result.c_nodes {
if c1 != c2 {
assert!(
!g.strong_neighbors[c1].contains(&c2),
"CLJP C-nodes {c1} and {c2} are directly connected"
);
}
}
}
}
#[test]
fn test_parallel_rs_cf_assignment() {
let a = laplacian_1d(16);
let g = serial_strength_of_connection(&a, 0.25);
let result = parallel_rs_coarsening(&a, &g, 2);
assert_eq!(result.cf_splitting.len(), 16);
assert!(!result.c_nodes.is_empty());
assert!(!result.f_nodes.is_empty());
assert_eq!(result.c_nodes.len() + result.f_nodes.len(), 16);
}
#[test]
fn test_parallel_rs_matches_serial() {
let a = laplacian_1d(20);
let g = serial_strength_of_connection(&a, 0.25);
let r1 = parallel_rs_coarsening(&a, &g, 1);
let r4 = parallel_rs_coarsening(&a, &g, 4);
let ratio1 = r1.coarsening_ratio();
let ratio4 = r4.coarsening_ratio();
assert!(
ratio1 > 0.0 && ratio1 < 1.0,
"1-thread ratio out of range: {ratio1}"
);
assert!(
ratio4 > 0.0 && ratio4 < 1.0,
"4-thread ratio out of range: {ratio4}"
);
}
#[test]
fn test_coarsening_ratio() {
let a = laplacian_1d(20);
let g = serial_strength_of_connection(&a, 0.25);
let result = pmis_coarsening(&g);
let ratio = result.coarsening_ratio();
assert!(
(0.15..=0.85).contains(&ratio),
"Coarsening ratio {ratio} out of expected range [0.15, 0.85]"
);
}
}