use crate::algorithms::four_d::GraphNode4D;
use std::collections::{HashMap, VecDeque};
#[derive(Debug, Clone)]
pub struct RicciEdge {
pub src: u64,
pub dst: u64,
pub curvature: f32,
pub w1: f32,
}
pub fn ollivier_ricci(nodes: &[GraphNode4D], alpha: f32) -> Vec<RicciEdge> {
let node_map: HashMap<u64, &GraphNode4D> = nodes.iter().map(|n| (n.id, n)).collect();
let mut results = Vec::new();
for node in nodes {
let u = node.id;
for edge in &node.successors {
let v = edge.dst;
if v <= u {
continue; }
let dist_u = bfs_dist(&node_map, u);
let d_uv = match dist_u.get(&v) {
Some(&d) if d > 0.0 => d,
_ => continue,
};
let mu = build_measure(&node_map, u, alpha);
let nu = build_measure(&node_map, v, alpha);
let w1 = wasserstein1(&mu, &nu, &node_map);
let curvature = 1.0 - w1 / d_uv;
results.push(RicciEdge {
src: u,
dst: v,
curvature,
w1,
});
}
}
results
}
pub fn curvature_map(nodes: &[GraphNode4D], alpha: f32) -> HashMap<(u64, u64), f32> {
let mut map = HashMap::new();
for edge in ollivier_ricci(nodes, alpha) {
map.insert((edge.src, edge.dst), edge.curvature);
map.insert((edge.dst, edge.src), edge.curvature);
}
map
}
pub fn curvature_map_fast(nodes: &[GraphNode4D], min_edge_weight: f32) -> HashMap<(u64, u64), f32> {
let threshold = min_edge_weight.max(0.0);
let n = nodes.len();
if n == 0 {
return HashMap::new();
}
let blocks = n.div_ceil(64);
let id_to_idx: HashMap<u64, usize> = nodes
.iter()
.enumerate()
.map(|(i, node)| (node.id, i))
.collect();
let mut adj_bits: Vec<Vec<u64>> = vec![vec![0u64; blocks]; n];
for (i, node) in nodes.iter().enumerate() {
for e in &node.successors {
if e.weight < threshold {
continue;
}
if let Some(&j) = id_to_idx.get(&e.dst) {
if i != j {
let block = j / 64;
let bit = j % 64;
adj_bits[i][block] |= 1u64 << bit;
}
}
}
}
let degrees: Vec<usize> = adj_bits
.iter()
.map(|bits| bits.iter().map(|b| b.count_ones() as usize).sum())
.collect();
let mut map = HashMap::new();
for (i, node) in nodes.iter().enumerate() {
if degrees[i] == 0 {
continue;
}
for e in &node.successors {
if e.weight < threshold {
continue;
}
let Some(&j) = id_to_idx.get(&e.dst) else {
continue;
};
if i == j || degrees[j] == 0 {
continue;
}
let mut common = 0usize;
for (b, row_i) in adj_bits[i].iter().enumerate() {
common += (row_i & adj_bits[j][b]).count_ones() as usize;
}
let deg_sum = degrees[i] + degrees[j];
let kappa = if deg_sum == 0 {
0.0f32
} else {
2.0f32 * common as f32 / deg_sum as f32 - 1.0f32
};
map.insert((node.id, e.dst), kappa);
map.insert((e.dst, node.id), kappa);
}
}
map
}
fn build_measure(node_map: &HashMap<u64, &GraphNode4D>, id: u64, alpha: f32) -> Vec<(u64, f32)> {
let node = match node_map.get(&id) {
Some(n) => n,
None => return vec![(id, 1.0)],
};
let deg = node.successors.len();
if deg == 0 {
return vec![(id, 1.0)];
}
let mut measure = Vec::with_capacity(deg + 1);
measure.push((id, alpha));
let w = (1.0 - alpha) / deg as f32;
for e in &node.successors {
measure.push((e.dst, w));
}
measure
}
fn bfs_dist(node_map: &HashMap<u64, &GraphNode4D>, start: u64) -> HashMap<u64, f32> {
let mut dist = HashMap::new();
let mut queue = VecDeque::new();
dist.insert(start, 0.0_f32);
queue.push_back(start);
while let Some(cur) = queue.pop_front() {
let d = dist[&cur];
if let Some(node) = node_map.get(&cur) {
for edge in &node.successors {
if let std::collections::hash_map::Entry::Vacant(e) = dist.entry(edge.dst) {
e.insert(d + 1.0);
queue.push_back(edge.dst);
}
}
}
}
dist
}
fn wasserstein1(
mu: &[(u64, f32)],
nu: &[(u64, f32)],
node_map: &HashMap<u64, &GraphNode4D>,
) -> f32 {
let m = mu.len();
let n = nu.len();
let mut cost = vec![0.0_f32; m * n];
for (i, &(s, _)) in mu.iter().enumerate() {
let dists = bfs_dist(node_map, s);
for (j, &(t, _)) in nu.iter().enumerate() {
cost[i * n + j] = dists.get(&t).copied().unwrap_or(f32::INFINITY);
}
}
let supply: Vec<f32> = mu.iter().map(|&(_, w)| w).collect();
let demand: Vec<f32> = nu.iter().map(|&(_, w)| w).collect();
solve_transport(&supply, &demand, &cost, m, n)
}
fn solve_transport(supply: &[f32], demand: &[f32], cost: &[f32], m: usize, n: usize) -> f32 {
let max_finite = cost
.iter()
.filter(|&&c| c.is_finite())
.cloned()
.fold(1.0_f32, f32::max);
let big_m = max_finite * 10.0 * (m + n) as f32;
let cost_buf: Vec<f32> = cost
.iter()
.map(|&c| if c.is_finite() { c } else { big_m })
.collect();
let cost = cost_buf.as_slice();
let mut s = supply.to_vec();
let mut d = demand.to_vec();
let mut alloc = vec![0.0_f32; m * n];
let mut basic = vec![false; m * n];
let eps = 1e-8_f32;
let (mut ii, mut jj) = (0_usize, 0_usize);
while ii < m && jj < n {
let x = s[ii].min(d[jj]);
alloc[ii * n + jj] = x;
basic[ii * n + jj] = true;
s[ii] -= x;
d[jj] -= x;
let si_done = s[ii] < eps;
let dj_done = d[jj] < eps;
if si_done && dj_done {
ii += 1;
if ii < m && jj < n {
basic[ii * n + jj] = true;
}
jj += 1;
} else if si_done {
ii += 1;
} else {
jj += 1;
}
}
for _ in 0..50 {
let mut u = vec![f32::NAN; m];
let mut v = vec![f32::NAN; n];
u[0] = 0.0;
let mut changed = true;
while changed {
changed = false;
for i in 0..m {
for j in 0..n {
if !basic[i * n + j] {
continue;
}
let c = cost[i * n + j];
if !u[i].is_nan() && v[j].is_nan() {
v[j] = c - u[i];
changed = true;
} else if u[i].is_nan() && !v[j].is_nan() {
u[i] = c - v[j];
changed = true;
}
}
}
}
let mut best_rc = -1e-6_f32;
let (mut enter_i, mut enter_j) = (m, n); for i in 0..m {
for j in 0..n {
if basic[i * n + j] || u[i].is_nan() || v[j].is_nan() {
continue;
}
let rc = cost[i * n + j] - u[i] - v[j];
if rc < best_rc {
best_rc = rc;
enter_i = i;
enter_j = j;
}
}
}
if enter_i == m {
break; }
let tree_cells = find_tree_path(&basic, m, n, enter_i, enter_j);
if tree_cells.is_empty() {
break; }
let mut loop_cells = vec![(enter_i, enter_j)];
loop_cells.extend_from_slice(&tree_cells);
let mut theta = f32::INFINITY;
let mut leave_k = loop_cells.len(); for (k, &(i, j)) in loop_cells.iter().enumerate() {
if k % 2 == 0 {
continue;
}
let a = alloc[i * n + j];
if a < theta {
theta = a;
leave_k = k;
}
}
if leave_k == loop_cells.len() {
break;
}
for (k, &(i, j)) in loop_cells.iter().enumerate() {
if k % 2 == 0 {
alloc[i * n + j] += theta;
basic[i * n + j] = true;
} else {
alloc[i * n + j] -= theta;
if k == leave_k {
alloc[i * n + j] = alloc[i * n + j].max(0.0);
basic[i * n + j] = false;
}
}
}
}
(0..m * n).map(|k| alloc[k] * cost[k]).sum()
}
fn find_tree_path(
basic: &[bool],
m: usize,
n: usize,
start_row: usize,
end_col: usize,
) -> Vec<(usize, usize)> {
let total = m + n;
let end_node = m + end_col;
let mut visited = vec![false; total];
let mut parent: Vec<Option<usize>> = vec![None; total];
visited[start_row] = true;
let mut queue = VecDeque::from([start_row]);
while let Some(cur) = queue.pop_front() {
if cur == end_node {
let mut edge_pairs = Vec::new();
let mut node = cur;
while let Some(par) = parent[node] {
let (row, col) = if par < m {
(par, node - m) } else {
(node, par - m) };
edge_pairs.push((row, col));
node = par;
}
edge_pairs.reverse();
return edge_pairs;
}
if cur < m {
for j in 0..n {
let next = m + j;
if !visited[next] && basic[cur * n + j] {
visited[next] = true;
parent[next] = Some(cur);
queue.push_back(next);
}
}
} else {
let j = cur - m;
for i in 0..m {
if !visited[i] && basic[i * n + j] {
visited[i] = true;
parent[i] = Some(cur);
queue.push_back(i);
}
}
}
}
Vec::new() }
#[cfg(test)]
mod tests {
use super::*;
use crate::algorithms::four_d::{GraphNode4D, GraphProperties, TemporalEdge};
fn node(id: u64, neighbors: &[u64]) -> GraphNode4D {
GraphNode4D {
id,
x: id as f32,
y: 0.0,
z: 0.0,
begin_ts: 0,
end_ts: 0,
properties: GraphProperties::default(),
successors: neighbors
.iter()
.map(|&dst| TemporalEdge {
dst,
weight: 1.0,
begin_ts: 0,
end_ts: 0,
})
.collect(),
}
}
fn find_edge(edges: &[RicciEdge], src: u64, dst: u64) -> Option<&RicciEdge> {
edges.iter().find(|e| e.src == src && e.dst == dst)
}
#[test]
fn test_ricci_two_node_complete_curvature_one() {
let nodes = vec![node(0, &[1]), node(1, &[0])];
let edges = ollivier_ricci(&nodes, 0.5);
let e = find_edge(&edges, 0, 1).expect("edge (0,1) missing");
assert!(
(e.curvature - 1.0).abs() < 1e-5,
"expected κ=1, got {}",
e.curvature
);
assert!(e.w1.abs() < 1e-5, "expected W₁=0, got {}", e.w1);
}
#[test]
fn test_ricci_triangle_positive_curvature() {
let nodes = vec![node(0, &[1, 2]), node(1, &[0, 2]), node(2, &[0, 1])];
let edges = ollivier_ricci(&nodes, 0.5);
let e = find_edge(&edges, 0, 1).expect("edge (0,1) missing");
assert!(
(e.w1 - 0.25).abs() < 1e-4,
"triangle W₁ expected 0.25, got {}",
e.w1
);
assert!(
(e.curvature - 0.75).abs() < 1e-4,
"triangle κ expected 0.75, got {}",
e.curvature
);
}
#[test]
fn test_ricci_path_graph_middle_edge_zero() {
let nodes = vec![
node(0, &[1]),
node(1, &[0, 2]),
node(2, &[1, 3]),
node(3, &[2]),
];
let edges = ollivier_ricci(&nodes, 0.5);
let e = find_edge(&edges, 1, 2).expect("edge (1,2) missing");
assert!(
e.curvature.abs() < 1e-4,
"path middle edge κ expected 0, got {}",
e.curvature
);
}
#[test]
fn test_ricci_c4_cycle_half_curvature() {
let nodes = vec![
node(0, &[1, 2]),
node(1, &[0, 3]),
node(2, &[0, 3]),
node(3, &[1, 2]),
];
let edges = ollivier_ricci(&nodes, 0.5);
for e in &edges {
assert!(
(e.curvature - 0.5).abs() < 1e-4,
"C4 edge ({},{}) κ expected 0.5, got {}",
e.src,
e.dst,
e.curvature
);
}
assert_eq!(edges.len(), 4, "C4 should have 4 undirected edges");
}
#[test]
fn test_ricci_star_graph_negative_curvature() {
let nodes = vec![
node(0, &[1, 2, 3, 4]),
node(1, &[0]),
node(2, &[0]),
node(3, &[0]),
node(4, &[0]),
];
let edges = ollivier_ricci(&nodes, 0.0);
assert_eq!(edges.len(), 4);
for e in &edges {
assert!(
e.curvature.abs() < 1e-4,
"star edge ({},{}) expected κ≈0 with α=0, got {}",
e.src,
e.dst,
e.curvature
);
}
}
#[test]
fn test_ricci_fully_lazy_curvature_zero() {
let nodes = vec![node(0, &[1, 2]), node(1, &[0, 2]), node(2, &[0, 1])];
let edges = ollivier_ricci(&nodes, 1.0);
for e in &edges {
assert!(
e.curvature.abs() < 1e-4,
"fully lazy edge ({},{}) expected κ=0, got {}",
e.src,
e.dst,
e.curvature
);
}
}
}