2025-11-08 09:39:16 +02:00
|
|
|
|
# This file is a part of JuliaFEM.
|
|
|
|
|
|
# License is MIT: see https://github.com/JuliaFEM/FEMBase.jl/blob/master/LICENSE
|
|
|
|
|
|
|
|
|
|
|
|
"""
|
|
|
|
|
|
AbstractFieldSet{N<:Int}
|
|
|
|
|
|
|
|
|
|
|
|
Abstract supertype for all field sets, where `N` is the length of the discrete
|
|
|
|
|
|
fields (typically is the number of the nodes in element).
|
|
|
|
|
|
"""
|
|
|
|
|
|
abstract type AbstractFieldSet{N} end
|
|
|
|
|
|
|
|
|
|
|
|
"""
|
|
|
|
|
|
EmptyFieldSet{N} <: AbstractFieldSet{N}
|
|
|
|
|
|
|
|
|
|
|
|
Empty field set used as a default for all elements.
|
|
|
|
|
|
"""
|
|
|
|
|
|
struct EmptyFieldSet{N} <: AbstractFieldSet{N}
|
|
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
|
|
const DefaultFieldSet = EmptyFieldSet
|
|
|
|
|
|
|
|
|
|
|
|
"""
|
2025-11-09 17:08:36 +02:00
|
|
|
|
AbstractElement{F, B}
|
2025-11-08 09:39:16 +02:00
|
|
|
|
|
|
|
|
|
|
Abstract supertype for all elements.
|
2025-11-09 17:08:36 +02:00
|
|
|
|
|
|
|
|
|
|
# Type Parameters
|
|
|
|
|
|
- `F`: Field type (any type-stable container: NamedTuple, struct, etc.)
|
|
|
|
|
|
- `B`: Basis function type (e.g., Lagrange{Triangle,1})
|
|
|
|
|
|
"""
|
|
|
|
|
|
abstract type AbstractElement{F,B} end
|
|
|
|
|
|
|
2025-11-08 09:39:16 +02:00
|
|
|
|
"""
|
2025-11-09 17:08:36 +02:00
|
|
|
|
Element{N,NIP,F,B} <: AbstractElement{F,B}
|
|
|
|
|
|
|
|
|
|
|
|
Immutable finite element with type-stable fields.
|
|
|
|
|
|
|
|
|
|
|
|
# Type Parameters
|
|
|
|
|
|
- `N`: Number of nodes (compile-time constant)
|
|
|
|
|
|
- `NIP`: Number of integration points (compile-time constant)
|
|
|
|
|
|
- `F`: Field container type (NamedTuple, struct, etc.) - **must be type-stable!**
|
|
|
|
|
|
- `B`: Basis function type (e.g., Lagrange{Triangle,1})
|
|
|
|
|
|
|
|
|
|
|
|
# Fields
|
|
|
|
|
|
- `id`: Element identifier
|
|
|
|
|
|
- `connectivity`: Node IDs as NTuple (zero-cost, immutable)
|
|
|
|
|
|
- `integration_points`: Integration points as NTuple (zero-cost, immutable)
|
|
|
|
|
|
- `fields`: Type-stable field container (can be empty tuple `()` if no fields)
|
|
|
|
|
|
- `basis`: Basis function type instance
|
|
|
|
|
|
|
|
|
|
|
|
# Design Philosophy
|
|
|
|
|
|
- **Type-stable fields**: `F` parameter ensures compile-time type knowledge
|
|
|
|
|
|
- **Immutable**: All fields immutable (GPU-compatible, thread-safe)
|
|
|
|
|
|
- **Zero-allocation**: NTuple for connectivity and IPs
|
|
|
|
|
|
- **GPU-ready**: Immutable fields can be transferred to GPU without copying
|
|
|
|
|
|
|
|
|
|
|
|
# Examples
|
|
|
|
|
|
```julia
|
|
|
|
|
|
# With fields (NamedTuple):
|
|
|
|
|
|
fields = (E = 210e3, ν = 0.3)
|
|
|
|
|
|
el = Element(UInt(1), (1,2,3), (), fields, Lagrange{Triangle,1}())
|
|
|
|
|
|
|
|
|
|
|
|
# Without fields (empty tuple):
|
|
|
|
|
|
el = Element(UInt(1), (1,2,3), (), (), Lagrange{Triangle,1}())
|
2025-11-08 09:39:16 +02:00
|
|
|
|
|
2025-11-09 17:08:36 +02:00
|
|
|
|
# Access fields (type-stable!):
|
|
|
|
|
|
E = el.fields.E # Float64, known at compile time
|
|
|
|
|
|
```
|
|
|
|
|
|
"""
|
|
|
|
|
|
struct Element{N,NIP,F,B} <: AbstractElement{F,B}
|
2025-11-09 03:29:36 +02:00
|
|
|
|
id::UInt
|
|
|
|
|
|
connectivity::NTuple{N,UInt} # Tuple for zero-cost, compile-time known size
|
|
|
|
|
|
integration_points::NTuple{NIP,IP} # Tuple for zero-cost
|
2025-11-09 17:08:36 +02:00
|
|
|
|
fields::F # Type-stable field container (NamedTuple, struct, or ())
|
|
|
|
|
|
basis::B
|
2025-11-08 09:39:16 +02:00
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
|
|
"""
|
2025-11-09 17:08:36 +02:00
|
|
|
|
Element(basis_type, connectivity; fields=(), id=UInt(0))
|
2025-11-08 09:39:16 +02:00
|
|
|
|
|
2025-11-09 17:08:36 +02:00
|
|
|
|
Construct element from basis type with optional fields.
|
2025-11-08 09:39:16 +02:00
|
|
|
|
|
2025-11-09 17:08:36 +02:00
|
|
|
|
# Arguments
|
|
|
|
|
|
- `basis_type`: Basis function type (e.g., Lagrange{Triangle,1})
|
|
|
|
|
|
- `connectivity`: Node IDs as tuple or vector
|
|
|
|
|
|
- `fields`: Optional field container (NamedTuple, struct, or empty tuple)
|
|
|
|
|
|
- `id`: Element identifier (default: 0)
|
|
|
|
|
|
|
|
|
|
|
|
# Examples
|
2025-11-08 09:39:16 +02:00
|
|
|
|
```julia
|
2025-11-09 17:08:36 +02:00
|
|
|
|
# Simple element without fields:
|
|
|
|
|
|
element = Element(Lagrange{Triangle,1}, (1, 2, 3))
|
|
|
|
|
|
|
|
|
|
|
|
# Element with material properties:
|
|
|
|
|
|
element = Element(Lagrange{Triangle,1}, (1, 2, 3),
|
|
|
|
|
|
fields=(E=210e3, ν=0.3))
|
|
|
|
|
|
|
|
|
|
|
|
# Element with complete specification:
|
|
|
|
|
|
element = Element(Lagrange{Quadrilateral,1}, (1,2,3,4),
|
|
|
|
|
|
fields=(E=210e3, ν=0.3, thickness=0.01),
|
|
|
|
|
|
id=UInt(42))
|
2025-11-08 09:39:16 +02:00
|
|
|
|
```
|
|
|
|
|
|
"""
|
2025-11-09 17:08:36 +02:00
|
|
|
|
function Element(::Type{B}, connectivity::NTuple{N,<:Integer};
|
|
|
|
|
|
fields::F=(), id::UInt=UInt(0)) where {N,B<:AbstractBasis,F}
|
2025-11-09 03:29:36 +02:00
|
|
|
|
connectivity_uint = UInt.(connectivity)
|
2025-11-09 17:08:36 +02:00
|
|
|
|
integration_points = ntuple(i -> IP(UInt(0), 0.0, ()), 0) # Empty initially
|
|
|
|
|
|
basis = B()
|
|
|
|
|
|
return Element{N,0,F,B}(id, connectivity_uint, integration_points, fields, basis)
|
2025-11-08 09:39:16 +02:00
|
|
|
|
end
|
|
|
|
|
|
|
2025-11-09 17:08:36 +02:00
|
|
|
|
function Element(::Type{B}, connectivity::Vector{<:Integer}; kwargs...) where B<:AbstractBasis
|
|
|
|
|
|
return Element(B, (connectivity...,); kwargs...)
|
2025-11-08 09:39:16 +02:00
|
|
|
|
end
|
|
|
|
|
|
|
2025-11-09 09:29:06 +02:00
|
|
|
|
# ============================================================================
|
2025-11-09 17:08:36 +02:00
|
|
|
|
# Topology → Basis constructors: Accept topology types, create Lagrange basis
|
2025-11-09 09:29:06 +02:00
|
|
|
|
# ============================================================================
|
2025-11-09 17:08:36 +02:00
|
|
|
|
|
|
|
|
|
|
"""
|
|
|
|
|
|
Element(::Type{<:AbstractTopology}, connectivity; fields=(), id=UInt(0))
|
|
|
|
|
|
|
|
|
|
|
|
Construct element from topology type. Creates linear Lagrange basis automatically.
|
|
|
|
|
|
|
|
|
|
|
|
# Examples
|
|
|
|
|
|
```julia
|
|
|
|
|
|
element = Element(Triangle, (1, 2, 3)) # → Lagrange{Triangle,1}
|
|
|
|
|
|
element = Element(Quadrilateral, (1,2,3,4), # → Lagrange{Quadrilateral,1}
|
|
|
|
|
|
fields=(E=210e3, ν=0.3))
|
|
|
|
|
|
element = Element(Tet10, (1,2,3,4,5,6,7,8,9,10)) # → Lagrange{Tet10,2}
|
|
|
|
|
|
```
|
|
|
|
|
|
"""
|
|
|
|
|
|
function Element(::Type{T}, connectivity::NTuple{N,<:Integer}; kwargs...) where {N,T<:AbstractTopology}
|
2025-11-12 01:10:22 +02:00
|
|
|
|
# Map to base topology for basis functions (Seg3 → Segment, Tri6 → Triangle, etc.)
|
|
|
|
|
|
base_topo = get_base_topology(T)
|
2025-11-09 17:08:36 +02:00
|
|
|
|
# Determine order from number of nodes
|
|
|
|
|
|
order = infer_lagrange_order(T, N)
|
2025-11-21 00:27:49 +02:00
|
|
|
|
# Use NEW API: basis order only (topology passed separately)
|
|
|
|
|
|
BasisType = Lagrange{order}
|
2025-11-09 17:08:36 +02:00
|
|
|
|
return Element(BasisType, connectivity; kwargs...)
|
|
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
|
|
function Element(::Type{T}, connectivity::Vector{<:Integer}; kwargs...) where T<:AbstractTopology
|
|
|
|
|
|
return Element(T, (connectivity...,); kwargs...)
|
|
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
|
|
"""Infer Lagrange order from topology type and number of nodes"""
|
|
|
|
|
|
function infer_lagrange_order(::Type{T}, n::Int) where T<:AbstractTopology
|
|
|
|
|
|
# Get the type name for pattern matching
|
|
|
|
|
|
tname = string(nameof(T))
|
|
|
|
|
|
|
|
|
|
|
|
# Check explicit higher-order topologies first (Tet10, Hex20, Tri6, etc.)
|
|
|
|
|
|
if tname == "Tet10" && n == 10
|
|
|
|
|
|
return 2
|
|
|
|
|
|
elseif tname == "Tri6" && n == 6
|
|
|
|
|
|
return 2
|
|
|
|
|
|
elseif tname == "Tri7" && n == 7
|
|
|
|
|
|
return 3
|
|
|
|
|
|
elseif tname == "Quad8" && n == 8
|
|
|
|
|
|
return 2 # Serendipity
|
|
|
|
|
|
elseif tname == "Quad9" && n == 9
|
|
|
|
|
|
return 2 # Full quadratic
|
|
|
|
|
|
elseif tname == "Hex20" && n == 20
|
|
|
|
|
|
return 2 # Serendipity
|
|
|
|
|
|
elseif tname == "Hex27" && n == 27
|
|
|
|
|
|
return 2 # Full quadratic
|
|
|
|
|
|
elseif tname == "Wedge15" && n == 15
|
|
|
|
|
|
return 2
|
|
|
|
|
|
elseif tname == "Seg3" && n == 3
|
|
|
|
|
|
return 2
|
|
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
|
|
# For base topologies (and their aliases), infer order from number of nodes
|
|
|
|
|
|
if T === Segment || T === Seg2
|
|
|
|
|
|
n == 2 && return 1
|
|
|
|
|
|
n == 3 && return 2
|
|
|
|
|
|
elseif T === Triangle || T === Tri3
|
|
|
|
|
|
n == 3 && return 1
|
|
|
|
|
|
n == 6 && return 2
|
|
|
|
|
|
n == 7 && return 3
|
|
|
|
|
|
elseif T === Quadrilateral || T === Quad4
|
|
|
|
|
|
n == 4 && return 1
|
|
|
|
|
|
n == 8 && return 2 # Serendipity
|
|
|
|
|
|
n == 9 && return 2 # Full
|
|
|
|
|
|
elseif T === Tetrahedron || T === Tet4
|
|
|
|
|
|
n == 4 && return 1
|
|
|
|
|
|
n == 10 && return 2
|
|
|
|
|
|
elseif T === Hexahedron || T === Hex8
|
|
|
|
|
|
n == 8 && return 1
|
|
|
|
|
|
n == 20 && return 2 # Serendipity
|
|
|
|
|
|
n == 27 && return 2 # Full
|
|
|
|
|
|
elseif T === Pyramid || T === Pyr5
|
|
|
|
|
|
n == 5 && return 1
|
|
|
|
|
|
elseif T === Wedge || T === Wedge6
|
|
|
|
|
|
n == 6 && return 1
|
|
|
|
|
|
n == 15 && return 2
|
|
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
|
|
# Default to order 1 (linear)
|
|
|
|
|
|
return 1
|
|
|
|
|
|
end
|
2025-11-09 09:29:06 +02:00
|
|
|
|
# Commenting out until new architecture is fully implemented.
|
|
|
|
|
|
|
|
|
|
|
|
# # Helper: Map topology type to basis type
|
|
|
|
|
|
# topology_to_basis(::Type{Poi1}) = Poi1Basis
|
|
|
|
|
|
# topology_to_basis(::Type{Seg2}) = Seg2Basis
|
|
|
|
|
|
# topology_to_basis(::Type{Seg3}) = Seg3Basis
|
|
|
|
|
|
# topology_to_basis(::Type{Tri3}) = Tri3Basis
|
|
|
|
|
|
# topology_to_basis(::Type{Tri6}) = Tri6Basis
|
|
|
|
|
|
# topology_to_basis(::Type{Tri7}) = Tri7Basis
|
|
|
|
|
|
# topology_to_basis(::Type{Quad4}) = Quad4Basis
|
|
|
|
|
|
# topology_to_basis(::Type{Quad8}) = Quad8Basis
|
|
|
|
|
|
# topology_to_basis(::Type{Quad9}) = Quad9Basis
|
|
|
|
|
|
# topology_to_basis(::Type{Tet4}) = Tet4Basis
|
|
|
|
|
|
# topology_to_basis(::Type{Tet10}) = Tet10Basis
|
|
|
|
|
|
# topology_to_basis(::Type{Hex8}) = Hex8Basis
|
|
|
|
|
|
# topology_to_basis(::Type{Hex20}) = Hex20Basis
|
|
|
|
|
|
# topology_to_basis(::Type{Hex27}) = Hex27Basis
|
|
|
|
|
|
# topology_to_basis(::Type{Pyr5}) = Pyr5Basis
|
|
|
|
|
|
# topology_to_basis(::Type{Wedge6}) = Wedge6Basis
|
|
|
|
|
|
# topology_to_basis(::Type{Wedge15}) = Wedge15Basis
|
|
|
|
|
|
|
|
|
|
|
|
# # Shim constructors that forward to basis-based constructors
|
|
|
|
|
|
# function Element(::Type{T}, connectivity::NTuple{N,<:Integer}) where {N,T<:AbstractTopology}
|
|
|
|
|
|
# return Element(topology_to_basis(T), connectivity)
|
|
|
|
|
|
# end
|
|
|
|
|
|
|
|
|
|
|
|
# function Element(::Type{T}, ::Type{M}, connectivity::NTuple{N,<:Integer}) where {N,M<:AbstractFieldSet,T<:AbstractTopology}
|
|
|
|
|
|
# return Element(topology_to_basis(T), M, connectivity)
|
|
|
|
|
|
# end
|
|
|
|
|
|
|
|
|
|
|
|
# function Element(::Type{T}, connectivity::Vector{<:Integer}) where T<:AbstractTopology
|
|
|
|
|
|
# return Element(topology_to_basis(T), connectivity)
|
|
|
|
|
|
# end
|
|
|
|
|
|
|
2025-11-10 22:25:59 +02:00
|
|
|
|
# ============================================================================
|
|
|
|
|
|
# Immutable update: Returns new element with updated fields
|
|
|
|
|
|
# ============================================================================
|
|
|
|
|
|
|
|
|
|
|
|
"""
|
|
|
|
|
|
update(element::Element, pairs::Pair...) -> Element
|
|
|
|
|
|
update(element::Element; kwargs...) -> Element
|
|
|
|
|
|
|
|
|
|
|
|
Create a new element with updated fields (immutable operation).
|
|
|
|
|
|
|
|
|
|
|
|
Since elements are immutable, this function returns a **new** element with merged fields.
|
|
|
|
|
|
The original element is unchanged.
|
|
|
|
|
|
|
|
|
|
|
|
# Arguments
|
|
|
|
|
|
- `element`: Original element
|
|
|
|
|
|
- `pairs`: Field updates as `Pair` objects (`:field => value`)
|
|
|
|
|
|
- `kwargs`: Field updates as keyword arguments
|
|
|
|
|
|
|
|
|
|
|
|
# Returns
|
|
|
|
|
|
New `Element` with updated fields (same connectivity, basis, id)
|
|
|
|
|
|
|
|
|
|
|
|
# Examples
|
|
|
|
|
|
```julia
|
|
|
|
|
|
# Create element with initial fields:
|
|
|
|
|
|
el = Element(Lagrange{Triangle,1}, (1,2,3), fields=(E=210e3, ν=0.3))
|
|
|
|
|
|
|
|
|
|
|
|
# Update single field (returns NEW element):
|
|
|
|
|
|
el2 = update(el, :E => 200e3) # el.fields.E still 210e3, el2.fields.E is 200e3
|
|
|
|
|
|
|
|
|
|
|
|
# Update multiple fields:
|
|
|
|
|
|
el3 = update(el, :E => 200e3, :ν => 0.35)
|
|
|
|
|
|
|
|
|
|
|
|
# Keyword syntax:
|
|
|
|
|
|
el4 = update(el; E=200e3, ν=0.35)
|
|
|
|
|
|
|
|
|
|
|
|
# Add new field:
|
|
|
|
|
|
el5 = update(el, :temperature => 293.15)
|
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
|
|
# Performance
|
|
|
|
|
|
Zero-allocation when fields are type-stable NamedTuples with same field types.
|
|
|
|
|
|
May allocate if adding new fields with different types.
|
|
|
|
|
|
|
|
|
|
|
|
# See Also
|
|
|
|
|
|
- Old API: `update!(element, "field", value)` (DEPRECATED - mutates element)
|
|
|
|
|
|
- New API: `update(element, :field => value)` (returns new element)
|
|
|
|
|
|
"""
|
|
|
|
|
|
function update(element::Element{N,NIP,F,B}, pairs::Pair{Symbol,<:Any}...) where {N,NIP,F,B}
|
|
|
|
|
|
# Convert pairs to NamedTuple
|
|
|
|
|
|
new_fields_dict = Dict{Symbol,Any}(pairs)
|
|
|
|
|
|
|
|
|
|
|
|
# Merge with existing fields
|
|
|
|
|
|
if F === Tuple{} || element.fields === ()
|
|
|
|
|
|
# No existing fields, create new NamedTuple
|
|
|
|
|
|
merged = NamedTuple(new_fields_dict)
|
|
|
|
|
|
else
|
|
|
|
|
|
# Merge existing fields with new ones
|
|
|
|
|
|
old_fields = element.fields
|
|
|
|
|
|
for field_name in fieldnames(typeof(old_fields))
|
|
|
|
|
|
if !haskey(new_fields_dict, field_name)
|
|
|
|
|
|
new_fields_dict[field_name] = getfield(old_fields, field_name)
|
|
|
|
|
|
end
|
|
|
|
|
|
end
|
|
|
|
|
|
merged = NamedTuple(new_fields_dict)
|
|
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
|
|
# Create new element with updated fields
|
|
|
|
|
|
return Element{N,NIP,typeof(merged),B}(
|
|
|
|
|
|
element.id,
|
|
|
|
|
|
element.connectivity,
|
|
|
|
|
|
element.integration_points,
|
|
|
|
|
|
merged,
|
|
|
|
|
|
element.basis
|
|
|
|
|
|
)
|
|
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
|
|
# Keyword argument version
|
|
|
|
|
|
function update(element::Element{N,NIP,F,B}; kwargs...) where {N,NIP,F,B}
|
|
|
|
|
|
pairs = [k => v for (k, v) in kwargs]
|
|
|
|
|
|
return update(element, pairs...)
|
|
|
|
|
|
end
|
|
|
|
|
|
|
2025-11-08 09:39:16 +02:00
|
|
|
|
function get_element_id(element::AbstractElement)
|
|
|
|
|
|
return element.id
|
|
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
|
|
function get_element_type(::AbstractElement{M,T}) where {M,T}
|
|
|
|
|
|
return T
|
|
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
|
|
function is_element_type(::AbstractElement{M,T}, element_type) where {M,T}
|
|
|
|
|
|
return T === element_type
|
|
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
|
|
function filter_by_element_type(element_type, elements)
|
|
|
|
|
|
return Iterators.filter(element -> is_element_type(element, element_type), elements)
|
|
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
|
|
function get_connectivity(element::AbstractElement)
|
|
|
|
|
|
return element.connectivity
|
|
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
|
|
"""
|
|
|
|
|
|
group_by_element_type(elements)
|
|
|
|
|
|
|
|
|
|
|
|
Given a vector of elements, group elements by element type to several vectors.
|
|
|
|
|
|
Returns a dictionary, where key is the element type and value is a vector
|
|
|
|
|
|
containing all elements of type `element_type`.
|
|
|
|
|
|
"""
|
|
|
|
|
|
function group_by_element_type(elements)
|
|
|
|
|
|
eltypes = map(T -> typeof(T), elements)
|
|
|
|
|
|
elgroups = Dict(T => T[] for T in eltypes)
|
|
|
|
|
|
for element in elements
|
|
|
|
|
|
T = typeof(element)
|
|
|
|
|
|
push!(elgroups[T], element)
|
|
|
|
|
|
end
|
|
|
|
|
|
return elgroups
|
|
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
|
|
### dfields - dynamically defined fields
|
|
|
|
|
|
|
2025-11-09 18:08:39 +02:00
|
|
|
|
# ============================================================================
|
|
|
|
|
|
# COMPATIBILITY SHIM: Old dfields API → New fields API
|
|
|
|
|
|
# ============================================================================
|
|
|
|
|
|
# NOTE: This is temporary until field system is redesigned (Phase 3)
|
|
|
|
|
|
# Old API: element.dfields (mutable Dict for dynamic fields)
|
|
|
|
|
|
# New API: element.fields (type-stable NamedTuple/struct, immutable)
|
|
|
|
|
|
#
|
|
|
|
|
|
# For now, we map dfields to the same place as sfields (element.fields)
|
|
|
|
|
|
# since the new API doesn't distinguish between static and dynamic fields.
|
2025-11-08 09:39:16 +02:00
|
|
|
|
|
|
|
|
|
|
function has_dfield(element, field_name)
|
2025-11-09 18:08:39 +02:00
|
|
|
|
# In new API, dfields don't exist as separate from sfields
|
|
|
|
|
|
# Check if field_name is in element.fields
|
|
|
|
|
|
F = typeof(element.fields)
|
|
|
|
|
|
if F === Tuple{}
|
|
|
|
|
|
return false # Empty fields
|
|
|
|
|
|
elseif F <: NamedTuple
|
|
|
|
|
|
return haskey(element.fields, field_name)
|
|
|
|
|
|
else
|
|
|
|
|
|
return isdefined(element.fields, field_name)
|
|
|
|
|
|
end
|
2025-11-08 09:39:16 +02:00
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
|
|
function get_dfield(element, field_name)
|
2025-11-09 18:08:39 +02:00
|
|
|
|
# Return the field value from element.fields
|
|
|
|
|
|
F = typeof(element.fields)
|
|
|
|
|
|
if F === Tuple{}
|
|
|
|
|
|
error("Element has no fields (empty tuple)")
|
|
|
|
|
|
end
|
|
|
|
|
|
return getfield(element.fields, field_name)
|
2025-11-08 09:39:16 +02:00
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
|
|
function create_dfield!(element, field_name, field_::AbstractField)
|
2025-11-09 18:08:39 +02:00
|
|
|
|
# WARNING: In new API, fields are IMMUTABLE!
|
|
|
|
|
|
# This function cannot actually create the field in-place
|
|
|
|
|
|
# TODO Phase 3: Redesign field system to support mutable time-varying fields
|
|
|
|
|
|
@warn "create_dfield!() called but fields are immutable in new API. Field not created." maxlog = 1
|
2025-11-08 09:39:16 +02:00
|
|
|
|
return
|
|
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
|
|
function create_dfield!(element, field_name, field_data)
|
2025-11-09 18:08:39 +02:00
|
|
|
|
# WARNING: In new API, fields are IMMUTABLE!
|
|
|
|
|
|
@warn "create_dfield!() called but fields are immutable in new API. Field not created." maxlog = 1
|
|
|
|
|
|
return
|
2025-11-08 09:39:16 +02:00
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
|
|
function update_dfield!(element, field_name, field_data)
|
2025-11-09 18:08:39 +02:00
|
|
|
|
# WARNING: In new API, fields are IMMUTABLE!
|
|
|
|
|
|
# This function cannot actually update the field in-place
|
|
|
|
|
|
# TODO Phase 3: Redesign field system to support mutable time-varying fields
|
|
|
|
|
|
@warn "update_dfield!() called but fields are immutable in new API. Field not updated." maxlog = 1
|
|
|
|
|
|
return
|
2025-11-08 09:39:16 +02:00
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
|
|
# A helper function to pick element data from dictionary
|
|
|
|
|
|
function pick_data_(element, field_data)
|
|
|
|
|
|
connectivity = get_connectivity(element)
|
|
|
|
|
|
N = length(connectivity)
|
|
|
|
|
|
picked_data = ntuple(i -> getindex(field_data, connectivity[i]), N)
|
|
|
|
|
|
return picked_data
|
|
|
|
|
|
end
|
|
|
|
|
|
|
2025-11-09 03:10:11 +02:00
|
|
|
|
function update_dfield!(element, field_name, (time, field_data)::Pair{Float64,Dict{Int,V}}) where V
|
2025-11-09 18:08:39 +02:00
|
|
|
|
# WARNING: In new API, fields are IMMUTABLE!
|
|
|
|
|
|
@warn "update_dfield!() called but fields are immutable in new API. Field not updated." maxlog = 1
|
|
|
|
|
|
return
|
2025-11-08 09:39:16 +02:00
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
|
|
function update_dfield!(element, field_name, field_data::Dict{Int,V}) where V
|
2025-11-09 18:08:39 +02:00
|
|
|
|
# WARNING: In new API, fields are IMMUTABLE!
|
|
|
|
|
|
@warn "update_dfield!() called but fields are immutable in new API. Field not updated." maxlog = 1
|
|
|
|
|
|
return
|
2025-11-08 09:39:16 +02:00
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
|
|
function update_dfield!(element, field_name, field_data::Function)
|
2025-11-09 18:08:39 +02:00
|
|
|
|
# WARNING: In new API, fields are IMMUTABLE!
|
|
|
|
|
|
@warn "update_dfield!() called but fields are immutable in new API. Field not updated." maxlog = 1
|
|
|
|
|
|
return
|
2025-11-08 09:39:16 +02:00
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
|
|
function interpolate_dfield(element, field_name, time)
|
2025-11-09 18:08:39 +02:00
|
|
|
|
# In new API, fields are static (no time variation)
|
|
|
|
|
|
# Just return the field value
|
|
|
|
|
|
return get_dfield(element, field_name)
|
2025-11-08 09:39:16 +02:00
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
|
|
### sfields statically defined fields
|
|
|
|
|
|
|
2025-11-09 18:08:39 +02:00
|
|
|
|
# ============================================================================
|
|
|
|
|
|
# COMPATIBILITY SHIM: Old sfields/dfields API → New fields API
|
|
|
|
|
|
# ============================================================================
|
|
|
|
|
|
# NOTE: This is temporary until field system is redesigned (Phase 3)
|
|
|
|
|
|
# Old API: element.sfields (static fields) and element.dfields (dynamic fields)
|
|
|
|
|
|
# New API: element.fields (type-stable NamedTuple/struct, immutable)
|
|
|
|
|
|
#
|
|
|
|
|
|
# Problem: Tests use update_field!() to mutate fields, but new fields are immutable
|
|
|
|
|
|
# Solution: Make has_sfield/get_sfield work with new API, but warn about mutations
|
2025-11-08 09:39:16 +02:00
|
|
|
|
|
|
|
|
|
|
function has_sfield(element, field_name)
|
2025-11-09 18:08:39 +02:00
|
|
|
|
# In new API, sfields don't exist - but we can check if field_name is in element.fields
|
|
|
|
|
|
F = typeof(element.fields)
|
|
|
|
|
|
if F === Tuple{}
|
|
|
|
|
|
return false # Empty fields
|
|
|
|
|
|
elseif F <: NamedTuple
|
|
|
|
|
|
return haskey(element.fields, field_name)
|
|
|
|
|
|
else
|
|
|
|
|
|
return isdefined(element.fields, field_name)
|
|
|
|
|
|
end
|
2025-11-08 09:39:16 +02:00
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
|
|
function get_sfield(element, field_name)
|
2025-11-09 18:08:39 +02:00
|
|
|
|
# Return the field value from element.fields
|
|
|
|
|
|
F = typeof(element.fields)
|
|
|
|
|
|
if F === Tuple{}
|
|
|
|
|
|
error("Element has no fields (empty tuple)")
|
|
|
|
|
|
end
|
|
|
|
|
|
return getfield(element.fields, field_name)
|
2025-11-08 09:39:16 +02:00
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
|
|
function update_sfield!(element, field_name, field_data)
|
2025-11-09 18:08:39 +02:00
|
|
|
|
# WARNING: In new API, fields are IMMUTABLE!
|
|
|
|
|
|
# This function cannot actually update the field in-place
|
|
|
|
|
|
# TODO Phase 3: Redesign field system to support mutable time-varying fields
|
|
|
|
|
|
@warn "update_sfield!() called but fields are immutable in new API. Field not updated." maxlog = 1
|
|
|
|
|
|
return nothing
|
2025-11-08 09:39:16 +02:00
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
|
|
function interpolate_sfield(element, field_name, time)
|
2025-11-09 18:08:39 +02:00
|
|
|
|
# In new API, fields are static (no time variation)
|
|
|
|
|
|
# Just return the field value
|
|
|
|
|
|
return get_sfield(element, field_name)
|
2025-11-08 09:39:16 +02:00
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
|
|
### dfield & sfield -- common routines
|
|
|
|
|
|
|
|
|
|
|
|
function has_field(element, field_name)
|
|
|
|
|
|
return has_sfield(element, field_name) || has_dfield(element, field_name)
|
|
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
|
|
function get_field(element, field_name)
|
|
|
|
|
|
if has_sfield(element, field_name)
|
|
|
|
|
|
return get_sfield(element, field_name)
|
|
|
|
|
|
else
|
|
|
|
|
|
return get_dfield(element, field_name)
|
|
|
|
|
|
end
|
|
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
|
|
function update_field!(element, field_name, field_data)
|
2025-11-09 18:08:39 +02:00
|
|
|
|
# In new API: fields are immutable, so we can't actually update them
|
|
|
|
|
|
# For now, just check if field exists and warn
|
2025-11-08 09:39:16 +02:00
|
|
|
|
if has_sfield(element, field_name)
|
|
|
|
|
|
update_sfield!(element, field_name, field_data)
|
2025-11-09 18:08:39 +02:00
|
|
|
|
elseif has_dfield(element, field_name)
|
2025-11-08 09:39:16 +02:00
|
|
|
|
update_dfield!(element, field_name, field_data)
|
2025-11-09 18:08:39 +02:00
|
|
|
|
else
|
|
|
|
|
|
# Field doesn't exist - this is OK in new API where fields are optional
|
|
|
|
|
|
# Silently ignore (many tests add fields dynamically that we don't need)
|
2025-11-08 09:39:16 +02:00
|
|
|
|
end
|
|
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
|
|
function interpolate_field(element, field_name::Symbol, time)
|
|
|
|
|
|
if has_sfield(element, field_name)
|
|
|
|
|
|
return interpolate_sfield(element, field_name, time)
|
|
|
|
|
|
elseif has_dfield(element, field_name)
|
|
|
|
|
|
return interpolate_dfield(element, field_name, time)
|
|
|
|
|
|
else
|
|
|
|
|
|
error("Cannot interpolate from field $field_name: no such field.")
|
|
|
|
|
|
end
|
|
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
|
|
function interpolate(element::AbstractElement, field_name, time)
|
|
|
|
|
|
return interpolate_field(element, field_name, time)
|
|
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
|
|
function update_field!(elements::Vector{Element}, field_name, field_data)
|
|
|
|
|
|
for element in elements
|
|
|
|
|
|
update_field!(element, field_name, field_data)
|
|
|
|
|
|
end
|
|
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
|
|
# Update fields when given a dictionary or time => dictionary:
|
|
|
|
|
|
# pick data from dictionary diven by the connectivity information of element
|
|
|
|
|
|
#=
|
|
|
|
|
|
function update_field!(element::AbstractElement, field::F,
|
|
|
|
|
|
data::Dict{T,V}) where {F<:DVTI,T,V}
|
|
|
|
|
|
connectivity = get_connectivity(element)
|
|
|
|
|
|
N = length(connectivity)
|
|
|
|
|
|
picked_data = ntuple(i -> data[connectivity[i]], N)
|
|
|
|
|
|
update_field!(field, picked_data)
|
|
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
|
|
function update_field!(element::AbstractElement, field::F,
|
|
|
|
|
|
ddata::Pair{Float64, Dict{T,V}}) where {F<:DVTV,T,V}
|
|
|
|
|
|
time, data = ddata
|
|
|
|
|
|
connectivity = get_connectivity(element)
|
|
|
|
|
|
N = length(connectivity)
|
|
|
|
|
|
picked_data = ntuple(i -> data[connectivity[i]], N)
|
|
|
|
|
|
update_field!(field, time => picked_data)
|
|
|
|
|
|
end
|
|
|
|
|
|
=#
|
|
|
|
|
|
|
|
|
|
|
|
"""
|
|
|
|
|
|
interpolate(element, field_name, time)
|
|
|
|
|
|
|
|
|
|
|
|
Interpolate field `field_name` from element at given `time`.
|
|
|
|
|
|
|
|
|
|
|
|
# Example
|
|
|
|
|
|
```
|
|
|
|
|
|
element = Element(Seg2, [1, 2])
|
|
|
|
|
|
data1 = Dict(1 => 1.0, 2 => 2.0)
|
|
|
|
|
|
data2 = Dict(1 => 2.0, 2 => 3.0)
|
|
|
|
|
|
update!(element, "my field", 0.0 => data1)
|
|
|
|
|
|
update!(element, "my field", 1.0 => data2)
|
|
|
|
|
|
interpolate(element, "my field", 0.5)
|
|
|
|
|
|
|
|
|
|
|
|
# output
|
|
|
|
|
|
|
|
|
|
|
|
(1.5, 2.5)
|
|
|
|
|
|
|
|
|
|
|
|
```
|
|
|
|
|
|
"""
|
|
|
|
|
|
function interpolate(element::AbstractElement, field_name::String, time::Float64)
|
|
|
|
|
|
field = element[field_name]
|
2025-11-12 01:10:22 +02:00
|
|
|
|
# If field is an AbstractField, interpolate it
|
|
|
|
|
|
if field isa AbstractField
|
|
|
|
|
|
result = interpolate(field, time)
|
|
|
|
|
|
else
|
|
|
|
|
|
# For raw data (Dict, NamedTuple, etc.), it's time-invariant
|
|
|
|
|
|
result = field
|
|
|
|
|
|
end
|
2025-11-08 09:39:16 +02:00
|
|
|
|
if isa(result, Dict)
|
|
|
|
|
|
connectivity = get_connectivity(element)
|
|
|
|
|
|
return tuple((result[i] for i in connectivity)...)
|
|
|
|
|
|
else
|
|
|
|
|
|
return result
|
|
|
|
|
|
end
|
|
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
|
|
function info_update_field(elements, field_name, data)
|
|
|
|
|
|
nelements = length(elements)
|
|
|
|
|
|
@info("Updating field `$field_name` for $nelements elements.")
|
|
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
|
|
function info_update_field(elements, field_name, data::Float64)
|
|
|
|
|
|
nelements = length(elements)
|
|
|
|
|
|
@info("Updating field `$field_name` => $data for $nelements elements.")
|
|
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
|
|
"""
|
|
|
|
|
|
update!(elements, field_name, data)
|
|
|
|
|
|
|
|
|
|
|
|
Given a list of elements, field name and data, update field to elements. Data
|
|
|
|
|
|
is passed directly to the `field`-function.
|
|
|
|
|
|
|
|
|
|
|
|
# Examples
|
|
|
|
|
|
|
|
|
|
|
|
Create two elements with topology `Seg2`, one is connecting to nodes (1, 2) and
|
|
|
|
|
|
the other is connecting to (2, 3). Some examples of updating fields:
|
|
|
|
|
|
|
|
|
|
|
|
```julia
|
|
|
|
|
|
elements = [Element(Seg2, [1, 2]), Element(Seg2, [2, 3])]
|
|
|
|
|
|
X = Dict(1 => 0.0, 2 => 1.0, 3 => 2.0)
|
|
|
|
|
|
u = Dict(1 => 0.0, 2 => 0.0, 3 => 0.0)
|
|
|
|
|
|
update!(elements, "geometry", X)
|
|
|
|
|
|
update!(elements, "displacement", 0.0 => u)
|
|
|
|
|
|
update!(elements, "youngs modulus", 210.0e9)
|
|
|
|
|
|
update!(elements, "time-dependent force", 0.0 => 0.0)
|
|
|
|
|
|
update!(elements, "time-dependent force", 1.0 => 100.0)
|
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
|
|
When using dictionaries in definition of fields, key of dictionary corresponds
|
|
|
|
|
|
to node id, that is, updating field `geometry` in the example above is updating
|
|
|
|
|
|
values `(0.0, 1.0)` for the first elements and values `(1.0, 2.0)` to the second
|
|
|
|
|
|
element. For time dependent field, syntax `time => data` is used. If field is
|
|
|
|
|
|
initialized without time-dependency, it cannot be changed to be time-dependent
|
|
|
|
|
|
afterwards. If unsure, it's better to initialize field with time dependency.
|
|
|
|
|
|
|
|
|
|
|
|
"""
|
|
|
|
|
|
function update!(elements, field_name, data)
|
|
|
|
|
|
info_update_field(elements, field_name, data)
|
|
|
|
|
|
for element in elements
|
|
|
|
|
|
update!(element, field_name, data)
|
|
|
|
|
|
end
|
|
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
## Interpolate fields in spatial direction
|
|
|
|
|
|
|
2025-11-09 03:10:11 +02:00
|
|
|
|
const ConstantField = Union{DCTI,DCTV}
|
|
|
|
|
|
const VariableFields = Union{DVTV,DVTI}
|
|
|
|
|
|
const DictionaryFields = Union{DVTVd,DVTId}
|
2025-11-08 09:39:16 +02:00
|
|
|
|
|
|
|
|
|
|
function interpolate_field(::AbstractElement, field::ConstantField, ip, time)
|
|
|
|
|
|
return interpolate_field(field, time)
|
|
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
|
|
function interpolate_field(element::AbstractElement, field::VariableFields, ip, time)
|
|
|
|
|
|
data = interpolate_field(field, time)
|
|
|
|
|
|
basis = get_basis(element, ip, time)
|
|
|
|
|
|
N = length(basis)
|
2025-11-09 03:10:11 +02:00
|
|
|
|
return sum(data[i] * basis[i] for i = 1:N)
|
2025-11-08 09:39:16 +02:00
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
|
|
function interpolate_field(element::AbstractElement, field::DictionaryFields, ip, time)
|
|
|
|
|
|
data = interpolate_field(field, time)
|
|
|
|
|
|
basis = element(ip, time)
|
|
|
|
|
|
N = length(element)
|
|
|
|
|
|
c = get_connectivity(element)
|
2025-11-09 03:10:11 +02:00
|
|
|
|
return sum(data[c[i]] * basis[i] for i = 1:N)
|
2025-11-08 09:39:16 +02:00
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
|
|
function interpolate_field(::AbstractElement, field::CVTV, ip, time)
|
|
|
|
|
|
return field(ip, time)
|
|
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
|
|
function interpolate(element::AbstractElement, field_name, ip, time)
|
|
|
|
|
|
field = get_field(element, field_name)
|
|
|
|
|
|
interpolate_field(element, field, ip, time)
|
|
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
## Other stuff
|
|
|
|
|
|
|
2025-11-21 00:27:49 +02:00
|
|
|
|
# TEMPORARY: Commented out during basis refactoring - uses old Lagrange{T,P} API
|
|
|
|
|
|
# TODO: Rewrite to use new Lagrange{P} API with topology passed separately
|
|
|
|
|
|
#=
|
2025-11-15 04:41:59 +02:00
|
|
|
|
# Helper: Create topology instance from basis type
|
|
|
|
|
|
function _create_topology_instance(::Type{Lagrange{T,P}}) where {T,P}
|
|
|
|
|
|
N_nodes = nnodes(Lagrange{T,P}())
|
|
|
|
|
|
if T <: Segment
|
|
|
|
|
|
return Segment{N_nodes}()
|
|
|
|
|
|
elseif T <: Triangle
|
|
|
|
|
|
return Triangle()
|
|
|
|
|
|
elseif T <: Quadrilateral
|
|
|
|
|
|
return Quadrilateral()
|
|
|
|
|
|
elseif T <: Tetrahedron
|
|
|
|
|
|
return Tetrahedron()
|
|
|
|
|
|
elseif T <: Hexahedron
|
|
|
|
|
|
return Hexahedron()
|
|
|
|
|
|
elseif T <: Wedge
|
|
|
|
|
|
return Wedge()
|
|
|
|
|
|
elseif T <: Pyramid
|
|
|
|
|
|
return Pyramid()
|
|
|
|
|
|
else
|
|
|
|
|
|
error("Unknown topology type: $T")
|
|
|
|
|
|
end
|
|
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
|
|
# Jacobian computation using new API
|
|
|
|
|
|
# Handles embedded elements (e.g., 1D element in 2D/3D space)
|
|
|
|
|
|
function jacobian(B::Lagrange{T,P}, X::Vector{<:Vec}, xi::Vec) where {T,P}
|
|
|
|
|
|
topo_instance = _create_topology_instance(Lagrange{T,P})
|
|
|
|
|
|
dN_dξ_tuple = get_basis_derivatives(topo_instance, B, xi)
|
|
|
|
|
|
|
|
|
|
|
|
# Handle embedding: X can be Vec{D_phys} while dN_dξ is Vec{D_param}
|
|
|
|
|
|
# where D_phys > D_param (e.g., 1D element in 2D/3D space)
|
|
|
|
|
|
dim_physical = length(first(X))
|
|
|
|
|
|
dim_parametric = length(xi)
|
|
|
|
|
|
|
|
|
|
|
|
# Build Jacobian matrix manually for embedding case
|
|
|
|
|
|
# J[i,j] = ∂X_i/∂ξ_j = sum_k X_k[i] * dN_k/dξ_j
|
|
|
|
|
|
J_data = zeros(dim_physical, dim_parametric)
|
|
|
|
|
|
@inbounds for k in 1:length(X)
|
|
|
|
|
|
for i in 1:dim_physical
|
|
|
|
|
|
for j in 1:dim_parametric
|
|
|
|
|
|
J_data[i, j] += X[k][i] * dN_dξ_tuple[k][j]
|
|
|
|
|
|
end
|
|
|
|
|
|
end
|
|
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
|
|
return J_data
|
|
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
|
|
function get_basis(element::AbstractElement{M,B}, ip, ::Any) where {M,B<:Lagrange}
|
2025-11-09 03:10:11 +02:00
|
|
|
|
# Handle both raw coordinates (Tuple) and IP struct
|
|
|
|
|
|
coords = isa(ip, IP) ? ip.coords : ip
|
|
|
|
|
|
T = typeof(first(coords))
|
|
|
|
|
|
# Convert to Vec for Tensors.jl compatibility
|
|
|
|
|
|
xi = Vec{length(coords),T}(coords)
|
2025-11-15 04:41:59 +02:00
|
|
|
|
|
|
|
|
|
|
# Create topology instance once per basis type (will be inlined/constant folded)
|
|
|
|
|
|
topo_instance = _create_topology_instance(B)
|
|
|
|
|
|
|
|
|
|
|
|
# Call new API
|
|
|
|
|
|
N_tuple = get_basis_functions(topo_instance, B(), xi)
|
2025-11-09 03:10:11 +02:00
|
|
|
|
# Return as row matrix for compatibility with old code
|
2025-11-09 03:29:36 +02:00
|
|
|
|
return reshape(collect(N_tuple), 1, length(element))
|
2025-11-08 09:39:16 +02:00
|
|
|
|
end
|
|
|
|
|
|
|
2025-11-15 04:41:59 +02:00
|
|
|
|
function get_dbasis(element::AbstractElement{M,B}, ip, ::Any) where {M,B<:Lagrange}
|
2025-11-09 03:10:11 +02:00
|
|
|
|
# Handle both raw coordinates (Tuple) and IP struct
|
|
|
|
|
|
coords = isa(ip, IP) ? ip.coords : ip
|
|
|
|
|
|
T = typeof(first(coords))
|
|
|
|
|
|
# Convert to Vec for Tensors.jl compatibility
|
|
|
|
|
|
xi = Vec{length(coords),T}(coords)
|
2025-11-15 04:41:59 +02:00
|
|
|
|
|
|
|
|
|
|
# Create topology instance once per basis type (will be inlined/constant folded)
|
|
|
|
|
|
topo_instance = _create_topology_instance(B)
|
|
|
|
|
|
|
|
|
|
|
|
# Call new API
|
|
|
|
|
|
dN_tuple = get_basis_derivatives(topo_instance, B(), xi)
|
2025-11-09 03:29:36 +02:00
|
|
|
|
# Return as Vector for compatibility with old code
|
|
|
|
|
|
return collect(dN_tuple)
|
2025-11-08 09:39:16 +02:00
|
|
|
|
end
|
2025-11-21 00:27:49 +02:00
|
|
|
|
=#
|
|
|
|
|
|
|
|
|
|
|
|
# END COMMENTED OUT SECTION
|
2025-11-08 09:39:16 +02:00
|
|
|
|
|
|
|
|
|
|
function (element::Element)(ip, time::Float64=0.0)
|
|
|
|
|
|
return get_basis(element, ip, time)
|
|
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
|
|
#"""
|
|
|
|
|
|
#Examples
|
|
|
|
|
|
#julia> el = Element(Quad4, [1, 2, 3, 4]);
|
|
|
|
|
|
#julia> el([0.0, 0.0], 0.0, 1)
|
|
|
|
|
|
#1x4 Array{Float64,2}:
|
|
|
|
|
|
# 0.25 0.25 0.25 0.25
|
|
|
|
|
|
#julia> el([0.0, 0.0], 0.0, 2)
|
|
|
|
|
|
#2x8 Array{Float64,2}:
|
|
|
|
|
|
# 0.25 0.0 0.25 0.0 0.25 0.0 0.25 0.0
|
|
|
|
|
|
# 0.0 0.25 0.0 0.25 0.0 0.25 0.0 0.25
|
|
|
|
|
|
#"""
|
|
|
|
|
|
function (element::Element)(ip, time::Float64, dim::Int)
|
|
|
|
|
|
dim == 1 && return get_basis(element, ip, time)
|
|
|
|
|
|
Ni = vec(get_basis(element, ip, time))
|
2025-11-09 03:10:11 +02:00
|
|
|
|
N = zeros(dim, length(element) * dim)
|
|
|
|
|
|
for i = 1:dim
|
|
|
|
|
|
N[i, i:dim:end] += Ni
|
2025-11-08 09:39:16 +02:00
|
|
|
|
end
|
|
|
|
|
|
return N
|
|
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
|
|
function (element::Element)(ip, time, ::Type{Val{:Jacobian}})
|
2025-11-09 03:10:11 +02:00
|
|
|
|
X_dict = element("geometry", time)
|
2025-11-12 01:10:22 +02:00
|
|
|
|
# Convert Dict to Vector{Vec} in connectivity order
|
|
|
|
|
|
# (Dict iteration is unordered, must use element.connectivity)
|
|
|
|
|
|
X = [Vec(X_dict[node_id]...) for node_id in element.connectivity]
|
2025-11-09 18:42:56 +02:00
|
|
|
|
# Convert ip to Vec - handle both Tuple and IntegrationPoint
|
|
|
|
|
|
if isa(ip, Tuple)
|
|
|
|
|
|
xi = Vec(ip)
|
|
|
|
|
|
else
|
|
|
|
|
|
xi = Vec(ip.coords)
|
|
|
|
|
|
end
|
|
|
|
|
|
J = jacobian(element.basis, X, xi)
|
2025-11-08 09:39:16 +02:00
|
|
|
|
return J
|
|
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
|
|
function (element::Element)(ip, time::Float64, ::Type{Val{:detJ}})
|
|
|
|
|
|
J = element(ip, time, Val{:Jacobian})
|
|
|
|
|
|
n, m = size(J)
|
|
|
|
|
|
if n == m # volume element
|
|
|
|
|
|
return det(J)
|
|
|
|
|
|
end
|
2025-11-09 18:42:56 +02:00
|
|
|
|
# For embedded elements (1D in 2D/3D, 2D in 3D):
|
|
|
|
|
|
# detJ = || ∂X/∂ξ || for 1D elements
|
|
|
|
|
|
# detJ = || ∂X/∂ξ₁ × ∂X/∂ξ₂ || for 2D elements
|
2025-11-08 09:39:16 +02:00
|
|
|
|
JT = transpose(J)
|
2025-11-09 18:42:56 +02:00
|
|
|
|
if m == 1 # 1D element (boundary of 2D or 3D), J is n×1, JT is 1×n
|
2025-11-08 09:39:16 +02:00
|
|
|
|
return norm(JT)
|
2025-11-09 18:42:56 +02:00
|
|
|
|
else # 2D element (manifold on 3D problem), J is 3×2, JT is 2×3
|
2025-11-09 03:10:11 +02:00
|
|
|
|
return norm(cross(JT[:, 1], JT[:, 2]))
|
2025-11-08 09:39:16 +02:00
|
|
|
|
end
|
|
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
|
|
function (element::Element)(ip, time::Float64, ::Type{Val{:Grad}})
|
|
|
|
|
|
J = element(ip, time, Val{:Jacobian})
|
2025-11-09 03:10:11 +02:00
|
|
|
|
return inv(J) * get_dbasis(element, ip, time)
|
2025-11-08 09:39:16 +02:00
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
|
|
function (element::Element)(field_name::String, ip, time::Float64, ::Type{Val{:Grad}})
|
|
|
|
|
|
X = element("geometry", time)
|
|
|
|
|
|
u = element(field_name, time)
|
|
|
|
|
|
return grad(element.properties, u, X, ip)
|
|
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
|
|
|
2025-11-09 03:29:36 +02:00
|
|
|
|
function get_integration_points(element::Element{N,NIP,M,B}) where {N,NIP,M,B}
|
|
|
|
|
|
# If integration points already set, return them
|
|
|
|
|
|
if NIP > 0
|
|
|
|
|
|
return element.integration_points
|
2025-11-08 09:39:16 +02:00
|
|
|
|
end
|
2025-11-12 01:10:22 +02:00
|
|
|
|
# Otherwise get default integration points based on basis type
|
|
|
|
|
|
ips = get_integration_points_from_basis(B)
|
2025-11-09 03:29:36 +02:00
|
|
|
|
return tuple([IP(UInt(i), w, xi) for (i, (w, xi)) in enumerate(ips)]...)
|
2025-11-08 09:39:16 +02:00
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
|
|
""" This is a special case, temporarily change order
|
|
|
|
|
|
of integration scheme mainly for mass matrix.
|
|
|
|
|
|
"""
|
2025-11-12 01:10:22 +02:00
|
|
|
|
function get_integration_points(element::AbstractElement{M,B}, change_order::Int) where {M,B}
|
|
|
|
|
|
ips = get_integration_points_from_basis(B, change_order)
|
|
|
|
|
|
# Convert from new API format (weight, Vec{D}) to old IP structs
|
|
|
|
|
|
# Vec{D} needs to be converted to Tuple for IP constructor
|
|
|
|
|
|
return tuple([IP(UInt(i), w, Tuple(xi)) for (i, (w, xi)) in enumerate(ips)]...)
|
|
|
|
|
|
end
|
|
|
|
|
|
|
2025-11-21 00:27:49 +02:00
|
|
|
|
# TEMPORARY: Commented out during basis refactoring - uses old Lagrange{T,P} API
|
|
|
|
|
|
#=
|
2025-11-12 01:10:22 +02:00
|
|
|
|
# Helper function to map Lagrange basis types to new integration points API
|
|
|
|
|
|
function get_integration_points_from_basis(::Type{Lagrange{T,P}}, order::Int=0) where {T,P}
|
|
|
|
|
|
# Map topology type to base topology for integration points
|
|
|
|
|
|
# (Seg2, Seg3 → Segment; Tri3, Tri6, Tri7 → Triangle, etc.)
|
|
|
|
|
|
base_topology = get_base_topology(T)
|
|
|
|
|
|
|
|
|
|
|
|
# Map polynomial order to Gauss quadrature order
|
|
|
|
|
|
# For polynomial order P, we need at least (P+1)/2 integration order
|
|
|
|
|
|
# With order parameter, we can increase accuracy
|
|
|
|
|
|
gauss_order = max(P, P + order)
|
|
|
|
|
|
|
|
|
|
|
|
# Return integration points using new zero-allocation API
|
|
|
|
|
|
# Note: get_gauss_points! expects Type{Gauss{N}}, not instance
|
|
|
|
|
|
if gauss_order == 1
|
|
|
|
|
|
return get_gauss_points!(base_topology, Gauss{1})
|
|
|
|
|
|
elseif gauss_order == 2
|
|
|
|
|
|
return get_gauss_points!(base_topology, Gauss{2})
|
|
|
|
|
|
elseif gauss_order == 3
|
|
|
|
|
|
return get_gauss_points!(base_topology, Gauss{3})
|
|
|
|
|
|
elseif gauss_order == 4
|
|
|
|
|
|
return get_gauss_points!(base_topology, Gauss{4})
|
|
|
|
|
|
elseif gauss_order == 5
|
|
|
|
|
|
return get_gauss_points!(base_topology, Gauss{5})
|
|
|
|
|
|
else
|
|
|
|
|
|
error("Gauss quadrature order $gauss_order not supported for topology $T")
|
|
|
|
|
|
end
|
|
|
|
|
|
end
|
2025-11-21 00:27:49 +02:00
|
|
|
|
=#
|
|
|
|
|
|
# END COMMENTED OUT SECTION (get_integration_points_from_basis)
|
2025-11-12 01:10:22 +02:00
|
|
|
|
|
|
|
|
|
|
# Map topology types to their base forms for integration
|
2025-11-21 00:27:49 +02:00
|
|
|
|
# NOTE: Restored from commented section - needed by Element constructor
|
2025-11-12 01:10:22 +02:00
|
|
|
|
function get_base_topology(::Type{T}) where T
|
|
|
|
|
|
name = string(nameof(T))
|
|
|
|
|
|
# 1D elements
|
|
|
|
|
|
if startswith(name, "Seg") || name == "Segment"
|
|
|
|
|
|
return Segment
|
|
|
|
|
|
# 2D elements
|
|
|
|
|
|
elseif startswith(name, "Tri") || name == "Triangle"
|
|
|
|
|
|
return Triangle
|
|
|
|
|
|
elseif startswith(name, "Quad") || name == "Quadrilateral"
|
|
|
|
|
|
return Quadrilateral
|
|
|
|
|
|
# 3D elements
|
|
|
|
|
|
elseif startswith(name, "Tet") || name == "Tetrahedron"
|
|
|
|
|
|
return Tetrahedron
|
|
|
|
|
|
elseif startswith(name, "Hex") || name == "Hexahedron"
|
|
|
|
|
|
return Hexahedron
|
|
|
|
|
|
elseif startswith(name, "Wedge")
|
|
|
|
|
|
return Wedge
|
|
|
|
|
|
elseif startswith(name, "Pyr") || name == "Pyramid"
|
|
|
|
|
|
return Pyramid
|
|
|
|
|
|
else
|
|
|
|
|
|
error("Unknown topology type: $T")
|
|
|
|
|
|
end
|
2025-11-09 03:29:36 +02:00
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
|
|
"""
|
|
|
|
|
|
with_integration_points(element, integration_points_tuple) -> Element
|
|
|
|
|
|
|
|
|
|
|
|
Create a new element with the given integration points. Since Element is immutable,
|
|
|
|
|
|
this returns a new instance with updated integration points.
|
|
|
|
|
|
"""
|
|
|
|
|
|
function with_integration_points(element::Element{N,NIP,M,B}, ips::NTuple{NNEW,IP}) where {N,NIP,M,B,NNEW}
|
|
|
|
|
|
return Element{N,NNEW,M,B}(element.id, element.connectivity, ips,
|
2025-11-09 03:30:29 +02:00
|
|
|
|
element.dfields, element.sfields, element.properties)
|
2025-11-08 09:39:16 +02:00
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
|
|
""" Find inverse isoparametric mapping of element. """
|
|
|
|
|
|
function get_local_coordinates(element::AbstractElement, X::Vector, time::Float64; max_iterations=10, tolerance=1.0e-6)
|
|
|
|
|
|
haskey(element, "geometry") || error("element geometry not defined, cannot calculate inverse isoparametric mapping")
|
|
|
|
|
|
dim = size(element, 1)
|
|
|
|
|
|
dim == length(X) || error("manifolds not supported.")
|
|
|
|
|
|
xi = zeros(dim)
|
|
|
|
|
|
dX = element("geometry", xi, time) - X
|
2025-11-09 03:10:11 +02:00
|
|
|
|
for i = 1:max_iterations
|
2025-11-08 09:39:16 +02:00
|
|
|
|
J = element(xi, time, Val{:Jacobian})'
|
|
|
|
|
|
xi -= J \ dX
|
|
|
|
|
|
dX = element("geometry", xi, time) - X
|
|
|
|
|
|
norm(dX) < tolerance && return xi
|
|
|
|
|
|
end
|
|
|
|
|
|
debug("get_local_coordinates", X, dX, xi)
|
|
|
|
|
|
error("Unable to find inverse isoparametric mapping for element $element for X = $X")
|
|
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
|
|
""" Test is X inside element. """
|
|
|
|
|
|
function inside(element::AbstractElement{M,B}, X, time) where {M,B}
|
|
|
|
|
|
xi = get_local_coordinates(element, X, time)
|
|
|
|
|
|
return inside(B, xi)
|
|
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
|
|
## Convenience functions
|
|
|
|
|
|
|
|
|
|
|
|
# element("displacement", 0.0)
|
|
|
|
|
|
function (element::Element)(field_name::String, time::Float64)
|
|
|
|
|
|
return interpolate(element, field_name, time)
|
|
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
|
|
# element("displacement", (0.0, 0.0), 0.0)
|
|
|
|
|
|
function (element::Element)(field_name::String, ip, time::Float64)
|
|
|
|
|
|
return interpolate(element, field_name, ip, time)
|
|
|
|
|
|
end
|
|
|
|
|
|
|
2025-11-09 17:08:36 +02:00
|
|
|
|
# OLD: Uses BasisInfo which was defined in math.jl (commented out)
|
|
|
|
|
|
# function element_info!(bi::BasisInfo{T}, element::AbstractElement{M,T}, ip, time) where {M,T}
|
|
|
|
|
|
# X = interpolate(element, "geometry", time)
|
|
|
|
|
|
# eval_basis!(bi, X, ip)
|
|
|
|
|
|
# return bi.J, bi.detJ, bi.N, bi.grad
|
|
|
|
|
|
# end
|