use rayon::prelude::*;
use petgraph::{dot::Dot, Graph, Undirected};
use csv::WriterBuilder;
use serde_json;
use serde::{Deserialize, Serialize};
use std::{fs::File, io::Write, path::Path};
use petgraph::visit::EdgeRef;
use core::f64::NAN;
use petgraph::graph::NodeIndex;
use std::collections::HashMap;
use crate::dist::make_symmetrical;
use crate::error::NetviewError;
pub fn k_mutual_nearest_neighbors(distance_matrix: &Vec<Vec<f64>>, k: usize) -> Result<Vec<Vec<usize>>, NetviewError> {
let n = distance_matrix.len();
if n == 0 || distance_matrix.iter().any(|row| row.len() > n) {
return Err(NetviewError::InvalidMatrix);
}
if k == 0 || k >= n {
return Err(NetviewError::InvalidK);
}
let matrix = make_symmetrical(distance_matrix)?;
let nearest_neighbors: Vec<Vec<usize>> = (0..n).into_par_iter().map(|i| {
let mut neighbors = vec![];
for j in 0..n {
if i != j {
neighbors.push((j, matrix[i][j]));
}
}
neighbors.sort_by(|a, b| a.1.partial_cmp(&b.1).unwrap());
neighbors.into_iter().map(|(index, _)| index).take(k).collect::<Vec<usize>>()
}).collect();
let mutual_nearest_neighbors: Vec<Vec<usize>> = nearest_neighbors.iter().enumerate().map(|(i, neighbors)| {
neighbors.iter().filter(|&&j| nearest_neighbors[j].contains(&i)).cloned().collect()
}).collect();
Ok(mutual_nearest_neighbors)
}
pub fn convert_to_graph(mutual_nearest_neighbors: &Vec<Vec<usize>>, distance_matrix: Option<&Vec<Vec<f64>>>) -> Result<Graph<usize, f64, Undirected>, NetviewError> {
let mut graph = Graph::<usize, f64, Undirected>::new_undirected();
let mut index_map: HashMap<usize, NodeIndex> = HashMap::new();
for (node, _) in mutual_nearest_neighbors.iter().enumerate(){
let node_index = graph.add_node(node);
index_map.insert(node, node_index);
}
for (node, neighbors) in mutual_nearest_neighbors.iter().enumerate() {
let node_index = *index_map.get(&node).ok_or(NetviewError::NodeIndexError)?;
for &neighbor in neighbors.iter() {
let distance = match distance_matrix {
Some(matrix) => matrix.get(node).and_then(|row| row.get(neighbor)).copied().unwrap_or(1.0), None => 1.0, };
let neighbor_index = *index_map.get(&neighbor).ok_or(NetviewError::NodeIndexError)?;
graph.add_edge(node_index, neighbor_index, distance);
}
}
Ok(graph)
}
#[derive(Serialize, Deserialize, Clone, Debug, clap::ValueEnum)]
pub enum GraphFormat {
Dot,
Json,
Adjacency,
}
pub fn write_graph_to_file<N, E>(
graph: &Graph<N, E, Undirected>,
path: &Path,
format: &GraphFormat,
) -> Result<(), NetviewError>
where
N: Serialize + std::fmt::Debug,
E: Serialize + Into<f64> + std::clone::Clone + std::fmt::Debug,
{
let mut file = File::create(path).map_err(|e| NetviewError::GraphFileError(e.to_string()))?;
match format {
GraphFormat::Dot => {
let dot = Dot::with_config(graph, &[petgraph::dot::Config::EdgeNoLabel]);
write!(file, "{:?}", dot).map_err(|e| NetviewError::GraphFileError(e.to_string()))?;
},
GraphFormat::Json => {
let adj_matrix = graph_to_adjacency_matrix(graph, false)?;
serde_json::to_writer(&file, &adj_matrix).map_err(|e| NetviewError::GraphSerializationError(e.to_string()))?;
},
GraphFormat::Adjacency => {
let adj_matrix = graph_to_adjacency_matrix(graph, false)?;
write_adjacency_matrix_to_file(&adj_matrix, path)?;
}
}
Ok(())
}
pub fn write_adjacency_matrix_to_file(matrix: &Vec<Vec<f64>>, path: impl AsRef<Path>) -> Result<(), NetviewError> {
let file = File::create(path).map_err(|e| NetviewError::WriteError(e.to_string()))?;
let mut wtr = WriterBuilder::new().delimiter(b'\t').from_writer(file);
for row in matrix {
wtr.serialize(row).map_err(NetviewError::CsvError)?;
}
wtr.flush().map_err(|err| NetviewError::CsvError(err.into()))
}
pub fn write_json_graph<N, E>(graph: &Graph<N, E>, path: &Path) -> Result<(), NetviewError>
where
N: Serialize + Clone,
E: Serialize + Clone,
{
#[derive(Serialize)]
struct EdgeData<N, E> {
source: N,
target: N,
weight: E,
}
#[derive(Serialize)]
struct GraphData<N, E> {
nodes: Vec<N>,
edges: Vec<EdgeData<usize, E>>,
}
let nodes: Vec<_> = graph.node_indices().map(|n| graph[n].clone()).collect();
let edges: Vec<_> = graph.edge_references().map(|e| {
EdgeData {
source: e.source().index(),
target: e.target().index(),
weight: e.weight().clone(),
}
}).collect();
let graph_data = GraphData { nodes, edges };
let file = File::create(path).map_err(|e| NetviewError::GraphFileError(e.to_string()))?;
serde_json::to_writer(file, &graph_data).map_err(|e| NetviewError::GraphSerializationError(e.to_string()))?;
Ok(())
}
pub fn graph_to_adjacency_matrix<N, E>(graph: &Graph<N, E, Undirected>, nan: bool) -> Result<Vec<Vec<f64>>, NetviewError>
where
E: Clone + Into<f64>
{
let node_count = graph.node_count();
let mut matrix = vec![vec![match nan { true => NAN, false => 0.}; node_count]; node_count];
for edge_ref in graph.edge_references() {
let (source, target) = (edge_ref.source().index(), edge_ref.target().index());
let weight: f64 = edge_ref.weight().clone().into();
matrix[source][target] = weight;
matrix[target][source] = weight;
}
Ok(matrix)
}
#[cfg(test)]
mod tests {
use super::*;
use petgraph::graph::NodeIndex;
use std::path::PathBuf;
use tempfile::tempdir;
use std::fs;
#[test]
fn test_empty_matrix() {
let distance_matrix = Vec::<Vec<f64>>::new();
let k = 1;
assert!(matches!(
k_mutual_nearest_neighbors(&distance_matrix, k),
Err(NetviewError::InvalidMatrix)
));
}
#[test]
fn test_invalid_k_zero() {
let distance_matrix = vec![vec![0.0, 1.0], vec![1.0, 0.0]];
let k = 0;
assert!(matches!(
k_mutual_nearest_neighbors(&distance_matrix, k),
Err(NetviewError::InvalidK)
));
}
#[test]
fn test_invalid_k_large() {
let distance_matrix = vec![vec![0.0, 1.0], vec![1.0, 0.0]];
let k = 3; assert!(matches!(
k_mutual_nearest_neighbors(&distance_matrix, k),
Err(NetviewError::InvalidK)
));
}
#[test]
fn test_single_element() {
let distance_matrix = vec![vec![0.0]];
let k = 1;
let result = k_mutual_nearest_neighbors(&distance_matrix, k).unwrap();
assert_eq!(result, vec![Vec::<usize>::new()]); }
#[test]
fn test_symmetrical_matrix_simple() {
let distance_matrix = vec![vec![0.0, 1.0], vec![1.0, 0.0]];
let k = 1;
let result = k_mutual_nearest_neighbors(&distance_matrix, k).unwrap();
assert_eq!(result, vec![vec![1], vec![0]]);
}
#[test]
fn test_lower_triangular_conversion() {
let distance_matrix = vec![vec![0.0], vec![1.0, 0.0]];
let k = 1;
let result = k_mutual_nearest_neighbors(&distance_matrix, k).unwrap();
assert_eq!(result, vec![vec![1], vec![0]]);
}
#[test]
fn test_no_mutual_neighbors() {
let distance_matrix = vec![vec![0.0, 2.0, 1.0], vec![2.0, 0.0, 3.0], vec![1.0, 3.0, 0.0]];
let k = 1;
let result = k_mutual_nearest_neighbors(&distance_matrix, k).unwrap();
assert_eq!(result, vec![Vec::<usize>::new(), Vec::<usize>::new(), Vec::<usize>::new()]);
}
#[test]
fn test_with_mutual_neighbors() {
let distance_matrix = vec![
vec![0.0, 1.0, 2.0, 3.0],
vec![1.0, 0.0, 3.0, 2.0],
vec![2.0, 3.0, 0.0, 1.0],
vec![3.0, 2.0, 1.0, 0.0],
];
let k = 2;
let result = k_mutual_nearest_neighbors(&distance_matrix, k).unwrap();
assert_eq!(result, vec![vec![1, 2], vec![0, 3], vec![0, 3], vec![1, 2]]);
}
#[test]
fn test_large_k_with_few_elements() {
let distance_matrix = vec![vec![0.0, 1.0], vec![1.0, 0.0]];
let k = 2; let result = k_mutual_nearest_neighbors(&distance_matrix, k).unwrap();
assert_eq!(result, vec![vec![1], vec![0]]);
}
#[test]
fn test_complex_mutual_neighbors() {
let distance_matrix = vec![
vec![0.0, 2.0, 3.0, 4.0],
vec![2.0, 0.0, 4.0, 5.0],
vec![3.0, 4.0, 0.0, 1.0],
vec![4.0, 5.0, 1.0, 0.0],
];
let k = 1;
let result = k_mutual_nearest_neighbors(&distance_matrix, k).unwrap();
assert_eq!(result, vec![Vec::<usize>::new(), Vec::<usize>::new(), vec![3], vec![2]]);
}
#[test]
fn test_identical_distances() {
let distance_matrix = vec![
vec![0.0, 1.0, 1.0],
vec![1.0, 0.0, 1.0],
vec![1.0, 1.0, 0.0],
];
let k = 2;
let result = k_mutual_nearest_neighbors(&distance_matrix, k).unwrap();
assert_eq!(result, vec![vec![1, 2], vec![0, 2], vec![0, 1]]);
}
#[test]
fn test_non_symmetrical_matrix_error() {
let distance_matrix = vec![vec![0.0, 2.0], vec![2.0, 0.0, 1.0]]; let k = 1;
assert!(matches!(
k_mutual_nearest_neighbors(&distance_matrix, k),
Err(NetviewError::InvalidMatrix)
));
}
#[test]
fn test_full_matrix_with_no_neighbors() {
let distance_matrix = vec![
vec![0.0, 100.0, 100.0, 100.0],
vec![100.0, 0.0, 100.0, 100.0],
vec![100.0, 100.0, 0.0, 100.0],
vec![100.0, 100.0, 100.0, 0.0],
];
let k = 1;
let result = k_mutual_nearest_neighbors(&distance_matrix, k).unwrap();
assert_eq!(result, vec![Vec::<usize>::new(), Vec::<usize>::new(), Vec::<usize>::new(), Vec::<usize>::new()]);
}
#[test]
fn test_matrix_with_self_loops() {
let distance_matrix = vec![
vec![0.0, 1.0, 2.0],
vec![1.0, 0.0, 2.0],
vec![2.0, 2.0, 0.0],
];
let k = 2;
let result = k_mutual_nearest_neighbors(&distance_matrix, k).unwrap();
assert_eq!(result, vec![vec![1, 2], vec![0, 2], vec![0, 1]]);
}
fn setup_test_graph() -> Graph<&'static str, i32> {
let mut graph = Graph::new();
let a = graph.add_node("A");
let b = graph.add_node("B");
graph.add_edge(a, b, 7);
graph
}
#[test]
fn test_write_empty_graph() {
let graph = Graph::<&str, i32>::new();
let dir = tempdir().unwrap();
let file_path = dir.path().join("empty_graph.json");
write_json_graph(&graph, &file_path).unwrap();
let metadata = fs::metadata(file_path).unwrap();
assert!(metadata.len() > 0);
}
#[test]
fn test_write_simple_graph() {
let graph = setup_test_graph();
let dir = tempdir().unwrap();
let file_path = dir.path().join("simple_graph.json");
write_json_graph(&graph, &file_path).unwrap();
assert!(file_path.exists());
let content = fs::read_to_string(file_path).unwrap();
assert!(content.contains("\"nodes\":[\"A\",\"B\"]"));
assert!(content.contains("\"weight\":7"));
}
#[test]
fn test_file_write_error() {
let graph = setup_test_graph();
let file_path = PathBuf::from("/non_existent_directory/graph.json");
let result = write_json_graph(&graph, &file_path);
assert!(matches!(result, Err(NetviewError::GraphFileError(_))));
}
#[test]
fn test_write_large_graph() {
let mut graph = Graph::new();
for i in 0..100 {
let node = graph.add_node(format!("Node {}", i));
if i != 0 {
let prev_node = NodeIndex::new(i as usize - 1);
graph.add_edge(prev_node, node, i as i32);
}
}
let dir = tempdir().unwrap();
let file_path = dir.path().join("large_graph.json");
write_json_graph(&graph, &file_path).unwrap();
let metadata = fs::metadata(&file_path).unwrap();
assert!(metadata.len() > 0, "File should contain serialized large graph data");
}
#[test]
fn test_write_graph_with_multiple_edges() {
let mut graph = Graph::new();
let a = graph.add_node("A");
let b = graph.add_node("B");
graph.add_edge(a, b, 1);
graph.add_edge(a, b, 2);
let dir = tempdir().unwrap();
let file_path = dir.path().join("multi_edge_graph.json");
write_json_graph(&graph, &file_path).unwrap();
let content = fs::read_to_string(file_path).unwrap();
assert!(content.contains("\"weight\":1") && content.contains("\"weight\":2"), "File should contain both edges");
}
#[test]
fn test_nonexistent_path() {
let graph = setup_test_graph();
let file_path = PathBuf::from(format!("{}/invalid_path/graph.json", tempdir().unwrap().path().to_string_lossy()));
let result = write_json_graph(&graph, &file_path);
assert!(matches!(result, Err(NetviewError::GraphFileError(_))));
}
#[test]
fn empty_graph() {
let graph: Graph<&str, f64, Undirected> = Graph::new_undirected();
let matrix = graph_to_adjacency_matrix(&graph, false).unwrap();
assert!(matrix.is_empty());
}
#[test]
fn two_node_graph_with_edge() {
let mut graph = Graph::new_undirected();
let a = graph.add_node("A");
let b = graph.add_node("B");
graph.add_edge(a, b, 2.5);
let matrix = graph_to_adjacency_matrix(&graph, true).unwrap();
assert_eq!(matrix[0][1], 2.5);
assert!(matrix[1][0].is_nan()); }
#[test]
fn graph_with_multiple_edges() {
let mut graph = Graph::new_undirected(); let a = graph.add_node("A");
let b = graph.add_node("B");
let c = graph.add_node("C");
graph.add_edge(a, b, 3.0);
graph.add_edge(a, c, 4.5);
let matrix = graph_to_adjacency_matrix(&graph, true).unwrap();
assert_eq!(matrix[0][1], 3.0);
assert_eq!(matrix[0][2], 4.5);
assert!(matrix[1][0].is_nan()); }
#[test]
fn non_existent_edge() {
let mut graph = Graph::new_undirected();
let a = graph.add_node("A");
let b = graph.add_node("B");
graph.add_node("C"); graph.add_edge(a, b, 1.0);
let matrix = graph_to_adjacency_matrix(&graph, true).unwrap();
assert!(matrix[2][0].is_nan() && matrix[2][1].is_nan(), "Edges involving 'C' should be NaN");
}
#[test]
fn graph_with_self_loops() {
let mut graph = Graph::new_undirected();
let a = graph.add_node("A");
graph.add_edge(a, a, 2.0); let matrix = graph_to_adjacency_matrix(&graph, true).unwrap();
assert_eq!(matrix[0][0], 2.0, "Self-loop at 'A' should have a weight of 2.0");
}
#[test]
fn large_graph_performance() {
let mut graph = Graph::new_undirected();
for i in 0..100 {
graph.add_node(format!("Node {}", i));
}
for i in 0..99 {
graph.add_edge(NodeIndex::new(i), NodeIndex::new(i + 1), i as f64 + 1.0);
}
let now = std::time::Instant::now();
let matrix = graph_to_adjacency_matrix(&graph, true).unwrap();
let elapsed = now.elapsed();
assert_eq!(matrix.len(), 100, "The graph should have 100 nodes.");
assert!(elapsed.as_secs_f64() < 1.0, "Function should be performant for large graphs.");
}
#[test]
fn test_missing_weights_default_to_nan() {
let mut graph = Graph::<&str, f64, Undirected>::new_undirected();
let a = graph.add_node("A");
let b = graph.add_node("B");
graph.add_node("C"); graph.add_edge(a, b, 2.5);
let matrix = graph_to_adjacency_matrix(&graph, true).unwrap();
assert!(matrix[0][2].is_nan());
assert!(matrix[2][1].is_nan());
assert!(matrix[2][2].is_nan());
}
#[test]
fn test_negative_weights() {
let mut graph = Graph::new_undirected();
let a = graph.add_node("A");
let b = graph.add_node("B");
graph.add_edge(a, b, -1.5);
let matrix = graph_to_adjacency_matrix(&graph, true).unwrap();
assert_eq!(matrix[0][1], -1.5);
}
#[test]
fn test_zero_weights() {
let mut graph = Graph::new_undirected();
let a = graph.add_node("A");
let b = graph.add_node("B");
graph.add_edge(a, b, 0.0);
let matrix = graph_to_adjacency_matrix(&graph, true).unwrap();
assert_eq!(matrix[0][1], 0.0);
}
#[test]
fn test_empty_mnn_output() {
let mnn_output = vec![];
let graph = convert_to_graph(&mnn_output, None).unwrap();
assert_eq!(graph.node_count(), 0, "Graph should have no nodes for empty input.");
assert_eq!(graph.edge_count(), 0, "Graph should have no edges for empty input.");
}
#[test]
fn test_simple_graph_conversion_with_distances() {
let mnn_output = vec![vec![1], vec![0]];
let distance_matrix = Some(vec![vec![0.0, 1.0], vec![1.0, 0.0]]);
let graph = convert_to_graph(&mnn_output, distance_matrix.as_ref()).unwrap();
assert_eq!(graph.node_count(), 2, "Graph should have 2 nodes.");
assert_eq!(graph.edge_count(), 2, "Graph should have 2 edges for mutual neighbors.");
}
#[test]
fn test_simple_graph_conversion_without_distances() {
let mnn_output = vec![vec![1], vec![0]];
let graph = convert_to_graph(&mnn_output, None).unwrap();
assert_eq!(graph.node_count(), 2, "Graph should have 2 nodes.");
assert_eq!(graph.edge_count(), 2, "Graph should have 2 edges for mutual neighbors, with default weights.");
}
#[test]
fn test_graph_with_self_loops() {
let mnn_output = vec![vec![0]]; let distance_matrix = Some(vec![vec![0.0]]);
let graph = convert_to_graph(&mnn_output, distance_matrix.as_ref()).unwrap();
assert_eq!(graph.node_count(), 1, "Graph should have 1 node.");
assert_eq!(graph.edge_count(), 0, "Self-loops are not expected to create edges.");
}
#[test]
fn test_non_existent_neighbors() {
let mnn_output = vec![vec![1], vec![2]]; let distance_matrix = Some(vec![vec![0.0, 1.0]]); let graph = convert_to_graph(&mnn_output, distance_matrix.as_ref()).unwrap();
assert_eq!(graph.node_count(), 2, "Graph should have 2 nodes despite referencing a non-existent neighbor.");
assert_eq!(graph.edge_count(), 0, "Graph should have no edges due to non-existent neighbor distances.");
}
#[test]
fn test_graph_conversion_with_missing_distances() {
let mnn_output = vec![vec![1], vec![0]];
let distance_matrix = Some(vec![vec![0.0], vec![0.0]]);
let graph = convert_to_graph(&mnn_output, distance_matrix.as_ref()).unwrap();
assert_eq!(graph.node_count(), 2, "Graph should have 2 nodes.");
assert_eq!(graph.edge_count(), 0, "Graph should have no edges due to missing distances.");
}
#[test]
fn two_node_graph_with_edge_false_nan() {
let mut graph = Graph::new_undirected();
let a = graph.add_node("A");
let b = graph.add_node("B");
graph.add_edge(a, b, 2.5);
let matrix = graph_to_adjacency_matrix(&graph, false).unwrap();
assert_eq!(matrix[0][1], 2.5);
assert_eq!(matrix[1][0], 2.5); assert_eq!(matrix[0][0], 0.0, "No self-loop should result in 0.0");
assert_eq!(matrix[1][1], 0.0, "No self-loop should result in 0.0");
}
#[test]
fn graph_with_multiple_edges_false_nan() {
let mut graph = Graph::new_undirected();
let a = graph.add_node("A");
let b = graph.add_node("B");
let c = graph.add_node("C");
graph.add_edge(a, b, 3.0);
graph.add_edge(a, c, 4.5);
let matrix = graph_to_adjacency_matrix(&graph, false).unwrap();
assert_eq!(matrix[0][1], 3.0);
assert_eq!(matrix[0][2], 4.5);
assert_eq!(matrix[1][0], 3.0, "Mirror edge should have the same weight for undirected graph");
assert_eq!(matrix[2][0], 4.5, "Mirror edge should have the same weight for undirected graph");
}
#[test]
fn non_existent_edge_false_nan() {
let mut graph = Graph::new_undirected();
let a = graph.add_node("A");
let b = graph.add_node("B");
graph.add_node("C"); graph.add_edge(a, b, 1.0);
let matrix = graph_to_adjacency_matrix(&graph, false).unwrap();
assert_eq!(matrix[2][0], 0.0, "Edges involving 'C' should be 0.0");
assert_eq!(matrix[2][1], 0.0, "Edges involving 'C' should be 0.0");
}
#[test]
fn graph_with_self_loops_false_nan() {
let mut graph = Graph::new_undirected();
let a = graph.add_node("A");
graph.add_edge(a, a, 2.0); let matrix = graph_to_adjacency_matrix(&graph, false).unwrap();
assert_eq!(matrix[0][0], 2.0, "Self-loop at 'A' should have a weight of 2.0");
}
#[test]
fn test_missing_weights_default_to_zero() {
let mut graph = Graph::<&str, f64, Undirected>::new_undirected();
let a = graph.add_node("A");
let b = graph.add_node("B");
graph.add_node("C"); graph.add_edge(a, b, 2.5);
let matrix = graph_to_adjacency_matrix(&graph, false).unwrap();
assert_eq!(matrix[0][2], 0.0, "Missing edge weights should default to 0.0");
assert_eq!(matrix[2][1], 0.0, "Missing edge weights should default to 0.0");
assert_eq!(matrix[2][2], 0.0, "Missing edge weights should default to 0.0");
}
}