Files
JuliaFEM.jl/docs/book/adr-005-element-integration-point-indices.md
T
Jukka Aho e2917cdba8 docs: Add ADR-005 on integration point indices architecture
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.
2025-11-12 00:49:17 +02:00

231 lines
7.2 KiB
Markdown
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.
---
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.