runmat-analysis-fea 0.5.6

Finite element assembly/solve/post scaffolding for RunMat
Documentation
use runmat_analysis_core::{validate_model, AnalysisField, AnalysisModel};

use crate::{
    assembly::assemble_linear_system,
    contracts::{
        fea_modal_mode_shape_field_id, ComputeBackend, FeaModalRunResult, FeaRunError,
        FeaRunResult, ModalSolveOptions, FEA_FIELD_MODAL_EIGENVALUE, FEA_FIELD_MODAL_FREQUENCY_HZ,
        FEA_FIELD_MODAL_MODAL_MASS, FEA_FIELD_MODAL_MODAL_STIFFNESS,
        FEA_FIELD_MODAL_M_ORTHOGONALITY, FEA_FIELD_MODAL_PARTICIPATION_FACTOR,
        FEA_FIELD_MODAL_RELATIVE_FREQUENCY_SEPARATION, FEA_FIELD_MODAL_RESIDUAL_NORM,
        FEA_FIELD_STRUCTURAL_DISPLACEMENT, FEA_FIELD_STRUCTURAL_VON_MISES,
    },
    diagnostics::builders::{extend_common_run_diagnostics, CommonRunDiagnosticInputs},
    operator::{apply_k, apply_m},
    progress::{check_cancelled, emit_phase, FeaProgressPhase, FeaProgressStatus},
    solve::modal::solve_modal_system,
};

pub fn run_modal(
    model: &AnalysisModel,
    backend: ComputeBackend,
) -> Result<FeaModalRunResult, FeaRunError> {
    run_modal_with_options(model, backend, ModalSolveOptions::default())
}

