runmat-analysis-fea 0.5.6

Finite element assembly/solve/post scaffolding for RunMat
Documentation
use crate::diagnostics::{FeaDiagnostic, FeaDiagnosticSeverity};

use super::TransientSolveOptions;

pub(super) struct TransientQualityDiagnosticInputs<'a> {
    pub(super) options: &'a TransientSolveOptions,
    pub(super) dt_final: f64,
    pub(super) converged_steps: usize,
    pub(super) retry_budget_hits: usize,
    pub(super) accepted_time_steps_s: &'a [f64],
    pub(super) residual_norms: &'a [f64],
    pub(super) energies: &'a [f64],
    pub(super) use_runtime_tensor: bool,
    pub(super) prepared_cache_entries: usize,
    pub(super) prepared_cache_hits: usize,
    pub(super) prepared_cache_misses: usize,
    pub(super) prepared_build_ms: f64,
    pub(super) solve_ms: f64,
    pub(super) fallback_apply_count: u32,
    pub(super) adapt_increase_steps: usize,
    pub(super) adapt_decrease_steps: usize,
    pub(super) adapt_hold_steps: usize,
    pub(super) adapt_scale_mean: f64,
    pub(super) adapt_scale_min: f64,
    pub(super) adapt_scale_max: f64,
    pub(super) dt_bucket_rel_tolerance: f64,
    pub(super) max_step_l2_jump_ratio: f64,
    pub(super) nonfinite_displacement_count: usize,
    pub(super) thermo_severity_mean: f64,
    pub(super) thermo_time_scale_mean: f64,
    pub(super) thermo_severity_peak: f64,
    pub(super) thermo_temporal_variation: f64,
    pub(super) thermo_time_extrapolated: usize,
    pub(super) thermo_time_clamped: usize,
    pub(super) effective_residual_target_peak: f64,
    pub(super) thermo_growth_limit_min: f64,
    pub(super) thermo_nonconverged_shrink_min: f64,
}

