* Heat transfer and thermal stress: thermal materials, temperature BCs, heat loads, heat transfer and coupled temperature–displacement steps, solve.heat
Model: Material.thermal (conductivity, specific heat, expansion, isotropic or
per axis; reference temperature), BoundaryCondition::Temperature (dof 11),
loads SurfaceFlux, Convection, HeatSource and Temperature (a field in static
steps), procedures HeatTransfer { transient (θ-method) } and
CoupledTemperatureDisplacement; checks for missing conductivity/specific
heat/expansion. fem: heat conduction kernels (conductivity, capacity,
source, face flux/film, heat flux) on the DofLayout (one T per node), thermal
strain load and stress D(ε − ε_th) for every solid formulation incl.
incompatible modes. Solver: steady and transient heat transfer, thermal loads
in static steps, coupled steps (exact one-way coupling). Engine: solve.heat,
temperature/heat-flux result fields, probes. Decks refuse heat transfer by
name. Analytic verification: slab, source+convection/flux, thick cylinder,
erfc semi-infinite solid, free/constrained expansion, bimetal strip, coupled
step.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01NmAttHhUZTokgPeEJia8sm
* thermal: strains in 2D elements and explicit dynamics
Plane::thermal_load and stresses_with for plane stress (εzz free), plane
strain (σzz = −D₃·ε_th) and axisymmetric elements, incompatible modes
included; the solver's 2D kernels take them. Explicit dynamics turns a
step's temperature loads into a per-element thermal strain rate (reached
linearly over the step) subtracted from each strain increment, so it feeds
J2 and Johnson–Cook updates. Verified: free expansion in every 2D theory
and element, a biaxially held plate, an explicit free bar (ΔL = αΔT L,
stress-free) and a clamped bar at −EαΔT, capped at the yield stress with
perfect plasticity.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01NmAttHhUZTokgPeEJia8sm
* clippy
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01NmAttHhUZTokgPeEJia8sm
* thermal: trusses take the axial thermal strain; keep the roadmap's M7 line
Truss::thermal_load and stresses_with project the thermal strain on the
member's tangent (Σ αₖ tₖ² ΔT); the static solver applies them to rod2/rod3
(#38). Explicit trusses take none yet and a warning says so.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01NmAttHhUZTokgPeEJia8sm
---------
Co-authored-by: jyviko <[email protected]>
Co-authored-by: Claude Opus 5.5 <[email protected]>
9.0 KiB
FEM foundation: degrees of freedom and shape functions
What every element in crates/fem and every solver builds on. Code: crates/fem/src/dof.rs,
crates/fem/src/basis.rs, crates/fem/src/jet.rs.
Degrees of freedom
Every unknown is a pair (node, DofKind):
DofKind |
Meaning | Model / deck number |
|---|---|---|
Ux, Uy, Uz |
translations along X, Y, Z | 1, 2, 3 |
Rx, Ry, Rz |
rotations about X, Y, Z | 4, 5, 6 |
P |
pressure (mixed u–p elements) | 8 |
T |
temperature | 11 |
The enum order (Ux, Uy, Uz, Rx, Ry, Rz, T, P) is the numbering order within a node.
DofKind::from_number / number convert to the 1-based numbers boundary conditions use;
from_direction / direction to the structural directions 0–5.
Elements declare their unknowns with ElementDofs { node: DofSet, internal: Internal }:
the kinds every node of the element carries (DofSet::TRANSLATIONS for solids, PLANE for
2D elements, STRUCTURAL for shells and beams, TEMPERATURE for heat transfer, translations
plus PRESSURE for continuous-pressure mixed elements) and the internal unknowns the element
condenses out itself (incompatible_modes, discontinuous pressure), which never reach the
global system. An element's matrices are ordered node by node, its node kinds in DofKind
order within a node (a shell's 24 × 24 stiffness: node 1 Ux Uy Uz Rx Ry Rz, node 2 …). Solid
elements: Formulation::dofs() / Solid::dofs().
Numbering (DofLayout): collect what every node needs with DofLayout::builder()
(element(labels, dofs), node(label, kinds) for constraints and boundary conditions that
need a kind no element gives, point(label, kinds) for reference points), then build(). A
node carries the union of what was asked for. Unknowns are numbered mesh nodes in label order,
each node's kinds in DofKind order, then the points in the order added; a slot's unknowns are
contiguous, so a node pair is one dense block in the assembled matrix. The same model always
gives the same numbering.
Lookups: node(label) / slot(label) (a slot is a position: mesh nodes 0..nodes(), points
point_slot(k)), dof(slot, kind), range(slot), kinds(slot), locate(dof) -> (slot, kind), element_dofs(slots, kinds), describe(dof), stride() (uniform layouts).
Trusses declare modelcraft_fem::truss::DOFS (translations, on Basis::Line2/Line3).
Point masses, springs and dashpots are not mesh elements: the implicit solver asks the layout
for what each needs at its nodes (modelcraft_solver::discrete::needs: translations for a mass
or a translational spring, rotations for an inertia or a torsional spring; a reference point
they use is a point with six) and finds its unknowns with discrete::dof_of.
Solvers: modelcraft_solver::system::Mesh::layout numbers the implicit system (mesh nodes,
then six per coupled reference point); assembly (sparse::add_block(k, layout, slots, kinds, data), add_scalar, add_diagonal) works for any mix of kinds per node, so a solid sharing
nodes with a shell adds its translation block into the shell nodes' six-unknown blocks.
Boundary conditions and loads address (node, kind): a boundary condition holds the kinds a node
carries, and a moment needs rotations. The explicit solver has solid nodes only and checks that
its layout is the uniform node-major one (NODE_DOFS = 3).
Shape functions
Basis is every family's interpolation:
| Family | Reference element | Nodes |
|---|---|---|
Line2, Line3 |
ξ ∈ [−1, 1] | ends −1, 1; line3 adds the middle |
Tri3, Tri6 |
ξ, η ≥ 0, ξ + η ≤ 1 | corners (0,0), (1,0), (0,1); mid-sides of 1-2, 2-3, 3-1 |
Quad4, Quad8, Quad9 |
[−1, 1]² | corners counter-clockwise from (−1,−1); mid-sides of 1-2, 2-3, 3-4, 4-1; centre |
Tet4, Tet10 |
unit tetrahedron | corners (0,0,0), (1,0,0), (0,1,0), (0,0,1); mid-sides of 1-2, 2-3, 3-1, 1-4, 2-4, 3-4 |
Wedge6, Wedge15 |
triangle × ζ ∈ [−1, 1] | bottom 1-3, top 4-6; mid-sides of the bottom edges, top edges, vertical edges |
Pyramid5 |
base [−1, 1]² at ζ = 0, apex (0, 0, 1) | base corners as quad4, then the apex |
Hex8, Hex20, Hex27 |
[−1, 1]³ | corners 1-4 at ζ = −1, 5-8 above; hex20 mid-sides (bottom, top, vertical edges); hex27 face centres in ElementKind::Hex8.faces() order, then the centre |
Node orders are the model's (ElementKind::edges() / faces() are the single source):
corners, mid-side nodes in edge order, face nodes, body nodes. Basis::of(ElementKind) maps a
model element; nodes() gives natural node coordinates; dim(), count(), corners(),
degree() (the complete polynomial degree reproduced: 1 for linear families including the
rational pyramid, 2 for the quadratic ones).
Evaluation at a natural point xi: [f64; 3] (unused coordinates are zero):
eval(xi) -> Shape: valuesnand first derivativesdn[i] = ∂Nᵢ/∂(ξ, η, ζ). tet4, tet10, wedge6, hex8, hex20 use the hand-written functions ofshape.rs(the solid kernels' path).eval2(xi) -> Shape2: addsd2n[i][a][b] = ∂²Nᵢ/∂ξₐ∂ξ_b, from second-order forward-mode automatic differentiation of the same formulas (jet::Jet): exact to round-off.gradients(&shape, xyz) -> Gradients: physicaldx[i] = ∇ₓNᵢ = J⁻¹ ∇_ξNᵢanddet J, with J_ab = ∂x_b/∂ξₐ; an inverted or collapsed element isError::Degenerate.hessians(&shape2, xyz) -> Hessians: physicald2x[i]= J⁻¹ (H_ξ − Σ_b (∂N/∂x_b) X_b″) J⁻ᵀ with X_b″ = Σᵢ x_{i,b} H_ξ(Nᵢ), the map's second derivative (T. J. R. Hughes, The Finite Element Method, 2000, §3.9, §4.2), andlaplacian(i). Without the X″ term a field linear in x would have a nonzero Hessian on a curved element.
A family of dimension d maps onto the first d physical coordinates (plane elements in x–y, lines along x). Elements embedded in a higher dimension (shells, beams, trusses in 3D) pass node coordinates in their own local frame.
Verification (basis::tests, dof::tests, jet::tests)
For every family: Kronecker delta at the nodes; partition of unity (Σ N = 1, Σ ∇N = 0,
Σ H = 0); exact reproduction of every monomial up to degree() with its gradient and Hessian
in natural coordinates; first and second derivatives against central differences;
on an affine map, physical gradients, Hessians and Laplacians exact for a complete polynomial
of the family's degree; on a curved map, linear fields exact with zero Hessian, and the
physical Hessian of a quadratic interpolant consistent with finite differences of its physical
gradient. The solver and explicit migration to DofLayout was checked bit for bit: every
result of the solver, explicit, engine and io test suites hashes identically before and after.
Heat transfer and thermal strain
A heat conduction element carries one temperature per node (heat::HEAT_DOFS, kind T);
heat::Conduction gives the conductivity ∫ ∇Nᵀ k ∇N dV (full rule), the capacity ∫ ρc N Nᵀ dV
and a source ∫ N Q dV (mass rule), and the heat flux −k∇T at the integration points;
heat::face_weights and face_film integrate surface flux and convection on tri3/tri6/quad4/quad8
faces. The solver (modelcraft_solver::heat) numbers the temperatures with
DofLayout::uniform(…, DofSet::TEMPERATURE) and assembles with the same sparse::add_block.
Thermal strain ε_th = α (T − T_ref) along the material axes enters a solid as the load
Solid::thermal_load(d, &ThermalStrain { alpha, delta }) = ∫ Bᵀ D ε_th dV and its stress as
Solid::stresses_with(d, u, Some(&thermal)) = D (ε − ε_th); delta is the nodal T − T_ref.
2D elements have the same pair (Plane::thermal_load, Plane::stresses_with): plane stress
leaves εzz free (σzz = 0), plane strain holds it (σzz = −D₃·ε_th), axisymmetric elements strain
the hoop direction too. Trusses (rod2, rod3) take the axial part Σ αₖ tₖ² ΔT along their tangent (Truss::thermal_load,
Truss::stresses_with). Shells and beams will provide the same two functions for their strains;
springs, dashpots and point masses have no thermal strain.
Explicit dynamics takes a step's temperature loads as a thermal strain rate per element, α ΔT / end time (the change reached linearly over the step, like a prescribed displacement, so there is no thermal shock), subtracted from each increment's strain increment before the constitutive update, so it feeds J2 and Johnson–Cook plasticity alike; isotropic expansion only (the rate is the same in any frame). Explicit trusses take no thermal strain yet (a warning says so).
Model side: Material::thermal (conductivity, specific_heat, expansion, each isotropic
or per axis, and reference_temperature), BoundaryCondition::Temperature, loads
SurfaceFlux, Convection, HeatSource (heat transfer) and Temperature (a temperature field
in a static step), procedures HeatTransfer { transient } and CoupledTemperatureDisplacement.
Commands: solve.heat, and solve.static for coupled steps. The deck writers refuse heat
transfer by name for now.