runmat-meshing 0.6.0

Topology-first meshing pipeline orchestration for RunMat
Documentation
use super::*;
use runmat_meshing_core::{
    contracts::{
        AnalysisBoundaryEdge, AnalysisBoundaryFace, AnalysisMeshArtifact, AnalysisMeshNode,
        AnalysisMeshProvenance, AnalysisVolumeElement, BoundaryElementKind, MeshBackendSummary,
        VolumeElementKind, ANALYSIS_MESH_BOUNDARY_FACE_TOPOLOGY_ID,
        ANALYSIS_MESH_FIELD_TOPOLOGY_ID,
    },
    quality::AnalysisMeshQualityReport,
    MeshSizingField,
};

#[test]
fn maps_element_scalar_values_to_boundary_faces() {
    let mesh = field_mapping_mesh();

    let values = map_volume_scalar_field_to_boundary_faces(&mesh, &[10.0, 20.0])
        .expect("boundary scalar mapping should succeed");

    assert_eq!(
        values,
        vec![
            BoundaryFaceScalarValue {
                face_id: "bf1".to_string(),
                value: 10.0,
            },
            BoundaryFaceScalarValue {
                face_id: "bf2".to_string(),
                value: 15.0,
            },
        ]
    );
}

#[test]
fn maps_nodal_vector_values_to_boundary_nodes() {
    let mesh = field_mapping_mesh();

    let values = map_nodal_vector_field_to_boundary_nodes(&mesh, &nodal_vector_values())
        .expect("boundary node vector mapping should succeed");

    assert_eq!(
        values,
        vec![
            BoundaryNodeVectorValue {
                node_id: 1,
                value: [1.0, 0.0, 0.0],
            },
            BoundaryNodeVectorValue {
                node_id: 2,
                value: [2.0, 0.0, 0.0],
            },
            BoundaryNodeVectorValue {
                node_id: 3,
                value: [3.0, 0.0, 0.0],
            },
            BoundaryNodeVectorValue {
                node_id: 4,
                value: [4.0, 0.0, 0.0],
            },
        ]
    );
}

#[test]
fn maps_nodal_vector_values_to_boundary_faces() {
    let mesh = field_mapping_mesh();

    let values = map_nodal_vector_field_to_boundary_faces(&mesh, &nodal_vector_values())
        .expect("boundary face vector mapping should succeed");

    assert_eq!(
        values,
        vec![
            BoundaryFaceVectorValue {
                face_id: "bf1".to_string(),
                value: [7.0 / 3.0, 0.0, 0.0],
            },
            BoundaryFaceVectorValue {
                face_id: "bf2".to_string(),
                value: [2.0, 0.0, 0.0],
            },
        ]
    );
}

#[test]
fn rejects_unmapped_boundary_faces() {
    let mut mesh = field_mapping_mesh();
    mesh.boundary_faces[0].adjacent_volume_element_ids.clear();

    let err = map_volume_scalar_field_to_boundary_faces(&mesh, &[10.0, 20.0])
        .expect_err("missing adjacency should fail");

    assert_eq!(
        err,
        FieldMappingError::BoundaryFaceMissingAdjacentVolume {
            face_id: "bf1".to_string(),
        }
    );
}

#[test]
fn rejects_element_field_length_mismatch() {
    let mesh = field_mapping_mesh();

    let err = map_volume_scalar_field_to_boundary_faces(&mesh, &[10.0])
        .expect_err("field length mismatch should fail");

    assert_eq!(
        err,
        FieldMappingError::ElementFieldLengthMismatch {
            element_value_count: 1,
            volume_element_count: 2,
        }
    );
}

#[test]
fn rejects_missing_field_topology_descriptor() {
    let mut mesh = field_mapping_mesh();
    mesh.field_topology.clear();

    let err = map_volume_scalar_field_to_boundary_faces(&mesh, &[10.0, 20.0])
        .expect_err("missing field topology should fail");

    assert_eq!(
        err,
        FieldMappingError::FieldTopologyMissing {
            topology_id: ANALYSIS_MESH_FIELD_TOPOLOGY_ID.to_string(),
            location: runmat_meshing_core::AnalysisFieldTopologyLocation::VolumeElement,
            element_kind: Some("tetrahedron4".to_string()),
        }
    );
}

