use crate::decomposition_advice::community::{SimilarityEdge, build_adjacency};
use thiserror::Error;
#[derive(Clone, Copy, Debug)]
pub struct EdgeInput {
pub left: usize,
pub right: usize,
pub weight: u64,
}
#[derive(Clone, Debug, Eq, Error, PartialEq)]
pub enum AdjacencyError {
#[error("edge {index}: left ({left}) must be less than right ({right})")]
NonCanonicalEdge {
index: usize,
left: usize,
right: usize,
},
#[error("edge {index}: right ({right}) is out of range for node_count {node_count}")]
EndpointOutOfRange {
index: usize,
right: usize,
node_count: usize,
},
#[error("edge {index}: weight must be positive")]
ZeroWeight {
index: usize,
},
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct AdjacencyReport {
node_count: usize,
neighbours: Vec<Vec<(usize, u64)>>,
}
impl AdjacencyReport {
#[must_use]
pub fn node_count(&self) -> usize {
self.node_count
}
#[must_use]
pub fn neighbours_of(&self, node: usize) -> Option<&[(usize, u64)]> {
self.neighbours.get(node).map(Vec::as_slice)
}
#[must_use]
pub fn all_indices_in_bounds(&self) -> bool {
self.neighbours.iter().all(|bucket| {
bucket
.iter()
.all(|&(neighbour, _)| neighbour < self.node_count)
})
}
#[must_use]
pub fn is_symmetric(&self) -> bool {
self.neighbours.iter().enumerate().all(|(node, bucket)| {
bucket
.iter()
.all(|&(neighbour, weight)| has_mirror(&self.neighbours, neighbour, node, weight))
})
}
#[must_use]
pub fn is_sorted(&self) -> bool {
self.neighbours
.iter()
.all(|bucket| bucket.windows(2).all(|pair| pair[0].0 <= pair[1].0))
}
}
#[expect(
clippy::unnecessary_map_or,
reason = "Keep the explicit non-panicking fallback requested in review."
)]
fn has_mirror(
neighbours: &[Vec<(usize, u64)>],
neighbour: usize,
node: usize,
weight: u64,
) -> bool {
debug_assert!(
neighbour < neighbours.len(),
"has_mirror: neighbour index out of bounds - callers (e.g. adjacency_report) must guarantee valid indices"
);
neighbours.get(neighbour).map_or(false, |list| {
list.iter()
.any(|&(mirror, mirror_weight)| mirror == node && mirror_weight == weight)
})
}
pub fn adjacency_report(
node_count: usize,
edges: &[EdgeInput],
) -> Result<AdjacencyReport, AdjacencyError> {
let similarity_edges = validate_edges(node_count, edges)?;
let neighbours = build_adjacency(node_count, &similarity_edges);
Ok(AdjacencyReport {
node_count,
neighbours,
})
}
pub(crate) fn validate_edges(
node_count: usize,
edges: &[EdgeInput],
) -> Result<Vec<SimilarityEdge>, AdjacencyError> {
let mut result = Vec::with_capacity(edges.len());
for (index, edge) in edges.iter().enumerate() {
if edge.left >= edge.right {
return Err(AdjacencyError::NonCanonicalEdge {
index,
left: edge.left,
right: edge.right,
});
}
if edge.right >= node_count {
return Err(AdjacencyError::EndpointOutOfRange {
index,
right: edge.right,
node_count,
});
}
if edge.weight == 0 {
return Err(AdjacencyError::ZeroWeight { index });
}
result.push(SimilarityEdge::new(edge.left, edge.right, edge.weight));
}
Ok(result)
}
#[cfg(test)]
mod tests {
use super::{AdjacencyError, EdgeInput, adjacency_report};
#[test]
fn adjacency_report_rejects_non_canonical_edges_with_structured_error() {
let result = adjacency_report(
3,
&[EdgeInput {
left: 1,
right: 1,
weight: 7,
}],
);
assert_eq!(
result,
Err(AdjacencyError::NonCanonicalEdge {
index: 0,
left: 1,
right: 1,
})
);
}
#[test]
fn adjacency_report_rejects_out_of_range_endpoints_with_structured_error() {
let result = adjacency_report(
3,
&[EdgeInput {
left: 1,
right: 3,
weight: 7,
}],
);
assert_eq!(
result,
Err(AdjacencyError::EndpointOutOfRange {
index: 0,
right: 3,
node_count: 3,
})
);
}
#[test]
fn adjacency_report_rejects_zero_weight_with_structured_error() {
let result = adjacency_report(
3,
&[EdgeInput {
left: 0,
right: 2,
weight: 0,
}],
);
assert_eq!(result, Err(AdjacencyError::ZeroWeight { index: 0 }));
}
}