runmat-analysis-fea 0.5.5

Finite element assembly/solve/post scaffolding for RunMat
Documentation
pub fn displacement_increment_norm(a: &[f64], b: &[f64]) -> f64 {
    let mut sum = 0.0_f64;
    for (av, bv) in a.iter().zip(b.iter()) {
        let d = av - bv;
        sum += d * d;
    }
    sum.sqrt()
}

#[derive(Debug, Clone, Copy, PartialEq)]
pub struct TransientAdaptivityInput {
    pub step_dt: f64,
    pub residual_norm: f64,
    pub residual_target: f64,
    pub min_dt: f64,
    pub max_dt: f64,
    pub converged: bool,
    pub retries: usize,
    pub adapt_nonconverged_shrink: f64,
    pub adapt_growth_exponent: f64,
    pub adapt_min_scale: f64,
    pub adapt_max_scale: f64,
    pub adapt_retry_growth_cap: f64,
    pub thermo_growth_limit: f64,
    pub thermo_nonconverged_shrink: f64,
}

pub fn recommend_next_time_step(input: TransientAdaptivityInput) -> f64 {
    if !input.converged {
        return (input.step_dt
            * input.adapt_nonconverged_shrink.clamp(0.2, 1.0)
            * input.thermo_nonconverged_shrink)
            .clamp(input.min_dt, input.max_dt);
    }

    let target = input.residual_target.max(1.0e-12);
    let ratio = (target / input.residual_norm.max(1.0e-12)).clamp(0.25, 4.0);
    let mut factor = ratio.powf(input.adapt_growth_exponent.clamp(0.1, 1.0));
    factor = factor.clamp(
        input.adapt_min_scale.clamp(0.2, 1.0),
        input.adapt_max_scale.clamp(1.0, 2.0),
    );
    if input.retries > 0 {
        factor = factor.min(input.adapt_retry_growth_cap.clamp(1.0, 1.5));
    } else if input.residual_norm <= target * 0.1 {
        factor = factor.max((1.0 + (input.adapt_max_scale - 1.0) * 0.6).clamp(1.0, 1.5));
    }
    factor = factor.min(input.thermo_growth_limit.max(0.5));
    (input.step_dt * factor).clamp(input.min_dt, input.max_dt)
}

pub fn nonlinear_iteration_damping(
    line_search_enabled: bool,
    refresh_tangent: bool,
    thermo_severity: f64,
) -> f64 {
    let mut damping = if line_search_enabled { 0.62 } else { 0.72 };
    if refresh_tangent {
        damping *= 0.85;
    }
    damping * (1.0 - 0.08 * thermo_severity).clamp(0.65, 1.0)
}

pub fn nonlinear_trial_residual(residual: f64, trial_scale: f64) -> f64 {
    residual * (0.85 * trial_scale + 0.1)
}

pub fn nonlinear_spike_threshold(max_newton_iters: usize, complexity_scale: f64) -> usize {
    ((max_newton_iters as f64) * 0.7 / complexity_scale.sqrt()).ceil() as usize
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn displacement_increment_norm_matches_expected_delta() {
        let a = vec![1.0, 2.0, 3.0];
        let b = vec![1.0, 0.0, 3.0];
        let norm = displacement_increment_norm(&a, &b);
        assert!((norm - 2.0).abs() < 1.0e-12);
    }

    #[test]
    fn transient_adaptivity_recommendation_stays_bounded() {
        let next = recommend_next_time_step(TransientAdaptivityInput {
            step_dt: 1.0e-3,
            residual_norm: 1.0e-7,
            residual_target: 1.0e-6,
            min_dt: 1.0e-6,
            max_dt: 2.0e-2,
            converged: true,
            retries: 0,
            adapt_nonconverged_shrink: 0.75,
            adapt_growth_exponent: 0.35,
            adapt_min_scale: 0.8,
            adapt_max_scale: 1.25,
            adapt_retry_growth_cap: 1.05,
            thermo_growth_limit: 1.0,
            thermo_nonconverged_shrink: 1.0,
        });
        assert!((1.0e-6..=2.0e-2).contains(&next));
    }

    #[test]
    fn nonlinear_policy_helpers_produce_finite_values() {
        let damping = nonlinear_iteration_damping(true, true, 0.8);
        let trial = nonlinear_trial_residual(0.1, 0.5);
        let spike = nonlinear_spike_threshold(24, 4.0);
        assert!(damping.is_finite() && damping > 0.0);
        assert!(trial.is_finite() && trial > 0.0);
        assert!(spike >= 1);
    }
}