Files
JuliaFEM.jl/src/materials/material_cache.jl
T
Jukka Aho 56d4a5ae70 feat(materials): add global material cache for state variables
New 362-line material cache system:
- GlobalMaterialCache{StateType}: stores state variables at all IPs/elements
- material_state_type(): infer state NamedTuple type from material traits
- create_global_material_cache(): factory function with automatic type inference
- create_zero_state(): zero-initialize state NamedTuples
- Supports stateless (empty NamedTuple) and stateful materials
- Type-stable NamedTuple-based storage
- Zero-allocation state updates (functional style)
- Already integrated in JuliaFEM.jl (line 437)

Provides persistent state storage for time-stepping and material state management.
2025-12-15 06:16:16 +02:00

363 lines
11 KiB
Julia
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# This file is a part of JuliaFEM.
# License is MIT: see https://github.com/JuliaFEM/JuliaFEM.jl/blob/master/LICENSE.md
"""
Material cache for storing state variables at integration points.
Uses the compositional state variable trait system to automatically construct
caches based on material requirements.
# Design Philosophy
1. **Automatic construction**: Cache structure inferred from material traits
2. **Type-stable**: NamedTuple-based storage with compile-time known types
3. **Zero-allocation**: Immutable state updates (functional style)
4. **Compositional**: State variables are independent, combinable building blocks
# Example
```julia
# For LinearElastic (stateless)
material = LinearElastic(E=210e9, ν=0.3)
cache = create_global_material_cache(material, n_ips=8, n_elems=100)
# → GlobalMaterialCache{NamedTuple{(),Tuple{}}} with empty states
# For PerfectPlasticity (stateful)
material = PerfectPlasticity(E=210e9, ν=0.3, σ_y=250e6, H=1e9)
cache = create_global_material_cache(material, n_ips=8, n_elems=100)
# → GlobalMaterialCache{NamedTuple{(:ε_p, :α, :κ), Tuple{...}}} with state variables
```
"""
using Tensors
# ============================================================================
# MATERIAL CACHE STRUCTURE
# ============================================================================
"""
GlobalMaterialCache{StateType}
Global material state cache for all integration points across all elements.
Stores material state variables using the compositional state variable system.
Used for time-stepping and persistent state storage.
# Type Parameters
- `StateType`: NamedTuple type for state variables (inferred from material)
# Fields
- `states::Matrix{StateType}`: State at each IP and element [nips, nelems]
- `states_old::Matrix{StateType}`: State from previous time step [nips, nelems]
# Design
The cache uses NamedTuples to store state variables, where:
- Keys are state variable symbols (e.g., `:ε_p`, `:α`, `:κ`)
- Values are concrete types (e.g., `SymmetricTensor{2,3,Float64,6}`, `Float64`)
For stateless materials, `StateType = NamedTuple{(),Tuple{}}` (empty).
# Example
```julia
# Stateful material (plasticity)
StateType = NamedTuple{(:ε_p, :α, :κ), Tuple{SymmetricTensor{2,3,Float64,6}, SymmetricTensor{2,3,Float64,6}, Float64}}
cache = GlobalMaterialCache{StateType}(n_ips, n_elems)
cache.states[q, elem_id] # → (ε_p=..., α=..., κ=...)
# Stateless material (linear elastic)
StateType = NamedTuple{(),Tuple{}}
cache = GlobalMaterialCache{StateType}(n_ips, n_elems)
cache.states[q, elem_id] # → NamedTuple()
```
"""
struct GlobalMaterialCache{StateType<:NamedTuple}
states::Matrix{StateType}
states_old::Matrix{StateType}
function GlobalMaterialCache{StateType}(n_ips::Int, n_elems::Int) where {StateType<:NamedTuple}
# Initialize with zero/default states
zero_state = create_zero_state(StateType)
states = [zero_state for _ in 1:n_ips, _ in 1:n_elems]
states_old = [zero_state for _ in 1:n_ips, _ in 1:n_elems]
new{StateType}(states, states_old)
end
end
"""
create_zero_state(::Type{StateType}) where {StateType<:NamedTuple}
Create a zero-initialized state of the given NamedTuple type.
# Example
```julia
StateType = NamedTuple{(:ε_p, :α, :κ), Tuple{SymmetricTensor{2,3,Float64,6}, SymmetricTensor{2,3,Float64,6}, Float64}}
zero_state = create_zero_state(StateType)
# → (ε_p=zero(SymmetricTensor{2,3}), α=zero(SymmetricTensor{2,3}), κ=0.0)
```
"""
function create_zero_state(::Type{StateType}) where {StateType<:NamedTuple}
# Get field names and types
field_names = fieldnames(StateType)
field_types = [fieldtype(StateType, name) for name in field_names]
if isempty(field_names)
return NamedTuple()
else
# Create zero value for each field
zero_values = [zero(T) for T in field_types]
return NamedTuple{field_names}(zero_values)
end
end
# ============================================================================
# CACHE CONSTRUCTION FROM MATERIAL TRAITS
# ============================================================================
"""
material_state_type(material::AbstractMaterial) -> Type{<:NamedTuple}
Infer state NamedTuple type from material's required state variables.
Uses the compositional trait system:
1. Query `required_state_variables(material)` → tuple of state variable types
2. For each state variable, query `state_variable_type(var)` → concrete type
3. For each state variable, query `default_symbol(var)` → symbol name
4. Construct NamedTuple type from symbols and concrete types
# Example
```julia
# LinearElastic (stateless)
material = LinearElastic(E=210e9, ν=0.3)
StateType = material_state_type(material)
# → NamedTuple{(),Tuple{}}
# PerfectPlasticity (stateful)
material = PerfectPlasticity(E=210e9, ν=0.3, σ_y=250e6, H=1e9)
StateType = material_state_type(material)
# → NamedTuple{(:ε_p, :α, :κ), Tuple{SymmetricTensor{2,3,Float64,6}, SymmetricTensor{2,3,Float64,6}, Float64}}
```
"""
function material_state_type(material::AbstractMaterial)
# Query required state variables from material instance
vars = required_state_variables(material)
if isempty(vars)
# Stateless material
return NamedTuple{(),Tuple{}}
else
# Stateful material - build NamedTuple type
symbols = tuple([default_symbol(var) for var in vars]...)
types = tuple([state_variable_type(var) for var in vars]...)
return NamedTuple{symbols, Tuple{types...}}
end
end
"""
create_global_material_cache(material::AbstractMaterial; n_ips::Int, n_elems::Int)
Create global material cache for the given material, integration points, and elements.
Automatically infers the correct cache structure from material traits.
# Arguments
- `material`: Material model instance
- `n_ips`: Number of integration points per element
- `n_elems`: Number of elements
# Returns
`GlobalMaterialCache{StateType}` where `StateType` is inferred from material
# Example
```julia
# Stateless material
mat = LinearElastic(E=210e9, ν=0.3)
cache = create_global_material_cache(mat, n_ips=8, n_elems=100)
# → GlobalMaterialCache{NamedTuple{(),Tuple{}}}
# Stateful material
mat = PerfectPlasticity(E=210e9, ν=0.3, σ_y=250e6, H=1e9)
cache = create_global_material_cache(mat, n_ips=8, n_elems=100)
# → GlobalMaterialCache{NamedTuple{(:ε_p, :α, :κ), Tuple{...}}}
```
"""
function create_global_material_cache(material::AbstractMaterial; n_ips::Int, n_elems::Int)
StateType = material_state_type(material)
return GlobalMaterialCache{StateType}(n_ips, n_elems)
end
# ============================================================================
# CACHE ACCESS AND UPDATE FUNCTIONS
# ============================================================================
"""
get_state(cache::GlobalMaterialCache, ip::Int, elem_id::Int) -> NamedTuple
Get current state at integration point for element.
# Arguments
- `cache`: Global material cache
- `ip`: Integration point index (1-based)
- `elem_id`: Element ID (1-based)
# Returns
NamedTuple of state variables (empty for stateless materials)
"""
@inline function get_state(cache::GlobalMaterialCache, ip::Int, elem_id::Int)
return cache.states[ip, elem_id]
end
"""
get_old_state(cache::GlobalMaterialCache, ip::Int, elem_id::Int) -> NamedTuple
Get state from previous time step at integration point for element.
# Arguments
- `cache`: Global material cache
- `ip`: Integration point index (1-based)
- `elem_id`: Element ID (1-based)
# Returns
NamedTuple of state variables from previous step
"""
@inline function get_old_state(cache::GlobalMaterialCache, ip::Int, elem_id::Int)
@inbounds return cache.states_old[ip, elem_id]
end
"""
set_state!(cache::GlobalMaterialCache, ip::Int, elem_id::Int, state::NamedTuple)
Update current state at integration point for element (in-place).
# Arguments
- `cache`: Global material cache
- `ip`: Integration point index (1-based)
- `elem_id`: Element ID (1-based)
- `state`: New state NamedTuple
# Note
This is an in-place operation.
"""
@inline function set_state!(cache::GlobalMaterialCache, ip::Int, elem_id::Int, state::NamedTuple)
@inbounds cache.states[ip, elem_id] = state
return nothing
end
"""
update_cache!(cache::GlobalMaterialCache)
Copy current states to states_old (for time stepping).
Call this at the beginning of each time/load step to save the converged
state from the previous step.
# Example
```julia
# At the start of a new time step
update_cache!(cache)
# Now solve for new states
for elem_id in 1:n_elems
for ip in 1:n_ips
state_old = get_old_state(cache, ip, elem_id)
# ... solve for state_new ...
set_state!(cache, ip, elem_id, state_new)
end
end
```
"""
function update_cache!(cache::GlobalMaterialCache)
cache.states_old .= cache.states
return nothing
end
"""
reset_cache!(cache::GlobalMaterialCache)
Reset all states to zero (for restarting analysis).
# Example
```julia
reset_cache!(cache)
# All states now zero-initialized
```
"""
function reset_cache!(cache::GlobalMaterialCache{StateType}) where {StateType}
zero_state = create_zero_state(StateType)
fill!(cache.states, zero_state)
fill!(cache.states_old, zero_state)
return nothing
end
# ============================================================================
# HELPER FUNCTIONS FOR STATE VARIABLE ACCESS
# ============================================================================
"""
get_state_variable(state::NamedTuple, var_type::Type{<:AbstractStateVariable})
Extract a specific state variable from a state NamedTuple.
# Arguments
- `state`: State NamedTuple (e.g., from `get_state(cache, ip)`)
- `var_type`: State variable type (e.g., `PlasticStrain`)
# Returns
Value of the requested state variable
# Example
```julia
state = get_state(cache, 1) # → (ε_p=..., α=..., κ=...)
ε_p = get_state_variable(state, PlasticStrain)
α = get_state_variable(state, Backstress)
κ = get_state_variable(state, EquivalentPlasticStrain)
```
"""
@generated function get_state_variable(state::StateType, var_type::Type{VarType}) where {StateType<:NamedTuple, VarType<:AbstractStateVariable}
# Get the symbol for this state variable
sym = default_symbol(VarType)
# Check if state has this field
if hasfield(StateType, sym)
return :(state.$sym)
else
error("State type $StateType does not have field $sym for variable $VarType")
end
end
"""
set_state_variable(state::NamedTuple, var_type::Type{<:AbstractStateVariable}, value)
Create new state NamedTuple with updated variable value.
Returns a new NamedTuple (immutable update).
# Arguments
- `state`: Current state NamedTuple
- `var_type`: State variable type to update
- `value`: New value for the state variable
# Returns
New state NamedTuple with updated value
# Example
```julia
state = get_state(cache, 1)
new_state = set_state_variable(state, PlasticStrain, new_ε_p)
set_state!(cache, 1, new_state)
```
"""
@generated function set_state_variable(state::StateType, var_type::Type{VarType}, value) where {StateType<:NamedTuple, VarType<:AbstractStateVariable}
# Get the symbol for this state variable
sym = default_symbol(VarType)
# Check if state has this field
if hasfield(StateType, sym)
return :(merge(state, NamedTuple{($(QuoteNode(sym)),)}((value,))))
else
error("State type $StateType does not have field $sym for variable $VarType")
end
end