Skip to main content

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}