use crate::flow::{EntropyFlow, FlowNetwork};
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
pub struct HodgeDecomposition {
pub exact: Vec<EntropyFlow>,
pub harmonic: Vec<EntropyFlow>,
pub coexact: Vec<EntropyFlow>,
}
impl HodgeDecomposition {
pub fn decompose(network: &FlowNetwork) -> Self {
let n = network.modules.len();
if n == 0 || network.flows.is_empty() {
return Self {
exact: vec![],
harmonic: vec![],
coexact: vec![],
};
}
let idx: std::collections::HashMap<&str, usize> = network
.modules
.iter()
.enumerate()
.map(|(i, m)| (m.as_str(), i))
.collect();
let mut divergence = vec![0.0; n];
for f in &network.flows {
if let Some(&j) = idx.get(f.sink.as_str()) {
divergence[j] += f.rate;
}
if let Some(&i) = idx.get(f.source.as_str()) {
divergence[i] -= f.rate;
}
}
let laplacian = network.laplacian();
let phi = solve_laplacian_jacobi(&laplacian, &divergence, n, 1000);
let mut exact = Vec::new();
for f in &network.flows {
if let (Some(&i), Some(&j)) = (idx.get(f.source.as_str()), idx.get(f.sink.as_str())) {
let grad_flow = phi[j] - phi[i];
if grad_flow.abs() > 1e-15 {
exact.push(EntropyFlow {
source: f.source.clone(),
sink: f.sink.clone(),
rate: grad_flow,
});
}
}
}
let mut residual: Vec<EntropyFlow> = network.flows.iter().map(|f| f.clone()).collect();
for e in &exact {
for r in &mut residual {
if r.source == e.source && r.sink == e.sink {
r.rate -= e.rate;
}
}
}
let mut harmonic = Vec::new();
let mut coexact = Vec::new();
let mut res_div = vec![0.0; n];
for r in &residual {
if let Some(&j) = idx.get(r.sink.as_str()) {
res_div[j] += r.rate;
}
if let Some(&i) = idx.get(r.source.as_str()) {
res_div[i] -= r.rate;
}
}
for r in &residual {
let div_source = idx.get(r.source.as_str()).map(|&i| res_div[i]).unwrap_or(0.0);
let div_sink = idx.get(r.sink.as_str()).map(|&j| res_div[j]).unwrap_or(0.0);
if div_source.abs() < 1e-10 && div_sink.abs() < 1e-10 {
harmonic.push(r.clone());
} else {
coexact.push(r.clone());
}
}
Self {
exact,
harmonic,
coexact,
}
}
pub fn reconstruct(&self) -> Vec<EntropyFlow> {
let mut combined = Vec::new();
combined.extend(self.exact.iter().cloned());
combined.extend(self.harmonic.iter().cloned());
combined.extend(self.coexact.iter().cloned());
let mut merged: std::collections::HashMap<(String, String), f64> = std::collections::HashMap::new();
for f in &combined {
*merged.entry((f.source.clone(), f.sink.clone())).or_default() += f.rate;
}
merged
.into_iter()
.map(|((source, sink), rate)| EntropyFlow { source, sink, rate })
.collect()
}
}
fn solve_laplacian_jacobi(
laplacian: &ndarray::Array2<f64>,
div: &[f64],
n: usize,
max_iter: usize,
) -> Vec<f64> {
let mut phi = vec![0.0; n];
let reg = 1e-8;
for _ in 0..max_iter {
let mut new_phi = vec![0.0; n];
for i in 0..n {
let diag = laplacian[[i, i]] + reg;
if diag.abs() < 1e-15 {
new_phi[i] = phi[i];
continue;
}
let mut off_diag_sum = 0.0;
for j in 0..n {
if j != i {
off_diag_sum += laplacian[[i, j]] * phi[j];
}
}
new_phi[i] = (-div[i] - off_diag_sum) / diag;
}
let change: f64 = new_phi.iter().zip(phi.iter()).map(|(a, b)| (a - b).abs()).sum();
phi = new_phi;
if change < 1e-12 {
break;
}
}
phi
}
#[cfg(test)]
mod tests {
use super::*;
fn make_flow_network() -> FlowNetwork {
FlowNetwork::new(vec![
EntropyFlow { source: "A".into(), sink: "B".into(), rate: 2.0 },
EntropyFlow { source: "A".into(), sink: "C".into(), rate: 1.0 },
EntropyFlow { source: "B".into(), sink: "C".into(), rate: 2.0 },
EntropyFlow { source: "C".into(), sink: "A".into(), rate: 3.0 },
])
}
#[test]
fn hodge_decomposition_sums_to_original() {
let net = make_flow_network();
let hodge = HodgeDecomposition::decompose(&net);
let reconstructed = hodge.reconstruct();
let original = net.flows.clone();
let mut orig_map: std::collections::HashMap<(String, String), f64> = std::collections::HashMap::new();
for f in &original {
*orig_map.entry((f.source.clone(), f.sink.clone())).or_default() += f.rate;
}
let mut recon_map: std::collections::HashMap<(String, String), f64> = std::collections::HashMap::new();
for f in &reconstructed {
*recon_map.entry((f.source.clone(), f.sink.clone())).or_default() += f.rate;
}
for (edge, &rate) in &orig_map {
let recon_rate = recon_map.get(edge).copied().unwrap_or(0.0);
assert!(
(rate - recon_rate).abs() < 0.1,
"edge {:?}: original={}, reconstructed={}",
edge, rate, recon_rate
);
}
}
#[test]
fn hodge_empty_network() {
let net = FlowNetwork::new(vec![]);
let hodge = HodgeDecomposition::decompose(&net);
assert!(hodge.exact.is_empty());
assert!(hodge.harmonic.is_empty());
assert!(hodge.coexact.is_empty());
}
#[test]
fn hodge_cycle_has_harmonic() {
let net = FlowNetwork::new(vec![
EntropyFlow { source: "A".into(), sink: "B".into(), rate: 1.0 },
EntropyFlow { source: "B".into(), sink: "C".into(), rate: 1.0 },
EntropyFlow { source: "C".into(), sink: "A".into(), rate: 1.0 },
]);
let hodge = HodgeDecomposition::decompose(&net);
let harmonic_flow: f64 = hodge.harmonic.iter().map(|f| f.rate.abs()).sum();
assert!(harmonic_flow > 0.5, "cycle should have significant harmonic component");
}
#[test]
fn hodge_components_non_negative_flows() {
let net = make_flow_network();
let hodge = HodgeDecomposition::decompose(&net);
assert!(!hodge.exact.is_empty() || !hodge.harmonic.is_empty() || !hodge.coexact.is_empty());
}
}