outram_foam_basic_lib/fv_options/temperature_table.rs
1// SPDX-License-Identifier: GPL-3.0-only
2// Copyright (C) 2026 OUTRAM PARK contributors
3// Derived from OpenFOAM (www.openfoam.com / openfoam.org)
4// Copyright (C) 2017-2026 OpenFOAM Foundation
5// Upstream project: OpenFOAM-dev (OpenFOAM Foundation), vendored read-only at
6// crates/outram-foam-turbulence-lib/upstream_source/OpenFOAM
7// (README.org dated 14th July 2026; etc/bashrc WM_PROJECT_VERSION=dev).
8// Upstream source: src/OpenFOAM/primitives/functions/Function1/ — the `table`
9// entry of `Foam::Function1<scalar>`, as used by
10// `porosityModels::solidification` (`D`) and `fv::VoFSolidificationMelting`
11// (`alphaSolidT`).
12// Upstream licence: GPL-3.0-or-later.
13//
14// This file is part of OUTRAM PARK. See `src/lib.rs` for the full licence
15// notice.
16
17//! A tabulated function of temperature — the `Function1` `table` entry the
18//! solidification models take their coefficients from.
19//!
20//! # What belongs here, and what does not
21//!
22//! Only the table lookup: ascending temperature knots, linear interpolation
23//! between them, clamping outside them. It carries no unit and no physical
24//! meaning of its own — the model that owns one says what its values are.
25//!
26//! This is *not* a general port of `Foam::Function1`. Upstream's `Function1`
27//! also covers constants, polynomials, sine waves, CSV files and coded
28//! expressions, dispatched at runtime; none of that is here, and adding it
29//! would belong in its own module rather than this one.
30
31use crate::interpolation::interpolate_xy;
32use std::sync::Arc;
33
34/// A monotone-in-temperature lookup table with linear interpolation and
35/// end-clamping.
36///
37/// Upstream's `Function1<scalar>` `table` entry. Two knots are the usual case
38/// for a phase-change coefficient — one value below the freezing point, another
39/// above it — which makes the function ramp linearly across the interval
40/// between them instead of stepping discontinuously.
41///
42/// # Units
43///
44/// `temperatures` are kelvin \[K\]. **`values` have whatever unit the owning
45/// model gives them** — a rate \[1/s\] for
46/// [`SolidificationPorosity`](super::SolidificationPorosity)'s drag `D(T)`, a
47/// dimensionless fraction \[-\] for
48/// [`VofSolidificationMelting`](super::VofSolidificationMelting)'s
49/// `alphaSolid(T)`. The table itself does not know and does not check.
50///
51/// # Valid ranges
52///
53/// `temperatures` must be **strictly ascending**; `values` need not be monotone
54/// in either direction (the drag table descends, and a fraction table may do
55/// either). A query outside the tabulated range returns the nearest end value
56/// rather than extrapolating, which is `Function1`'s `table` behaviour and is
57/// what lets a two-knot table act as a smoothed step.
58#[derive(Debug, Clone, PartialEq)]
59pub struct TemperatureTable {
60 temperatures: Arc<Vec<f64>>,
61 values: Arc<Vec<f64>>,
62}
63
64impl TemperatureTable {
65 /// Build a table from ascending temperatures \[K\] and their values.
66 ///
67 /// # Panics
68 ///
69 /// Panics if the two vectors differ in length, if either is empty, or if
70 /// the temperatures are not strictly ascending. All three are case-setup
71 /// errors that would otherwise yield a silently wrong coefficient, so they
72 /// fail loudly at construction rather than at the first lookup.
73 #[must_use]
74 pub fn new(temperatures: Vec<f64>, values: Vec<f64>) -> Self {
75 assert_eq!(
76 temperatures.len(),
77 values.len(),
78 "temperature table: the temperature and value columns must have equal length"
79 );
80 assert!(
81 !temperatures.is_empty(),
82 "temperature table: at least one knot is required"
83 );
84 assert!(
85 temperatures.windows(2).all(|w| w[1] > w[0]),
86 "temperature table: temperatures must be strictly ascending"
87 );
88 Self {
89 temperatures: Arc::new(temperatures),
90 values: Arc::new(values),
91 }
92 }
93
94 /// A two-knot table: `cold_value` at and below `t_cold`, `hot_value` at and
95 /// above `t_hot`, linear in between.
96 ///
97 /// This is the shape both upstream examples use — `(330 10000) (330.5 0)`
98 /// for the drag table, `(330 1) (335 0)` for the solid-fraction table.
99 ///
100 /// # Panics
101 ///
102 /// Panics if `t_hot <= t_cold`.
103 #[must_use]
104 pub fn two_knot(t_cold: f64, t_hot: f64, cold_value: f64, hot_value: f64) -> Self {
105 Self::new(vec![t_cold, t_hot], vec![cold_value, hot_value])
106 }
107
108 /// The tabulated value at temperature `t` \[K\], clamped at both ends.
109 #[must_use]
110 pub fn value(&self, t: f64) -> f64 {
111 interpolate_xy(t, &self.temperatures, &self.values)
112 }
113
114 /// The tabulated temperatures \[K\], ascending.
115 #[must_use]
116 pub fn temperatures(&self) -> &[f64] {
117 &self.temperatures
118 }
119
120 /// The tabulated values, in the unit the owning model assigns them.
121 #[must_use]
122 pub fn values(&self) -> &[f64] {
123 &self.values
124 }
125}