dda-rs 0.2.0

Pure Rust Delay Differential Analysis engine
Documentation
use crate::engine::{run_request_on_matrix_with_progress, PureRustProgress};
use crate::error::{DDAError, Result};
use crate::types::{DDARequest, DDAResult};
use rayon::prelude::*;
use std::fs::File;
use std::io::{BufRead, BufReader};
use std::path::Path;

const PARALLEL_BATCH_MIN_LEN: usize = 4;

pub fn run_request_on_ascii_file<P: AsRef<Path>>(
    request: &DDARequest,
    path: P,
    start_bound: Option<u64>,
    end_bound: Option<u64>,
) -> Result<DDAResult> {
    run_request_on_ascii_file_with_progress(request, path, start_bound, end_bound, |_| {})
}

pub fn run_request_on_ascii_file_with_progress<P: AsRef<Path>, F>(
    request: &DDARequest,
    path: P,
    start_bound: Option<u64>,
    end_bound: Option<u64>,
    on_progress: F,
) -> Result<DDAResult>
where
    F: FnMut(&PureRustProgress),
{
    let samples = load_ascii_matrix_from_path(path)?;
    let mut adjusted_request = request.clone();
    if let (Some(start), Some(end)) = (start_bound, end_bound) {
        adjusted_request.time_range.start = start as f64;
        adjusted_request.time_range.end = end as f64;
    }
    run_request_on_matrix_with_progress(&adjusted_request, &samples, None, on_progress)
}

pub fn run_request_on_f64_matrix_file_with_progress<P: AsRef<Path>, F>(
    request: &DDARequest,
    path: P,
    rows: usize,
    cols: usize,
    channel_labels: Option<&[String]>,
    on_progress: F,
) -> Result<DDAResult>
where
    F: FnMut(&PureRustProgress),
{
    let samples = load_f64_matrix_from_path(path, rows, cols)?;
    run_request_on_matrix_with_progress(request, &samples, channel_labels, on_progress)
}

pub fn load_ascii_matrix_from_path<P: AsRef<Path>>(path: P) -> Result<Vec<Vec<f64>>> {
    let file = File::open(path.as_ref()).map_err(DDAError::IoError)?;
    let reader = BufReader::new(file);
    let mut rows = Vec::new();
    let mut expected_columns = None;

    for (line_idx, line) in reader.lines().enumerate() {
        let line = line.map_err(DDAError::IoError)?;
        let trimmed = line.trim();
        if trimmed.is_empty() || trimmed.starts_with('#') {
            continue;
        }
        let row = trimmed
            .split(|c: char| c == ',' || c.is_ascii_whitespace())
            .filter(|token| !token.is_empty())
            .map(parse_ascii_token)
            .collect::<Result<Vec<_>>>()?;

        if row.is_empty() {
            continue;
        }

        match expected_columns {
            Some(expected) if expected != row.len() => {
                return Err(DDAError::ParseError(format!(
                    "ASCII row {} has {} columns but previous rows have {}",
                    line_idx + 1,
                    row.len(),
                    expected
                )));
            }
            None => expected_columns = Some(row.len()),
            _ => {}
        }

        rows.push(row);
    }

    if rows.is_empty() {
        return Err(DDAError::ParseError(
            "No numeric rows found in ASCII input".to_string(),
        ));
    }

    Ok(rows)
}

pub fn load_f64_matrix_from_path<P: AsRef<Path>>(
    path: P,
    rows: usize,
    cols: usize,
) -> Result<Vec<Vec<f64>>> {
    if rows == 0 || cols == 0 {
        return Err(DDAError::InvalidParameter(
            "Raw matrix input requires non-zero rows and columns".to_string(),
        ));
    }
    let mmap = crate::mmap_utils::mmap_file(path.as_ref())?;
    let expected_len = rows
        .checked_mul(cols)
        .and_then(|value| value.checked_mul(std::mem::size_of::<f64>()))
        .ok_or_else(|| {
            DDAError::InvalidParameter("Raw matrix dimensions overflow usize".to_string())
        })?;
    if mmap.len() != expected_len {
        return Err(DDAError::ParseError(format!(
            "Raw matrix file has {} bytes but {} were expected for a {}x{} f64 matrix",
            mmap.len(),
            expected_len,
            rows,
            cols
        )));
    }

    let row_byte_width = cols * std::mem::size_of::<f64>();
    let decode_row = |row_bytes: &[u8]| -> Result<Vec<f64>> {
        row_bytes
            .chunks_exact(std::mem::size_of::<f64>())
            .map(|chunk| {
                let bytes: [u8; 8] = chunk.try_into().map_err(|_| {
                    DDAError::ParseError("Could not decode raw matrix bytes".to_string())
                })?;
                Ok(f64::from_le_bytes(bytes))
            })
            .collect()
    };

    let rows_result: Vec<Result<Vec<f64>>> = if rows >= PARALLEL_BATCH_MIN_LEN {
        mmap.par_chunks_exact(row_byte_width)
            .map(decode_row)
            .collect()
    } else {
        mmap.chunks_exact(row_byte_width).map(decode_row).collect()
    };
    rows_result.into_iter().collect()
}

fn parse_ascii_token(token: &str) -> Result<f64> {
    if token.eq_ignore_ascii_case("nan") {
        return Ok(f64::NAN);
    }
    token.parse::<f64>().map_err(|_| {
        DDAError::ParseError(format!("Failed to parse ASCII numeric token '{}'", token))
    })
}