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.
# License is MIT: see https://github.com/JuliaFEM/JuliaFEM.jl/blob/master/LICENSE.md
# SPDX-FileCopyrightText: 2015-2026 Jukka Aho
# SPDX-License-Identifier: MIT
"""
Abstract types for continuum mechanics domain.
@@ -24,7 +24,7 @@ DESIGN PHILOSOPHY: Formulations are DOMAIN-AGNOSTIC dimensionality concepts.
Formulation (domain-agnostic):
- Describes DIMENSIONALITY and geometric simplifications
- Used by multiple physics domains
- Examples: FullThreeD, Axisymmetric
- Examples: ThreeDimensional, Axisymmetric
- Can be reused across continuum, heat, acoustics, etc.
Theory (domain-specific):
@@ -34,8 +34,8 @@ Theory (domain-specific):
# Why Separate Them?
Problem: Heat transfer needs FullThreeD and Axisymmetric, just like continuum!
If FullThreeD is defined in domains/continuum/, heat can't use it without duplication.
Problem: Heat transfer needs ThreeDimensional and Axisymmetric, just like continuum!
If ThreeDimensional is defined in domains/continuum/, heat can't use it without duplication.
Solution: Formulations are dimensionality (shared), theories are physics (domain-specific).
@@ -43,7 +43,7 @@ Solution: Formulations are dimensionality (shared), theories are physics (domain
```julia
# Domain-agnostic formulations
FullThreeD() # Used by: continuum, heat, poisson, acoustics
ThreeDimensional() # Used by: continuum, heat, poisson, acoustics
Axisymmetric() # Used by: continuum, heat, etc.
# Domain-specific theories (in domains/*/types.jl)
@@ -75,7 +75,7 @@ PlaneStrain (ε_xx, ε_yy, ε_xy, ε_zz = 0):
- Out-of-plane strain ε_zz = 0
- Examples: Dams, tunnels, retaining walls, long cylinders
FullThreeD:
ThreeDimensional:
- No simplifications, all six stress/strain components
- Most accurate but most expensive
@@ -97,6 +97,6 @@ Plane Strain (thick section):
- Constitutive: 3×3 reduced stiffness matrix (different from plane stress!)
# See Also
- Concrete theories: `continuum/types.jl` (FullThreeD, PlaneStress, PlaneStrain, Axisymmetric)
- Concrete theories: `continuum/types.jl` (ThreeDimensional, PlaneStress, PlaneStrain, Axisymmetric)
"""
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
S = @DOFSet{u::DOF{Displacement{3}, Vertex}, σ::DOF{SymmetricTensor{2,3}, Cell}}
kernel = HellingerReissnerKernel(
ContinuumFormulation{FullThreeD}(),
ContinuumFormulation{ThreeDimensional}(),
LinearElastic(E = 210e9, ν = 0.3),
)
```
+1 -1
View File
@@ -101,7 +101,7 @@ S = @DOFSet{
sig::DOF{SymmetricTensor{2,3}, Cell},
}
kernel = HuWashizuKernel(
ContinuumFormulation{FullThreeD}(),
ContinuumFormulation{ThreeDimensional}(),
LinearElastic(E = 210e9, ν = 0.3),
)
```
+37 -7
View File
@@ -1,5 +1,5 @@
# This file is a part of JuliaFEM.
# License is MIT: see https://github.com/JuliaFEM/JuliaFEM.jl/blob/master/LICENSE.md
# SPDX-FileCopyrightText: 2015-2026 Jukka Aho
# SPDX-License-Identifier: MIT
"""
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)
The DOF-based / matrix-free assembler microkernel surface
(`qpoint_buffer_eltype`, `update_qpoint_buffer!`, `evaluate_entry`,
`evaluate_mass_entry`, `reference_fields`) is implemented further
down in this file. Everything else (geometry preprocessing,
(`qpoint_buffer_eltype`, `prepare_dof_based_material_workspace!`,
`update_qpoint_buffer!`, `evaluate_entry`, `evaluate_mass_entry`,
`reference_fields`) is implemented in this file and in
`dof_based_pass1.jl`. Everything else (geometry preprocessing,
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).
# Type Parameters
- `Theory`: Continuum theory (FullThreeD, PlaneStress, PlaneStrain, Axisymmetric)
- `Theory`: Continuum theory (ThreeDimensional, PlaneStress, PlaneStrain, Axisymmetric)
- `Mat`: Material model (LinearElastic, NeoHookean, etc.)
# Fields
@@ -45,7 +46,7 @@ existing material structs stay untouched).
```julia
kernel = ContinuumKernel(
ContinuumFormulation{FullThreeD}(),
ContinuumFormulation{ThreeDimensional}(),
LinearElastic(E=210e9, ν=0.3),
Displacement{3}();
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_β))
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(
grad_k::Vec{D},
@@ -101,7 +101,7 @@ function hex8_symmetric_uniaxial_eliminated_dirichlet(
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`
with elimination lift, and return `u`.
@@ -115,7 +115,7 @@ function material_lab_linear_elastic_uniaxial_solve(
E::Float64,
ν::Float64,
δx::Float64;
formulation = ContinuumFormulation{FullThreeD}(),
formulation = ContinuumFormulation{ThreeDimensional}(),
)
material = LinearElastic(E = E, ν = ν)
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 ..JuliaFEM: AbstractKernel, AbstractFormulation
using ..JuliaFEM: ContinuumFormulation, FullThreeD, AbstractContinuumTheory
using ..JuliaFEM: ContinuumFormulation, ThreeDimensional, AbstractContinuumTheory
using ..JuliaFEM: AbstractMaterial, Displacement
using ..JuliaFEM: AssemblyMaterialWorkspace, compute_stress
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.
# Fields
- `formulation::ContinuumFormulation{Theory}` — geometric driver (`FullThreeD`, …)
- `formulation::ContinuumFormulation{Theory}` — geometric driver (`ThreeDimensional`, …)
- `material::Mat` — mechanical material (`LinearElastic`, …)
- `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
S = @DOFSet{u::DOF{Displacement{3}, Vertex}, p::DOF{Float64, Cell}}
kernel = MixedUPKernel(
ContinuumFormulation{FullThreeD}(),
ContinuumFormulation{ThreeDimensional}(),
LinearElastic(E = 210e9, ν = 0.3),
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.
# Fields
- `formulation::ContinuumFormulation{Theory}` geometric driver (`FullThreeD`, )
- `formulation::ContinuumFormulation{Theory}` geometric driver (`ThreeDimensional`, )
- `μ::Float64` dynamic viscosity (Stokes: `σ = 2μ ε(u)` with symmetric gradient `ε`)
- `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
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
+13 -10
View File
@@ -1,12 +1,13 @@
# This file is a part of JuliaFEM.
# License is MIT: see https://github.com/JuliaFEM/JuliaFEM.jl/blob/master/LICENSE.md
# SPDX-FileCopyrightText: 2015-2026 Jukka Aho
# SPDX-License-Identifier: MIT
"""
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
are carried; no geometric simplifications. Domain-agnostic used by both
`ContinuumKernel` (solid mechanics) and `HeatKernel` (heat conduction).
Bulk three-dimensional model: no in-plane or axisymmetric reduction. All
independent tensor components are retained at the quadrature point (six for
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
@@ -60,7 +63,7 @@ Used as a type tag inside `ContinuumKernel{Theory, Material, Field}`.
# Examples
```julia
ContinuumFormulation{FullThreeD}()
ContinuumFormulation{ThreeDimensional}()
ContinuumFormulation{PlaneStress}()
ContinuumFormulation{PlaneStrain}()
ContinuumFormulation{Axisymmetric}()
+79 -24
View File
@@ -1,5 +1,5 @@
# This file is a part of JuliaFEM.
# License is MIT: see https://github.com/JuliaFEM/JuliaFEM.jl/blob/master/LICENSE.md
# SPDX-FileCopyrightText: 2015-2026 Jukka Aho
# SPDX-License-Identifier: MIT
"""
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.N_data[ip, k]` basis values
- `geometry_cache.∇N_data[ip, k]` physical gradients `∇N`
- `geometry_cache.detJ_w[ip]` `det(J) * w` for integration
- `geometry_cache.∇N_data[ip, k]` physical gradients `∇N` as `Vec{3}` (tangent to the
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
integration points from `element_cache` and writes back into the
pre-allocated `geometry_cache` arrays. For each integration point the
Jacobian `J = X ⊗ ∇_ξ N` is built on the fly, then physical gradients
are obtained via `J^{-T} · ∇_ξ N`.
pre-allocated `geometry_cache` arrays.
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!(
geometry_cache::GeometryCache,
@@ -35,8 +45,6 @@ are obtained via `J^{-T} · ∇_ξ N`.
conn = mesh.connectivity[elem_id]
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
node = conn[i]
geometry_cache.X[i] = mesh.nodes[node]
@@ -44,28 +52,75 @@ are obtained via `J^{-T} · ∇_ξ N`.
ips = element_cache.ips
nips = length(ips)
D = dim(element_cache.topology)
@inbounds for ip_idx in 1:nips
ip = ips[ip_idx]
ξ = ip.coords
if D == 3
@inbounds for ip_idx in 1:nips
ip = ips[ip_idx]
ξ = ip.coords
N_vals = get_basis_functions( element_cache.topology, element_cache.basis, ξ)
dN_dξ = get_basis_derivatives(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, ξ)
# Jacobian J = X ⊗ ∇_ξ N
J = geometry_cache.X[1] dN_dξ[1]
for i in 2:nnodes
J += geometry_cache.X[i] dN_dξ[i]
J = geometry_cache.X[1] dN_dξ[1]
for i in 2:nnodes
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
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
geometry_cache.N_data[ip_idx, k] = N_vals[k]
geometry_cache.∇N_data[ip_idx, k] = J_inv_T dN_dξ[k]
v1 = zero(Vec{3,F})
v2 = zero(Vec{3,F})
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
geometry_cache.detJ_w[ip_idx] = det(J) * ip.weight
else
throw(ArgumentError(
"update_geometry_cache!: topology spatial dimension $D is not supported " *
"(expected 2 or 3 for continuum assembly)",
))
end
return nothing
+55 -26
View File
@@ -1,5 +1,5 @@
# This file is a part of JuliaFEM.
# License is MIT: see https://github.com/JuliaFEM/JuliaFEM.jl/blob/master/LICENSE.md
# SPDX-FileCopyrightText: 2015-2026 Jukka Aho
# SPDX-License-Identifier: MIT
"""
Material cache update functions for continuum elements.
@@ -10,6 +10,7 @@ Computes stress, tangent modulus, and internal state at integration points.
using Tensors
using ..JuliaFEM: GlobalMaterialCache, get_old_state, set_state!
using ..JuliaFEM: continuum_kinematics, SmallStrainKinematics, GreenLagrangeKinematics
using ..JuliaFEM: material_behavior, StatelessStrainDependent
# ============================================================================
# GLOBAL MATERIAL CACHE — behavior-dispatched implementations
@@ -126,6 +127,55 @@ end
return nothing
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.
@inline function update_material_cache!(
material_workspace::AssemblyMaterialWorkspace,
@@ -137,30 +187,9 @@ end
elem_id::Int,
Δt::Float64,
)
nips = length(element_cache.ips)
nnodes = length(geometry_cache.X)
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
return update_material_cache_stateless_strain!(
material_workspace, geometry_cache, material, element_cache, Δt,
)
end
# StatefulStrainDependent — read old state from `global_cache`, compute the