feat(continuum): DOF-based Pass~1 hooks and mixed kernel updates

Add dof_based_pass1.jl prepare_dof_based_material_workspace! overrides; dim-2 geometry cache branch; update Hu–Washizu, Hellinger–Reissner, Stokes, mixed-up kernels and material cache wiring.
This commit is contained in:
Jukka Aho
2026-05-11 02:23:05 +03:00
parent 12f7f8c0bd
commit 799c4d6a0f
11 changed files with 373 additions and 84 deletions
+8 -8
View File
@@ -1,5 +1,5 @@
# This file is a part of JuliaFEM. # SPDX-FileCopyrightText: 2015-2026 Jukka Aho
# License is MIT: see https://github.com/JuliaFEM/JuliaFEM.jl/blob/master/LICENSE.md # SPDX-License-Identifier: MIT
""" """
Abstract types for continuum mechanics domain. Abstract types for continuum mechanics domain.
@@ -24,7 +24,7 @@ DESIGN PHILOSOPHY: Formulations are DOMAIN-AGNOSTIC dimensionality concepts.
Formulation (domain-agnostic): Formulation (domain-agnostic):
- Describes DIMENSIONALITY and geometric simplifications - Describes DIMENSIONALITY and geometric simplifications
- Used by multiple physics domains - Used by multiple physics domains
- Examples: FullThreeD, Axisymmetric - Examples: ThreeDimensional, Axisymmetric
- Can be reused across continuum, heat, acoustics, etc. - Can be reused across continuum, heat, acoustics, etc.
Theory (domain-specific): Theory (domain-specific):
@@ -34,8 +34,8 @@ Theory (domain-specific):
# Why Separate Them? # Why Separate Them?
Problem: Heat transfer needs FullThreeD and Axisymmetric, just like continuum! Problem: Heat transfer needs ThreeDimensional and Axisymmetric, just like continuum!
If FullThreeD is defined in domains/continuum/, heat can't use it without duplication. If ThreeDimensional is defined in domains/continuum/, heat can't use it without duplication.
Solution: Formulations are dimensionality (shared), theories are physics (domain-specific). Solution: Formulations are dimensionality (shared), theories are physics (domain-specific).
@@ -43,7 +43,7 @@ Solution: Formulations are dimensionality (shared), theories are physics (domain
```julia ```julia
# Domain-agnostic formulations # Domain-agnostic formulations
FullThreeD() # Used by: continuum, heat, poisson, acoustics ThreeDimensional() # Used by: continuum, heat, poisson, acoustics
Axisymmetric() # Used by: continuum, heat, etc. Axisymmetric() # Used by: continuum, heat, etc.
# Domain-specific theories (in domains/*/types.jl) # Domain-specific theories (in domains/*/types.jl)
@@ -75,7 +75,7 @@ PlaneStrain (ε_xx, ε_yy, ε_xy, ε_zz = 0):
- Out-of-plane strain ε_zz = 0 - Out-of-plane strain ε_zz = 0
- Examples: Dams, tunnels, retaining walls, long cylinders - Examples: Dams, tunnels, retaining walls, long cylinders
FullThreeD: ThreeDimensional:
- No simplifications, all six stress/strain components - No simplifications, all six stress/strain components
- Most accurate but most expensive - Most accurate but most expensive
@@ -97,6 +97,6 @@ Plane Strain (thick section):
- Constitutive: 3×3 reduced stiffness matrix (different from plane stress!) - Constitutive: 3×3 reduced stiffness matrix (different from plane stress!)
# See Also # See Also
- Concrete theories: `continuum/types.jl` (FullThreeD, PlaneStress, PlaneStrain, Axisymmetric) - Concrete theories: `continuum/types.jl` (ThreeDimensional, PlaneStress, PlaneStrain, Axisymmetric)
""" """
abstract type AbstractContinuumTheory end abstract type AbstractContinuumTheory end
+172
View File
@@ -0,0 +1,172 @@
# SPDX-FileCopyrightText: 2015-2026 Jukka Aho
# SPDX-License-Identifier: MIT
"""
Pass~1 helpers for DOF-based assembly with [`ContinuumKernel`](@ref):
vertex displacement scatter and material-workspace preparation.
"""
using Tensors
using Tensors: basevec, symmetric, Tensor
using ..JuliaFEM: AbstractMaterial, compute_stress, continuum_kinematics,
GreenLagrangeKinematics, SmallStrainKinematics
"""
scatter_vertex_displacements_from_global!(
u_buffer, dofs_storage, configuration, ::Type{E},
) -> Nothing
Scatter global displacement DOFs from `configuration` into
`u_buffer` (vertex-major `Vec{3}` per topology node) using compile-time
[`local_dof_layout`](@ref)`(E)` and the element's global DOF list in
`dofs_storage`. Only entries with `1 ≤ component ≤ 3` and valid
`entity_local` vertex indices participate.
No heap allocation in the loop.
"""
@inline function scatter_vertex_displacements_from_global!(
u_buffer::Vector{Vec{3,Float64}},
dofs_storage::Vector{Int},
configuration::AbstractVector{Float64},
::Type{E},
) where {E<:AbstractElement}
layout = local_dof_layout(E)
nbuf = length(u_buffer)
@inbounds for v in 1:nbuf
u_buffer[v] = zero(Vec{3,Float64})
end
@inbounds for li in eachindex(layout)
ent = entity_local(layout[li])
(1 ent nbuf) || continue
comp = component(layout[li])
(1 comp 3) || continue
d = dofs_storage[li]
u_buffer[ent] = u_buffer[ent] + basevec(Vec{3,Float64}, comp) * configuration[d]
end
return nothing
end
"""
_continuum_fill_workspace_stress_from_displacement!(
material_workspace, geometry_cache, element_cache, material, Δt, empty_state,
) -> Nothing
Recompute `(σ, 𝔻)` at every IP from nodal displacements in `element_cache.u_buffer`
using [`continuum_kinematics`](@ref)`(material)` (small strain or GreenLagrange).
Used when `material_behavior(material) isa StatelessConstantTangent` but a global
`configuration` is supplied so Cauchy stress tracks the current displacement while
the tangent remains the constitutive tangent returned by [`compute_stress`](@ref).
Zero-allocation in the IP loop.
"""
@inline function _continuum_fill_workspace_stress_from_displacement!(
material_workspace::AssemblyMaterialWorkspace{FieldType, StateType},
geometry_cache::GeometryCache,
element_cache::ElementCache,
material::AbstractMaterial,
Δt::Float64,
empty_state::NamedTuple,
) where {FieldType, StateType}
fields_mw = getfield(material_workspace, 1)
states_mw = getfield(material_workspace, 2)
nips = length(element_cache.ips)
nnodes = length(geometry_cache.X)
kin = continuum_kinematics(material)
I = one(Tensor{2,3,Float64,9})
@inbounds for q in 1:nips
strain_measure = if kin isa SmallStrainKinematics
ε = zero(SymmetricTensor{2,3,Float64,6})
for k in 1:nnodes
u_k = element_cache.u_buffer[k]
∇N_k_q = geometry_cache.∇N_data[q, k]
ε += symmetric(u_k ∇N_k_q)
end
ε
elseif kin isa GreenLagrangeKinematics
F = I
for k in 1:nnodes
u_k = element_cache.u_buffer[k]
∇N_k_q = geometry_cache.∇N_data[q, k]
F += u_k ∇N_k_q
end
C_tensor = symmetric(F' F)
SymmetricTensor{2,3}(0.5 * (C_tensor - I))
else
throw(ArgumentError("unknown continuum kinematics $(typeof(kin))"))
end
σ, 𝔻, _ = compute_stress(material, strain_measure, NamedTuple(), Δt)
fields_mw[q] = (σ=σ, 𝔻=𝔻)
states_mw[q] = empty_state
end
return nothing
end
@inline function prepare_dof_based_material_workspace!(
k_e::ContinuumKernel,
material_workspace::AssemblyMaterialWorkspace,
geometry_cache::GeometryCache,
element_cache::ElementCache,
eid::Int,
configuration::Union{Nothing,AbstractVector{Float64}},
global_material_cache::Union{Nothing,GlobalMaterialCache},
Δt::Float64,
::Type{E},
) where {E<:AbstractElement}
mat = k_e.material
beh = material_behavior(mat)
fields_mw = getfield(material_workspace, 1)
states_mw = getfield(material_workspace, 2)
ips_ec = getfield(element_cache, :ips)
nips = length(ips_ec)
if beh isa StatelessConstantTangent
fields_ref_e, empty_state_e = reference_fields(k_e)
if configuration !== nothing
scatter_vertex_displacements_from_global!(
element_cache.u_buffer, element_cache.dofs, configuration, E,
)
_continuum_fill_workspace_stress_from_displacement!(
material_workspace, geometry_cache, element_cache, mat, Δt, empty_state_e,
)
else
@inbounds for q in 1:nips
fields_mw[q] = fields_ref_e
states_mw[q] = empty_state_e
end
end
elseif beh isa StatelessStrainDependent
if configuration !== nothing
scatter_vertex_displacements_from_global!(
element_cache.u_buffer, element_cache.dofs, configuration, E,
)
end
update_material_cache_stateless_strain!(
material_workspace, geometry_cache, mat, element_cache, Δt,
)
elseif beh isa StatefulStrainDependent
global_material_cache === nothing && throw(ArgumentError(
"DOF-based Pass 1: StatefulStrainDependent material requires keyword " *
"`global_material_cache=create_global_material_cache(mat; n_ips, n_elems)`",
))
if configuration !== nothing
scatter_vertex_displacements_from_global!(
element_cache.u_buffer, element_cache.dofs, configuration, E,
)
end
update_material_cache!(
material_workspace,
geometry_cache,
mat,
element_cache,
global_material_cache,
eid,
Δt,
)
else
throw(ArgumentError(
"unsupported material behavior $(typeof(beh)) for ContinuumKernel in DOF-based Pass 1",
))
end
return nothing
end
@@ -78,7 +78,7 @@ the discrete `σ``σ` block uses `G M⁻¹ G` in the Voigt component basis.
```julia ```julia
S = @DOFSet{u::DOF{Displacement{3}, Vertex}, σ::DOF{SymmetricTensor{2,3}, Cell}} S = @DOFSet{u::DOF{Displacement{3}, Vertex}, σ::DOF{SymmetricTensor{2,3}, Cell}}
kernel = HellingerReissnerKernel( kernel = HellingerReissnerKernel(
ContinuumFormulation{FullThreeD}(), ContinuumFormulation{ThreeDimensional}(),
LinearElastic(E = 210e9, ν = 0.3), LinearElastic(E = 210e9, ν = 0.3),
) )
``` ```
+1 -1
View File
@@ -101,7 +101,7 @@ S = @DOFSet{
sig::DOF{SymmetricTensor{2,3}, Cell}, sig::DOF{SymmetricTensor{2,3}, Cell},
} }
kernel = HuWashizuKernel( kernel = HuWashizuKernel(
ContinuumFormulation{FullThreeD}(), ContinuumFormulation{ThreeDimensional}(),
LinearElastic(E = 210e9, ν = 0.3), LinearElastic(E = 210e9, ν = 0.3),
) )
``` ```
+37 -7
View File
@@ -1,5 +1,5 @@
# This file is a part of JuliaFEM. # SPDX-FileCopyrightText: 2015-2026 Jukka Aho
# License is MIT: see https://github.com/JuliaFEM/JuliaFEM.jl/blob/master/LICENSE.md # SPDX-License-Identifier: MIT
""" """
Continuum mechanics kernel - defines the weak form only. Continuum mechanics kernel - defines the weak form only.
@@ -10,9 +10,10 @@ This module defines:
3. Block builder: compute_stiffness_block (builds D×D blocks) 3. Block builder: compute_stiffness_block (builds D×D blocks)
The DOF-based / matrix-free assembler microkernel surface The DOF-based / matrix-free assembler microkernel surface
(`qpoint_buffer_eltype`, `update_qpoint_buffer!`, `evaluate_entry`, (`qpoint_buffer_eltype`, `prepare_dof_based_material_workspace!`,
`evaluate_mass_entry`, `reference_fields`) is implemented further `update_qpoint_buffer!`, `evaluate_entry`, `evaluate_mass_entry`,
down in this file. Everything else (geometry preprocessing, `reference_fields`) is implemented in this file and in
`dof_based_pass1.jl`. Everything else (geometry preprocessing,
integration, assembly, DOF mapping) belongs elsewhere. integration, assembly, DOF mapping) belongs elsewhere.
""" """
@@ -29,7 +30,7 @@ optional density (carried on the kernel rather than the material so
existing material structs stay untouched). existing material structs stay untouched).
# Type Parameters # Type Parameters
- `Theory`: Continuum theory (FullThreeD, PlaneStress, PlaneStrain, Axisymmetric) - `Theory`: Continuum theory (ThreeDimensional, PlaneStress, PlaneStrain, Axisymmetric)
- `Mat`: Material model (LinearElastic, NeoHookean, etc.) - `Mat`: Material model (LinearElastic, NeoHookean, etc.)
# Fields # Fields
@@ -45,7 +46,7 @@ existing material structs stay untouched).
```julia ```julia
kernel = ContinuumKernel( kernel = ContinuumKernel(
ContinuumFormulation{FullThreeD}(), ContinuumFormulation{ThreeDimensional}(),
LinearElastic(E=210e9, ν=0.3), LinearElastic(E=210e9, ν=0.3),
Displacement{3}(); Displacement{3}();
density = 7850.0, # for mass matrix; omit for static-only density = 7850.0, # for mass matrix; omit for static-only
@@ -223,6 +224,35 @@ keeps the entire chain inside the symmetric-tensor methods of
return dcontract(B_k_α, dcontract(C, B_l_β)) return dcontract(B_k_α, dcontract(C, B_l_β))
end end
"""
compute_internal_force_value(grad_i::Vec{3,F}, σ::SymmetricTensor{2,3,F}, α::Int) where {F}
Scalar factor for the Galerkin internal-force row of a displacement test function
associated with shape function ``N_i`` (gradient ``\\nabla N_i``) and Cartesian
component ``\\alpha``:
``\\sigma_{j\\alpha} \\, \\partial N_i / \\partial x_j``
(sum over ``j = 1\\ldots 3``). The caller multiplies by ``\\det J \\cdot w`` per
quadrature point and accumulates over IPs and elements.
Cauchy stress ``\\sigma`` is the value stored in the material workspace at the IP
(small-strain or finite-strain model, depending on the constitutive update).
Zero allocation.
"""
@inline function compute_internal_force_value(
grad_i::Vec{3,F},
σ::SymmetricTensor{2,3,F},
α::Int,
) where {F<:AbstractFloat}
s = zero(F)
@inbounds for j in 1:3
s += grad_i[j] * σ[j, α]
end
return s
end
""" """
compute_stiffness_block( compute_stiffness_block(
grad_k::Vec{D}, grad_k::Vec{D},
@@ -101,7 +101,7 @@ function hex8_symmetric_uniaxial_eliminated_dirichlet(
end end
""" """
material_lab_linear_elastic_uniaxial_solve(mesh, handler, elements, E, ν, δx; formulation = ContinuumFormulation{FullThreeD}()) material_lab_linear_elastic_uniaxial_solve(mesh, handler, elements, E, ν, δx; formulation = ContinuumFormulation{ThreeDimensional}())
Assemble `K`, apply [`hex8_symmetric_uniaxial_eliminated_dirichlet`](@ref), solve `K u = 0` Assemble `K`, apply [`hex8_symmetric_uniaxial_eliminated_dirichlet`](@ref), solve `K u = 0`
with elimination lift, and return `u`. with elimination lift, and return `u`.
@@ -115,7 +115,7 @@ function material_lab_linear_elastic_uniaxial_solve(
E::Float64, E::Float64,
ν::Float64, ν::Float64,
δx::Float64; δx::Float64;
formulation = ContinuumFormulation{FullThreeD}(), formulation = ContinuumFormulation{ThreeDimensional}(),
) )
material = LinearElastic(E = E, ν = ν) material = LinearElastic(E = E, ν = ν)
kernel = ContinuumKernel(formulation, material, Displacement{3}()) kernel = ContinuumKernel(formulation, material, Displacement{3}())
+3 -3
View File
@@ -47,7 +47,7 @@ This is the first mixed kernel: `evaluate_entry` dispatches on
using Tensors using Tensors
using ..JuliaFEM: AbstractKernel, AbstractFormulation using ..JuliaFEM: AbstractKernel, AbstractFormulation
using ..JuliaFEM: ContinuumFormulation, FullThreeD, AbstractContinuumTheory using ..JuliaFEM: ContinuumFormulation, ThreeDimensional, AbstractContinuumTheory
using ..JuliaFEM: AbstractMaterial, Displacement using ..JuliaFEM: AbstractMaterial, Displacement
using ..JuliaFEM: AssemblyMaterialWorkspace, compute_stress using ..JuliaFEM: AssemblyMaterialWorkspace, compute_stress
import ..JuliaFEM: qpoint_buffer_eltype, update_qpoint_buffer!, evaluate_entry, import ..JuliaFEM: qpoint_buffer_eltype, update_qpoint_buffer!, evaluate_entry,
@@ -63,7 +63,7 @@ Mixed `u``p` kernel: 3D vertex displacement (field 1) + scalar cell pressure
(field 2). See the file-level docstring for the weak form. (field 2). See the file-level docstring for the weak form.
# Fields # Fields
- `formulation::ContinuumFormulation{Theory}` — geometric driver (`FullThreeD`, …) - `formulation::ContinuumFormulation{Theory}` — geometric driver (`ThreeDimensional`, …)
- `material::Mat` — mechanical material (`LinearElastic`, …) - `material::Mat` — mechanical material (`LinearElastic`, …)
- `inv_bulk::Float64` — `1/κ` for the `−κ⁻¹ ∫ p q dΩ` term (`0` = incompressible limit) - `inv_bulk::Float64` — `1/κ` for the `−κ⁻¹ ∫ p q dΩ` term (`0` = incompressible limit)
@@ -72,7 +72,7 @@ Mixed `u``p` kernel: 3D vertex displacement (field 1) + scalar cell pressure
```julia ```julia
S = @DOFSet{u::DOF{Displacement{3}, Vertex}, p::DOF{Float64, Cell}} S = @DOFSet{u::DOF{Displacement{3}, Vertex}, p::DOF{Float64, Cell}}
kernel = MixedUPKernel( kernel = MixedUPKernel(
ContinuumFormulation{FullThreeD}(), ContinuumFormulation{ThreeDimensional}(),
LinearElastic(E = 210e9, ν = 0.3), LinearElastic(E = 210e9, ν = 0.3),
inv_bulk = 1.0 / (210e9 / 3), # order-of-magnitude compressible term inv_bulk = 1.0 / (210e9 / 3), # order-of-magnitude compressible term
) )
+2 -2
View File
@@ -42,7 +42,7 @@ Newtonian Stokes mixed kernel: `u` (vertex, three components) + scalar
`p` on `Cell`. See the file-level docstring for the weak form. `p` on `Cell`. See the file-level docstring for the weak form.
# Fields # Fields
- `formulation::ContinuumFormulation{Theory}` geometric driver (`FullThreeD`, ) - `formulation::ContinuumFormulation{Theory}` geometric driver (`ThreeDimensional`, )
- `μ::Float64` dynamic viscosity (Stokes: `σ = 2μ ε(u)` with symmetric gradient `ε`) - `μ::Float64` dynamic viscosity (Stokes: `σ = 2μ ε(u)` with symmetric gradient `ε`)
- `inv_bulk::Float64` `1/κ` for the `−κ⁻¹ ∫ p q dΩ` term (`0` = incompressible) - `inv_bulk::Float64` `1/κ` for the `−κ⁻¹ ∫ p q dΩ` term (`0` = incompressible)
@@ -50,7 +50,7 @@ Newtonian Stokes mixed kernel: `u` (vertex, three components) + scalar
```julia ```julia
S = @DOFSet{u::DOF{Displacement{3}, Vertex}, p::DOF{Float64, Cell}} S = @DOFSet{u::DOF{Displacement{3}, Vertex}, p::DOF{Float64, Cell}}
kernel = StokesMixedKernel(ContinuumFormulation{FullThreeD}(); μ = 1.0e-3, inv_bulk = 0.0) kernel = StokesMixedKernel(ContinuumFormulation{ThreeDimensional}(); μ = 1.0e-3, inv_bulk = 0.0)
``` ```
""" """
struct StokesMixedKernel{Theory<:AbstractContinuumTheory} <: AbstractKernel struct StokesMixedKernel{Theory<:AbstractContinuumTheory} <: AbstractKernel
+13 -10
View File
@@ -1,12 +1,13 @@
# This file is a part of JuliaFEM. # SPDX-FileCopyrightText: 2015-2026 Jukka Aho
# License is MIT: see https://github.com/JuliaFEM/JuliaFEM.jl/blob/master/LICENSE.md # SPDX-License-Identifier: MIT
""" """
Concrete types for continuum mechanics formulations and theories. Concrete types for continuum mechanics formulations and theories.
Abstract types are in abstract.jl, implementations are in formulations.jl. Abstract types are in `abstract.jl`; concrete theory structs and
`ContinuumFormulation` live in this file.
Must be included after abstract.jl. Must be included after `abstract.jl`.
""" """
# ============================================================================ # ============================================================================
@@ -14,13 +15,15 @@ Must be included after abstract.jl.
# ============================================================================ # ============================================================================
""" """
FullThreeD <: AbstractContinuumTheory ThreeDimensional <: AbstractContinuumTheory
Full 3D analysis with no simplifications. All six stress / flux components Bulk three-dimensional model: no in-plane or axisymmetric reduction. All
are carried; no geometric simplifications. Domain-agnostic used by both independent tensor components are retained at the quadrature point (six for
`ContinuumKernel` (solid mechanics) and `HeatKernel` (heat conduction). symmetric mechanical stress / strain; three for isotropic flux, etc.).
Domain-agnostic tag shared by `ContinuumKernel`, `HeatKernel`, Darcy-style
kernels, and other `ContinuumFormulation{…}` drivers on 3D meshes.
""" """
struct FullThreeD <: AbstractContinuumTheory end struct ThreeDimensional <: AbstractContinuumTheory end
""" """
PlaneStress <: AbstractContinuumTheory PlaneStress <: AbstractContinuumTheory
@@ -60,7 +63,7 @@ Used as a type tag inside `ContinuumKernel{Theory, Material, Field}`.
# Examples # Examples
```julia ```julia
ContinuumFormulation{FullThreeD}() ContinuumFormulation{ThreeDimensional}()
ContinuumFormulation{PlaneStress}() ContinuumFormulation{PlaneStress}()
ContinuumFormulation{PlaneStrain}() ContinuumFormulation{PlaneStrain}()
ContinuumFormulation{Axisymmetric}() ContinuumFormulation{Axisymmetric}()
+79 -24
View File
@@ -1,5 +1,5 @@
# This file is a part of JuliaFEM. # SPDX-FileCopyrightText: 2015-2026 Jukka Aho
# License is MIT: see https://github.com/JuliaFEM/JuliaFEM.jl/blob/master/LICENSE.md # SPDX-License-Identifier: MIT
""" """
Geometry cache update functions for continuum elements. Geometry cache update functions for continuum elements.
@@ -17,14 +17,24 @@ fields written are:
- `geometry_cache.X` node coordinates (one entry per element node) - `geometry_cache.X` node coordinates (one entry per element node)
- `geometry_cache.N_data[ip, k]` basis values - `geometry_cache.N_data[ip, k]` basis values
- `geometry_cache.∇N_data[ip, k]` physical gradients `∇N` - `geometry_cache.∇N_data[ip, k]` physical gradients `∇N` as `Vec{3}` (tangent to the
- `geometry_cache.detJ_w[ip]` `det(J) * w` for integration embedded surface for `dim(topology)==2`; all three components may be nonzero)
- `geometry_cache.detJ_w[ip]` `det(J) * w` for `D==3`, or `√(det G) · w` for `D==2`
The function is allocation-free; it reads topology, basis, and the The function is allocation-free; it reads topology, basis, and the
integration points from `element_cache` and writes back into the integration points from `element_cache` and writes back into the
pre-allocated `geometry_cache` arrays. For each integration point the pre-allocated `geometry_cache` arrays.
Jacobian `J = X ⊗ ∇_ξ N` is built on the fly, then physical gradients
are obtained via `J^{-T} · ∇_ξ N`. For **`dim(topology) == 3`**, the Jacobian `J = Σ_k X_k ⊗ ∇_ξ N_k` is a `Tensor{2,3}`,
inverted in the usual way, and `∇N` uses the full `Vec{3}` chain rule.
For **`dim(topology) == 2`**, node coordinates are `Vec{3}` but the isoparametric map
`x(ξ, η) ∈ ℝ³` is only two-parameter. Let `v_α = ∂x/∂ξ^α = Σ_k X_k ∂N_k/∂ξ^α` for
`α ∈ {1,2}` (columns of the `3 × 2` Jacobian), `G_{αβ} = v_α · v_β` (Gram matrix),
`detJ_w = √(det G) · w` (surface area measure on the embedded patch), and
`∇N_k = v_1 (G^{-1} ∂_ξ N_k)_1 + v_2 (G^{-1} ∂_ξ N_k)_2`. This reduces to the former
`(x, y)` / `J_2^{-T}` formula when the element lies in the global `xy` plane.
Degenerate `det(G) ≤ 0` throws `ArgumentError`.
""" """
@inline function update_geometry_cache!( @inline function update_geometry_cache!(
geometry_cache::GeometryCache, geometry_cache::GeometryCache,
@@ -35,8 +45,6 @@ are obtained via `J^{-T} · ∇_ξ N`.
conn = mesh.connectivity[elem_id] conn = mesh.connectivity[elem_id]
nnodes = length(conn) nnodes = length(conn)
# Extract node coordinates (mesh.nodes already contains Vec{3}).
# Indexed loop avoids the iterator allocation that `enumerate` introduces.
@inbounds for i in 1:nnodes @inbounds for i in 1:nnodes
node = conn[i] node = conn[i]
geometry_cache.X[i] = mesh.nodes[node] geometry_cache.X[i] = mesh.nodes[node]
@@ -44,28 +52,75 @@ are obtained via `J^{-T} · ∇_ξ N`.
ips = element_cache.ips ips = element_cache.ips
nips = length(ips) nips = length(ips)
D = dim(element_cache.topology)
@inbounds for ip_idx in 1:nips if D == 3
ip = ips[ip_idx] @inbounds for ip_idx in 1:nips
ξ = ip.coords ip = ips[ip_idx]
ξ = ip.coords
N_vals = get_basis_functions( element_cache.topology, element_cache.basis, ξ) N_vals = get_basis_functions( element_cache.topology, element_cache.basis, ξ)
dN_dξ = get_basis_derivatives(element_cache.topology, element_cache.basis, ξ) dN_dξ = get_basis_derivatives(element_cache.topology, element_cache.basis, ξ)
# Jacobian J = X ⊗ ∇_ξ N J = geometry_cache.X[1] dN_dξ[1]
J = geometry_cache.X[1] dN_dξ[1] for i in 2:nnodes
for i in 2:nnodes J += geometry_cache.X[i] dN_dξ[i]
J += geometry_cache.X[i] dN_dξ[i] end
J_inv_T = transpose(inv(J))
for k in 1:nnodes
geometry_cache.N_data[ip_idx, k] = N_vals[k]
geometry_cache.∇N_data[ip_idx, k] = J_inv_T dN_dξ[k]
end
geometry_cache.detJ_w[ip_idx] = det(J) * ip.weight
end end
elseif D == 2
F = geometry_eltype(geometry_cache)
@inbounds for ip_idx in 1:nips
ip = ips[ip_idx]
ξ = ip.coords
J_inv_T = transpose(inv(J)) N_vals = get_basis_functions( element_cache.topology, element_cache.basis, ξ)
dN_dξ = get_basis_derivatives(element_cache.topology, element_cache.basis, ξ)
for k in 1:nnodes v1 = zero(Vec{3,F})
geometry_cache.N_data[ip_idx, k] = N_vals[k] v2 = zero(Vec{3,F})
geometry_cache.∇N_data[ip_idx, k] = J_inv_T dN_dξ[k] for i in 1:nnodes
xk = geometry_cache.X[i]
di = dN_dξ[i]
v1 += xk * di[1]
v2 += xk * di[2]
end
G11 = v1 v1
G12 = v1 v2
G22 = v2 v2
G = Tensor{2,2,F,4}((G11, G12, G12, G22))
detG = det(G)
if !(detG > zero(F))
throw(ArgumentError(
"update_geometry_cache!: singular or non-right-handed 2D element " *
"(det(G) = $detG); check node ordering and geometry",
))
end
Ginv = inv(G)
detJ_w = sqrt(detG) * ip.weight
for k in 1:nnodes
geometry_cache.N_data[ip_idx, k] = N_vals[k]
h = Ginv dN_dξ[k]
g = v1 * h[1] + v2 * h[2]
geometry_cache.∇N_data[ip_idx, k] = Vec{3,F}((g[1], g[2], g[3]))
end
geometry_cache.detJ_w[ip_idx] = detJ_w
end end
else
geometry_cache.detJ_w[ip_idx] = det(J) * ip.weight throw(ArgumentError(
"update_geometry_cache!: topology spatial dimension $D is not supported " *
"(expected 2 or 3 for continuum assembly)",
))
end end
return nothing return nothing
+55 -26
View File
@@ -1,5 +1,5 @@
# This file is a part of JuliaFEM. # SPDX-FileCopyrightText: 2015-2026 Jukka Aho
# License is MIT: see https://github.com/JuliaFEM/JuliaFEM.jl/blob/master/LICENSE.md # SPDX-License-Identifier: MIT
""" """
Material cache update functions for continuum elements. Material cache update functions for continuum elements.
@@ -10,6 +10,7 @@ Computes stress, tangent modulus, and internal state at integration points.
using Tensors using Tensors
using ..JuliaFEM: GlobalMaterialCache, get_old_state, set_state! using ..JuliaFEM: GlobalMaterialCache, get_old_state, set_state!
using ..JuliaFEM: continuum_kinematics, SmallStrainKinematics, GreenLagrangeKinematics using ..JuliaFEM: continuum_kinematics, SmallStrainKinematics, GreenLagrangeKinematics
using ..JuliaFEM: material_behavior, StatelessStrainDependent
# ============================================================================ # ============================================================================
# GLOBAL MATERIAL CACHE — behavior-dispatched implementations # GLOBAL MATERIAL CACHE — behavior-dispatched implementations
@@ -126,6 +127,55 @@ end
return nothing return nothing
end end
"""
update_material_cache_stateless_strain!(
material_workspace, geometry_cache, material, element_cache, Δt,
) -> Nothing
Finite-strain / hyperelastic branch with **no** persistent integration-point
state in [`GlobalMaterialCache`](@ref). Requires
`material_behavior(material) isa StatelessStrainDependent`.
`element_cache.u_buffer` must hold the current nodal displacements (vertex
ordering matching `geometry_cache.∇N_data`). Used by the DOF-based Pass 1
when the configuration vector is supplied (or zero displacement when it
is not).
Zero-allocation in the integration loop.
"""
@inline function update_material_cache_stateless_strain!(
material_workspace::AssemblyMaterialWorkspace,
geometry_cache::GeometryCache,
material::AbstractMaterial,
element_cache::ElementCache,
Δt::Float64,
)
material_behavior(material) isa StatelessStrainDependent ||
throw(ArgumentError("update_material_cache_stateless_strain! requires StatelessStrainDependent material"))
nips = length(element_cache.ips)
nnodes = length(geometry_cache.X)
I = one(Tensor{2,3,Float64,9})
@inbounds for q in 1:nips
F = I
for k in 1:nnodes
u_k = element_cache.u_buffer[k]
∇N_k_q = geometry_cache.∇N_data[q, k]
F += u_k ∇N_k_q
end
C_tensor = symmetric(F' F)
E = SymmetricTensor{2,3}(0.5 * (C_tensor - I))
σ, 𝔻, _ = compute_stress(material, E, NamedTuple(), Δt)
@inbounds material_workspace.fields[q] = (σ=σ, 𝔻=𝔻)
material_workspace.states[q] = NamedTuple()
end
return nothing
end
# StatelessStrainDependent — strain at each IP, no persistent state. # StatelessStrainDependent — strain at each IP, no persistent state.
@inline function update_material_cache!( @inline function update_material_cache!(
material_workspace::AssemblyMaterialWorkspace, material_workspace::AssemblyMaterialWorkspace,
@@ -137,30 +187,9 @@ end
elem_id::Int, elem_id::Int,
Δt::Float64, Δt::Float64,
) )
nips = length(element_cache.ips) return update_material_cache_stateless_strain!(
nnodes = length(geometry_cache.X) material_workspace, geometry_cache, material, element_cache, Δt,
I = one(Tensor{2,3,Float64,9}) )
@inbounds for q in 1:nips
# Deformation gradient F = I + ∇u
F = I
for k in 1:nnodes
u_k = element_cache.u_buffer[k]
∇N_k_q = geometry_cache.∇N_data[q, k]
F += u_k ∇N_k_q
end
# GreenLagrange strain E = ½(F'F I)
C_tensor = symmetric(F' F)
E = SymmetricTensor{2,3}(0.5 * (C_tensor - I))
σ, 𝔻, _ = compute_stress(material, E, NamedTuple(), 0.0)
@inbounds material_workspace.fields[q] = (σ=σ, 𝔻=𝔻)
material_workspace.states[q] = NamedTuple()
end
return nothing
end end
# StatefulStrainDependent — read old state from `global_cache`, compute the # StatefulStrainDependent — read old state from `global_cache`, compute the