mirror of
https://github.com/JuliaFEM/JuliaFEM.jl.git
synced 2026-09-29 04:56:15 +00:00
856 lines
28 KiB
Julia
856 lines
28 KiB
Julia
|
|
# This file is a part of JuliaFEM.
|
|||
|
|
# License is MIT: see https://github.com/JuliaFEM/JuliaFEM.jl/blob/master/LICENSE.md
|
|||
|
|
|
|||
|
|
"""
|
|||
|
|
Mesh{N, T<:AbstractTopology{N}}
|
|||
|
|
|
|||
|
|
Parametric mesh structure with single topology type for type stability and GPU optimization.
|
|||
|
|
|
|||
|
|
# Type Parameters
|
|||
|
|
- `N`: Number of nodes per element (declared first, used in T constraint)
|
|||
|
|
- `T<:AbstractTopology{N}`: Concrete topology type (Tet4, Tet10, Hex8, Tri6, Seg2, etc.)
|
|||
|
|
|
|||
|
|
# Fields
|
|||
|
|
|
|||
|
|
## Core Data
|
|||
|
|
- `nodes::Vector{Vec{3,Float64}}`: Nodal coordinates (3D always, 2D uses z=0)
|
|||
|
|
- `connectivity::Vector{NTuple{N,UInt32}}`: Fixed-size connectivity where N equals nnodes(T)
|
|||
|
|
- `element_sets::Dict{Symbol,Set{UInt32}}`: Named element groups (element indices)
|
|||
|
|
- `node_sets::Dict{Symbol,Set{UInt32}}`: Named node groups (node indices)
|
|||
|
|
- `inverse_connectivity::Vector{Vector{Tuple{UInt32,UInt8}}}`: For each node: [(elem_id, local_idx), ...]
|
|||
|
|
|
|||
|
|
## Ordering and Bandwidth Optimization
|
|||
|
|
- `node_permutation::Vector{UInt32}`: Maps original → reordered (identity until RCM applied)
|
|||
|
|
- `node_inverse_permutation::Vector{UInt32}`: Maps reordered → original
|
|||
|
|
- `element_permutation::Vector{UInt32}`: Maps original → reordered (for cache optimization)
|
|||
|
|
|
|||
|
|
## Naming and Industrial Workflows
|
|||
|
|
- `node_ids::Dict{Union{Int,Symbol},UInt32}`: Named node lookup (e.g., :corner_node => 42, 10_000_001 => 1)
|
|||
|
|
- `element_ids::Dict{Union{Int,Symbol},UInt32}`: Named element lookup (e.g., :E1 => 1, 20_000_001 => 1)
|
|||
|
|
|
|||
|
|
## Parallel Computing and Domain Decomposition
|
|||
|
|
- `node_colors::Vector{UInt32}`: Color per node (0 = uncolored, for load balancing)
|
|||
|
|
- `element_colors::Vector{UInt32}`: Color per element (0 = uncolored, for thread-safe assembly)
|
|||
|
|
- `ghost_nodes::Set{UInt32}`: Nodes owned by other processes (MPI domain decomposition)
|
|||
|
|
- `ghost_elements::Set{UInt32}`: Elements owned by other processes
|
|||
|
|
|
|||
|
|
# Design Rationale
|
|||
|
|
- **Type Stability**: Mesh{Tet10} is fully concrete (10× faster than abstract mesh)
|
|||
|
|
- **Fixed-Size Connectivity**: NTuple{10,Int} enables matrix reinterpretation for GPU
|
|||
|
|
- **Real Workflows**: Matches multi-body assemblies (separate mesh per component)
|
|||
|
|
- **GPU-Ready**: connectivity_matrix() provides zero-copy Matrix{Int} for GPU transfer
|
|||
|
|
- **Bandwidth Optimization**: Node permutation for RCM/Cuthill-McKee minimizes matrix bandwidth
|
|||
|
|
- **Industrial CAE**: Node/element IDs support multi-part assemblies (part1: 10M+ nodes, part2: 20M+ nodes)
|
|||
|
|
- **Parallel-Ready**: Colors and ghost data structures prepared for threading and MPI
|
|||
|
|
|
|||
|
|
# Examples
|
|||
|
|
|
|||
|
|
```julia
|
|||
|
|
# Create simple Tet4 mesh
|
|||
|
|
nodes = [Vec(0.0, 0.0, 0.0), Vec(1.0, 0.0, 0.0),
|
|||
|
|
Vec(0.0, 1.0, 0.0), Vec(0.0, 0.0, 1.0)]
|
|||
|
|
connectivity = [(1, 2, 3, 4)]
|
|||
|
|
mesh = Mesh{Tet4}(nodes, connectivity)
|
|||
|
|
|
|||
|
|
# Add element sets
|
|||
|
|
mesh = Mesh{Tet4}(nodes, connectivity,
|
|||
|
|
element_sets=Dict(:all => Set(1)))
|
|||
|
|
|
|||
|
|
# Multi-body assembly
|
|||
|
|
block = Mesh{Tet10}(...)
|
|||
|
|
beams = Mesh{Seg2}(...)
|
|||
|
|
assembly = Assembly(meshes=Dict(:block => block, :beams => beams))
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
See also: [`connectivity_matrix`](@ref), [`extract_surface`](@ref), [`Assembly`](@ref)
|
|||
|
|
"""
|
|||
|
|
struct Mesh{N,T<:AbstractTopology{N}} <: AbstractMesh
|
|||
|
|
# Core data
|
|||
|
|
nodes::Vector{Vec{3,Float64}}
|
|||
|
|
connectivity::Vector{NTuple{N,UInt32}}
|
|||
|
|
element_sets::Dict{Symbol,Set{UInt32}}
|
|||
|
|
node_sets::Dict{Symbol,Set{UInt32}}
|
|||
|
|
inverse_connectivity::Vector{Vector{Tuple{UInt32,UInt8}}}
|
|||
|
|
|
|||
|
|
# Ordering and bandwidth optimization
|
|||
|
|
node_permutation::Vector{UInt32}
|
|||
|
|
node_inverse_permutation::Vector{UInt32}
|
|||
|
|
element_permutation::Vector{UInt32}
|
|||
|
|
|
|||
|
|
# Naming and industrial workflows
|
|||
|
|
node_ids::Dict{Union{Int,Symbol},UInt32}
|
|||
|
|
element_ids::Dict{Union{Int,Symbol},UInt32}
|
|||
|
|
|
|||
|
|
# Parallel computing
|
|||
|
|
node_colors::Vector{UInt32}
|
|||
|
|
element_colors::Vector{UInt32}
|
|||
|
|
ghost_nodes::Set{UInt32}
|
|||
|
|
ghost_elements::Set{UInt32}
|
|||
|
|
|
|||
|
|
# Inner constructor with validation
|
|||
|
|
function Mesh{N,T}(
|
|||
|
|
nodes::Vector{Vec{3,Float64}},
|
|||
|
|
connectivity::Vector{NTuple{N,UInt32}},
|
|||
|
|
element_sets::Dict{Symbol,Set{UInt32}}=Dict{Symbol,Set{UInt32}}(),
|
|||
|
|
node_sets::Dict{Symbol,Set{UInt32}}=Dict{Symbol,Set{UInt32}}()
|
|||
|
|
) where {N,T<:AbstractTopology{N}}
|
|||
|
|
# Validate connectivity size matches topology
|
|||
|
|
expected_nodes = nnodes(T())
|
|||
|
|
@assert N == expected_nodes "Connectivity tuple size ($N) must match nnodes($T) = $expected_nodes"
|
|||
|
|
|
|||
|
|
# Validate all node indices are in range
|
|||
|
|
n_nodes = length(nodes)
|
|||
|
|
for (i, conn) in enumerate(connectivity)
|
|||
|
|
for node_id in conn
|
|||
|
|
@assert 1 ≤ node_id ≤ n_nodes "Element $i: node index $node_id out of range [1, $n_nodes]"
|
|||
|
|
end
|
|||
|
|
end
|
|||
|
|
|
|||
|
|
# Validate element sets
|
|||
|
|
n_elements = length(connectivity)
|
|||
|
|
for (set_name, elem_ids) in element_sets
|
|||
|
|
for elem_id in elem_ids
|
|||
|
|
@assert 1 ≤ elem_id ≤ n_elements "Element set $set_name: element $elem_id out of range [1, $n_elements]"
|
|||
|
|
end
|
|||
|
|
end
|
|||
|
|
|
|||
|
|
# Validate node sets
|
|||
|
|
for (set_name, node_ids) in node_sets
|
|||
|
|
for node_id in node_ids
|
|||
|
|
@assert 1 ≤ node_id ≤ n_nodes "Node set $set_name: node $node_id out of range [1, $n_nodes]"
|
|||
|
|
end
|
|||
|
|
end
|
|||
|
|
|
|||
|
|
# Build inverse connectivity for nodal assembly
|
|||
|
|
# For each node, store list of (element_id, local_node_index) pairs
|
|||
|
|
inverse_connectivity = [Vector{Tuple{UInt32,UInt8}}() for _ in 1:n_nodes]
|
|||
|
|
for (elem_id, elem_conn) in enumerate(connectivity)
|
|||
|
|
for (local_idx, node_id) in enumerate(elem_conn)
|
|||
|
|
push!(inverse_connectivity[node_id], (UInt32(elem_id), UInt8(local_idx)))
|
|||
|
|
end
|
|||
|
|
end
|
|||
|
|
|
|||
|
|
# Initialize ordering (identity permutations until RCM/reordering applied)
|
|||
|
|
node_permutation = collect(UInt32(1):UInt32(n_nodes))
|
|||
|
|
node_inverse_permutation = collect(UInt32(1):UInt32(n_nodes))
|
|||
|
|
element_permutation = collect(UInt32(1):UInt32(n_elements))
|
|||
|
|
|
|||
|
|
# Initialize naming (empty until user assigns IDs)
|
|||
|
|
node_ids = Dict{Union{Int,Symbol},UInt32}()
|
|||
|
|
element_ids = Dict{Union{Int,Symbol},UInt32}()
|
|||
|
|
|
|||
|
|
# Initialize parallel data (uncolored, no ghosts until partitioning)
|
|||
|
|
node_colors = zeros(UInt32, n_nodes) # 0 = uncolored
|
|||
|
|
element_colors = zeros(UInt32, n_elements) # 0 = uncolored
|
|||
|
|
ghost_nodes = Set{UInt32}()
|
|||
|
|
ghost_elements = Set{UInt32}()
|
|||
|
|
|
|||
|
|
new{N,T}(nodes, connectivity, element_sets, node_sets, inverse_connectivity,
|
|||
|
|
node_permutation, node_inverse_permutation, element_permutation,
|
|||
|
|
node_ids, element_ids,
|
|||
|
|
node_colors, element_colors, ghost_nodes, ghost_elements)
|
|||
|
|
end
|
|||
|
|
end
|
|||
|
|
|
|||
|
|
# Positional constructor for convenience (backward compatibility)
|
|||
|
|
function Mesh{T}(
|
|||
|
|
nodes::Vector{Vec{3,Float64}},
|
|||
|
|
connectivity::Vector{NTuple{N,UInt32}};
|
|||
|
|
element_sets::Dict{Symbol,Set{UInt32}}=Dict{Symbol,Set{UInt32}}(),
|
|||
|
|
node_sets::Dict{Symbol,Set{UInt32}}=Dict{Symbol,Set{UInt32}}()
|
|||
|
|
) where {N,T<:AbstractTopology{N}}
|
|||
|
|
Mesh{N,T}(nodes, connectivity, element_sets, node_sets)
|
|||
|
|
end
|
|||
|
|
|
|||
|
|
# ============================================================================
|
|||
|
|
# Basic Accessors
|
|||
|
|
# ============================================================================
|
|||
|
|
|
|||
|
|
"""
|
|||
|
|
topology_type(mesh::Mesh{T}) -> Type{T}
|
|||
|
|
|
|||
|
|
Get the topology type of the mesh.
|
|||
|
|
"""
|
|||
|
|
topology_type(::Mesh{T}) where T = T
|
|||
|
|
|
|||
|
|
"""
|
|||
|
|
nnodes_per_element(mesh::Mesh{T}) -> Int
|
|||
|
|
|
|||
|
|
Get the number of nodes per element in the mesh.
|
|||
|
|
"""
|
|||
|
|
nnodes_per_element(mesh::Mesh{T}) where T = nnodes(T)
|
|||
|
|
|
|||
|
|
"""
|
|||
|
|
nelements(mesh::Mesh) -> Int
|
|||
|
|
|
|||
|
|
Get the number of elements in the mesh.
|
|||
|
|
"""
|
|||
|
|
nelements(mesh::Mesh) = length(mesh.connectivity)
|
|||
|
|
|
|||
|
|
"""
|
|||
|
|
nnodes_total(mesh::Mesh) -> Int
|
|||
|
|
|
|||
|
|
Get the total number of nodes in the mesh.
|
|||
|
|
"""
|
|||
|
|
nnodes_total(mesh::Mesh) = length(mesh.nodes)
|
|||
|
|
|
|||
|
|
# ============================================================================
|
|||
|
|
# Inverse Connectivity (for Nodal Assembly)
|
|||
|
|
# ============================================================================
|
|||
|
|
|
|||
|
|
"""
|
|||
|
|
get_elements_for_node(mesh::Mesh, node_id::UInt32) -> Vector{Tuple{UInt32,UInt8}}
|
|||
|
|
|
|||
|
|
Get all elements connected to a node with their local node indices.
|
|||
|
|
|
|||
|
|
Returns a vector of (element_id, local_node_index) tuples, where:
|
|||
|
|
- `element_id`: Global element index
|
|||
|
|
- `local_node_index`: Position of this node in the element's connectivity (1-based)
|
|||
|
|
|
|||
|
|
This is essential for nodal assembly, where we iterate over nodes and need to know
|
|||
|
|
which elements contribute to each node.
|
|||
|
|
|
|||
|
|
# Example
|
|||
|
|
```julia
|
|||
|
|
mesh = Mesh{Tet10}(nodes, connectivity)
|
|||
|
|
# Get all elements touching node 5
|
|||
|
|
elems = get_elements_for_node(mesh, UInt32(5))
|
|||
|
|
# elems might be: [(1, 3), (2, 1), (5, 7)]
|
|||
|
|
# Meaning: node 5 is the 3rd node in element 1, 1st node in element 2, etc.
|
|||
|
|
|
|||
|
|
# Nodal assembly pattern:
|
|||
|
|
for node_i in 1:nnodes_total(mesh)
|
|||
|
|
w_local = zero(Vec{3})
|
|||
|
|
for (elem_id, local_i) in get_elements_for_node(mesh, UInt32(node_i))
|
|||
|
|
elem_conn = mesh.connectivity[elem_id]
|
|||
|
|
for (local_j, node_j) in enumerate(elem_conn)
|
|||
|
|
K_ij = compute_stiffness_block(elem_id, local_i, local_j)
|
|||
|
|
v_j = get_dof(v, node_j)
|
|||
|
|
w_local += K_ij ⊡ v_j
|
|||
|
|
end
|
|||
|
|
end
|
|||
|
|
set_dof!(w, node_i, w_local)
|
|||
|
|
end
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
See also: [`nnodes_per_element`](@ref), [`get_node`](@ref)
|
|||
|
|
"""
|
|||
|
|
function get_elements_for_node(mesh::Mesh, node_id::UInt32)
|
|||
|
|
@assert 1 ≤ node_id ≤ nnodes_total(mesh) "Node index $node_id out of range [1, $(nnodes_total(mesh))]"
|
|||
|
|
return mesh.inverse_connectivity[node_id]
|
|||
|
|
end
|
|||
|
|
|
|||
|
|
"""
|
|||
|
|
get_elements_for_node(mesh::Mesh, node_id::Int) -> Vector{Tuple{UInt32,UInt8}}
|
|||
|
|
|
|||
|
|
Convenience wrapper accepting Int node_id (converts to UInt32).
|
|||
|
|
"""
|
|||
|
|
get_elements_for_node(mesh::Mesh, node_id::Int) = get_elements_for_node(mesh, UInt32(node_id))
|
|||
|
|
|
|||
|
|
# ============================================================================
|
|||
|
|
# Connectivity Matrix (GPU-Ready)
|
|||
|
|
# ============================================================================
|
|||
|
|
|
|||
|
|
"""
|
|||
|
|
connectivity_matrix(mesh::Mesh{T}) -> Matrix{UInt32}
|
|||
|
|
|
|||
|
|
Convert connectivity to a dense matrix for GPU transfer.
|
|||
|
|
|
|||
|
|
Returns a matrix of size (nnodes(T), nelements(mesh)) where each column
|
|||
|
|
contains the node indices for one element. This enables efficient GPU
|
|||
|
|
transfer and BLAS/LAPACK operations.
|
|||
|
|
|
|||
|
|
# Example
|
|||
|
|
```julia
|
|||
|
|
mesh = Mesh{Tet10}(nodes, connectivity) # 1000 elements
|
|||
|
|
conn_mat = connectivity_matrix(mesh) # 10×1000 matrix
|
|||
|
|
gpu_conn = CuArray(conn_mat) # Single contiguous transfer!
|
|||
|
|
```
|
|||
|
|
"""
|
|||
|
|
function connectivity_matrix(mesh::Mesh{T}) where T
|
|||
|
|
N = nnodes(T)
|
|||
|
|
n_elem = nelements(mesh)
|
|||
|
|
# Reinterpret Vector{NTuple{N,UInt32}} as flat UInt32 array, then reshape
|
|||
|
|
# This is zero-copy!
|
|||
|
|
return reshape(reinterpret(UInt32, mesh.connectivity), N, n_elem)
|
|||
|
|
end
|
|||
|
|
|
|||
|
|
# ============================================================================
|
|||
|
|
# Node Operations
|
|||
|
|
# ============================================================================
|
|||
|
|
|
|||
|
|
"""
|
|||
|
|
get_node(mesh::Mesh, node_id::Int) -> Vec{3,Float64}
|
|||
|
|
|
|||
|
|
Get coordinates of a node by its index.
|
|||
|
|
"""
|
|||
|
|
function get_node(mesh::Mesh, node_id::Int)
|
|||
|
|
@assert 1 ≤ node_id ≤ nnodes_total(mesh) "Node index $node_id out of range"
|
|||
|
|
return mesh.nodes[node_id]
|
|||
|
|
end
|
|||
|
|
|
|||
|
|
"""
|
|||
|
|
find_nearest_nodes(mesh::Mesh, coords::Vec{3,Float64}, npts::Int=1; node_set::Union{Nothing,Symbol}=nothing) -> Vector{UInt32}
|
|||
|
|
|
|||
|
|
Find the npts nearest nodes to the given coordinates.
|
|||
|
|
|
|||
|
|
# Arguments
|
|||
|
|
- `mesh::Mesh`: The mesh
|
|||
|
|
- `coords::Vec{3,Float64}`: Target coordinates
|
|||
|
|
- `npts::Int=1`: Number of nearest nodes to return
|
|||
|
|
- `node_set::Union{Nothing,Symbol}=nothing`: Restrict search to this node set
|
|||
|
|
|
|||
|
|
# Returns
|
|||
|
|
- `Vector{UInt32}`: Indices of nearest nodes, sorted by distance
|
|||
|
|
|
|||
|
|
# Example
|
|||
|
|
```julia
|
|||
|
|
# Find 3 nearest nodes to point (0.5, 0.5, 0.0)
|
|||
|
|
nearest = find_nearest_nodes(mesh, Vec(0.5, 0.5, 0.0), 3)
|
|||
|
|
|
|||
|
|
# Find nearest node in a specific node set
|
|||
|
|
nearest = find_nearest_nodes(mesh, coords, 1; node_set=:boundary)
|
|||
|
|
```
|
|||
|
|
"""
|
|||
|
|
function find_nearest_nodes(
|
|||
|
|
mesh::Mesh,
|
|||
|
|
coords::Vec{3,Float64},
|
|||
|
|
npts::Int=1;
|
|||
|
|
node_set::Union{Nothing,Symbol}=nothing
|
|||
|
|
)
|
|||
|
|
@assert npts ≥ 1 "Number of points must be at least 1"
|
|||
|
|
|
|||
|
|
# Build list of (node_id, distance) pairs
|
|||
|
|
distances = Tuple{UInt32,Float64}[]
|
|||
|
|
|
|||
|
|
if node_set === nothing
|
|||
|
|
# Search all nodes
|
|||
|
|
for (node_id, node_coords) in enumerate(mesh.nodes)
|
|||
|
|
dist = norm(coords - node_coords)
|
|||
|
|
push!(distances, (UInt32(node_id), dist))
|
|||
|
|
end
|
|||
|
|
else
|
|||
|
|
# Search only nodes in specified set
|
|||
|
|
@assert haskey(mesh.node_sets, node_set) "Node set $node_set not found"
|
|||
|
|
for node_id in mesh.node_sets[node_set]
|
|||
|
|
node_coords = mesh.nodes[node_id]
|
|||
|
|
dist = norm(coords - node_coords)
|
|||
|
|
push!(distances, (node_id, dist))
|
|||
|
|
end
|
|||
|
|
end
|
|||
|
|
|
|||
|
|
# Sort by distance and return first npts node IDs
|
|||
|
|
sort!(distances, by=x -> x[2])
|
|||
|
|
n_return = min(npts, length(distances))
|
|||
|
|
return UInt32[distances[i][1] for i in 1:n_return]
|
|||
|
|
end
|
|||
|
|
|
|||
|
|
"""
|
|||
|
|
find_nearest_node(mesh::Mesh, coords::Vec{3,Float64}; node_set::Union{Nothing,Symbol}=nothing) -> UInt32
|
|||
|
|
|
|||
|
|
Find the single nearest node to the given coordinates.
|
|||
|
|
|
|||
|
|
Convenience wrapper around `find_nearest_nodes(mesh, coords, 1; node_set=node_set)`.
|
|||
|
|
"""
|
|||
|
|
function find_nearest_node(
|
|||
|
|
mesh::Mesh,
|
|||
|
|
coords::Vec{3,Float64};
|
|||
|
|
node_set::Union{Nothing,Symbol}=nothing
|
|||
|
|
)
|
|||
|
|
return first(find_nearest_nodes(mesh, coords, 1; node_set=node_set))
|
|||
|
|
end
|
|||
|
|
|
|||
|
|
# ============================================================================
|
|||
|
|
# Element Set Operations
|
|||
|
|
# ============================================================================
|
|||
|
|
|
|||
|
|
"""
|
|||
|
|
get_element_set(mesh::Mesh, set_name::Symbol) -> Set{UInt32}
|
|||
|
|
|
|||
|
|
Get an element set by name.
|
|||
|
|
"""
|
|||
|
|
function get_element_set(mesh::Mesh, set_name::Symbol)
|
|||
|
|
@assert haskey(mesh.element_sets, set_name) "Element set $set_name not found"
|
|||
|
|
return mesh.element_sets[set_name]
|
|||
|
|
end
|
|||
|
|
|
|||
|
|
"""
|
|||
|
|
get_elements_in_set(mesh::Mesh, set_name::Symbol) -> Vector{UInt32}
|
|||
|
|
|
|||
|
|
Get element indices in a set as a vector (sorted).
|
|||
|
|
"""
|
|||
|
|
function get_elements_in_set(mesh::Mesh, set_name::Symbol)
|
|||
|
|
elem_set = get_element_set(mesh, set_name)
|
|||
|
|
return sort(collect(elem_set))
|
|||
|
|
end
|
|||
|
|
|
|||
|
|
# ============================================================================
|
|||
|
|
# Node Set Operations
|
|||
|
|
# ============================================================================
|
|||
|
|
|
|||
|
|
"""
|
|||
|
|
get_node_set(mesh::Mesh, set_name::Symbol) -> Set{UInt32}
|
|||
|
|
|
|||
|
|
Get a node set by name.
|
|||
|
|
"""
|
|||
|
|
function get_node_set(mesh::Mesh, set_name::Symbol)
|
|||
|
|
@assert haskey(mesh.node_sets, set_name) "Node set $set_name not found"
|
|||
|
|
return mesh.node_sets[set_name]
|
|||
|
|
end
|
|||
|
|
|
|||
|
|
"""
|
|||
|
|
get_nodes_in_set(mesh::Mesh, set_name::Symbol) -> Vector{UInt32}
|
|||
|
|
|
|||
|
|
Get node indices in a set as a vector (sorted).
|
|||
|
|
"""
|
|||
|
|
function get_nodes_in_set(mesh::Mesh, set_name::Symbol)
|
|||
|
|
node_set = get_node_set(mesh, set_name)
|
|||
|
|
return sort(collect(node_set))
|
|||
|
|
end
|
|||
|
|
|
|||
|
|
"""
|
|||
|
|
create_node_set_from_element_set!(mesh::Mesh, elem_set_name::Symbol, node_set_name::Symbol=elem_set_name)
|
|||
|
|
|
|||
|
|
Create a node set containing all nodes from elements in an element set.
|
|||
|
|
|
|||
|
|
# Arguments
|
|||
|
|
- `mesh::Mesh`: The mesh (modified in-place)
|
|||
|
|
- `elem_set_name::Symbol`: Source element set name
|
|||
|
|
- `node_set_name::Symbol`: Target node set name (defaults to same as element set)
|
|||
|
|
|
|||
|
|
# Example
|
|||
|
|
```julia
|
|||
|
|
# Create node set "surface" from element set "surface"
|
|||
|
|
create_node_set_from_element_set!(mesh, :surface)
|
|||
|
|
|
|||
|
|
# Or with different names
|
|||
|
|
create_node_set_from_element_set!(mesh, :volume_elements, :volume_nodes)
|
|||
|
|
```
|
|||
|
|
"""
|
|||
|
|
function create_node_set_from_element_set!(
|
|||
|
|
mesh::Mesh,
|
|||
|
|
elem_set_name::Symbol,
|
|||
|
|
node_set_name::Symbol=elem_set_name
|
|||
|
|
)
|
|||
|
|
@assert haskey(mesh.element_sets, elem_set_name) "Element set $elem_set_name not found"
|
|||
|
|
|
|||
|
|
node_ids = Set{UInt32}()
|
|||
|
|
for elem_id in mesh.element_sets[elem_set_name]
|
|||
|
|
# Add all nodes from this element
|
|||
|
|
for node_id in mesh.connectivity[elem_id]
|
|||
|
|
push!(node_ids, node_id)
|
|||
|
|
end
|
|||
|
|
end
|
|||
|
|
|
|||
|
|
mesh.node_sets[node_set_name] = node_ids
|
|||
|
|
@info "Created node set :$node_set_name with $(length(node_ids)) nodes from element set :$elem_set_name"
|
|||
|
|
return nothing
|
|||
|
|
end
|
|||
|
|
|
|||
|
|
# ============================================================================
|
|||
|
|
# Surface Extraction
|
|||
|
|
# ============================================================================
|
|||
|
|
|
|||
|
|
"""
|
|||
|
|
extract_surface(mesh::Mesh{T}, face_set::Symbol) -> Mesh{FaceT}
|
|||
|
|
|
|||
|
|
Extract a surface mesh from volume elements.
|
|||
|
|
|
|||
|
|
# Arguments
|
|||
|
|
- `mesh::Mesh{T}`: Volume mesh (T must be Tet4, Tet10, Hex8, Hex20, etc.)
|
|||
|
|
- `face_set::Symbol`: Element set defining surface elements
|
|||
|
|
|
|||
|
|
# Returns
|
|||
|
|
- `Mesh{FaceT}`: Surface mesh where FaceT = surface_topology(T)
|
|||
|
|
* Tet4 → Tri3
|
|||
|
|
* Tet10 → Tri6
|
|||
|
|
* Hex8 → Quad4
|
|||
|
|
* Hex20 → Quad4
|
|||
|
|
|
|||
|
|
# Example
|
|||
|
|
```julia
|
|||
|
|
volume = Mesh{Tet10}(nodes, connectivity,
|
|||
|
|
element_sets=Dict(:all => Set(1:100)))
|
|||
|
|
surface = extract_surface(volume, :all) # Returns Mesh{Tri6}
|
|||
|
|
```
|
|||
|
|
"""
|
|||
|
|
function extract_surface(mesh::Mesh{T}, face_set::Symbol) where T
|
|||
|
|
FaceT = surface_topology(T)
|
|||
|
|
n_face_nodes = nnodes(FaceT)
|
|||
|
|
|
|||
|
|
# Get elements in face set
|
|||
|
|
@assert haskey(mesh.element_sets, face_set) "Element set $face_set not found"
|
|||
|
|
face_elements = mesh.element_sets[face_set]
|
|||
|
|
|
|||
|
|
# Extract surface connectivity (simplified - assumes first n_face_nodes form a face)
|
|||
|
|
# TODO: Proper face extraction logic based on topology
|
|||
|
|
surface_conn = NTuple{n_face_nodes,UInt32}[]
|
|||
|
|
for elem_id in face_elements
|
|||
|
|
elem_conn = mesh.connectivity[elem_id]
|
|||
|
|
face_conn = ntuple(i -> elem_conn[i], n_face_nodes)
|
|||
|
|
push!(surface_conn, face_conn)
|
|||
|
|
end
|
|||
|
|
|
|||
|
|
# Reuse same nodes (surface mesh references volume nodes)
|
|||
|
|
return Mesh{FaceT}(mesh.nodes, surface_conn)
|
|||
|
|
end
|
|||
|
|
|
|||
|
|
# ============================================================================
|
|||
|
|
# Validation and Introspection
|
|||
|
|
# ============================================================================
|
|||
|
|
|
|||
|
|
"""
|
|||
|
|
validate(mesh::Mesh) -> Bool
|
|||
|
|
|
|||
|
|
Validate mesh integrity (connectivity, sets, etc.).
|
|||
|
|
"""
|
|||
|
|
function validate(mesh::Mesh{T}) where T
|
|||
|
|
n_nodes = nnodes_total(mesh)
|
|||
|
|
n_elements = nelements(mesh)
|
|||
|
|
expected_nodes_per_elem = nnodes(T)
|
|||
|
|
|
|||
|
|
# Check connectivity
|
|||
|
|
for (i, conn) in enumerate(mesh.connectivity)
|
|||
|
|
@assert length(conn) == expected_nodes_per_elem "Element $i: expected $expected_nodes_per_elem nodes, got $(length(conn))"
|
|||
|
|
for node_id in conn
|
|||
|
|
@assert 1 ≤ node_id ≤ n_nodes "Element $i: node $node_id out of range [1, $n_nodes]"
|
|||
|
|
end
|
|||
|
|
end
|
|||
|
|
|
|||
|
|
# Check element sets
|
|||
|
|
for (set_name, elem_ids) in mesh.element_sets
|
|||
|
|
for elem_id in elem_ids
|
|||
|
|
@assert 1 ≤ elem_id ≤ n_elements "Element set $set_name: element $elem_id out of range"
|
|||
|
|
end
|
|||
|
|
end
|
|||
|
|
|
|||
|
|
# Check node sets
|
|||
|
|
for (set_name, node_ids) in mesh.node_sets
|
|||
|
|
for node_id in node_ids
|
|||
|
|
@assert 1 ≤ node_id ≤ n_nodes "Node set $set_name: node $node_id out of range"
|
|||
|
|
end
|
|||
|
|
end
|
|||
|
|
|
|||
|
|
return true
|
|||
|
|
end
|
|||
|
|
|
|||
|
|
"""
|
|||
|
|
info(mesh::Mesh)
|
|||
|
|
|
|||
|
|
Print mesh information.
|
|||
|
|
"""
|
|||
|
|
function info(mesh::Mesh{T}) where T
|
|||
|
|
println("Mesh{$T}:")
|
|||
|
|
println(" Nodes: $(nnodes_total(mesh))")
|
|||
|
|
println(" Elements: $(nelements(mesh)) ($(nnodes(T)) nodes/element)")
|
|||
|
|
println(" Element sets: $(length(mesh.element_sets))")
|
|||
|
|
for (name, elems) in mesh.element_sets
|
|||
|
|
println(" :$name => $(length(elems)) elements")
|
|||
|
|
end
|
|||
|
|
println(" Node sets: $(length(mesh.node_sets))")
|
|||
|
|
for (name, nodes) in mesh.node_sets
|
|||
|
|
println(" :$name => $(length(nodes)) nodes")
|
|||
|
|
end
|
|||
|
|
end
|
|||
|
|
|
|||
|
|
Base.show(io::IO, mesh::Mesh{T}) where T = print(io, "Mesh{$T}($(nnodes_total(mesh)) nodes, $(nelements(mesh)) elements)")
|
|||
|
|
|
|||
|
|
# ============================================================================
|
|||
|
|
# Node and Element Naming (Industrial CAE Workflows)
|
|||
|
|
# ============================================================================
|
|||
|
|
|
|||
|
|
"""
|
|||
|
|
set_node_id!(mesh::Mesh, internal_index::UInt32, id::Union{Int,Symbol})
|
|||
|
|
|
|||
|
|
Assign a named ID to a node. Useful for industrial workflows where nodes have
|
|||
|
|
specific ID ranges (e.g., part1: 10_000_000+, part2: 20_000_000+) or symbolic
|
|||
|
|
names (e.g., :corner_node, :N1).
|
|||
|
|
|
|||
|
|
# Examples
|
|||
|
|
```julia
|
|||
|
|
# Industrial ID ranges (multi-part assembly)
|
|||
|
|
for i in 1:100
|
|||
|
|
set_node_id!(mesh, UInt32(i), 10_000_000 + i) # Part 1 nodes
|
|||
|
|
end
|
|||
|
|
|
|||
|
|
# Symbolic names (Code Aster style)
|
|||
|
|
set_node_id!(mesh, UInt32(1), :N1)
|
|||
|
|
set_node_id!(mesh, UInt32(42), :corner_node)
|
|||
|
|
```
|
|||
|
|
"""
|
|||
|
|
function set_node_id!(mesh::Mesh, internal_index::UInt32, id::Union{Int,Symbol})
|
|||
|
|
@assert 1 ≤ internal_index ≤ nnodes_total(mesh) "Node index out of range"
|
|||
|
|
mesh.node_ids[id] = internal_index
|
|||
|
|
return nothing
|
|||
|
|
end
|
|||
|
|
|
|||
|
|
"""
|
|||
|
|
get_node_by_id(mesh::Mesh, id::Union{Int,Symbol}) -> UInt32
|
|||
|
|
|
|||
|
|
Get internal node index from named ID.
|
|||
|
|
|
|||
|
|
# Example
|
|||
|
|
```julia
|
|||
|
|
set_node_id!(mesh, UInt32(42), :corner_node)
|
|||
|
|
idx = get_node_by_id(mesh, :corner_node) # Returns UInt32(42)
|
|||
|
|
```
|
|||
|
|
"""
|
|||
|
|
function get_node_by_id(mesh::Mesh, id::Union{Int,Symbol})
|
|||
|
|
@assert haskey(mesh.node_ids, id) "Node ID $id not found"
|
|||
|
|
return mesh.node_ids[id]
|
|||
|
|
end
|
|||
|
|
|
|||
|
|
"""
|
|||
|
|
set_element_id!(mesh::Mesh, internal_index::UInt32, id::Union{Int,Symbol})
|
|||
|
|
|
|||
|
|
Assign a named ID to an element. Similar to node IDs but for elements.
|
|||
|
|
|
|||
|
|
# Examples
|
|||
|
|
```julia
|
|||
|
|
# Industrial ID ranges
|
|||
|
|
set_element_id!(mesh, UInt32(1), 20_000_001)
|
|||
|
|
|
|||
|
|
# Symbolic names
|
|||
|
|
set_element_id!(mesh, UInt32(1), :E1)
|
|||
|
|
```
|
|||
|
|
"""
|
|||
|
|
function set_element_id!(mesh::Mesh, internal_index::UInt32, id::Union{Int,Symbol})
|
|||
|
|
@assert 1 ≤ internal_index ≤ nelements(mesh) "Element index out of range"
|
|||
|
|
mesh.element_ids[id] = internal_index
|
|||
|
|
return nothing
|
|||
|
|
end
|
|||
|
|
|
|||
|
|
"""
|
|||
|
|
get_element_by_id(mesh::Mesh, id::Union{Int,Symbol}) -> UInt32
|
|||
|
|
|
|||
|
|
Get internal element index from named ID.
|
|||
|
|
"""
|
|||
|
|
function get_element_by_id(mesh::Mesh, id::Union{Int,Symbol})
|
|||
|
|
@assert haskey(mesh.element_ids, id) "Element ID $id not found"
|
|||
|
|
return mesh.element_ids[id]
|
|||
|
|
end
|
|||
|
|
|
|||
|
|
# ============================================================================
|
|||
|
|
# Coloring for Parallel Assembly and Load Balancing
|
|||
|
|
# ============================================================================
|
|||
|
|
|
|||
|
|
"""
|
|||
|
|
set_node_color!(mesh::Mesh, node_index::UInt32, color::UInt32)
|
|||
|
|
|
|||
|
|
Assign a color to a node. Color 0 means uncolored. Used for:
|
|||
|
|
- Load balancing (assign nodes to MPI ranks)
|
|||
|
|
- Identifying process ownership in domain decomposition
|
|||
|
|
|
|||
|
|
# Example
|
|||
|
|
```julia
|
|||
|
|
# Assign nodes to 4 MPI ranks
|
|||
|
|
for i in 1:nnodes_total(mesh)
|
|||
|
|
rank = mod(i-1, 4) + 1 # Round-robin: 1,2,3,4,1,2,3,4,...
|
|||
|
|
set_node_color!(mesh, UInt32(i), UInt32(rank))
|
|||
|
|
end
|
|||
|
|
```
|
|||
|
|
"""
|
|||
|
|
function set_node_color!(mesh::Mesh, node_index::UInt32, color::UInt32)
|
|||
|
|
@assert 1 ≤ node_index ≤ nnodes_total(mesh) "Node index out of range"
|
|||
|
|
mesh.node_colors[node_index] = color
|
|||
|
|
return nothing
|
|||
|
|
end
|
|||
|
|
|
|||
|
|
"""
|
|||
|
|
get_node_color(mesh::Mesh, node_index::UInt32) -> UInt32
|
|||
|
|
|
|||
|
|
Get the color of a node (0 = uncolored).
|
|||
|
|
"""
|
|||
|
|
function get_node_color(mesh::Mesh, node_index::UInt32)
|
|||
|
|
@assert 1 ≤ node_index ≤ nnodes_total(mesh) "Node index out of range"
|
|||
|
|
return mesh.node_colors[node_index]
|
|||
|
|
end
|
|||
|
|
|
|||
|
|
"""
|
|||
|
|
set_element_color!(mesh::Mesh, elem_index::UInt32, color::UInt32)
|
|||
|
|
|
|||
|
|
Assign a color to an element. Color 0 means uncolored. Used for:
|
|||
|
|
- Thread-safe assembly (elements with same color can be assembled in parallel)
|
|||
|
|
- Graph coloring for lock-free nodal assembly
|
|||
|
|
|
|||
|
|
# Example
|
|||
|
|
```julia
|
|||
|
|
# After graph coloring algorithm
|
|||
|
|
for (color, elem_ids) in colored_groups
|
|||
|
|
for elem_id in elem_ids
|
|||
|
|
set_element_color!(mesh, elem_id, color)
|
|||
|
|
end
|
|||
|
|
end
|
|||
|
|
```
|
|||
|
|
"""
|
|||
|
|
function set_element_color!(mesh::Mesh, elem_index::UInt32, color::UInt32)
|
|||
|
|
@assert 1 ≤ elem_index ≤ nelements(mesh) "Element index out of range"
|
|||
|
|
mesh.element_colors[elem_index] = color
|
|||
|
|
return nothing
|
|||
|
|
end
|
|||
|
|
|
|||
|
|
"""
|
|||
|
|
get_element_color(mesh::Mesh, elem_index::UInt32) -> UInt32
|
|||
|
|
|
|||
|
|
Get the color of an element (0 = uncolored).
|
|||
|
|
"""
|
|||
|
|
function get_element_color(mesh::Mesh, elem_index::UInt32)
|
|||
|
|
@assert 1 ≤ elem_index ≤ nelements(mesh) "Element index out of range"
|
|||
|
|
return mesh.element_colors[elem_index]
|
|||
|
|
end
|
|||
|
|
|
|||
|
|
"""
|
|||
|
|
get_elements_with_color(mesh::Mesh, color::UInt32) -> Vector{UInt32}
|
|||
|
|
|
|||
|
|
Get all elements with a specific color. Useful for parallel assembly loops.
|
|||
|
|
|
|||
|
|
# Example
|
|||
|
|
```julia
|
|||
|
|
# Parallel assembly by color
|
|||
|
|
for color in 1:n_colors
|
|||
|
|
elems = get_elements_with_color(mesh, UInt32(color))
|
|||
|
|
Threads.@threads for elem_id in elems
|
|||
|
|
assemble_element!(K, mesh, elem_id) # Thread-safe within same color
|
|||
|
|
end
|
|||
|
|
end
|
|||
|
|
```
|
|||
|
|
"""
|
|||
|
|
function get_elements_with_color(mesh::Mesh, color::UInt32)
|
|||
|
|
return [UInt32(i) for (i, c) in enumerate(mesh.element_colors) if c == color]
|
|||
|
|
end
|
|||
|
|
|
|||
|
|
# ============================================================================
|
|||
|
|
# Ghost Nodes and Elements (MPI Domain Decomposition)
|
|||
|
|
# ============================================================================
|
|||
|
|
|
|||
|
|
"""
|
|||
|
|
mark_ghost_node!(mesh::Mesh, node_index::UInt32)
|
|||
|
|
|
|||
|
|
Mark a node as ghost (owned by another MPI rank). Ghost nodes are needed for
|
|||
|
|
assembly at partition boundaries but are not part of the local DOF ownership.
|
|||
|
|
"""
|
|||
|
|
function mark_ghost_node!(mesh::Mesh, node_index::UInt32)
|
|||
|
|
@assert 1 ≤ node_index ≤ nnodes_total(mesh) "Node index out of range"
|
|||
|
|
push!(mesh.ghost_nodes, node_index)
|
|||
|
|
return nothing
|
|||
|
|
end
|
|||
|
|
|
|||
|
|
"""
|
|||
|
|
is_ghost_node(mesh::Mesh, node_index::UInt32) -> Bool
|
|||
|
|
|
|||
|
|
Check if a node is a ghost node.
|
|||
|
|
"""
|
|||
|
|
function is_ghost_node(mesh::Mesh, node_index::UInt32)
|
|||
|
|
return node_index in mesh.ghost_nodes
|
|||
|
|
end
|
|||
|
|
|
|||
|
|
"""
|
|||
|
|
mark_ghost_element!(mesh::Mesh, elem_index::UInt32)
|
|||
|
|
|
|||
|
|
Mark an element as ghost (owned by another MPI rank).
|
|||
|
|
"""
|
|||
|
|
function mark_ghost_element!(mesh::Mesh, elem_index::UInt32)
|
|||
|
|
@assert 1 ≤ elem_index ≤ nelements(mesh) "Element index out of range"
|
|||
|
|
push!(mesh.ghost_elements, elem_index)
|
|||
|
|
return nothing
|
|||
|
|
end
|
|||
|
|
|
|||
|
|
"""
|
|||
|
|
is_ghost_element(mesh::Mesh, elem_index::UInt32) -> Bool
|
|||
|
|
|
|||
|
|
Check if an element is a ghost element.
|
|||
|
|
"""
|
|||
|
|
function is_ghost_element(mesh::Mesh, elem_index::UInt32)
|
|||
|
|
return elem_index in mesh.ghost_elements
|
|||
|
|
end
|
|||
|
|
|
|||
|
|
"""
|
|||
|
|
get_local_nodes(mesh::Mesh) -> Vector{UInt32}
|
|||
|
|
|
|||
|
|
Get all non-ghost (locally owned) node indices.
|
|||
|
|
"""
|
|||
|
|
function get_local_nodes(mesh::Mesh)
|
|||
|
|
return [UInt32(i) for i in 1:nnodes_total(mesh) if !is_ghost_node(mesh, UInt32(i))]
|
|||
|
|
end
|
|||
|
|
|
|||
|
|
"""
|
|||
|
|
get_local_elements(mesh::Mesh) -> Vector{UInt32}
|
|||
|
|
|
|||
|
|
Get all non-ghost (locally owned) element indices.
|
|||
|
|
"""
|
|||
|
|
function get_local_elements(mesh::Mesh)
|
|||
|
|
return [UInt32(i) for i in 1:nelements(mesh) if !is_ghost_element(mesh, UInt32(i))]
|
|||
|
|
end
|
|||
|
|
|
|||
|
|
# ============================================================================
|
|||
|
|
# Node Permutation (Bandwidth Minimization)
|
|||
|
|
# ============================================================================
|
|||
|
|
|
|||
|
|
"""
|
|||
|
|
apply_node_permutation!(mesh::Mesh, permutation::Vector{UInt32})
|
|||
|
|
|
|||
|
|
Apply a node permutation (e.g., from RCM/Cuthill-McKee bandwidth minimization).
|
|||
|
|
Updates both permutation and inverse permutation. Does NOT reorder actual node
|
|||
|
|
data (nodes remain in original order, permutation is used during assembly).
|
|||
|
|
|
|||
|
|
# Example
|
|||
|
|
```julia
|
|||
|
|
# After computing RCM permutation
|
|||
|
|
perm = reverse_cuthill_mckee(adjacency_matrix(mesh))
|
|||
|
|
apply_node_permutation!(mesh, perm)
|
|||
|
|
|
|||
|
|
# Now mesh.node_permutation[i] gives reordered index for node i
|
|||
|
|
# And mesh.node_inverse_permutation[j] gives original index for reordered position j
|
|||
|
|
```
|
|||
|
|
"""
|
|||
|
|
function apply_node_permutation!(mesh::Mesh, permutation::Vector{UInt32})
|
|||
|
|
n = nnodes_total(mesh)
|
|||
|
|
@assert length(permutation) == n "Permutation size must match number of nodes"
|
|||
|
|
@assert sort(permutation) == collect(UInt32(1):UInt32(n)) "Invalid permutation"
|
|||
|
|
|
|||
|
|
mesh.node_permutation .= permutation
|
|||
|
|
|
|||
|
|
# Compute inverse permutation: inv_perm[perm[i]] = i
|
|||
|
|
for (i, j) in enumerate(permutation)
|
|||
|
|
mesh.node_inverse_permutation[j] = UInt32(i)
|
|||
|
|
end
|
|||
|
|
|
|||
|
|
return nothing
|
|||
|
|
end
|
|||
|
|
|
|||
|
|
"""
|
|||
|
|
apply_element_permutation!(mesh::Mesh, permutation::Vector{UInt32})
|
|||
|
|
|
|||
|
|
Apply an element permutation for cache-optimal memory access patterns.
|
|||
|
|
"""
|
|||
|
|
function apply_element_permutation!(mesh::Mesh, permutation::Vector{UInt32})
|
|||
|
|
n = nelements(mesh)
|
|||
|
|
@assert length(permutation) == n "Permutation size must match number of elements"
|
|||
|
|
@assert sort(permutation) == collect(UInt32(1):UInt32(n)) "Invalid permutation"
|
|||
|
|
|
|||
|
|
mesh.element_permutation .= permutation
|
|||
|
|
|
|||
|
|
return nothing
|
|||
|
|
end
|
|||
|
|
|
|||
|
|
"""
|
|||
|
|
get_reordered_node_index(mesh::Mesh, original_index::UInt32) -> UInt32
|
|||
|
|
|
|||
|
|
Get the reordered (permuted) index for an original node index.
|
|||
|
|
"""
|
|||
|
|
function get_reordered_node_index(mesh::Mesh, original_index::UInt32)
|
|||
|
|
@assert 1 ≤ original_index ≤ nnodes_total(mesh) "Node index out of range"
|
|||
|
|
return mesh.node_permutation[original_index]
|
|||
|
|
end
|
|||
|
|
|
|||
|
|
"""
|
|||
|
|
get_original_node_index(mesh::Mesh, reordered_index::UInt32) -> UInt32
|
|||
|
|
|
|||
|
|
Get the original index for a reordered (permuted) node index.
|
|||
|
|
"""
|
|||
|
|
function get_original_node_index(mesh::Mesh, reordered_index::UInt32)
|
|||
|
|
@assert 1 ≤ reordered_index ≤ nnodes_total(mesh) "Node index out of range"
|
|||
|
|
return mesh.node_inverse_permutation[reordered_index]
|
|||
|
|
end
|
|||
|
|
|