pub(super) fn push_transient_quality_diagnostics(
    diagnostics: &mut Vec<FeaDiagnostic>,
    inputs: TransientQualityDiagnosticInputs<'_>,
) {
    let options = inputs.options;
    let converged_all = inputs.converged_steps == options.step_count;
    diagnostics.push(FeaDiagnostic {
        code: "FEA_TRANSIENT_CONVERGENCE".to_string(),
        severity: if converged_all {
            FeaDiagnosticSeverity::Info
        } else {
            FeaDiagnosticSeverity::Warning
        },
        message: format!(
            "step_count={} converged_steps={} dt_initial={} dt_final={}",
            options.step_count, inputs.converged_steps, options.time_step_s, inputs.dt_final
        ),
    });
    if !inputs.accepted_time_steps_s.is_empty() {
        let min_accepted_dt = inputs
            .accepted_time_steps_s
            .iter()
            .copied()
            .reduce(f64::min)
            .unwrap_or(options.min_time_step_s);
        let max_accepted_dt = inputs
            .accepted_time_steps_s
            .iter()
            .copied()
            .reduce(f64::max)
            .unwrap_or(options.max_time_step_s);
        let max_residual = inputs
            .residual_norms
            .iter()
            .copied()
            .fold(0.0_f64, f64::max);
        diagnostics.push(FeaDiagnostic {
            code: "FEA_TRANSIENT_STABILITY".to_string(),
            severity: if max_residual <= options.residual_target * 4.0 {
                FeaDiagnosticSeverity::Info
            } else {
                FeaDiagnosticSeverity::Warning
            },
            message: format!(
                "adaptive={} dt_min={} dt_max={} max_residual_norm={}",
                options.adaptive_time_step, min_accepted_dt, max_accepted_dt, max_residual
            ),
        });
    }
    if inputs.retry_budget_hits > 0 {
        diagnostics.push(FeaDiagnostic {
            code: "FEA_TRANSIENT_STEP_FAILURE".to_string(),
            severity: FeaDiagnosticSeverity::Warning,
            message: format!("retry_budget_hits={}", inputs.retry_budget_hits),
        });
    }
    if inputs.use_runtime_tensor {
        diagnostics.push(FeaDiagnostic {
            code: "FEA_TRANSIENT_CACHE".to_string(),
            severity: FeaDiagnosticSeverity::Info,
            message: format!(
                "prepared_cache_entries={} prepared_cache_hits={} prepared_cache_misses={}",
                inputs.prepared_cache_entries,
                inputs.prepared_cache_hits,
                inputs.prepared_cache_misses
            ),
        });
    }
    diagnostics.push(FeaDiagnostic {
        code: "FEA_TRANSIENT_COST".to_string(),
        severity: FeaDiagnosticSeverity::Info,
        message: format!(
            "prepared_build_ms={} solve_ms={} fallback_apply_count={}",
            inputs.prepared_build_ms, inputs.solve_ms, inputs.fallback_apply_count
        ),
    });
    diagnostics.push(FeaDiagnostic {
        code: "FEA_TRANSIENT_ADAPTIVITY".to_string(),
        severity: FeaDiagnosticSeverity::Info,
        message: format!(
            "increase_steps={} decrease_steps={} hold_steps={} scale_min={} scale_max={} scale_mean={}",
            inputs.adapt_increase_steps,
            inputs.adapt_decrease_steps,
            inputs.adapt_hold_steps,
            inputs.adapt_scale_min,
            inputs.adapt_scale_max,
            inputs.adapt_scale_mean
        ),
    });
    diagnostics.push(FeaDiagnostic {
        code: "FEA_TRANSIENT_BUCKETING".to_string(),
        severity: FeaDiagnosticSeverity::Info,
        message: format!(
            "enabled={} rel_tolerance={}",
            inputs.dt_bucket_rel_tolerance > 0.0,
            inputs.dt_bucket_rel_tolerance
        ),
    });
    diagnostics.push(FeaDiagnostic {
        code: "FEA_TRANSIENT_PHYSICS".to_string(),
        severity: if inputs.nonfinite_displacement_count == 0
            && inputs.max_step_l2_jump_ratio <= 4.0
        {
            FeaDiagnosticSeverity::Info
        } else {
            FeaDiagnosticSeverity::Warning
        },
        message: format!(
            "max_step_l2_jump_ratio={} nonfinite_displacement_count={}",
            inputs.max_step_l2_jump_ratio, inputs.nonfinite_displacement_count
        ),
    });

    if inputs.thermo_severity_peak > 0.0 {
        diagnostics.push(FeaDiagnostic {
            code: "FEA_TM_TRANSIENT".to_string(),
            severity: if inputs.thermo_severity_peak <= 0.6
                && inputs.thermo_temporal_variation <= 0.5
            {
                FeaDiagnosticSeverity::Info
            } else {
                FeaDiagnosticSeverity::Warning
            },
            message: format!(
                "severity_mean={} time_scale_mean={} severity_peak={} temporal_variation={} field_extrapolation_ratio={} field_clamp_ratio={} effective_residual_target_peak={} growth_limit_min={} nonconverged_shrink_min={}",
                inputs.thermo_severity_mean,
                inputs.thermo_time_scale_mean,
                inputs.thermo_severity_peak,
                inputs.thermo_temporal_variation,
                if options.step_count == 0 {
                    0.0
                } else {
                    inputs.thermo_time_extrapolated as f64 / options.step_count as f64
                },
                if options.step_count == 0 {
                    0.0
                } else {
                    inputs.thermo_time_clamped as f64 / options.step_count as f64
                },
                inputs.effective_residual_target_peak,
                inputs.thermo_growth_limit_min,
                inputs.thermo_nonconverged_shrink_min,
            ),
        });
    }

    if !inputs.energies.is_empty() {
        let baseline_energy = inputs
            .energies
            .iter()
            .copied()
            .skip(1)
            .find(|energy| *energy > 1.0e-12)
            .unwrap_or_else(|| inputs.energies[0].abs().max(1.0e-12));
        let max_energy = inputs.energies.iter().copied().fold(0.0_f64, f64::max);
        let growth_ratio = max_energy / baseline_energy;
        diagnostics.push(FeaDiagnostic {
            code: "FEA_TRANSIENT_ENERGY".to_string(),
            severity: if growth_ratio <= 5.0 {
                FeaDiagnosticSeverity::Info
            } else {
                FeaDiagnosticSeverity::Warning
            },
            message: format!("max_energy_growth_ratio={growth_ratio}"),
        });
    }
}