runmat-analysis-fea 0.5.4

Finite element assembly/solve/post scaffolding for RunMat
Documentation
use crate::FeaElectroThermalContext;

pub fn severity(context: Option<&FeaElectroThermalContext>) -> f64 {
    let Some(context) = context else {
        return 0.0;
    };
    if !context.enabled {
        return 0.0;
    }
    (context.applied_voltage_v.powi(2)
        * context.base_electrical_conductivity_s_per_m.max(1.0e-9)
        * context.resistive_heating_coefficient.max(0.0)
        / 1.0e7)
        .clamp(0.0, 1.0)
}

pub fn time_scale(context: Option<&FeaElectroThermalContext>, normalized_time: f64) -> f64 {
    let Some(context) = context else {
        return 1.0;
    };
    if context.time_profile.is_empty() {
        return 1.0;
    }
    let t = normalized_time.clamp(0.0, 1.0);
    let mut points = context.time_profile.clone();
    points.sort_by(|a, b| a.normalized_time.total_cmp(&b.normalized_time));
    if t <= points[0].normalized_time {
        return points[0].current_scale.clamp(0.2, 2.0);
    }
    for pair in points.windows(2) {
        let a = &pair[0];
        let b = &pair[1];
        if t >= a.normalized_time && t <= b.normalized_time {
            let span = (b.normalized_time - a.normalized_time).abs().max(1.0e-9);
            let alpha = (t - a.normalized_time) / span;
            return (a.current_scale + (b.current_scale - a.current_scale) * alpha).clamp(0.2, 2.0);
        }
    }
    points
        .last()
        .map(|p| p.current_scale.clamp(0.2, 2.0))
        .unwrap_or(1.0)
}

pub fn temporal_profile_variation(context: Option<&FeaElectroThermalContext>) -> f64 {
    let Some(context) = context else {
        return 0.0;
    };
    if context.time_profile.len() < 2 {
        return 0.0;
    }
    let mut min_scale = f64::INFINITY;
    let mut max_scale = -f64::INFINITY;
    for point in &context.time_profile {
        min_scale = min_scale.min(point.current_scale);
        max_scale = max_scale.max(point.current_scale);
    }
    if !min_scale.is_finite() || !max_scale.is_finite() {
        return 0.0;
    }
    ((max_scale - min_scale).abs() / 2.0).clamp(0.0, 1.0)
}

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

    #[test]
    fn electro_time_scale_interpolates_in_range() {
        let context = FeaElectroThermalContext {
            enabled: true,
            reference_temperature_k: 293.15,
            applied_voltage_v: 24.0,
            base_electrical_conductivity_s_per_m: 3.0e7,
            resistive_heating_coefficient: 5.0e-4,
            region_conductivity_scales: Vec::new(),
            time_profile: vec![
                crate::FeaElectroTimeProfilePoint {
                    normalized_time: 0.0,
                    current_scale: 0.5,
                },
                crate::FeaElectroTimeProfilePoint {
                    normalized_time: 1.0,
                    current_scale: 1.5,
                },
            ],
        };
        let scale = time_scale(Some(&context), 0.5);
        assert!((0.5..=1.5).contains(&scale));
    }
}