dda-rs 0.2.0

Pure Rust Delay Differential Analysis engine
Documentation
use serde::{Deserialize, Serialize};

/// Default DDA parameter values shared across wrappers.
pub const DEFAULT_MODEL_TERMS: [i32; 3] = [1, 2, 10];
pub const DEFAULT_MODEL_DIMENSION: u32 = 4;
pub const DEFAULT_POLYNOMIAL_ORDER: u32 = 4;
pub const DEFAULT_NUM_TAU: u32 = 2;
pub const DEFAULT_WINDOW_LENGTH: u32 = 200;
pub const DEFAULT_WINDOW_STEP: u32 = 100;
pub const DEFAULT_DELAYS: [i32; 2] = [7, 10];

/// Time range for analysis
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct TimeRange {
    pub start: f64,
    pub end: f64,
}

/// Preprocessing options
/// Note: Preprocessing should be done before DDA analysis, not by this package
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PreprocessingOptions {
    pub highpass: Option<f64>,
    pub lowpass: Option<f64>,
}

/// Algorithm variant selection
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct AlgorithmSelection {
    pub enabled_variants: Vec<String>,
    /// SELECT mask as 6-bit string (e.g., "1 0 1 0 0 0" for ST and CD)
    /// Format: ST CT CD RESERVED DE SY
    /// - ST: Single Timeseries (native output: _ST; compatibility: _DDA_ST)
    /// - CT: Cross-Timeseries (native output: _CT; compatibility: _DDA_CT)
    /// - CD: Cross-Dynamical (native outputs: _CD_DDA_ST and _CD_DDA_CT)
    /// - RESERVED: Internal development function (not for user use)
    /// - DE: Delay Embedding (output: _DE)
    /// - SY: Synchronization (output: _SY)
    #[serde(skip_serializing_if = "Option::is_none")]
    pub select_mask: Option<String>,
}

/// Window parameters for DDA analysis
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct WindowParameters {
    pub window_length: u32,
    pub window_step: u32,
    /// CT-specific window length (for Cross-Timeseries variant)
    #[serde(skip_serializing_if = "Option::is_none")]
    pub ct_window_length: Option<u32>,
    /// CT-specific window step (for Cross-Timeseries variant)
    #[serde(skip_serializing_if = "Option::is_none")]
    pub ct_window_step: Option<u32>,
}

/// Delay parameters for DDA analysis
/// These are the tau values passed directly to the -TAU CLI argument
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct DelayParameters {
    /// List of delay values (tau) passed directly to the binary
    /// Example: [1, 2, 3, 4, 5] will be passed as -TAU 1 2 3 4 5
    pub delays: Vec<i32>,
}

/// MODEL parameters for DDA analysis (expert mode)
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ModelParameters {
    pub dm: u32,     // Embedding dimension (default: 4)
    pub order: u32,  // Polynomial order (default: 4)
    pub nr_tau: u32, // Number of tau values (default: 2)
}

/// Strategy for choosing the CCD conditioning set.
#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "snake_case")]
pub enum CcdConditioningStrategy {
    /// Use every selected conditioning channel except the current target/source pair.
    AllSelected,
    /// Choose confounds from channels that best explain the target alone.
    AutoTargetSparse,
    /// Choose confounds from channels that look like shared parents of target and source.
    AutoSharedParents,
    /// Choose confounds with a block-OMP style greedy selector on the conditioned baseline fit.
    AutoGroupOmp,
}

/// Per-variant channel configuration
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct VariantChannelConfig {
    /// Selected channel indices for single-channel variants (ST, DE, SY)
    #[serde(skip_serializing_if = "Option::is_none")]
    pub selected_channels: Option<Vec<usize>>,
    /// Channel pairs for CT variant
    #[serde(skip_serializing_if = "Option::is_none")]
    pub ct_channel_pairs: Option<Vec<[usize; 2]>>,
    /// Directed channel pairs for CD variant
    #[serde(skip_serializing_if = "Option::is_none")]
    pub cd_channel_pairs: Option<Vec<[usize; 2]>>,
    /// Optional conditioning set for CCD (Conditional Cross-Dynamical)
    /// If omitted, the pure-Rust engine conditions on all other selected channels.
    #[serde(skip_serializing_if = "Option::is_none")]
    pub conditioning_channels: Option<Vec<usize>>,
    /// Optional policy for choosing the CCD conditioning set.
    #[serde(skip_serializing_if = "Option::is_none")]
    pub conditioning_strategy: Option<CcdConditioningStrategy>,
    /// Optional circular-shift surrogate offsets used by CCD significance variants.
    #[serde(skip_serializing_if = "Option::is_none")]
    pub surrogate_shifts: Option<Vec<usize>>,
    /// Optional temporal smoothness strength for temporally regularized CCD variants.
    #[serde(skip_serializing_if = "Option::is_none")]
    pub temporal_lambda: Option<f64>,
    /// Optional cap on the number of active sources for sparse multivariate CCD variants.
    #[serde(skip_serializing_if = "Option::is_none")]
    pub max_active_sources: Option<usize>,
}