pub fn run_modal_with_options(
    model: &AnalysisModel,
    backend: ComputeBackend,
    options: ModalSolveOptions,
) -> Result<FeaModalRunResult, FeaRunError> {
    emit_phase(
        "fea.run_modal",
        FeaProgressPhase::RegionResolution,
        FeaProgressStatus::Started,
        "validating modal FEA model",
        Some(0),
        Some(5),
    );
    check_cancelled("fea.run_modal")?;
    validate_model(model).map_err(|err| FeaRunError::InvalidModel(err.to_string()))?;
    emit_phase(
        "fea.run_modal",
        FeaProgressPhase::RegionResolution,
        FeaProgressStatus::Completed,
        "modal model validation complete",
        Some(1),
        Some(5),
    );
    check_cancelled("fea.run_modal")?;

    emit_phase(
        "fea.run_modal",
        FeaProgressPhase::ModelAssembly,
        FeaProgressStatus::Started,
        "assembling modal system",
        Some(1),
        Some(5),
    );
    let summary = assemble_linear_system(
        model,
        options.prep_context.clone(),
        options.thermo_mechanical_context,
        options.electro_thermal_context,
    );
    super::validate_rotational_dof_targets(model, &summary)?;
    emit_phase(
        "fea.run_modal",
        FeaProgressPhase::ModelAssembly,
        FeaProgressStatus::Completed,
        "modal system assembly complete",
        Some(2),
        Some(5),
    );
    check_cancelled("fea.run_modal")?;

    emit_phase(
        "fea.run_modal",
        FeaProgressPhase::Solve,
        FeaProgressStatus::Started,
        "solving modal basis",
        Some(2),
        Some(5),
    );
    let modal = solve_modal_system(&summary, options.mode_count, backend);
    emit_phase(
        "fea.run_modal",
        FeaProgressPhase::Solve,
        FeaProgressStatus::Completed,
        "modal solve complete",
        Some(3),
        Some(5),
    );
    check_cancelled("fea.run_modal")?;

    emit_phase(
        "fea.run_modal",
        FeaProgressPhase::Postprocess,
        FeaProgressStatus::Started,
        "recovering modal fields",
        Some(3),
        Some(5),
    );
    let mut diagnostics = modal.diagnostics.clone();
    extend_common_run_diagnostics(
        &mut diagnostics,
        CommonRunDiagnosticInputs {
            model,
            summary: &summary,
            prep_context: options.prep_context.clone(),
            iteration_metric: mode_shapes_iteration_count_estimate(&modal.residual_norms),
            residual_metric: modal.residual_norms.iter().copied().fold(0.0_f64, f64::max),
            requested_preconditioner: "auto",
            effective_preconditioner: if backend == ComputeBackend::Gpu {
                "jacobi"
            } else {
                "none"
            },
        },
    );

    let displacement = modal
        .mode_shapes
        .first()
        .cloned()
        .unwrap_or_else(|| vec![0.0; summary.dof_count.max(3)]);
    let von_mises = displacement
        .iter()
        .map(|value| value.abs())
        .fold(0.0_f64, f64::max)
        * 1.0e11;
    let modal_fields = recover_modal_result_fields(
        &summary,
        &modal.eigenvalues_hz,
        &modal.mode_shapes,
        &modal.residual_norms,
    );
    let mut run_fields = vec![
        AnalysisField::host_f64(
            FEA_FIELD_STRUCTURAL_DISPLACEMENT,
            vector_shape(summary.dof_count),
            padded_vector_values(displacement, summary.dof_count),
        ),
        AnalysisField::host_f64(FEA_FIELD_STRUCTURAL_VON_MISES, vec![1], vec![von_mises]),
    ];
    run_fields.extend(modal_fields);

    let run = FeaRunResult {
        backend,
        solver_backend: modal.solver_backend,
        solver_device_apply_k_ratio: if modal.device_apply_k_attempt_count == 0 {
            0.0
        } else {
            modal.device_apply_k_count as f64 / modal.device_apply_k_attempt_count as f64
        },
        solver_method: modal.solver_method,
        preconditioner: if backend == ComputeBackend::Gpu {
            "jacobi".to_string()
        } else {
            "none".to_string()
        },
        solver_host_sync_count: modal.solver_host_sync_count,
        diagnostics,
        fields: run_fields,
    };

    let mode_shape = vector_shape(summary.dof_count);
    let mode_shape_value_count = mode_shape.iter().product();
    let mode_shapes = modal
        .mode_shapes
        .into_iter()
        .enumerate()
        .map(|(index, shape)| {
            let mut values = shape;
            values.resize(mode_shape_value_count, 0.0);
            AnalysisField::host_f64(
                fea_modal_mode_shape_field_id(index + 1),
                mode_shape.clone(),
                values,
            )
        })
        .collect();

    emit_phase(
        "fea.run_modal",
        FeaProgressPhase::Postprocess,
        FeaProgressStatus::Completed,
        "modal result field recovery complete",
        Some(4),
        Some(5),
    );
    check_cancelled("fea.run_modal")?;
    emit_phase(
        "fea.run_modal",
        FeaProgressPhase::Complete,
        FeaProgressStatus::Completed,
        "FEA modal run complete",
        Some(5),
        Some(5),
    );

    Ok(FeaModalRunResult {
        run,
        eigenvalues_hz: modal.eigenvalues_hz,
        mode_shapes,
        residual_norms: modal.residual_norms,
    })
}

fn mode_shapes_iteration_count_estimate(residual_norms: &[f64]) -> f64 {
    residual_norms.len() as f64
}

fn vector_shape(dof_count: usize) -> Vec<usize> {
    vec![dof_count.div_ceil(3).max(1), 3]
}

fn padded_vector_values(mut values: Vec<f64>, dof_count: usize) -> Vec<f64> {
    values.resize(vector_shape(dof_count).iter().product(), 0.0);
    values
}

