use std::sync::Arc;
use vyre_foundation::ir::model::expr::{GeneratorRef, Ident};
use vyre_foundation::ir::{BufferAccess, BufferDecl, DataType, Expr, Node, Program};
pub const OP_ID: &str = "vyre-primitives::math::sheaf_laplacian_eigenvalue";
const POWER_ITERATION_PHASE_OP_ID: &str =
"vyre-primitives::math::sheaf_laplacian_eigenvalue::power_iteration_phase";
#[must_use]
pub fn sheaf_laplacian_eigenvalue(
restriction_diag: &str,
v: &str,
lambda: &str,
n_nodes: u32,
d: u32,
iterations: u32,
) -> Program {
let _ = iterations;
if n_nodes == 0 || d == 0 {
return crate::invalid_output_program(
OP_ID,
lambda,
DataType::U32,
format!(
"Fix: sheaf_laplacian_eigenvalue requires n_nodes > 0 and d > 0, got n_nodes={n_nodes}, d={d}."
),
);
}
let Some(cells) = n_nodes.checked_mul(d) else {
return crate::invalid_output_program(
OP_ID,
lambda,
DataType::U32,
format!(
"Fix: sheaf_laplacian_eigenvalue n_nodes*d overflows vector cell count for n_nodes={n_nodes}, d={d}; shard the sheaf spectrum before GPU dispatch."
),
);
};
let one_fp = Expr::load("one_fp_buf", Expr::u32(0));
let nodes = vec![
Node::let_bind("eig_max", Expr::u32(0)),
Node::let_bind("eig_argmax", Expr::u32(0)),
Node::loop_for(
"eig_scan_i",
Expr::u32(0),
Expr::u32(cells),
vec![
Node::let_bind(
"eig_ri",
Expr::load(restriction_diag, Expr::var("eig_scan_i")),
),
Node::assign(
"eig_argmax",
Expr::select(
Expr::gt(Expr::var("eig_ri"), Expr::var("eig_max")),
Expr::var("eig_scan_i"),
Expr::var("eig_argmax"),
),
),
Node::assign(
"eig_max",
Expr::select(
Expr::gt(Expr::var("eig_ri"), Expr::var("eig_max")),
Expr::var("eig_ri"),
Expr::var("eig_max"),
),
),
],
),
Node::store(lambda, Expr::u32(0), Expr::var("eig_max")),
Node::loop_for(
"eig_write_j",
Expr::u32(0),
Expr::u32(cells),
vec![Node::store(
v,
Expr::var("eig_write_j"),
Expr::select(
Expr::eq(Expr::var("eig_write_j"), Expr::var("eig_argmax")),
one_fp.clone(),
Expr::u32(0),
),
)],
),
];
Program::wrapped(
vec![
BufferDecl::storage(restriction_diag, 0, BufferAccess::ReadOnly, DataType::U32)
.with_count(cells),
BufferDecl::storage(v, 1, BufferAccess::ReadWrite, DataType::U32).with_count(cells),
BufferDecl::storage(lambda, 2, BufferAccess::ReadWrite, DataType::U32).with_count(1),
BufferDecl::storage("one_fp_buf", 3, BufferAccess::ReadOnly, DataType::U32)
.with_count(1),
],
[1, 1, 1],
vec![Node::Region {
generator: Ident::from(OP_ID),
source_region: None,
body: Arc::new(vec![Node::Region {
generator: Ident::from(POWER_ITERATION_PHASE_OP_ID),
source_region: Some(GeneratorRef {
name: OP_ID.to_string(),
}),
body: Arc::new(vec![Node::if_then(
Expr::eq(Expr::InvocationId { axis: 0 }, Expr::u32(0)),
nodes,
)]),
}]),
}],
)
}
#[must_use]
#[cfg(any(test, feature = "cpu-parity"))]
pub fn cpu_ref(restriction_diag: &[f64], v_init: &[f64], iterations: u32) -> (f64, Vec<f64>) {
let mut v = Vec::new();
let mut v_next = Vec::new();
let lambda = try_cpu_ref_into(restriction_diag, v_init, iterations, &mut v, &mut v_next)
.expect("Fix: replace expect with fallible API or document caller precondition; panic only on programmer error - sheaf_laplacian_eigenvalue cpu_ref failed: invalid CPU buffers");
(lambda, v)
}
#[cfg(any(test, feature = "cpu-parity"))]
pub fn try_cpu_ref(
restriction_diag: &[f64],
v_init: &[f64],
iterations: u32,
) -> Result<(f64, Vec<f64>), String> {
let mut v = Vec::new();
let mut v_next = Vec::new();
let lambda = try_cpu_ref_into(restriction_diag, v_init, iterations, &mut v, &mut v_next)?;
Ok((lambda, v))
}
#[cfg(any(test, feature = "cpu-parity"))]
pub fn cpu_ref_into(
restriction_diag: &[f64],
v_init: &[f64],
iterations: u32,
v: &mut Vec<f64>,
v_next: &mut Vec<f64>,
) -> f64 {
try_cpu_ref_into(restriction_diag, v_init, iterations, v, v_next)
.expect("Fix: replace expect with fallible API or document caller precondition; panic only on programmer error - sheaf_laplacian_eigenvalue cpu_ref_into failed: invalid CPU buffers")
}
#[cfg(any(test, feature = "cpu-parity"))]
pub fn try_cpu_ref_into(
restriction_diag: &[f64],
v_init: &[f64],
iterations: u32,
v: &mut Vec<f64>,
v_next: &mut Vec<f64>,
) -> Result<f64, String> {
let _ = iterations;
if restriction_diag.len() < v_init.len() {
return Err(format!(
"sheaf_laplacian_eigenvalue CPU oracle restriction_diag too short: got {}, need {}.",
restriction_diag.len(),
v_init.len()
));
}
let len = v_init.len();
reserve_eigen_tmp(v, len, "eigenvector output")?;
reserve_eigen_tmp(v_next, len, "next-vector scratch")?;
v.clear();
v.resize(len, 0.0);
v_next.clear();
v_next.resize(len, 0.0);
let mut max_r = 0.0f64;
let mut argmax = 0usize;
for (i, &ri) in restriction_diag.iter().take(len).enumerate() {
if ri > max_r {
max_r = ri;
argmax = i;
}
}
if len > 0 {
v[argmax] = 1.0;
}
Ok(max_r)
}
#[cfg(any(test, feature = "cpu-parity"))]
fn reserve_eigen_tmp(out: &mut Vec<f64>, len: usize, name: &str) -> Result<(), String> {
if len > out.capacity() {
crate::graph::scratch::reserve_graph_items(
out,
len - out.len(),
"sheaf Laplacian eigenvalue CPU oracle",
name,
)?;
}
Ok(())
}
#[cfg(feature = "inventory-registry")]
inventory::submit! {
crate::harness::OpEntry::new(
OP_ID,
|| sheaf_laplacian_eigenvalue("r", "v", "l", 4, 1, 4),
Some(|| {
let to_bytes = |words: &[u32]| crate::wire::pack_u32_slice(words);
vec![vec![
to_bytes(&[0; 4]), to_bytes(&[0; 4]), to_bytes(&[0]), to_bytes(&[1u32 << 16]), ]]
}),
Some(|| {
let to_bytes = |words: &[u32]| crate::wire::pack_u32_slice(words);
vec![vec![
to_bytes(&[1u32 << 16, 0, 0, 0]), to_bytes(&[0]), ]]
}),
)
}
#[cfg(feature = "inventory-registry")]
inventory::submit! {
crate::harness::OpEntry::new(
POWER_ITERATION_PHASE_OP_ID,
|| {
Program::wrapped(
vec![
BufferDecl::storage("input", 0, BufferAccess::ReadOnly, DataType::U32)
.with_count(1),
BufferDecl::output("out", 1, DataType::U32).with_count(1),
],
[1, 1, 1],
vec![Node::Region {
generator: Ident::from(POWER_ITERATION_PHASE_OP_ID),
source_region: None,
body: Arc::new(vec![Node::store(
"out",
Expr::u32(0),
Expr::load("input", Expr::u32(0)),
)]),
}],
)
},
Some(|| {
let to_bytes = |words: &[u32]| crate::wire::pack_u32_slice(words);
vec![vec![to_bytes(&[11]), to_bytes(&[0])]]
}),
Some(|| {
let to_bytes = |words: &[u32]| crate::wire::pack_u32_slice(words);
vec![vec![to_bytes(&[11])]]
}),
)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn cpu_ref_diagonal_max() {
let r = vec![1.0, 2.0, 5.0, 3.0];
let v = vec![1.0, 1.0, 1.0, 1.0];
let (lambda, vec_final) = cpu_ref(&r, &v, 20);
assert_eq!(lambda, 5.0);
assert_eq!(vec_final, vec![0.0, 0.0, 1.0, 0.0]);
}
#[test]
fn cpu_ref_uniform() {
let r = vec![2.0, 2.0];
let v = vec![1.0, 0.0];
let (lambda, vec_final) = cpu_ref(&r, &v, 5);
assert_eq!(lambda, 2.0);
assert_eq!(vec_final, vec![1.0, 0.0]);
}
#[test]
fn cpu_ref_zero() {
let r = vec![0.0, 0.0];
let v = vec![1.0, 1.0];
let (lambda, vec_final) = cpu_ref(&r, &v, 5);
assert_eq!(lambda, 0.0);
assert_eq!(vec_final, vec![1.0, 0.0]);
}
#[test]
fn cpu_ref_single() {
let r = vec![42.0];
let v = vec![1.0];
let (lambda, vec_final) = cpu_ref(&r, &v, 1);
assert_eq!(lambda, 42.0);
assert_eq!(vec_final, vec![1.0]);
}
#[test]
fn cpu_ref_asymmetric() {
let r = vec![1.0, 10.0, 0.1];
let v = vec![1.0, 1.0, 1.0];
let (lambda, vec_final) = cpu_ref(&r, &v, 10);
assert_eq!(lambda, 10.0);
assert_eq!(vec_final, vec![0.0, 1.0, 0.0]);
}
#[test]
fn cpu_ref_is_iteration_independent() {
let r = vec![1.0, 7.0, 3.0, 2.0];
let v = vec![1.0, 1.0, 1.0, 1.0];
let (lambda_1, vec_1) = cpu_ref(&r, &v, 1);
let (lambda_50, vec_50) = cpu_ref(&r, &v, 50);
assert_eq!(lambda_1, 7.0);
assert_eq!(lambda_1, lambda_50);
assert_eq!(vec_1, vec_50);
assert_eq!(vec_1, vec![0.0, 1.0, 0.0, 0.0]);
}
#[test]
fn cpu_ref_into_reuses_vectors_and_truncates_stale_tail() {
let r = vec![1.0, 2.0, 5.0, 3.0];
let init = vec![1.0, 1.0, 1.0, 1.0];
let mut v = Vec::with_capacity(8);
let mut next = Vec::with_capacity(8);
v.extend([99.0; 8]);
next.extend([99.0; 8]);
let v_ptr = v.as_ptr();
let next_ptr = next.as_ptr();
let lambda = try_cpu_ref_into(&r, &init, 20, &mut v, &mut next).unwrap();
assert_eq!(lambda, 5.0);
assert_eq!(v, vec![0.0, 0.0, 1.0, 0.0]);
assert_eq!(v.len(), init.len());
assert_eq!(next.len(), init.len());
assert_eq!(v.as_ptr(), v_ptr);
assert_eq!(next.as_ptr(), next_ptr);
}
#[test]
fn generated_cpu_ref_matches_independent_arg_max() {
for case in 0..48 {
let n = 1 + (case % 8);
let restriction: Vec<f64> = (0..n)
.map(|idx| 0.5 + ((idx * 7 + case * 3) % 11) as f64 * 0.25)
.collect();
let init: Vec<f64> = vec![1.0; n];
let iterations = 1 + (case % 8) as u32;
let mut v = Vec::with_capacity(n + 3);
let mut next = Vec::with_capacity(n + 3);
let lambda =
try_cpu_ref_into(&restriction, &init, iterations, &mut v, &mut next).unwrap();
let (expected_lambda, expected_arg) = independent_diagonal_dominant(&restriction);
assert_eq!(lambda, expected_lambda, "case {case}: lambda");
for (idx, &value) in v.iter().enumerate() {
let want = if idx == expected_arg { 1.0 } else { 0.0 };
assert_eq!(value, want, "case {case} idx {idx}: eigenvector entry");
}
}
}
#[test]
fn try_cpu_ref_rejects_short_restriction_diag() {
let err = try_cpu_ref(&[1.0], &[1.0, 2.0], 1).unwrap_err();
assert!(err.contains("restriction_diag too short"), "{err}");
}
#[test]
fn program_buffer_count() {
let p = sheaf_laplacian_eigenvalue("r", "v", "l", 4, 1, 4);
assert_eq!(p.buffers.len(), 4);
let writable = p
.buffers
.iter()
.filter(|b| b.access() == BufferAccess::ReadWrite)
.count();
assert_eq!(writable, 2, "only v and lambda are writable outputs");
}
fn independent_diagonal_dominant(restriction_diag: &[f64]) -> (f64, usize) {
let mut max_r = 0.0f64;
let mut arg = 0usize;
for (idx, &value) in restriction_diag.iter().enumerate() {
if value > max_r {
max_r = value;
arg = idx;
}
}
(max_r, arg)
}
}