Skip to main content

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}