/// Complete DDA request configuration
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct DDARequest {
    pub file_path: String,
    #[serde(alias = "channel_list")]
    pub channels: Option<Vec<usize>>, // Channel indices (0-based)
    pub time_range: TimeRange,
    pub preprocessing_options: PreprocessingOptions,
    pub algorithm_selection: AlgorithmSelection,
    pub window_parameters: WindowParameters,
    pub delay_parameters: DelayParameters,
    /// Channel pairs for CT (Cross-Timeseries) analysis
    /// Each pair is [channel_i, channel_j] where channels are 0-based indices
    #[serde(skip_serializing_if = "Option::is_none")]
    pub ct_channel_pairs: Option<Vec<[usize; 2]>>,
    /// Channel pairs for CD (Cross-Dynamical) analysis
    /// Each pair is [from_channel, to_channel] representing directed relationships
    /// Format: [(1, 2), (1, 3), (1, 4)] → CH_list: 1 2 1 3 1 4
    #[serde(skip_serializing_if = "Option::is_none")]
    pub cd_channel_pairs: Option<Vec<[usize; 2]>>,
    /// MODEL parameters (expert mode)
    /// If not provided, defaults to dm=4, order=4, nr_tau=2
    #[serde(skip_serializing_if = "Option::is_none")]
    pub model_parameters: Option<ModelParameters>,
    /// Model term indices passed to `-MODEL`.
    /// If not provided, defaults to [1, 2, 10] for compatibility with dda-py/jl.
    #[serde(
        default,
        skip_serializing_if = "Option::is_none",
        alias = "model_params",
        alias = "model_encoding",
        alias = "model"
    )]
    pub model_terms: Option<Vec<i32>>,
    /// Per-variant channel configurations (new format)
    /// Maps variant IDs to their specific channel configurations
    #[serde(skip_serializing_if = "Option::is_none")]
    pub variant_configs: Option<std::collections::HashMap<String, VariantChannelConfig>>,
    /// Input file sampling rate in Hz
    /// When > 1000 Hz, the -SR argument will be added as [SR/2, SR]
    #[serde(skip_serializing_if = "Option::is_none")]
    pub sampling_rate: Option<f64>,
}

/// Variant-specific DDA result
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct VariantResult {
    pub variant_id: String,      // "ST", "CT", "CD", "DE"
    pub variant_name: String,    // "Single Timeseries (ST)", etc.
    pub q_matrix: Vec<Vec<f64>>, // Q matrix for this variant [channels × timepoints]
    #[serde(skip_serializing_if = "Option::is_none")]
    pub channel_labels: Option<Vec<String>>, // Optional channel labels specific to this variant
    #[serde(skip_serializing_if = "Option::is_none")]
    pub error_values: Option<Vec<f64>>, // Error/rho values per window from DDA output
}

/// DDA analysis result
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct DDAResult {
    pub id: String,
    pub file_path: String,
    pub channels: Vec<String>,
    pub q_matrix: Vec<Vec<f64>>, // Primary variant Q matrix (for backward compatibility)
    pub variant_results: Option<Vec<VariantResult>>, // All variant results
    pub raw_output: Option<String>, // Optional: keep raw output for debugging
    pub window_parameters: WindowParameters,
    pub delay_parameters: DelayParameters,
    pub created_at: String,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub error_values: Option<Vec<f64>>, // Error/rho values per window from DDA output
}

impl DDAResult {
    pub fn new(
        id: String,
        file_path: String,
        channels: Vec<String>,
        q_matrix: Vec<Vec<f64>>,
        window_parameters: WindowParameters,
        delay_parameters: DelayParameters,
    ) -> Self {
        Self {
            id,
            file_path,
            channels,
            q_matrix,
            variant_results: None,
            raw_output: None,
            window_parameters,
            delay_parameters,
            created_at: chrono::Utc::now().to_rfc3339(),
            error_values: None,
        }
    }

    pub fn with_raw_output(mut self, raw_output: String) -> Self {
        self.raw_output = Some(raw_output);
        self
    }

    pub fn with_variant_results(mut self, variant_results: Vec<VariantResult>) -> Self {
        self.variant_results = Some(variant_results);
        self
    }

    pub fn with_error_values(mut self, error_values: Vec<f64>) -> Self {
        self.error_values = Some(error_values);
        self
    }
}