fn recover_modal_result_fields(
    summary: &crate::assembly::AssemblySummary,
    frequencies_hz: &[f64],
    mode_shapes: &[Vec<f64>],
    residual_norms: &[f64],
) -> Vec<AnalysisField> {
    let mode_count = frequencies_hz.len().min(mode_shapes.len());
    let frequencies = frequencies_hz
        .iter()
        .copied()
        .take(mode_count)
        .collect::<Vec<_>>();
    let eigenvalues = frequencies
        .iter()
        .map(|frequency| (2.0 * std::f64::consts::PI * frequency).powi(2))
        .collect::<Vec<_>>();
    let mut modal_mass = Vec::with_capacity(mode_count);
    let mut modal_stiffness = Vec::with_capacity(mode_count);
    let mut participation_factor = Vec::with_capacity(mode_count);
    let residual_norm = (0..mode_count)
        .map(|index| residual_norms.get(index).copied().unwrap_or(f64::INFINITY))
        .collect::<Vec<_>>();
    let relative_frequency_separation = relative_frequency_separation(&frequencies);
    let mut m_orthogonality = Vec::with_capacity(mode_count.saturating_mul(mode_count));

    for mode_shape in mode_shapes.iter().take(mode_count) {
        let mass_applied = apply_m(&summary.operator, mode_shape);
        let stiffness_applied = apply_k(&summary.operator, mode_shape);
        let mass = dot(mode_shape, &mass_applied).abs();
        let stiffness = dot(mode_shape, &stiffness_applied).abs();
        modal_mass.push(mass);
        modal_stiffness.push(stiffness);
        participation_factor.push(mass_applied.iter().sum::<f64>() / mass.sqrt().max(1.0e-12));
    }

    for left in mode_shapes.iter().take(mode_count) {
        for right in mode_shapes.iter().take(mode_count) {
            let mass_applied = apply_m(&summary.operator, right);
            m_orthogonality.push(dot(left, &mass_applied));
        }
    }

    vec![
        AnalysisField::host_f64(FEA_FIELD_MODAL_FREQUENCY_HZ, vec![mode_count], frequencies),
        AnalysisField::host_f64(FEA_FIELD_MODAL_EIGENVALUE, vec![mode_count], eigenvalues),
        AnalysisField::host_f64(FEA_FIELD_MODAL_MODAL_MASS, vec![mode_count], modal_mass),
        AnalysisField::host_f64(
            FEA_FIELD_MODAL_MODAL_STIFFNESS,
            vec![mode_count],
            modal_stiffness,
        ),
        AnalysisField::host_f64(
            FEA_FIELD_MODAL_PARTICIPATION_FACTOR,
            vec![mode_count],
            participation_factor,
        ),
        AnalysisField::host_f64(
            FEA_FIELD_MODAL_RESIDUAL_NORM,
            vec![mode_count],
            residual_norm,
        ),
        AnalysisField::host_f64(
            FEA_FIELD_MODAL_RELATIVE_FREQUENCY_SEPARATION,
            vec![mode_count],
            relative_frequency_separation,
        ),
        AnalysisField::host_f64(
            FEA_FIELD_MODAL_M_ORTHOGONALITY,
            vec![mode_count, mode_count],
            m_orthogonality,
        ),
    ]
}

fn relative_frequency_separation(frequencies: &[f64]) -> Vec<f64> {
    if frequencies.len() < 2 {
        return vec![1.0; frequencies.len()];
    }

    let mut separation = vec![f64::INFINITY; frequencies.len()];
    for (index, window) in frequencies.windows(2).enumerate() {
        let a = window[0].abs().max(1.0e-12);
        let b = window[1].abs().max(1.0e-12);
        let value = (b - a).abs() / a.max(b);
        separation[index] = separation[index].min(value);
        separation[index + 1] = separation[index + 1].min(value);
    }
    separation
        .into_iter()
        .map(|value| if value.is_finite() { value } else { 1.0 })
        .collect()
}

fn dot(left: &[f64], right: &[f64]) -> f64 {
    left.iter()
        .zip(right.iter())
        .map(|(lhs, rhs)| lhs * rhs)
        .sum()
}