mirror of
https://github.com/JuliaFEM/JuliaFEM.jl.git
synced 2026-09-28 20:46:58 +00:00
e2917cdba8
Document decision to store integration point indices instead of data in Element struct. Key rationale: Elements should store relationships (indices), not data, for memory efficiency and consistency with node connectivity pattern. Aligns with nodal assembly approach and GPU-friendly architecture.
231 lines
7.2 KiB
Markdown
231 lines
7.2 KiB
Markdown
---
|
||
title: "ADR-005: Integration Point Indices Instead of Data"
|
||
date: 2025-11-11
|
||
author: "Jukka Aho"
|
||
status: "Accepted"
|
||
tags: ["architecture", "elements", "integration-points", "material-state"]
|
||
---
|
||
|
||
## Status
|
||
|
||
**Accepted** (November 11, 2025)
|
||
|
||
## Context
|
||
|
||
Current `Element` struct stores integration points as data:
|
||
|
||
```julia
|
||
struct Element{N,NIP,F,B} <: AbstractElement{F,B}
|
||
id::UInt
|
||
connectivity::NTuple{N,UInt} # Node IDs
|
||
integration_points::NTuple{NIP,IP} # ← Integration point DATA
|
||
fields::F
|
||
basis::B
|
||
end
|
||
```
|
||
|
||
**Problems with this approach:**
|
||
|
||
1. **Redundancy**: All elements of the same type have identical integration points
|
||
- Seg2 elements all use same 2-point Gauss quadrature
|
||
- Tri3 elements all use same 3-point triangle quadrature
|
||
- Storing this in every element wastes memory
|
||
|
||
2. **Inconsistent with connectivity pattern**: Connectivity stores **indices** (node IDs), not node data
|
||
- We don't store node coordinates in element
|
||
- We store node **IDs** and look up coordinates elsewhere
|
||
|
||
3. **Inconsistent with material state design**: Material state is stored separately
|
||
- Material state per integration point stored in global vector
|
||
- Element needs **indices** to find its material state
|
||
- Current design has no way to link element to material state
|
||
|
||
4. **Assembly workflow needs indices**:
|
||
|
||
```julia
|
||
for element in elements
|
||
node_ids = element.connectivity # Get node indices
|
||
node_coords = X[node_ids] # Look up coordinates
|
||
|
||
# What about material state?
|
||
# Need: ip_ids = element.ip_indices # Get IP indices
|
||
# mat_state = material_states[ip_ids] # Look up state
|
||
```
|
||
|
||
## Decision
|
||
|
||
**Store integration point INDICES instead of DATA in elements.**
|
||
|
||
### New Element Structure
|
||
|
||
```julia
|
||
struct Element{N,NIP,F,B} <: AbstractElement{F,B}
|
||
id::UInt
|
||
connectivity::NTuple{N,UInt} # Node IDs (indices)
|
||
ip_indices::NTuple{NIP,UInt} # ← Integration point IDs (indices)
|
||
fields::F
|
||
basis::B
|
||
end
|
||
```
|
||
|
||
### Global Integration Point Registry
|
||
|
||
Integration points are computed once per topology+order combination:
|
||
|
||
```julia
|
||
# Precompute integration points for each topology type
|
||
const INTEGRATION_POINTS = Dict{Tuple{Type,Int}, Vector{Tuple{Float64,Vec}}}(
|
||
(Triangle, 1) => get_gauss_points!(Triangle, Gauss{1}),
|
||
(Triangle, 2) => get_gauss_points!(Triangle, Gauss{2}),
|
||
(Quadrilateral, 2) => get_gauss_points!(Quadrilateral, Gauss{2}),
|
||
# ... etc
|
||
)
|
||
|
||
# Material state stored separately (one entry per integration point)
|
||
struct MaterialState
|
||
stress::SymmetricTensor{2,3}
|
||
strain::SymmetricTensor{2,3}
|
||
plastic_strain::SymmetricTensor{2,3}
|
||
# ... etc
|
||
end
|
||
|
||
material_states::Vector{MaterialState} = [MaterialState(...) for _ in 1:n_total_ips]
|
||
```
|
||
|
||
### Assembly Workflow
|
||
|
||
```julia
|
||
# Element creation (during mesh setup)
|
||
element_id = 1
|
||
node_ids = (1, 2, 3) # Triangle nodes
|
||
ip_start = 1000 # First IP for this element
|
||
ip_indices = (ip_start, ip_start+1, ip_start+2) # 3 IPs for Tri3
|
||
element = Element(Triangle, node_ids, ip_indices=ip_indices)
|
||
|
||
# Assembly loop
|
||
for element in elements
|
||
# Get node data
|
||
node_ids = element.connectivity
|
||
X = node_coords[node_ids] # Coordinates
|
||
u = displacements[node_ids] # Displacements
|
||
|
||
# Get integration points (from global registry)
|
||
topology = typeof(element.basis).parameters[1] # Triangle
|
||
order = typeof(element.basis).parameters[2] # 1
|
||
ips = INTEGRATION_POINTS[(topology, order)]
|
||
|
||
# Get material state (from global vector)
|
||
ip_ids = element.ip_indices
|
||
mat_states = material_states[ip_ids]
|
||
|
||
# Assembly loop over integration points
|
||
for (i, (w, ξ)) in enumerate(ips)
|
||
mat_state = mat_states[i]
|
||
|
||
# Compute strain from displacements
|
||
ε = compute_strain(element, u, ξ)
|
||
|
||
# Compute stress from material state
|
||
σ = compute_stress(mat_state, ε)
|
||
|
||
# Assemble...
|
||
end
|
||
end
|
||
```
|
||
|
||
## Consequences
|
||
|
||
### Positive
|
||
|
||
1. **Memory efficiency**:
|
||
- Old: 1M Tet10 elements × 4 IPs × 32 bytes = 128 MB for IP data
|
||
- New: 1 global table × 4 IPs × 32 bytes = 128 bytes (1000× reduction!)
|
||
|
||
2. **Consistency**: Same pattern for all element data
|
||
- Nodes: Store indices, look up data
|
||
- Integration points: Store indices, look up data
|
||
- Material state: Store indices, look up data
|
||
|
||
3. **Clear data ownership**:
|
||
- Node coordinates: Owned by mesh/node array
|
||
- Integration points: Owned by global registry
|
||
- Material state: Owned by material state vector
|
||
- Element: Only owns **indices** to these
|
||
|
||
4. **GPU-friendly**:
|
||
- All material states in one contiguous array (easy to transfer)
|
||
- All integration point data in global tables (transfer once)
|
||
- Elements are small (just indices)
|
||
|
||
5. **Natural partitioning**:
|
||
- Element ownership → node ownership (already clear)
|
||
- Integration point ownership → element ownership (via indices)
|
||
- Clear for domain decomposition / MPI
|
||
|
||
### Negative
|
||
|
||
1. **API change**: Existing code expects `element.integration_points`
|
||
- **Mitigation**: Provide compatibility function `get_integration_points(element)`
|
||
|
||
2. **Index management**: Need to assign IP indices during mesh setup
|
||
- **Mitigation**: Automatic during element creation
|
||
|
||
3. **Lookup overhead**: Extra indirection to get IP data
|
||
- **Mitigation**: Negligible (one array lookup), data still cache-friendly
|
||
|
||
## Implementation Plan
|
||
|
||
### Phase 1: Add ip_indices field (keep integration_points for compatibility)
|
||
|
||
```julia
|
||
struct Element{N,NIP,F,B}
|
||
id::UInt
|
||
connectivity::NTuple{N,UInt}
|
||
integration_points::NTuple{NIP,IP} # DEPRECATED, keep temporarily
|
||
ip_indices::NTuple{NIP,UInt} # NEW
|
||
fields::F
|
||
basis::B
|
||
end
|
||
```
|
||
|
||
### Phase 2: Update assembly code to use indices
|
||
|
||
- Update `assemble!` functions to use `element.ip_indices`
|
||
- Look up IP data from global registry
|
||
- Look up material state from global vector
|
||
|
||
### Phase 3: Remove integration_points field
|
||
|
||
```julia
|
||
struct Element{N,NIP,F,B}
|
||
id::UInt
|
||
connectivity::NTuple{N,UInt}
|
||
ip_indices::NTuple{NIP,UInt} # Only indices remain
|
||
fields::F
|
||
basis::B
|
||
end
|
||
```
|
||
|
||
## Rationale
|
||
|
||
**Core principle**: Elements should store **relationships** (indices), not **data**.
|
||
|
||
Just as we don't store node coordinates in elements, we shouldn't store integration point data. The element's job is to define **which** nodes and **which** integration points participate in its stiffness matrix, not to own their data.
|
||
|
||
This aligns perfectly with the nodal assembly approach documented in `docs/src/book/multigpu_nodal_assembly.md`:
|
||
- Nodes own displacement DOFs
|
||
- Integration points own material state
|
||
- Elements define relationships between them
|
||
|
||
## References
|
||
|
||
- **Golden Standard**: `docs/src/book/multigpu_nodal_assembly.md` - Nodal assembly architecture
|
||
- **ADR-004**: Integration Points API Design
|
||
- **Material State Design**: `llm/FIELDS_DESIGN.md` - Field system redesign
|
||
|
||
## Notes
|
||
|
||
This decision was made during test migration (November 11, 2025) when updating Dirichlet tests to use immutable Element API. The question arose: "Why does each element store integration points when they're identical for all elements of the same type?"
|
||
|
||
The answer: **They shouldn't.** This ADR documents the correct architecture.
|