outram_foam_basic_lib/steady_conduction/mod.rs
1// SPDX-License-Identifier: GPL-3.0-only
2// Copyright (C) 2026 OUTRAM PARK contributors
3// Derived from OpenFOAM (www.openfoam.com)
4// applications/solvers/basic/laplacianFoam/laplacianFoam.C
5// Copyright (C) 2011-2017 OpenFOAM Foundation
6// Copyright (C) 2019 OpenCFD Ltd.
7// src/thermoTools/derivedFvPatchFields/externalWallHeatFluxTemperature/
8// externalWallHeatFluxTemperatureFvPatchScalarField.C (convective arm,
9// lines 424-447 of the vendored copy: valueFraction = hp/(hp + kappa
10// deltaCoeffs), refValue = Ta)
11// src/finiteVolume/interpolation/surfaceInterpolation/schemes/harmonic/
12// harmonic.H (face conductivity 1/reverseLinear(1/k))
13// Upstream licence: GPL-3.0-or-later.
14//
15// This file is part of OUTRAM PARK.
16//
17// OUTRAM PARK is free software: you can redistribute it and/or modify it
18// under the terms of the GNU General Public License as published by the
19// Free Software Foundation, either version 3 of the License, or (at your
20// option) any later version.
21//
22// OUTRAM PARK is distributed in the hope that it will be useful, but
23// WITHOUT ANY WARRANTY; without even the implied warranty of
24// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
25// General Public License for more details.
26//
27// You should have received a copy of the GNU General Public License along
28// with OUTRAM PARK. If not, see <https://www.gnu.org/licenses/>.
29
30//! # Steady heat conduction with a volumetric source (gh:#949)
31//!
32//! A small steady conduction solver: the steady form of OpenFOAM's
33//! `laplacianFoam`,
34//!
35//! ```text
36//! -div( k(T) grad T ) = q''' in the solid
37//! ```
38//!
39//! with a per-zone uniform heat source `q'''` (W/m^3), a per-zone
40//! conductivity `k(T)` (W/(m K)) supplied by the caller, and one thermal
41//! condition per boundary patch: adiabatic, fixed temperature, or convective
42//! (`-k dT/dn = h (T - T_ambient)`).
43//!
44//! It exists for the **beginner thermal profile** of `outram-mc-libs`
45//! (gh:#949, under #945): a coarse temperature field for any shape, with an
46//! **energy balance** (heat generated against heat leaving through the
47//! boundary) reported beside it. Serious thermal-hydraulics is `nee_soon`'s.
48//!
49//! ## What is ported, and what is new
50//!
51//! | piece | source |
52//! |---|---|
53//! | the equation, `fvm::laplacian(k, T) == source` | `laplacianFoam.C` (`TEqn`), run steady (`ddt` dropped, as with `ddtSchemes steadyState`) |
54//! | the convective boundary as a mixed (Robin) condition, `valueFraction = h/(h + k/d)`, `refValue = T_a` | `externalWallHeatFluxTemperatureFvPatchScalarField.C` (`hp` without radiation or solid layers) |
55//! | `k(T)` re-evaluated between outer passes (Picard) | the outer `simple.loop()` of `laplacianFoam`, here with `k` updated each pass (upstream's `DT` is constant) |
56//! | **face conductivity as series resistance**, `A / (d_P/k_P + d_N/k_N)` | OpenFOAM's `harmonic` interpolation (`schemes/harmonic/harmonic.H`: `1/reverseLinear(1/k)`), chosen over the default `Gauss linear`, which averages `k` linearly and is wrong at a material interface |
57//! | **energy balance** from the boundary condition definitions, not from the matrix | new here |
58//! | radial (slab / cylinder / sphere) and voxel meshers | new here ([`mesh`]) |
59//!
60//! ## The energy balance, and what it can catch
61//!
62//! The heat leaving each boundary face is recomputed from that face's own
63//! condition (`h`, `T_a`, `k` of its cell, the face distance) and the solved
64//! cell temperature, independently of the assembled matrix. In a correct
65//! finite-volume solve the interior fluxes cancel pairwise, so generated minus
66//! removed equals the sum of the linear-solver residuals: it is small by
67//! construction. Its job is to catch an assembly that drops or doubles a
68//! boundary contribution (a zero boundary conductivity was such a defect
69//! elsewhere in this workspace; see `outram-foam-appbuilder-lib`'s
70//! `thermo_mechanics::mesh_solve`) and an unconverged solve. It does **not**
71//! measure discretisation error; the closed-form tests in this module do.
72//!
73//! ## Verification
74//!
75//! [`closed_form`] is the exact solution for uniform `q'''` in nested slab,
76//! cylinder and sphere shells with a fixed-temperature or convective surface.
77//! The tests in `tests.rs` compare the solve against it and measure the
78//! convergence order; the methodology and results are in their doc comments.
79//!
80//! Status: untrusted AI-generated draft (2026-10-17) per `RESPONSIBLE_USE.md`.
81//! For research, education and V&V only.
82
83pub mod closed_form;
84pub mod mesh;
85mod solve;
86
87#[cfg(test)]
88mod tests;
89
90pub use closed_form::{RadialClosedForm, RadialLayer};
91pub use mesh::{
92 radial_mesh, voxel_mesh, ConductionMesh, RadialGeometry, RadialShell, VoxelGrid, VoxelMesh,
93};
94pub use solve::{
95 EnergyBalance, PatchCondition, SteadyConductionProblem, SteadyConductionSettings,
96 SteadyConductionSolution, ThermalBoundary,
97};
98
99use thiserror::Error;
100
101/// What can go wrong setting up or solving a steady conduction problem.
102#[derive(Debug, Clone, PartialEq, Error)]
103pub enum SteadyConductionError {
104 /// An input was out of range or inconsistent (the message says which).
105 #[error("invalid steady-conduction input: {0}")]
106 InvalidInput(String),
107 /// The mesh could not be assembled.
108 #[error("mesh assembly failed: {0}")]
109 Mesh(String),
110 /// The linear solve missed its tolerance.
111 #[error("linear solve did not converge: residual {residual:e} after {iterations} iterations")]
112 LinearSolveFailed {
113 /// Normalised residual at exit.
114 residual: f64,
115 /// Iterations performed.
116 iterations: usize,
117 },
118 /// The outer (Picard) iteration on `k(T)` did not settle.
119 #[error("Picard iteration on k(T) did not converge: last change {last_change_kelvin:e} K after {iterations} passes")]
120 PicardNotConverged {
121 /// Largest cell temperature change of the last pass, K.
122 last_change_kelvin: f64,
123 /// Passes performed.
124 iterations: usize,
125 },
126 /// The caller's conductivity returned a non-positive or non-finite value.
127 #[error("conductivity for zone {zone} at {temperature_kelvin} K is {value} W/(m K); it must be positive and finite")]
128 BadConductivity {
129 /// Zone index.
130 zone: usize,
131 /// Temperature it was asked at, K.
132 temperature_kelvin: f64,
133 /// What it returned, W/(m K).
134 value: f64,
135 },
136}