From febcffefd6f0cdad4b5d837db8e94e9d26e5fc21 Mon Sep 17 00:00:00 2001 From: jyviko <2501648+jyviko@users.noreply.github.com> Date: Thu, 15 Oct 2026 08:25:50 -0400 Subject: [PATCH] verification: series-resistance heat flow across a tie (#358) Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01UhhZMBTviDQxZvwnyP89fo --- docs/fem.md | 28 ++++- docs/oracle.md | 8 +- tests/oracle/tie-heat-series/expected.json | 122 +++++++++++++++++++++ tests/oracle/tie-heat-series/fixture.json | 25 +++++ tests/oracle/tie-heat-series/recipe.json | 19 ++++ 5 files changed, 195 insertions(+), 7 deletions(-) create mode 100644 tests/oracle/tie-heat-series/expected.json create mode 100644 tests/oracle/tie-heat-series/fixture.json create mode 100644 tests/oracle/tie-heat-series/recipe.json diff --git a/docs/fem.md b/docs/fem.md index b447273..f3d2110 100644 --- a/docs/fem.md +++ b/docs/fem.md @@ -109,7 +109,9 @@ A heat conduction element carries one temperature per node (`heat::HEAT_DOFS`, k 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 (`fieldcraft_solver::heat`) numbers the temperatures with -`DofLayout::uniform(…, DofSet::TEMPERATURE)` and assembles with the same `sparse::add_block`. +`DofLayout::uniform(…, DofSet::TEMPERATURE)`, assembles with the same `sparse::add_block`, and +eliminates prescribed temperatures and ties (see Ties) with the structural solver's +`constraints::Reduction`. 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 @@ -586,12 +588,26 @@ frequency, buckling and nonlinear steps (small-displacement constraints). Dividi keeps rigid motions exact at a surface's edge where a face is only partly covered. Main faces count when the secondary face overlaps them along its normal with a mean gap within the position tolerance (default half the main element's thickness); uncovered secondary nodes stay free with a -warning. Heat transfer and explicit dynamics refuse ties by name. +warning. Explicit dynamics refuses ties by name. -**Verification** (`crates/solver/tests/contact.rs`, `tests/oracle/tie-patch`): a linear field -across non-matching hex8/tet4 meshes reproduced to round-off (to 10⁻⁶ on distorted quadrilateral -faces, where the overlap integration is accurate to its degree) in linear and nonlinear static -steps; uniaxial tension across a tie, σ_zz = Eε in every element to 10⁻⁹. +**Heat** (`fieldcraft_solver::heat`): the same weights tie temperatures, T_A = Σ_J w_AJ T_J (one row +per secondary node, `tie::rows_of` with the temperature kind), in steady and transient heat +transfer and in the heat solution of coupled temperature–displacement steps. The heat solver +eliminates them with the prescribed temperatures through the same transformation +(`constraints::Reduction`), so the interface has no resistance, the heat leaving one side enters +the other, and the reactions at prescribed temperatures (T_pᵀ (K T + C Ṫ − f)) balance the heat +supplied. + +**Verification** (`crates/solver/tests/contact.rs`, `tests/oracle/tie-patch`, +`tests/oracle/tie-heat-series`): a linear field across non-matching hex8/tet4 meshes reproduced to +round-off (to 10⁻⁶ on distorted quadrilateral faces, where the overlap integration is accurate to +its degree) in linear and nonlinear static steps; uniaxial tension across a tie, σ_zz = Eε in +every element to 10⁻⁹. Heat: steady conduction between held ends through tied hex8/tet4 blocks +(matching, graded and mixed meshes), the linear profile, the flux and the heat flow k A ΔT / L to +10⁻⁹; with conductivities k₁ ≠ k₂ the series-resistance interface temperature +(k₁ T₀ + k₂ T₁)/(k₁ + k₂) and heat flow ΔT/(L₁/k₁ + L₂/k₂); a transient tied bar equal to the +single-body bar; a coupled step with a tie at a uniform ΔT expands freely (u = α ΔT x, no stress). +Ties take linear faces only (tet4, wedge6, hex8), for heat as for displacement. ## Hyperelastic materials in explicit dynamics diff --git a/docs/oracle.md b/docs/oracle.md index 1a389ac..02a44b3 100644 --- a/docs/oracle.md +++ b/docs/oracle.md @@ -2,7 +2,7 @@ Generated by `cargo xtask oracle` from [`tests/oracle/`](../tests/oracle/); the policy is [verification.md](verification.md). Every case is our own model, built with FieldCraft commands from the problem's published description, and compared with the reference value its `expected.json` cites, within the stated tolerance and for the stated reason. Accepted results are frozen in each case's `fixture.json`: a later change that moves one beyond its band (10⁻⁹ of its size) fails CI even while the reference still passes. No reference value was produced with a commercial product. -**270 of 276 quantities pass** (6 known failure(s)) in 70 active cases: 61 theory, 9 published benchmark, 0 experiment; 10 pending cases wait for capabilities still being built. +**275 of 281 quantities pass** (6 known failure(s)) in 71 active cases: 62 theory, 9 published benchmark, 0 experiment; 10 pending cases wait for capabilities still being built. ## Theory: closed-form solutions, patch tests and mesh invariants @@ -249,6 +249,11 @@ Generated by `cargo xtask oracle` from [`tests/oracle/`](../tests/oracle/); the | [rotating-disc-plane-stress](../tests/oracle/rotating-disc-plane-stress/) | hoop stress σ_θ = σ_yy at the bore (20, 0) | 65.312 | 65.188 | -0.19% | 1% | pass | matches | | [rotating-disc-plane-stress](../tests/oracle/rotating-disc-plane-stress/) | radial stress σ_r = σ_xx at r = √(ab), its maximum | 20.724 | 20.683 | -0.2% | 1% | pass | matches | | [rotating-disc-plane-stress](../tests/oracle/rotating-disc-plane-stress/) | hoop stress σ_θ = σ_yy at the rim (100, 0) | 16.328 | 16.352 | 0.149% | 1% | pass | matches | +| [tie-heat-series](../tests/oracle/tie-heat-series/) | T at the tied secondary node (0.5, 0.5, 1) | 84 | 84 | < 1e-7% | 1e-9 rel. | pass | matches | +| [tie-heat-series](../tests/oracle/tie-heat-series/) | T in the lower block at (0, 0, 0.5) | 52 | 52 | < 1e-7% | 1e-9 rel. | pass | matches | +| [tie-heat-series](../tests/oracle/tie-heat-series/) | T in the upper block at (1, 1, 1.5) | 92 | 92 | < 1e-7% | 1e-9 rel. | pass | matches | +| [tie-heat-series](../tests/oracle/tie-heat-series/) | heat flux q_z at (0, 0, 0.5) | -640 | -640 | < 1e-7% | 1e-9 rel. | pass | matches | +| [tie-heat-series](../tests/oracle/tie-heat-series/) | heat flux q_z at (1, 1, 1.5) | -640 | -640 | < 1e-7% | 1e-9 rel. | pass | matches | | [tie-patch](../tests/oracle/tie-patch/) | σ_zz of the lower block's corner element under the tie (element 10) | 10 | 10 | < 1e-7% | 1e-9 rel. | pass | matches | | [tie-patch](../tests/oracle/tie-patch/) | σ_zz of the lower block's centre element under the tie (element 14) | 10 | 10 | < 1e-7% | 1e-9 rel. | pass | matches | | [tie-patch](../tests/oracle/tie-patch/) | σ_zz of the lower block's bottom corner element (element 1) | 10 | 10 | < 1e-7% | 1e-9 rel. | pass | matches | @@ -360,6 +365,7 @@ Generated by `cargo xtask oracle` from [`tests/oracle/`](../tests/oracle/); the - **[rotating-cylinder-axisymmetric](../tests/oracle/rotating-cylinder-axisymmetric/)** (theory): Long rotating solid cylinder, axisymmetric. Source: Timoshenko and Goodier, Theory of Elasticity, 3rd ed., McGraw-Hill 1970, §32 (long rotating cylinder, ε_z = 0). Tolerances — radial_axis: stress extrapolated to the axis, where σ_r = σ_θ: under 1% with sixteen quadratic elements across; hoop_rim: stress extrapolated to the surface: under 1% - **[rotating-disc-plane-stress](../tests/oracle/rotating-disc-plane-stress/)** (theory): Rotating disc with a central hole, plane stress. Source: Timoshenko and Goodier, Theory of Elasticity, 3rd ed., McGraw-Hill 1970, §32 (rotating disc of uniform thickness with a hole). Tolerances — hoop_bore: stress extrapolated to the bore surface, where the a²b²/r² term is steepest: 16 radial quadratic elements graded towards the bore leave about 0.2% (8 uniform: −2.4%; 32 graded: −0.05%); radial_max: the nearest node to √(ab) is within half an element of it, where σ_r is stationary (an error of order (h/r)², under 0.5%), and nodal averaging adds a few tenths of a percent; hoop_rim: as at the bore - **[scordelis-lo-roof-solid](../tests/oracle/scordelis-lo-roof-solid/)** (benchmark): Scordelis–Lo roof with one layer of hex20 reduced-integration solids. Source: MacNeal and Harder, 'A proposed standard set of problems to test finite element accuracy', Finite Elements in Analysis and Design 1 (1985) 3–20: Fig. 8 (p. 8) geometry, material and load; Table 5(a) (p. 11) reference displacement 0.3024; Belytschko et al., Computer Methods in Applied Mechanics and Engineering 51 (1985) 221–258 (shell obstacle course). Tolerances — uz: MacNeal and Harder grade an element A within 2% of the theoretical value on this prescribed coarse mesh (p. 14); MacNeal and Harder normalise by 0.3024 (the deep-shell value quoted in their [5] is 0.3086), and a solid model of R/t = 100 differs from shell theory by O(t/R) +- **[tie-heat-series](../tests/oracle/tie-heat-series/)** (theory): Heat through a tie: two blocks of conductivities 10 and 40 with non-matching meshes, 20 and 100 at the ends. Source: Series thermal resistances: steady one-dimensional conduction through two layers in perfect contact, T_i = (k1 T0 + k2 T1)/(k1 + k2) and q = ΔT/(L1/k1 + L2/k2) (F. P. Incropera, D. P. DeWitt, Fundamentals of Heat and Mass Transfer, §3.1, the composite wall). A linear field in each layer is the exact discrete solution, and the dual mortar tie reproduces a linear temperature across non-matching meshes. Tolerances — t_interface: exact discrete solution: linear in each block, and the tie holds the interface temperature for non-matching meshes; t_lower: exact discrete solution: linear in each block, and the tie holds the interface temperature for non-matching meshes; t_upper: exact discrete solution: linear in each block, and the tie holds the interface temperature for non-matching meshes; q_lower: the heat flow ΔT/(1/k1 + 1/k2) = 640 per unit area, −k ∂T/∂z uniform in each block; q_upper: the same heat flow crosses the tie: nothing is lost at the interface - **[tie-patch](../tests/oracle/tie-patch/)** (theory): Tie patch test: two blocks with non-matching meshes tied together in uniaxial tension. Source: The patch test for mesh tying: a tie across non-matching meshes must transmit a constant stress exactly; B. I. Wohlmuth, A mortar finite element method using dual spaces for the Lagrange multiplier, SIAM J. Numer. Anal. 38(3):989–1012, 2000; M. A. Puso, A 3D mortar method for solid mechanics, IJNME 59:315–336, 2004. Uniaxial stress between frictionless symmetry planes is uniform in both blocks. Tolerances — szz_10: uniaxial stress E ε = 1000 × 0.01 is the exact discrete solution: the dual mortar conditions hold for the linear field on both meshes; szz_14: uniaxial stress E ε = 1000 × 0.01 is the exact discrete solution: the dual mortar conditions hold for the linear field on both meshes; szz_1: uniaxial stress E ε = 1000 × 0.01 is the exact discrete solution: the dual mortar conditions hold for the linear field on both meshes; szz_19: uniaxial stress E ε = 1000 × 0.01 is the exact discrete solution: the dual mortar conditions hold for the linear field on both meshes; szz_26: uniaxial stress E ε = 1000 × 0.01 is the exact discrete solution: the dual mortar conditions hold for the linear field on both meshes; sxx_14: the tie must not hold the lateral contraction of either mesh; uz_interface: the tied node follows the uniform strain field - **[tube-torsion-coupling](../tests/oracle/tube-torsion-coupling/)** (theory): Torsion of a tube through a kinematically coupled reference point. Source: Saint-Venant torsion of a circular tube, φ = TL/(GJ), τ = Tr/J with J = π(b⁴ − a⁴)/2; Timoshenko and Goodier, Theory of Elasticity, 3rd ed., 1970, §104. Tolerances — twist: Saint-Venant torsion of a circular tube is exact in elasticity; the rigid end coupling only stiffens a short zone near the tip; quadratic elements with 16 divisions around are within 0.1%; shear: stress extrapolated to the outer surface and averaged where two blocks meet: about 0.3% with four radial quadratic elements (two gave 1.1%) diff --git a/tests/oracle/tie-heat-series/expected.json b/tests/oracle/tie-heat-series/expected.json new file mode 100644 index 0000000..21f8b9e --- /dev/null +++ b/tests/oracle/tie-heat-series/expected.json @@ -0,0 +1,122 @@ +{ + "title": "Heat through a tie: two blocks of conductivities 10 and 40 with non-matching meshes, 20 and 100 at the ends", + "kind": "theory", + "source": "Series thermal resistances: steady one-dimensional conduction through two layers in perfect contact, T_i = (k1 T0 + k2 T1)/(k1 + k2) and q = ΔT/(L1/k1 + L2/k2) (F. P. Incropera, D. P. DeWitt, Fundamentals of Heat and Mass Transfer, §3.1, the composite wall). A linear field in each layer is the exact discrete solution, and the dual mortar tie reproduces a linear temperature across non-matching meshes", + "quantities": [ + { + "id": "t_interface", + "quantity": "T at the tied secondary node (0.5, 0.5, 1)", + "measure": { + "command": "solve.probe", + "params": { + "step": "Heat", + "at": [ + [ + 0.5, + 0.5, + 1.0 + ] + ] + }, + "pointer": "/nodes/0/temperature" + }, + "value": 84.0, + "tolerance": { + "relative": 1e-09 + }, + "reason": "exact discrete solution: linear in each block, and the tie holds the interface temperature for non-matching meshes" + }, + { + "id": "t_lower", + "quantity": "T in the lower block at (0, 0, 0.5)", + "measure": { + "command": "solve.probe", + "params": { + "step": "Heat", + "at": [ + [ + 0.0, + 0.0, + 0.5 + ] + ] + }, + "pointer": "/nodes/0/temperature" + }, + "value": 52.0, + "tolerance": { + "relative": 1e-09 + }, + "reason": "exact discrete solution: linear in each block, and the tie holds the interface temperature for non-matching meshes" + }, + { + "id": "t_upper", + "quantity": "T in the upper block at (1, 1, 1.5)", + "measure": { + "command": "solve.probe", + "params": { + "step": "Heat", + "at": [ + [ + 1.0, + 1.0, + 1.5 + ] + ] + }, + "pointer": "/nodes/0/temperature" + }, + "value": 92.0, + "tolerance": { + "relative": 1e-09 + }, + "reason": "exact discrete solution: linear in each block, and the tie holds the interface temperature for non-matching meshes" + }, + { + "id": "q_lower", + "quantity": "heat flux q_z at (0, 0, 0.5)", + "measure": { + "command": "solve.probe", + "params": { + "step": "Heat", + "at": [ + [ + 0.0, + 0.0, + 0.5 + ] + ] + }, + "pointer": "/nodes/0/heat_flux/2" + }, + "value": -640.0, + "tolerance": { + "relative": 1e-09 + }, + "reason": "the heat flow ΔT/(1/k1 + 1/k2) = 640 per unit area, −k ∂T/∂z uniform in each block" + }, + { + "id": "q_upper", + "quantity": "heat flux q_z at (1, 1, 1.5)", + "measure": { + "command": "solve.probe", + "params": { + "step": "Heat", + "at": [ + [ + 1.0, + 1.0, + 1.5 + ] + ] + }, + "pointer": "/nodes/0/heat_flux/2" + }, + "value": -640.0, + "tolerance": { + "relative": 1e-09 + }, + "reason": "the same heat flow crosses the tie: nothing is lost at the interface" + } + ] +} \ No newline at end of file diff --git a/tests/oracle/tie-heat-series/fixture.json b/tests/oracle/tie-heat-series/fixture.json new file mode 100644 index 0000000..067abf6 --- /dev/null +++ b/tests/oracle/tie-heat-series/fixture.json @@ -0,0 +1,25 @@ +{ + "note": "FieldCraft's accepted results for this case, written by `cargo xtask oracle --bless`. A result outside its band fails CI.", + "values": { + "q_lower": { + "band": 6.400000000000002e-7, + "value": -640.0000000000002 + }, + "q_upper": { + "band": 6.400000000000015e-7, + "value": -640.0000000000015 + }, + "t_interface": { + "band": 8.400000000000002e-8, + "value": 84.00000000000001 + }, + "t_lower": { + "band": 5.2000000000000035e-8, + "value": 52.000000000000036 + }, + "t_upper": { + "band": 9.200000000000008e-8, + "value": 92.00000000000007 + } + } +} diff --git a/tests/oracle/tie-heat-series/recipe.json b/tests/oracle/tie-heat-series/recipe.json new file mode 100644 index 0000000..6232280 --- /dev/null +++ b/tests/oracle/tie-heat-series/recipe.json @@ -0,0 +1,19 @@ +[ + {"command": "mesh.block", "params": {"body": "Lower", "kind": "hex8", "corners": [[0, 0, 0], [1, 0, 0], [1, 1, 0], [0, 1, 0], [0, 0, 1], [1, 0, 1], [1, 1, 1], [0, 1, 1]], "divisions": [3, 3, 2], "faces": {"1": "bottom", "2": "top"}, "bias": [2.0, 0.6, 1.0]}}, + {"command": "mesh.block", "params": {"body": "Upper", "kind": "hex8", "corners": [[0, 0, 1], [1, 0, 1], [1, 1, 1], [0, 1, 1], [0, 0, 2], [1, 0, 2], [1, 1, 2], [0, 1, 2]], "divisions": [2, 2, 2], "faces": {"1": "bottom", "2": "top"}, "merge": 0}}, + {"command": "model.set.create", "params": {"name": "lower", "geometry": [{"body": "Lower"}]}}, + {"command": "model.set.create", "params": {"name": "upper", "geometry": [{"body": "Upper"}]}}, + {"command": "model.set.create", "params": {"name": "lower_top", "geometry": [{"body": "Lower", "face": "top"}]}}, + {"command": "model.set.create", "params": {"name": "upper_bottom", "geometry": [{"body": "Upper", "face": "bottom"}]}}, + {"command": "model.set.create", "params": {"name": "cold", "geometry": [{"body": "Lower", "face": "bottom"}]}}, + {"command": "model.set.create", "params": {"name": "hot", "geometry": [{"body": "Upper", "face": "top"}]}}, + {"command": "model.material.create", "params": {"name": "Low", "elastic": {"type": "isotropic", "young": 1000.0, "poisson": 0.3}, "thermal": {"conductivity": 10.0}}}, + {"command": "model.material.create", "params": {"name": "High", "elastic": {"type": "isotropic", "young": 1000.0, "poisson": 0.3}, "thermal": {"conductivity": 40.0}}}, + {"command": "model.section.create", "params": {"name": "lower", "set": "lower", "material": "Low", "kind": {"type": "solid"}}}, + {"command": "model.section.create", "params": {"name": "upper", "set": "upper", "material": "High", "kind": {"type": "solid"}}}, + {"command": "interaction.tie", "params": {"name": "glue", "main": "lower_top", "secondary": "upper_bottom"}}, + {"command": "model.boundary_condition.create", "params": {"name": "cold", "type": "temperature", "target": {"set": "cold"}, "value": 20.0}}, + {"command": "model.boundary_condition.create", "params": {"name": "hot", "type": "temperature", "target": {"set": "hot"}, "value": 100.0}}, + {"command": "model.step.create", "params": {"name": "Heat", "procedure": {"type": "heat_transfer"}, "boundary_conditions": ["cold", "hot"], "loads": []}}, + {"command": "solve.heat", "params": {"step": "Heat"}, "id": "heat"} +]