#[test]
fn rejects_stale_field_topology_count_before_mapping() {
    let mut mesh = field_mapping_mesh();
    let boundary_face_descriptor = mesh
        .field_topology
        .iter_mut()
        .find(|descriptor| descriptor.topology_id == ANALYSIS_MESH_BOUNDARY_FACE_TOPOLOGY_ID)
        .expect("boundary face topology descriptor");
    boundary_face_descriptor.entity_count += 1;

    let err = map_nodal_vector_field_to_boundary_faces(&mesh, &nodal_vector_values())
        .expect_err("stale field topology count should fail");

    assert_eq!(
        err,
        FieldMappingError::FieldTopologyCountMismatch {
            topology_id: ANALYSIS_MESH_BOUNDARY_FACE_TOPOLOGY_ID.to_string(),
            location: runmat_meshing_core::AnalysisFieldTopologyLocation::BoundaryFace,
            expected_entity_count: 3,
            actual_entity_count: 2,
        }
    );
}

#[test]
fn rejects_node_vector_field_length_mismatch() {
    let mesh = field_mapping_mesh();

    let err = map_nodal_vector_field_to_boundary_nodes(&mesh, &[[1.0, 0.0, 0.0]])
        .expect_err("node field length mismatch should fail");

    assert_eq!(
        err,
        FieldMappingError::NodeVectorFieldLengthMismatch {
            node_value_count: 1,
            node_count: 5,
        }
    );
}

#[test]
fn rejects_nonfinite_node_vector_values() {
    let mesh = field_mapping_mesh();
    let mut values = nodal_vector_values();
    values[2][1] = f64::INFINITY;

    let err = map_nodal_vector_field_to_boundary_faces(&mesh, &values)
        .expect_err("nonfinite node vector should fail");

    assert_eq!(
        err,
        FieldMappingError::NonFiniteNodeVectorValue {
            node_index: 2,
            component_index: 1,
        }
    );
}

#[test]
fn rejects_nonfinite_element_values() {
    let mesh = field_mapping_mesh();

    let err = map_volume_scalar_field_to_boundary_faces(&mesh, &[10.0, f64::NAN])
        .expect_err("nonfinite element value should fail");

    assert_eq!(
        err,
        FieldMappingError::NonFiniteElementValue { element_index: 1 }
    );
}

#[test]
fn rejects_boundary_faces_referencing_unknown_volume_elements() {
    let mut mesh = field_mapping_mesh();
    mesh.boundary_faces[0].adjacent_volume_element_ids = vec!["missing".to_string()];

    let err = map_volume_scalar_field_to_boundary_faces(&mesh, &[10.0, 20.0])
        .expect_err("unknown adjacent volume element should fail");

    assert_eq!(
        err,
        FieldMappingError::BoundaryFaceReferencesUnknownVolume {
            face_id: "bf1".to_string(),
            volume_element_id: "missing".to_string(),
        }
    );
}

#[test]
fn rejects_boundary_faces_referencing_unknown_nodes() {
    let mut mesh = field_mapping_mesh();
    mesh.boundary_faces[0].node_ids = vec![1, 2, 99];

    let err = map_nodal_vector_field_to_boundary_faces(&mesh, &nodal_vector_values())
        .expect_err("unknown boundary face node should fail");

    assert_eq!(
        err,
        FieldMappingError::BoundaryFaceReferencesUnknownNode {
            face_id: "bf1".to_string(),
            node_id: 99,
        }
    );
}

#[test]
fn rejects_boundary_edges_referencing_unknown_nodes() {
    let mut mesh = field_mapping_mesh();
    mesh.boundary_edges.push(AnalysisBoundaryEdge {
        edge_id: "be1".to_string(),
        node_ids: [1, 99],
        adjacent_boundary_face_ids: Vec::new(),
        region_ids: Vec::new(),
        provenance: Vec::new(),
    });
    mesh.refresh_field_topology();

    let err = map_nodal_vector_field_to_boundary_nodes(&mesh, &nodal_vector_values())
        .expect_err("unknown boundary edge node should fail");

    assert_eq!(
        err,
        FieldMappingError::BoundaryEdgeReferencesUnknownNode {
            edge_id: "be1".to_string(),
            node_id: 99,
        }
    );
}

