extern crate rayon;
use csv::{ReaderBuilder, Trim};
use serde::Deserialize;
use std::fs::File;
use std::path::Path;
use rayon::prelude::*;
use rayon::ThreadPoolBuilder;
use crate::error::NetviewError;
#[derive(Deserialize)]
struct MatrixRow(Vec<f64>);
pub fn parse_distance_matrix<P: AsRef<Path>>(file_path: P) -> Result<Vec<Vec<f64>>, NetviewError> {
let file = File::open(file_path.as_ref()).map_err(|_| NetviewError::FileReadError)?;
let is_tsv = file_path.as_ref().extension()
.and_then(|ext| ext.to_str())
.map_or(false, |ext| ext.eq_ignore_ascii_case("tsv"));
log::info!("Input matrix is tab-delimited: {is_tsv}");
let mut rdr = ReaderBuilder::new()
.delimiter(if is_tsv { b'\t' } else { b',' })
.trim(Trim::All)
.has_headers(false)
.from_reader(file);
let mut matrix = Vec::new();
for result in rdr.deserialize() {
let record: MatrixRow = result.map_err(|e| NetviewError::ParseError(e.to_string()))?;
matrix.push(record.0);
}
log::info!("Input matrix dimensions: {}", match matrix.is_empty() {
true => "0 x 0".to_string(),
false => format!("{} x {}", matrix.len(), matrix[matrix.len()-1].len())
});
if !is_matrix_valid(&matrix) {
return Err(NetviewError::MatrixFormatError);
}
Ok(matrix)
}
fn is_matrix_valid(matrix: &[Vec<f64>]) -> bool {
let n = matrix.len();
for (i, row) in matrix.iter().enumerate() {
if row.len() > n || (row.len() != n && row.len() != i + 1) {
return false;
}
}
true
}
pub fn make_symmetrical(distance_matrix: &Vec<Vec<f64>>) -> Result<Vec<Vec<f64>>, NetviewError> {
let n = distance_matrix.len();
if distance_matrix.is_empty() {
return Err(NetviewError::EmptyMatrix);
}
let matrix = if distance_matrix.iter().all(|row| row.len() == n) {
log::debug!("Distance matrix input for make_symmetrical is a symmetrical distance matrix. Returning input distance matrix.");
distance_matrix.clone()
} else {
let mut sym_matrix = vec![vec![0.0; n]; n];
for i in 0..n {
for j in 0..=i {
sym_matrix[i][j] = distance_matrix[i][j];
sym_matrix[j][i] = distance_matrix[i][j];
}
}
sym_matrix
};
Ok(matrix)
}
pub fn euclidean_distance_of_distances(
distance_matrix: &Vec<Vec<f64>>,
is_lower_triangular: bool,
parallel: bool,
num_threads: Option<usize>,
) -> Result<Vec<Vec<f64>>, NetviewError> {
let n = distance_matrix.len();
if parallel && num_threads.is_some() {
ThreadPoolBuilder::new()
.num_threads(num_threads.unwrap())
.build_global()
.map_err(|_| NetviewError::ThreadPoolBuildError)?;
}
let mut distances = vec![];
let compute_distance = |i: usize, j: usize| -> f64 {
let mut sum = 0.0;
for k in 0..n {
let val_i = if is_lower_triangular && i < k { distance_matrix[k][i] } else { distance_matrix[i][k] };
let val_j = if is_lower_triangular && j < k { distance_matrix[k][j] } else { distance_matrix[j][k] };
sum += (val_i - val_j).powi(2);
}
sum.sqrt()
};
if parallel {
distances = (0..n).into_par_iter().flat_map(|i| {
(i + 1..n).into_par_iter().map(move |j| (i, j, compute_distance(i, j)))
}).collect();
} else {
for i in 0..n {
for j in i + 1..n {
distances.push((i, j, compute_distance(i, j)));
}
}
}
let mut result_matrix = vec![vec![0.0; n]; n];
for (i, j, distance) in distances {
result_matrix[i][j] = distance;
result_matrix[j][i] = distance;
}
Ok(result_matrix)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn euclidean_empty_matrix() {
let matrix = vec![];
let empty_matrix: Vec<Vec<f64>> = vec![];
let result = euclidean_distance_of_distances(&matrix, false, false, None);
assert!(result.is_ok());
assert_eq!(result.unwrap(), empty_matrix);
}
#[test]
fn euclidean_invalid_thread_number() {
let matrix = vec![vec![0.0, 1.0], vec![1.0, 0.0]];
let result = euclidean_distance_of_distances(&matrix, false, true, Some(0)); assert!(result.is_ok());
}
#[test]
fn euclidean_non_square_matrix() {
let matrix = vec![vec![1.0], vec![2.0, 3.0]];
let result = euclidean_distance_of_distances(&matrix, false, false, None);
assert!(matches!(result, Err(NetviewError::NonSquareMatrix)));
}
#[test]
fn euclidean_with_uniform_values() {
let matrix = vec![vec![2.0, 2.0], vec![2.0, 2.0]];
let result = euclidean_distance_of_distances(&matrix, false, false, None).unwrap();
assert_eq!(result, vec![vec![0.0, 0.0], vec![0.0, 0.0]]);
}
#[test]
fn euclidean_parallel_vs_sequential() {
let matrix = vec![
vec![0.0, 1.0, 1.0],
vec![1.0, 0.0, 1.0],
vec![1.0, 1.0, 0.0],
];
let parallel_result = euclidean_distance_of_distances(&matrix.clone(), false, true, Some(4)).unwrap();
let sequential_result = euclidean_distance_of_distances(&matrix, false, false, None).unwrap();
assert_eq!(parallel_result, sequential_result);
}
#[test]
fn euclidean_invalid_thread_pool() {
let matrix = vec![vec![0.0, 1.0], vec![1.0, 0.0]];
let result = euclidean_distance_of_distances(&matrix, false, true, Some(0)); assert!(matches!(result, Err(NetviewError::ThreadPoolBuildError)));
}
#[test]
fn euclidean_with_large_matrix() {
let size = 50; let matrix = (0..size).map(|i| (0..=i).map(|_| 1.0).collect()).collect();
let result = euclidean_distance_of_distances(&matrix, true, true, Some(8)).unwrap();
assert_eq!(result.len(), size);
for i in 0..size {
for j in i + 1..size {
assert_ne!(result[i][j], 0.0);
}
}
}
#[test]
fn symmetrical_empty_matrix() {
let matrix = vec![];
let result = make_symmetrical(&matrix);
assert!(matches!(result, Err(NetviewError::EmptyMatrix)));
}
#[test]
fn symmetrical_already_symmetrical() {
let matrix = vec![vec![0.0, 2.0], vec![2.0, 0.0]];
let result = make_symmetrical(&matrix.clone());
assert!(result.is_ok());
assert_eq!(result.unwrap(), matrix);
}
#[test]
fn symmetrical_lower_triangular_to_symmetrical() {
let matrix = vec![vec![0.0], vec![1.0, 0.0]];
let expected = vec![vec![0.0, 1.0], vec![1.0, 0.0]];
let result = make_symmetrical(&matrix);
assert!(result.is_ok());
assert_eq!(result.unwrap(), expected);
}
#[test]
fn symmetrical_invalid_lower_triangular_format() {
let matrix = vec![vec![0.0], vec![1.0]]; let result = make_symmetrical(&matrix);
assert!(result.is_ok()); }
#[test]
fn symmetrical_large_matrix() {
let size = 100;
let matrix = (0..size).map(|i| (0..=i).map(|j| (i + j) as f64).collect::<Vec<_>>()).collect::<Vec<_>>();
let result = make_symmetrical(&matrix);
assert!(result.is_ok());
let sym_matrix = result.unwrap();
for i in 0..size {
for j in 0..=i {
assert_eq!(sym_matrix[i][j], sym_matrix[j][i], "Mismatch at ({}, {})", i, j);
}
}
}
#[test]
fn make_symmetrical_with_increasing_values() {
let matrix = vec![
vec![1.0],
vec![2.0, 3.0],
vec![4.0, 5.0, 6.0],
];
let expected = vec![
vec![1.0, 2.0, 4.0],
vec![2.0, 3.0, 5.0],
vec![4.0, 5.0, 6.0],
];
let result = make_symmetrical(&matrix).unwrap();
assert_eq!(result, expected);
}
#[test]
fn make_symmetrical_already_full_and_symmetrical() {
let matrix = vec![
vec![0.0, 1.0, 2.0],
vec![1.0, 0.0, 3.0],
vec![2.0, 3.0, 0.0],
];
let result = make_symmetrical(&matrix.clone()).unwrap();
assert_eq!(result, matrix);
}
use std::fs::File;
use std::io::Write;
use std::path::PathBuf;
use tempfile::tempdir;
fn create_temp_matrix_file(contents: &str, extension: &str) -> PathBuf {
let dir = tempdir().unwrap();
let file_path = dir.path().join(format!("temp_file.{}", extension));
let mut file = File::create(&file_path).unwrap();
writeln!(file, "{}", contents).unwrap();
file_path
}
#[test]
fn parse_symmetrical_csv() {
let path = create_temp_matrix_file("0,1\n1,0", "csv");
let matrix = parse_distance_matrix(path).unwrap();
assert_eq!(matrix, vec![vec![0.0, 1.0], vec![1.0, 0.0]]);
}
#[test]
fn parse_lower_triangular_tsv() {
let path = create_temp_matrix_file("0\n1\t0", "tsv");
let matrix = parse_distance_matrix(path).unwrap();
assert_eq!(matrix, vec![vec![0.0], vec![1.0, 0.0]]);
}
#[test]
fn empty_file_error() {
let path = create_temp_matrix_file("", "csv");
let result = parse_distance_matrix(path);
assert!(matches!(result, Err(NetviewError::MatrixFormatError)));
}
#[test]
fn invalid_format_error() {
let path = create_temp_matrix_file("0,1,2\n1,0", "csv");
let result = parse_distance_matrix(path);
assert!(matches!(result, Err(NetviewError::MatrixFormatError)));
}
#[test]
fn non_numeric_values_error() {
let path = create_temp_matrix_file("0,a\nb,0", "csv");
let result = parse_distance_matrix(path);
assert!(matches!(result, Err(NetviewError::ParseError(_))));
}
#[test]
fn file_not_found_error() {
let path = PathBuf::from("non_existent_file.csv");
let result = parse_distance_matrix(path);
assert!(matches!(result, Err(NetviewError::FileReadError)));
}
#[test]
fn parse_symmetrical_with_additional_whitespaces_csv() {
let path = create_temp_matrix_file("0 , 1\n 1,0 ", "csv");
let matrix = parse_distance_matrix(path).unwrap();
assert_eq!(matrix, vec![vec![0.0, 1.0], vec![1.0, 0.0]]);
}
#[test]
fn inconsistent_row_lengths_error() {
let path = create_temp_matrix_file("0,1,2\n1,0", "csv");
let result = parse_distance_matrix(path);
assert!(matches!(result, Err(NetviewError::MatrixFormatError)));
}
#[test]
fn large_symmetrical_csv() {
let path = create_temp_matrix_file("0,1,2\n1,0,3\n2,3,0", "csv");
let matrix = parse_distance_matrix(path).unwrap();
assert_eq!(matrix, vec![vec![0.0, 1.0, 2.0], vec![1.0, 0.0, 3.0], vec![2.0, 3.0, 0.0]]);
}
#[test]
fn valid_tsv_with_mixed_delimiters_error() {
let path = create_temp_matrix_file("0\t1\n1,0", "tsv");
let result = parse_distance_matrix(path);
assert!(matches!(result, Err(NetviewError::MatrixFormatError)));
}
}