use serde::{Deserialize, Serialize};
use crate::obstruction::compute_direct_obstruction;
use crate::room_graph::RoomGraph;
use crate::sheaf_state::SheafSections;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ReconciliationResult {
pub converged: bool,
pub rounds: usize,
pub final_h1: f64,
pub history: Vec<crate::gossip_schedule::GossipRound>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub enum ReconciliationStrategy {
Averaging { weight: f64 },
WeightedMerge,
MajorityVote { tolerance: f64 },
}
impl Default for ReconciliationStrategy {
fn default() -> Self {
ReconciliationStrategy::Averaging { weight: 0.5 }
}
}
fn reconcile_averaging_round(
sections: &mut SheafSections,
graph: &RoomGraph,
weight: f64,
) -> usize {
let room_ids = sections.room_ids();
let mut updates: Vec<(String, Vec<f64>)> = Vec::new();
let mut messages_sent = 0;
for room_id in &room_ids {
let neighbors: Vec<&str> = graph.edges.iter()
.filter_map(|(u, v, _)| {
if u == room_id { Some(v.as_str()) }
else if v == room_id { Some(u.as_str()) }
else { None }
})
.collect();
if neighbors.is_empty() {
continue;
}
if let Some(my_section) = sections.get(room_id) {
let my_data = my_section.data.clone();
let dim = my_data.len();
let mut neighbor_sum = vec![0.0; dim];
let mut neighbor_count = 0usize;
for neighbor_id in &neighbors {
if let Some(neighbor) = sections.get(neighbor_id) {
for (i, v) in neighbor.data.iter().enumerate().take(dim) {
neighbor_sum[i] += v;
}
neighbor_count += 1;
messages_sent += 1;
}
}
if neighbor_count > 0 {
let avg: Vec<f64> = neighbor_sum.iter()
.map(|s| s / neighbor_count as f64)
.collect();
let blended: Vec<f64> = my_data.iter().zip(avg.iter())
.map(|(old, avg)| (1.0 - weight) * old + weight * avg)
.collect();
updates.push((room_id.clone(), blended));
}
}
}
for (room_id, new_data) in updates {
if let Some(section) = sections.get_mut(&room_id) {
section.data = new_data;
}
}
messages_sent
}
fn reconcile_weighted_merge_round(
sections: &mut SheafSections,
graph: &RoomGraph,
) -> usize {
let room_ids = sections.room_ids();
let mut updates: Vec<(String, Vec<f64>)> = Vec::new();
let mut messages_sent = 0;
for room_id in &room_ids {
let edges: Vec<(&str, f64)> = graph.edges.iter()
.filter_map(|(u, v, w)| {
if u == room_id { Some((v.as_str(), *w)) }
else if v == room_id { Some((u.as_str(), *w)) }
else { None }
})
.collect();
if edges.is_empty() {
continue;
}
let total_weight: f64 = edges.iter().map(|(_, w)| w).sum();
if total_weight < 1e-15 {
continue;
}
if let Some(my_section) = sections.get(room_id) {
let dim = my_section.data.len();
let mut blended = vec![0.0; dim];
for (neighbor_id, edge_weight) in &edges {
if let Some(neighbor) = sections.get(neighbor_id) {
let frac = edge_weight / total_weight;
for (i, v) in neighbor.data.iter().enumerate().take(dim) {
blended[i] += frac * v;
}
messages_sent += 1;
}
}
updates.push((room_id.clone(), blended));
}
}
for (room_id, new_data) in updates {
if let Some(section) = sections.get_mut(&room_id) {
section.data = new_data;
}
}
messages_sent
}
fn reconcile_majority_vote_round(
sections: &mut SheafSections,
graph: &RoomGraph,
tolerance: f64,
) -> usize {
let room_ids = sections.room_ids();
let mut updates: Vec<(String, Vec<f64>)> = Vec::new();
let mut messages_sent = 0;
for room_id in &room_ids {
let neighbors: Vec<&str> = graph.edges.iter()
.filter_map(|(u, v, _)| {
if u == room_id { Some(v.as_str()) }
else if v == room_id { Some(u.as_str()) }
else { None }
})
.collect();
if neighbors.is_empty() {
continue;
}
if let Some(my_section) = sections.get(room_id) {
let dim = my_section.data.len();
let mut result = vec![0.0; dim];
for i in 0..dim {
let my_val = my_section.data[i];
let mut votes: Vec<f64> = vec![my_val];
for neighbor_id in &neighbors {
if let Some(neighbor) = sections.get(neighbor_id) {
if i < neighbor.data.len() {
votes.push(neighbor.data[i]);
messages_sent += 1;
}
}
}
result[i] = majority_value(&votes, tolerance);
}
updates.push((room_id.clone(), result));
}
}
for (room_id, new_data) in updates {
if let Some(section) = sections.get_mut(&room_id) {
section.data = new_data;
}
}
messages_sent
}
fn majority_value(values: &[f64], tolerance: f64) -> f64 {
let mut best_val = values[0];
let mut best_count = 0usize;
for v in values {
let count = values.iter().filter(|u| (*u - v).abs() <= tolerance).count();
if count > best_count {
best_count = count;
best_val = *v;
}
}
best_val
}
pub fn reconcile(
sections: &mut SheafSections,
graph: &RoomGraph,
strategy: &ReconciliationStrategy,
max_rounds: usize,
convergence_threshold: f64,
) -> ReconciliationResult {
let mut history = Vec::new();
for round in 1..=max_rounds {
let h1_before = compute_direct_obstruction(sections, graph).magnitude;
let messages_sent = match strategy {
ReconciliationStrategy::Averaging { weight } => {
reconcile_averaging_round(sections, graph, *weight)
}
ReconciliationStrategy::WeightedMerge => {
reconcile_weighted_merge_round(sections, graph)
}
ReconciliationStrategy::MajorityVote { tolerance } => {
reconcile_majority_vote_round(sections, graph, *tolerance)
}
};
let h1_after = compute_direct_obstruction(sections, graph).magnitude;
history.push(crate::gossip_schedule::GossipRound {
round,
messages_sent,
h1_before,
h1_after,
});
if h1_after < convergence_threshold {
return ReconciliationResult {
converged: true,
rounds: round,
final_h1: h1_after,
history,
};
}
}
let final_h1 = compute_direct_obstruction(sections, graph).magnitude;
ReconciliationResult {
converged: final_h1 < convergence_threshold,
rounds: max_rounds,
final_h1,
history,
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::sheaf_state::RoomSection;
#[test]
fn averaging_two_rooms_converges() {
let mut sections = SheafSections::new(vec![
RoomSection::new("A", vec![0.0]),
RoomSection::new("B", vec![2.0]),
]);
let graph = RoomGraph::new(
vec!["A".into(), "B".into()],
vec![("A".into(), "B".into(), 1.0)],
);
let result = reconcile(
&mut sections,
&graph,
&ReconciliationStrategy::Averaging { weight: 0.5 },
100,
1e-10,
);
assert!(result.converged);
assert!(result.final_h1 < 1e-6);
}
#[test]
fn averaging_reduces_h1_each_round() {
let mut sections = SheafSections::new(vec![
RoomSection::new("A", vec![0.0]),
RoomSection::new("B", vec![10.0]),
]);
let graph = RoomGraph::new(
vec!["A".into(), "B".into()],
vec![("A".into(), "B".into(), 1.0)],
);
let result = reconcile(
&mut sections,
&graph,
&ReconciliationStrategy::Averaging { weight: 0.5 },
50,
1e-10,
);
for round in &result.history {
assert!(round.h1_after <= round.h1_before + 1e-10);
}
}
#[test]
fn weighted_merge_converges() {
let mut sections = SheafSections::new(vec![
RoomSection::new("A", vec![0.0]),
RoomSection::new("B", vec![4.0]),
RoomSection::new("C", vec![8.0]),
]);
let graph = RoomGraph::new(
vec!["A".into(), "B".into(), "C".into()],
vec![
("A".into(), "B".into(), 1.0),
("B".into(), "C".into(), 1.0),
("A".into(), "C".into(), 0.5),
],
);
let result = reconcile(
&mut sections,
&graph,
&ReconciliationStrategy::WeightedMerge,
100,
1e-6,
);
assert!(result.converged);
}
#[test]
fn majority_vote_discrete_data() {
let mut sections = SheafSections::new(vec![
RoomSection::new("A", vec![1.0]),
RoomSection::new("B", vec![1.0]),
RoomSection::new("C", vec![2.0]),
RoomSection::new("D", vec![1.0]),
]);
let graph = RoomGraph::new(
vec!["A".into(), "B".into(), "C".into(), "D".into()],
vec![
("A".into(), "B".into(), 1.0),
("B".into(), "C".into(), 1.0),
("C".into(), "D".into(), 1.0),
("D".into(), "A".into(), 1.0),
],
);
let result = reconcile(
&mut sections,
&graph,
&ReconciliationStrategy::MajorityVote { tolerance: 0.5 },
10,
1e-10,
);
assert!(result.converged);
assert!((sections.get("A").unwrap().data[0] - 1.0).abs() < 0.5);
assert!((sections.get("C").unwrap().data[0] - 1.0).abs() < 0.5);
}
#[test]
fn disconnected_graph_never_converges() {
let mut sections = SheafSections::new(vec![
RoomSection::new("A", vec![0.0]),
RoomSection::new("B", vec![10.0]),
]);
let graph = RoomGraph::new(
vec!["A".into(), "B".into()],
vec![],
);
let result = reconcile(
&mut sections,
&graph,
&ReconciliationStrategy::Averaging { weight: 0.5 },
50,
1e-6,
);
assert!(result.converged); }
#[test]
fn disconnected_components_different_values_no_reconciliation() {
let mut sections = SheafSections::new(vec![
RoomSection::new("A1", vec![0.0]),
RoomSection::new("A2", vec![0.0]),
RoomSection::new("B1", vec![10.0]),
RoomSection::new("B2", vec![10.0]),
]);
let graph = RoomGraph::new(
vec!["A1".into(), "A2".into(), "B1".into(), "B2".into()],
vec![
("A1".into(), "A2".into(), 1.0),
("B1".into(), "B2".into(), 1.0),
],
);
let result = reconcile(
&mut sections,
&graph,
&ReconciliationStrategy::Averaging { weight: 0.5 },
50,
1e-6,
);
assert!(result.converged);
}
}