use vyre_primitives::math::mori_zwanzig::mz_project_step_cpu_into;
#[derive(Debug, Default)]
pub struct RegionCoarsenScratch {
cluster_sizes: Vec<u32>,
projection: Vec<f64>,
coarse_state: Vec<f64>,
}
impl RegionCoarsenScratch {
#[must_use]
pub fn new() -> Self {
Self::default()
}
#[cfg(test)]
fn projection_ptr(&self) -> *const f64 {
self.projection.as_ptr()
}
}
#[must_use]
pub fn cluster_projection_matrix(assignments: &[u32], n: u32, k: u32) -> Vec<f64> {
let mut scratch = RegionCoarsenScratch::new();
cluster_projection_matrix_into(assignments, n, k, &mut scratch).to_vec()
}
#[must_use]
pub fn cluster_projection_matrix_into<'a>(
assignments: &[u32],
n: u32,
k: u32,
scratch: &'a mut RegionCoarsenScratch,
) -> &'a [f64] {
use crate::observability::{bump, mori_zwanzig_region_coarsen_calls};
bump(&mori_zwanzig_region_coarsen_calls);
assert!(n > 0);
assert!(k > 0);
assert_eq!(assignments.len(), n as usize);
let n = n as usize;
let k = k as usize;
scratch.cluster_sizes.clear();
scratch.cluster_sizes.resize(k, 0);
for &c in assignments {
assert!(
(c as usize) < k,
"Fix: assignment {c} exceeds cluster count {k}."
);
scratch.cluster_sizes[c as usize] += 1;
}
scratch.projection.clear();
scratch.projection.resize(n * n, 0.0);
for i in 0..n {
let ci = assignments[i] as usize;
let size = scratch.cluster_sizes[ci] as f64;
if size == 0.0 {
continue;
}
let inv = 1.0 / size;
#[allow(clippy::needless_range_loop)]
for j in 0..n {
if assignments[j] as usize == ci {
scratch.projection[i * n + j] = inv;
}
}
}
&scratch.projection
}
#[must_use]
pub fn coarsen_region_state(p_matrix: &[f64], state: &[f64], n: u32) -> Vec<f64> {
let mut out = Vec::new();
coarsen_region_state_into(p_matrix, state, n, &mut out);
out
}
pub fn coarsen_region_state_into(p_matrix: &[f64], state: &[f64], n: u32, out: &mut Vec<f64>) {
use crate::observability::{bump, mori_zwanzig_region_coarsen_calls};
bump(&mori_zwanzig_region_coarsen_calls);
mz_project_step_cpu_into(p_matrix, state, n, out);
}
#[must_use]
pub fn coarsen_via_clustering(state: &[f64], assignments: &[u32], n: u32, k: u32) -> Vec<f64> {
let mut scratch = RegionCoarsenScratch::new();
coarsen_via_clustering_into(state, assignments, n, k, &mut scratch).to_vec()
}
#[must_use]
pub fn coarsen_via_clustering_into<'a>(
state: &[f64],
assignments: &[u32],
n: u32,
k: u32,
scratch: &'a mut RegionCoarsenScratch,
) -> &'a [f64] {
let _ = cluster_projection_matrix_into(assignments, n, k, scratch);
let RegionCoarsenScratch {
projection,
coarse_state,
..
} = scratch;
mz_project_step_cpu_into(projection, state, n, coarse_state);
coarse_state
}
#[cfg(test)]
mod tests {
#![allow(clippy::identity_op, clippy::erasing_op)]
use super::*;
fn approx_eq(a: f64, b: f64) -> bool {
(a - b).abs() < 1e-9
}
#[test]
fn projection_matrix_normalizes_within_cluster() {
let assignments = vec![0u32, 0, 1, 1];
let p = cluster_projection_matrix(&assignments, 4, 2);
assert!(approx_eq(p[0], 0.5));
assert!(approx_eq(p[1], 0.5));
assert!(approx_eq(p[2], 0.0));
assert!(approx_eq(p[3], 0.0));
assert!(approx_eq(p[2 * 4 + 2], 0.5));
assert!(approx_eq(p[2 * 4 + 3], 0.5));
assert!(approx_eq(p[2 * 4 + 0], 0.0));
}
#[test]
fn coarsening_replaces_with_cluster_mean() {
let assignments = vec![0u32, 0, 1, 1];
let state = vec![10.0, 20.0, 100.0, 200.0];
let coarse = coarsen_via_clustering(&state, &assignments, 4, 2);
assert!(approx_eq(coarse[0], 15.0));
assert!(approx_eq(coarse[1], 15.0));
assert!(approx_eq(coarse[2], 150.0));
assert!(approx_eq(coarse[3], 150.0));
}
#[test]
fn singleton_clusters_preserve_state() {
let assignments = vec![0u32, 1, 2, 3];
let state = vec![10.0, 20.0, 30.0, 40.0];
let coarse = coarsen_via_clustering(&state, &assignments, 4, 4);
for (a, b) in state.iter().zip(coarse.iter()) {
assert!(approx_eq(*a, *b));
}
}
#[test]
fn single_global_cluster_yields_uniform_mean() {
let assignments = vec![0u32; 4];
let state = vec![10.0, 20.0, 30.0, 40.0];
let coarse = coarsen_via_clustering(&state, &assignments, 4, 1);
let mean = (10.0 + 20.0 + 30.0 + 40.0) / 4.0;
for v in coarse {
assert!(approx_eq(v, mean));
}
}
#[test]
#[should_panic(expected = "exceeds cluster count")]
fn rejects_out_of_range_assignment() {
let assignments = vec![0u32, 1, 5, 0];
let _ = cluster_projection_matrix(&assignments, 4, 2);
}
#[test]
fn coarsen_via_clustering_into_reuses_projection_storage() {
let assignments = vec![0u32, 0, 1, 1];
let state = vec![10.0, 20.0, 100.0, 200.0];
let mut scratch = RegionCoarsenScratch::new();
let first = coarsen_via_clustering_into(&state, &assignments, 4, 2, &mut scratch).to_vec();
let ptr = scratch.projection_ptr();
let second = coarsen_via_clustering_into(&state, &assignments, 4, 2, &mut scratch).to_vec();
assert!(approx_eq(first[0], 15.0));
assert_eq!(first, second);
assert_eq!(scratch.projection_ptr(), ptr);
}
}