sciforge 0.0.3

A comprehensive scientific computing library in pure Rust with zero dependencies
Documentation
pub fn toggle_switch(
    u: f64,
    v: f64,
    alpha1: f64,
    alpha2: f64,
    beta: f64,
    gamma: f64,
    n: f64,
) -> (f64, f64) {
    let du = alpha1 / (1.0 + v.powf(n)) - beta * u;
    let dv = alpha2 / (1.0 + u.powf(n)) - gamma * v;
    (du, dv)
}

pub fn bistable_toggle_switch(
    u0: f64,
    v0: f64,
    alpha1: f64,
    alpha2: f64,
    beta: f64,
    gamma: f64,
    n: f64,
    dt: f64,
    steps: usize,
) -> Vec<(f64, f64)> {
    let mut result = Vec::with_capacity(steps + 1);
    let (mut u, mut v) = (u0, v0);
    result.push((u, v));
    for _ in 0..steps {
        let (du, dv) = toggle_switch(u, v, alpha1, alpha2, beta, gamma, n);
        u = (u + du * dt).max(0.0);
        v = (v + dv * dt).max(0.0);
        result.push((u, v));
    }
    result
}

pub fn ultrasensitivity_index(ec10: f64, ec90: f64) -> f64 {
    if ec10 < 1e-30 {
        return 0.0;
    }
    (ec90 / ec10).log10() / (81.0_f64).log10()
}

pub fn network_robustness(
    output_nominal: f64,
    output_perturbed: f64,
    perturbation_fraction: f64,
) -> f64 {
    if output_nominal.abs() < 1e-30 || perturbation_fraction.abs() < 1e-30 {
        return 0.0;
    }
    let relative_change = (output_perturbed - output_nominal).abs() / output_nominal.abs();
    1.0 - relative_change / perturbation_fraction
}

pub fn adaptation_precision(response_peak: f64, response_steady: f64, response_basal: f64) -> f64 {
    if (response_peak - response_basal).abs() < 1e-30 {
        return 0.0;
    }
    1.0 - (response_steady - response_basal).abs() / (response_peak - response_basal).abs()
}

pub fn bifurcation_parameter_scan(
    f: fn(f64, f64) -> f64,
    x0: f64,
    param_range: &[f64],
    dt: f64,
    settle: usize,
) -> Vec<(f64, f64)> {
    let mut result = Vec::with_capacity(param_range.len());
    for &p in param_range {
        let mut x = x0;
        for _ in 0..settle {
            x += f(x, p) * dt;
        }
        result.push((p, x));
    }
    result
}

pub fn hysteresis_width(forward_thresholds: &[f64], backward_thresholds: &[f64]) -> f64 {
    let fwd = forward_thresholds
        .iter()
        .cloned()
        .fold(f64::NEG_INFINITY, f64::max);
    let bwd = backward_thresholds
        .iter()
        .cloned()
        .fold(f64::INFINITY, f64::min);
    (fwd - bwd).abs()
}

pub fn nullcline_intersection(
    f: fn(f64, f64) -> f64,
    g: fn(f64, f64) -> f64,
    x_range: (f64, f64),
    y_range: (f64, f64),
    resolution: usize,
) -> Vec<(f64, f64)> {
    let dx = (x_range.1 - x_range.0) / resolution as f64;
    let dy = (y_range.1 - y_range.0) / resolution as f64;
    let mut intersections = Vec::new();
    for i in 0..resolution {
        for j in 0..resolution {
            let x = x_range.0 + i as f64 * dx;
            let y = y_range.0 + j as f64 * dy;
            let fv = f(x, y);
            let gv = g(x, y);
            if fv.abs() < dx * 10.0 && gv.abs() < dy * 10.0 {
                intersections.push((x, y));
            }
        }
    }
    intersections
}

pub fn saddle_node_condition(jacobian: &[[f64; 2]; 2]) -> f64 {
    jacobian[0][0] * jacobian[1][1] - jacobian[0][1] * jacobian[1][0]
}

pub fn potential_landscape_1d(
    f: fn(f64) -> f64,
    x_range: (f64, f64),
    n_points: usize,
) -> Vec<(f64, f64)> {
    let dx = (x_range.1 - x_range.0) / n_points as f64;
    let mut potential = 0.0;
    let mut result = Vec::with_capacity(n_points);
    for i in 0..n_points {
        let x = x_range.0 + i as f64 * dx;
        potential -= f(x) * dx;
        result.push((x, potential));
    }
    result
}