runmat-analysis-fea 0.5.6

Finite element assembly/solve/post scaffolding for RunMat
Documentation
pub mod electro_thermal;
pub mod electromagnetic;
pub mod linear_static;
pub mod modal;
pub mod nonlinear;
pub mod thermal;
pub mod thermo_mechanical;
pub mod transient;

use runmat_analysis_core::{
    AnalysisModel, BoundaryConditionKind, LoadKind, StructuralElementKind, StructuralModel,
};

use crate::{
    assembly::{dofs::StructuralDofKind, AssemblySummary},
    contracts::FeaRunError,
};

const MOMENT_REQUIRES_ROTATIONAL_DOF_MESSAGE: &str =
    "moment loads require rotational-DOF structural elements";
const ROTATION_REQUIRES_ROTATIONAL_DOF_MESSAGE: &str =
    "prescribed rotations require rotational-DOF structural elements";

pub(crate) fn validate_rotational_dof_targets(
    model: &AnalysisModel,
    summary: &AssemblySummary,
) -> Result<(), FeaRunError> {
    for load in &model.loads {
        if !matches!(load.kind, LoadKind::Moment { .. }) {
            continue;
        }
        if !summary.structural_dof_layout.has_rotational_dofs() {
            return Err(moment_load_error(load));
        }
        let Some(structural) = model.structural.as_ref() else {
            continue;
        };
        let target_nodes = structural_target_nodes(structural, &load.region_id);
        if target_nodes.is_empty()
            || target_nodes
                .iter()
                .any(|node_index| !node_has_rotational_dofs(summary, *node_index))
        {
            return Err(moment_load_error(load));
        }
    }
    for bc in &model.boundary_conditions {
        if !matches!(bc.kind, BoundaryConditionKind::PrescribedRotation { .. }) {
            continue;
        }
        if !summary.structural_dof_layout.has_rotational_dofs() {
            return Err(rotation_boundary_condition_error(bc));
        }
        let Some(structural) = model.structural.as_ref() else {
            continue;
        };
        let target_nodes = structural_target_nodes(structural, &bc.region_id);
        if target_nodes.is_empty()
            || target_nodes
                .iter()
                .any(|node_index| !node_has_rotational_dofs(summary, *node_index))
        {
            return Err(rotation_boundary_condition_error(bc));
        }
    }
    Ok(())
}

pub(crate) fn reject_moment_loads_for_nonstructural_pipeline(
    model: &AnalysisModel,
    family: &str,
) -> Result<(), FeaRunError> {
    let Some(load) = model
        .loads
        .iter()
        .find(|load| matches!(load.kind, LoadKind::Moment { .. }))
    else {
        return Ok(());
    };

    Err(FeaRunError::InvalidModel(format!(
        "moment loads are structural loads and cannot be used in {family} solves; load_id={} region_id={}",
        load.load_id, load.region_id
    )))
}

fn moment_load_error(load: &runmat_analysis_core::LoadCase) -> FeaRunError {
    FeaRunError::InvalidModel(format!(
        "{}; load_id={} region_id={}",
        MOMENT_REQUIRES_ROTATIONAL_DOF_MESSAGE, load.load_id, load.region_id
    ))
}

fn rotation_boundary_condition_error(bc: &runmat_analysis_core::BoundaryCondition) -> FeaRunError {
    FeaRunError::InvalidModel(format!(
        "{}; bc_id={} region_id={}",
        ROTATION_REQUIRES_ROTATIONAL_DOF_MESSAGE, bc.bc_id, bc.region_id
    ))
}

fn node_has_rotational_dofs(summary: &AssemblySummary, node_index: usize) -> bool {
    [
        StructuralDofKind::Rx,
        StructuralDofKind::Ry,
        StructuralDofKind::Rz,
    ]
    .iter()
    .any(|kind| {
        summary
            .structural_dof_layout
            .index(node_index, *kind)
            .is_some()
    })
}

fn structural_target_nodes(structural: &StructuralModel, region_id: &str) -> Vec<usize> {
    if let Some(node_id) = structural_node_selector(region_id) {
        return structural_node_index(structural, node_id)
            .into_iter()
            .collect();
    }
    let mut nodes = Vec::new();
    for element in &structural.elements {
        if element.region_id != region_id {
            continue;
        }
        match &element.kind {
            StructuralElementKind::Beam(beam) => {
                for node_id in beam.node_ids {
                    if let Some(index) = structural_node_index(structural, node_id) {
                        if !nodes.contains(&index) {
                            nodes.push(index);
                        }
                    }
                }
            }
            StructuralElementKind::Shell(shell) => {
                for node_id in shell.node_ids {
                    if let Some(index) = structural_node_index(structural, node_id) {
                        if !nodes.contains(&index) {
                            nodes.push(index);
                        }
                    }
                }
            }
        }
    }
    nodes
}

fn structural_node_index(structural: &StructuralModel, node_id: u32) -> Option<usize> {
    structural
        .nodes
        .iter()
        .position(|node| node.node_id == node_id)
}

fn structural_node_selector(region_id: &str) -> Option<u32> {
    region_id
        .strip_prefix("node:")
        .unwrap_or(region_id)
        .parse::<u32>()
        .ok()
}