From fde2c6a4a8bb3a0f925569ecbe47c97c8cee9257 Mon Sep 17 00:00:00 2001 From: Jukka Aho Date: Mon, 15 Dec 2025 09:04:22 +0200 Subject: [PATCH] test(mesh): add parallel features test New 297-line test file for mesh parallel features: - Tests node and element naming (industrial ID ranges, symbolic names) - Tests node coloring for load balancing (MPI ranks) - Tests element coloring for graph coloring (threading) - Tests ghost nodes and elements (MPI domain decomposition) - Tests node and element permutation (bandwidth minimization, cache optimization) - Tests combined features in industrial workflow - Validates forward/inverse permutation consistency Comprehensive test suite for parallel computing features including MPI domain decomposition and threading support. --- test/mesh/test_parallel_features.jl | 297 ++++++++++++++++++++++++++++ 1 file changed, 297 insertions(+) create mode 100644 test/mesh/test_parallel_features.jl diff --git a/test/mesh/test_parallel_features.jl b/test/mesh/test_parallel_features.jl new file mode 100644 index 0000000..88c3edb --- /dev/null +++ b/test/mesh/test_parallel_features.jl @@ -0,0 +1,297 @@ +# This file is a part of JuliaFEM. +# License is MIT: see https://github.com/JuliaFEM/JuliaFEM.jl/blob/master/LICENSE.md + +using Test +using Tensors + +# Mock topology types +abstract type AbstractTopology end +struct Tet4 <: AbstractTopology end +nnodes(::Type{Tet4}) = 4 + +# Include mesh implementation +include("../src/mesh/mesh.jl") + +@testset "Mesh Parallel Features - Naming, Coloring, Permutation, Ghosts" begin + + # Setup: Create a simple two-element mesh + function make_test_mesh() + nodes = [ + Vec(0.0, 0.0, 0.0), # 1 + Vec(1.0, 0.0, 0.0), # 2 + Vec(0.0, 1.0, 0.0), # 3 + Vec(0.0, 0.0, 1.0), # 4 + Vec(1.0, 1.0, 0.0) # 5 + ] + connectivity = [ + (UInt32(1), UInt32(2), UInt32(3), UInt32(4)), # Element 1 + (UInt32(2), UInt32(5), UInt32(3), UInt32(4)) # Element 2 + ] + return Mesh{Tet4}(nodes, connectivity) + end + + @testset "Node Naming - Industrial ID Ranges" begin + mesh = make_test_mesh() + + # Assign industrial ID ranges (part 1: 10M+, part 2: 20M+) + set_node_id!(mesh, UInt32(1), 10_000_001) + set_node_id!(mesh, UInt32(2), 10_000_002) + set_node_id!(mesh, UInt32(3), 20_000_001) + + # Retrieve by ID + @test get_node_by_id(mesh, 10_000_001) == UInt32(1) + @test get_node_by_id(mesh, 10_000_002) == UInt32(2) + @test get_node_by_id(mesh, 20_000_001) == UInt32(3) + + # Non-existent ID should error + @test_throws AssertionError get_node_by_id(mesh, 99999) + end + + @testset "Node Naming - Symbolic Names (Code Aster Style)" begin + mesh = make_test_mesh() + + # Assign symbolic names + set_node_id!(mesh, UInt32(1), :N1) + set_node_id!(mesh, UInt32(2), :N2) + set_node_id!(mesh, UInt32(5), :corner_node) + + # Retrieve by symbol + @test get_node_by_id(mesh, :N1) == UInt32(1) + @test get_node_by_id(mesh, :N2) == UInt32(2) + @test get_node_by_id(mesh, :corner_node) == UInt32(5) + + # Non-existent symbol should error + @test_throws AssertionError get_node_by_id(mesh, :nonexistent) + end + + @testset "Element Naming - Industrial ID Ranges" begin + mesh = make_test_mesh() + + # Assign element IDs + set_element_id!(mesh, UInt32(1), 30_000_001) + set_element_id!(mesh, UInt32(2), 30_000_002) + + # Retrieve by ID + @test get_element_by_id(mesh, 30_000_001) == UInt32(1) + @test get_element_by_id(mesh, 30_000_002) == UInt32(2) + + # Non-existent ID should error + @test_throws AssertionError get_element_by_id(mesh, 99999) + end + + @testset "Element Naming - Symbolic Names" begin + mesh = make_test_mesh() + + # Assign symbolic names + set_element_id!(mesh, UInt32(1), :E1) + set_element_id!(mesh, UInt32(2), :E2) + + # Retrieve by symbol + @test get_element_by_id(mesh, :E1) == UInt32(1) + @test get_element_by_id(mesh, :E2) == UInt32(2) + end + + @testset "Node Coloring - Load Balancing" begin + mesh = make_test_mesh() + + # Initially all nodes uncolored (color = 0) + for i in 1:nnodes_total(mesh) + @test get_node_color(mesh, UInt32(i)) == UInt32(0) + end + + # Assign nodes to MPI ranks (colors 1-4) + for i in 1:nnodes_total(mesh) + rank = mod(i - 1, 4) + 1 # Round-robin: 1,2,3,4,1 + set_node_color!(mesh, UInt32(i), UInt32(rank)) + end + + # Verify colors + @test get_node_color(mesh, UInt32(1)) == UInt32(1) + @test get_node_color(mesh, UInt32(2)) == UInt32(2) + @test get_node_color(mesh, UInt32(3)) == UInt32(3) + @test get_node_color(mesh, UInt32(4)) == UInt32(4) + @test get_node_color(mesh, UInt32(5)) == UInt32(1) + end + + @testset "Element Coloring - Graph Coloring for Threading" begin + mesh = make_test_mesh() + + # Initially all elements uncolored + @test get_element_color(mesh, UInt32(1)) == UInt32(0) + @test get_element_color(mesh, UInt32(2)) == UInt32(0) + + # Assign colors (elements sharing nodes get different colors) + set_element_color!(mesh, UInt32(1), UInt32(1)) + set_element_color!(mesh, UInt32(2), UInt32(2)) # Shares nodes with elem 1 + + # Verify colors + @test get_element_color(mesh, UInt32(1)) == UInt32(1) + @test get_element_color(mesh, UInt32(2)) == UInt32(2) + + # Get elements by color + color1_elems = get_elements_with_color(mesh, UInt32(1)) + color2_elems = get_elements_with_color(mesh, UInt32(2)) + + @test UInt32(1) in color1_elems + @test UInt32(2) in color2_elems + @test length(color1_elems) == 1 + @test length(color2_elems) == 1 + end + + @testset "Ghost Nodes - MPI Domain Decomposition" begin + mesh = make_test_mesh() + + # Initially no ghost nodes + @test !is_ghost_node(mesh, UInt32(1)) + @test !is_ghost_node(mesh, UInt32(2)) + + # Mark nodes 2,3,4 as ghosts (owned by another rank) + mark_ghost_node!(mesh, UInt32(2)) + mark_ghost_node!(mesh, UInt32(3)) + mark_ghost_node!(mesh, UInt32(4)) + + # Verify ghost status + @test !is_ghost_node(mesh, UInt32(1)) # Local + @test is_ghost_node(mesh, UInt32(2)) # Ghost + @test is_ghost_node(mesh, UInt32(3)) # Ghost + @test is_ghost_node(mesh, UInt32(4)) # Ghost + @test !is_ghost_node(mesh, UInt32(5)) # Local + + # Get local nodes (non-ghost) + local_nodes = get_local_nodes(mesh) + @test UInt32(1) in local_nodes + @test UInt32(5) in local_nodes + @test !(UInt32(2) in local_nodes) + @test !(UInt32(3) in local_nodes) + @test !(UInt32(4) in local_nodes) + @test length(local_nodes) == 2 + end + + @testset "Ghost Elements - MPI Domain Decomposition" begin + mesh = make_test_mesh() + + # Initially no ghost elements + @test !is_ghost_element(mesh, UInt32(1)) + @test !is_ghost_element(mesh, UInt32(2)) + + # Mark element 2 as ghost + mark_ghost_element!(mesh, UInt32(2)) + + # Verify ghost status + @test !is_ghost_element(mesh, UInt32(1)) # Local + @test is_ghost_element(mesh, UInt32(2)) # Ghost + + # Get local elements + local_elems = get_local_elements(mesh) + @test UInt32(1) in local_elems + @test !(UInt32(2) in local_elems) + @test length(local_elems) == 1 + end + + @testset "Node Permutation - Identity (Initial State)" begin + mesh = make_test_mesh() + + # Initially identity permutation + for i in 1:nnodes_total(mesh) + @test mesh.node_permutation[i] == UInt32(i) + @test mesh.node_inverse_permutation[i] == UInt32(i) + end + + # Forward and inverse should be consistent + for i in 1:nnodes_total(mesh) + j = get_reordered_node_index(mesh, UInt32(i)) + @test get_original_node_index(mesh, j) == UInt32(i) + end + end + + @testset "Node Permutation - Custom Reordering" begin + mesh = make_test_mesh() + + # Apply custom permutation (reverse order for simplicity) + n = nnodes_total(mesh) + perm = UInt32[n, n-1, n-2, n-3, n-4] # [5, 4, 3, 2, 1] + apply_node_permutation!(mesh, perm) + + # Verify permutation + @test mesh.node_permutation == perm + + # Verify inverse permutation + @test mesh.node_inverse_permutation == UInt32[5, 4, 3, 2, 1] + + # Check forward mapping: original 1 → reordered 5 + @test get_reordered_node_index(mesh, UInt32(1)) == UInt32(5) + @test get_reordered_node_index(mesh, UInt32(5)) == UInt32(1) + + # Check inverse mapping: reordered 1 → original 5 + @test get_original_node_index(mesh, UInt32(1)) == UInt32(5) + @test get_original_node_index(mesh, UInt32(5)) == UInt32(1) + + # Verify consistency + for i in 1:n + j = get_reordered_node_index(mesh, UInt32(i)) + @test get_original_node_index(mesh, j) == UInt32(i) + end + end + + @testset "Node Permutation - Invalid Permutation" begin + mesh = make_test_mesh() + + # Wrong size + @test_throws AssertionError apply_node_permutation!(mesh, UInt32[1, 2]) + + # Invalid permutation (duplicate) + @test_throws AssertionError apply_node_permutation!(mesh, UInt32[1, 1, 2, 3, 4]) + + # Invalid permutation (out of range) + @test_throws AssertionError apply_node_permutation!(mesh, UInt32[1, 2, 3, 4, 6]) + end + + @testset "Element Permutation - Cache Optimization" begin + mesh = make_test_mesh() + + # Initially identity + @test mesh.element_permutation == UInt32[1, 2] + + # Apply custom permutation (swap elements) + perm = UInt32[2, 1] + apply_element_permutation!(mesh, perm) + + # Verify permutation + @test mesh.element_permutation == perm + end + + @testset "Combined Features - Industrial Workflow" begin + mesh = make_test_mesh() + + # 1. Assign industrial IDs (multi-part assembly) + set_node_id!(mesh, UInt32(1), 10_000_001) + set_node_id!(mesh, UInt32(2), 10_000_002) + set_node_id!(mesh, UInt32(3), 20_000_001) # Part 2 starts here + + set_element_id!(mesh, UInt32(1), 30_000_001) + set_element_id!(mesh, UInt32(2), 30_000_002) + + # 2. Apply bandwidth minimization (RCM-like) + perm = UInt32[2, 1, 3, 4, 5] # Simulated RCM result + apply_node_permutation!(mesh, perm) + + # 3. Color elements for parallel assembly + set_element_color!(mesh, UInt32(1), UInt32(1)) + set_element_color!(mesh, UInt32(2), UInt32(2)) + + # 4. Mark ghost nodes (MPI partitioning) + mark_ghost_node!(mesh, UInt32(3)) + mark_ghost_node!(mesh, UInt32(4)) + + # Verify everything works together + @test get_node_by_id(mesh, 10_000_001) == UInt32(1) + @test get_element_by_id(mesh, 30_000_001) == UInt32(1) + @test get_reordered_node_index(mesh, UInt32(1)) == UInt32(2) + @test get_element_color(mesh, UInt32(1)) == UInt32(1) + @test is_ghost_node(mesh, UInt32(3)) + @test length(get_local_nodes(mesh)) == 3 # 5 nodes - 2 ghosts = 3 local + end + +end + +println("✅ All parallel features tests passed!")