mirror of
https://github.com/JuliaFEM/JuliaFEM.jl.git
synced 2026-08-06 04:21:33 +00:00
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.
This commit is contained in:
@@ -0,0 +1,230 @@
|
||||
---
|
||||
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.
|
||||
Reference in New Issue
Block a user