#[test]
fn rejects_boundary_faces_without_nodes() {
    let mut mesh = field_mapping_mesh();
    mesh.boundary_faces[0].node_ids.clear();

    let err = map_nodal_vector_field_to_boundary_faces(&mesh, &nodal_vector_values())
        .expect_err("empty boundary face should fail");

    assert_eq!(
        err,
        FieldMappingError::BoundaryFaceHasNoNodes {
            face_id: "bf1".to_string(),
        }
    );
}

fn nodal_vector_values() -> Vec<[f64; 3]> {
    vec![
        [1.0, 0.0, 0.0],
        [2.0, 0.0, 0.0],
        [3.0, 0.0, 0.0],
        [4.0, 0.0, 0.0],
        [5.0, 0.0, 0.0],
    ]
}

fn field_mapping_mesh() -> AnalysisMeshArtifact {
    let mut mesh = AnalysisMeshArtifact {
        schema_version: "analysis-mesh/v1".to_string(),
        mesh_id: "field_mapping_fixture".to_string(),
        nodes: vec![
            AnalysisMeshNode {
                node_id: 1,
                coordinates_m: [0.0, 0.0, 0.0],
                provenance: Vec::new(),
            },
            AnalysisMeshNode {
                node_id: 2,
                coordinates_m: [1.0, 0.0, 0.0],
                provenance: Vec::new(),
            },
            AnalysisMeshNode {
                node_id: 3,
                coordinates_m: [0.0, 1.0, 0.0],
                provenance: Vec::new(),
            },
            AnalysisMeshNode {
                node_id: 4,
                coordinates_m: [0.0, 0.0, 1.0],
                provenance: Vec::new(),
            },
            AnalysisMeshNode {
                node_id: 5,
                coordinates_m: [0.0, 0.0, -1.0],
                provenance: Vec::new(),
            },
        ],
        volume_elements: vec![
            AnalysisVolumeElement {
                element_id: "e1".to_string(),
                kind: VolumeElementKind::Tetrahedron4,
                node_ids: vec![1, 2, 3, 4],
                material_region_id: "mat".to_string(),
                provenance: Vec::new(),
            },
            AnalysisVolumeElement {
                element_id: "e2".to_string(),
                kind: VolumeElementKind::Tetrahedron4,
                node_ids: vec![1, 3, 2, 5],
                material_region_id: "mat".to_string(),
                provenance: Vec::new(),
            },
        ],
        boundary_faces: vec![
            AnalysisBoundaryFace {
                face_id: "bf1".to_string(),
                kind: BoundaryElementKind::Tri3,
                node_ids: vec![1, 2, 4],
                adjacent_volume_element_ids: vec!["e1".to_string()],
                region_ids: Vec::new(),
                provenance: Vec::new(),
            },
            AnalysisBoundaryFace {
                face_id: "bf2".to_string(),
                kind: BoundaryElementKind::Tri3,
                node_ids: vec![1, 2, 3],
                adjacent_volume_element_ids: vec!["e1".to_string(), "e2".to_string()],
                region_ids: Vec::new(),
                provenance: Vec::new(),
            },
        ],
        boundary_edges: Vec::new(),
        quality: AnalysisMeshQualityReport::default(),
        sizing: MeshSizingField::default(),
        field_topology: Vec::new(),
        backend: MeshBackendSummary::default(),
        adaptive_iterations: Vec::new(),
        provenance: AnalysisMeshProvenance {
            algorithm: "fixture".to_string(),
            source_geometry_id: "field_mapping_fixture".to_string(),
            source_geometry_revision: 1,
            source_geometry_sha256: None,
        },
    };
    mesh.refresh_field_topology();
    mesh
}