netview 0.1.0

Netview: mutual nearest neighbor graphs for genome-wide, fine-scale population structure determination and visualization
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
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;


/// Represents a row in the matrix for easier handling with serde.
#[derive(Deserialize)]
struct MatrixRow(Vec<f64>);

/// Parses a distance matrix from a CSV/TSV file.
///
/// The function can handle both symmetrical and lower triangular matrices.
/// It automatically detects whether the file is CSV or TSV based on the extension.
///
/// # Arguments
///
/// * `file_path` - The path to the input CSV/TSV file is extracted from file path
///                 extensions `.tsv` and `.csv`. Defaults to CSV if extension
///                 fails to be extracted from file path (i.e. no extension).
///
/// # Returns
///
/// A `Result` with either:
/// - `Ok(Vec<Vec<f64>>)` containing the parsed distance matrix.
/// - `Err(NetviewError)` indicating the error encountered.
///
/// # Examples
///
/// ```
/// use netview::parse_distance_matrix;
/// use std::path::Path;
/// 
/// let distance_matrix = parse_distance_matrix(
///     Path::new("distance_matrix.csv")
/// ).unwrap();
/// 
/// println!("{:#?}", distance_matrix);
/// ```
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()) 
    });

    // Validate matrix format (symmetrical or lower triangular)
    // This step is crucial to ensure the matrix conforms to expectations
    if !is_matrix_valid(&matrix) {
        return Err(NetviewError::MatrixFormatError);
    }

    Ok(matrix)
}

/// Validates if the given matrix is symmetrical or lower triangular.
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
}

/// Transforms a lower triangular matrix into a symmetrical matrix, with error handling.
///
/// # Arguments
///
/// * `distance_matrix` - A slice of Vec<Vec<f64>> representing the lower triangular or full symmetrical matrix.
///
/// # Returns
///
/// Returns a `Result` with either:
/// - `Ok(Vec<Vec<f64>>)`: A symmetrical matrix as a vector of vectors of f64.
/// - `Err(NetviewError)`: An error indicating what went wrong in the process.
///
/// # Examples
///
/// ```
/// use netview::make_symmetrical;
/// use netview::NetviewError; 
/// 
/// let lower_triangular_matrix = vec![
///     vec![0.0],
///     vec![1.0, 0.0],
/// ];
/// 
/// let symmetrical_matrix = make_symmetrical(
///     &lower_triangular_matrix
/// ).unwrap();
/// 
/// assert_eq!(symmetrical_matrix, vec![
///     vec![0.0, 1.0],
///     vec![1.0, 0.0],
/// ]);
/// ```
///
/// # Errors
///
/// This function will return an error if:
/// - The input matrix is empty but was expected to be non-empty.
/// - The input matrix's dimensions do not match the expected dimensions.
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)
}



/// Computes the Euclidean distance matrix with options for parallel computation,
/// handling lower triangular matrices, and manually setting the number of threads.
///
/// # Arguments
///
/// * `matrix` - A symmetrical distance matrix or its lower triangular part as `Vec<Vec<f64>>`.
/// * `is_lower_triangular` - Indicates if the input matrix is lower triangular.
/// * `parallel` - Indicates if parallel computation should be used.
/// * `num_threads` - An optional number of threads for parallel computation.
///
/// # Returns
///
/// A `Result` containing either:
/// - `Ok(Vec<Vec<f64>>)`: The Euclidean distance matrix.
/// - `Err(NetviewError)`: An error indicating what went wrong.
///
/// # Examples
///
/// ```
/// use netview::dist::euclidean_distance_of_distances;
/// 
/// let distance_matrix = vec![
///     vec![0.0, 1.0],
///     vec![1.0, 0.0],
/// ];
/// 
/// let result = euclidean_distance_of_distances(
///     &distance_matrix, false, false, None
/// ).unwrap();
/// 
/// assert_eq!(result, vec![vec![0.0, 1.0], vec![1.0, 0.0]]);
/// ```
///
/// # Errors
///
/// This function can return `NetviewError::NonSquareMatrix` if the input is not a square matrix
/// when `is_lower_triangular` is false, or `NetviewError::ThreadPoolBuildError` if the thread pool
/// cannot be initialized with the specified number of threads.
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();

    // Initialize thread pool for parallel computation if requested
    if parallel && num_threads.is_some() {
        ThreadPoolBuilder::new()
            .num_threads(num_threads.unwrap())
            .build_global()
            .map_err(|_| NetviewError::ThreadPoolBuildError)?;
    }

    // Prepare a vector to store distance computations
    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 {
        // Collect computed distances in 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 {
        // Collect computed distances sequentially
        for i in 0..n {
            for j in i + 1..n {
                distances.push((i, j, compute_distance(i, j)));
            }
        }
    }

    // Create the result matrix and fill in the computed distances
    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::*;

    // Tests for compute_euclidean_distance_of_distances

    #[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)); // Zero threads
        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() {
        // Test a matrix with uniform values to ensure distances are computed correctly
        let matrix = vec![vec![2.0, 2.0], vec![2.0, 2.0]];
        let result = euclidean_distance_of_distances(&matrix, false, false, None).unwrap();
        // Expect all distances to be zero since all points are identical
        assert_eq!(result, vec![vec![0.0, 0.0], vec![0.0, 0.0]]);
    }

    #[test]
    fn euclidean_parallel_vs_sequential() {
        // Compare results from parallel and sequential execution to ensure they match
        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() {
        // Test behavior when an invalid thread pool size is specified
        let matrix = vec![vec![0.0, 1.0], vec![1.0, 0.0]];
        let result = euclidean_distance_of_distances(&matrix, false, true, Some(0)); // Invalid thread count
        // Expect an error due to thread pool build failure
        assert!(matches!(result, Err(NetviewError::ThreadPoolBuildError)));
    }

    #[test]
    fn euclidean_with_large_matrix() {
        // Test with a large matrix to ensure scalability of the function
        let size = 50; // Note: Increase size based on your system's capabilities for more intensive testing
        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();
        // Check that the result matrix is of the correct size and that distances are computed
        assert_eq!(result.len(), size);
        for i in 0..size {
            for j in i + 1..size {
                assert_ne!(result[i][j], 0.0);
            }
        }
    }

    // Tests for make_symmetrical

    #[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]]; // This no longer directly triggers an error due to the removed `n` parameter check
        let result = make_symmetrical(&matrix);
        // Instead of expecting an error, this test now expects a successful operation or a different handling strategy
        assert!(result.is_ok()); // Adjust based on new logic
    }

    #[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() {
        // Test a lower triangular matrix with increasing values to ensure correct symmetrical transformation
        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() {
        // Test a full matrix that is already 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);
    }

    // Tests for parse_distance_matrix

    use std::fs::File;
    use std::io::Write;
    use std::path::PathBuf;
    use tempfile::tempdir;

    // Helper function to create a temporary file with specified contents
    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() {
        // More columns than rows - invalid matrix
        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() {
        // Creates a 3x3 matrix
        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() {
        // Uses both comma and tab as delimiters, which should result in an error
        let path = create_temp_matrix_file("0\t1\n1,0", "tsv");
        let result = parse_distance_matrix(path);
        // Expected to fail due to inconsistent delimiters within a TSV file
        assert!(matches!(result, Err(NetviewError::MatrixFormatError)));
    }

}