te1d/
func.rs

1//! Describes real-valued functions defined on the real line.
2
3use std::rc::Rc;
4
5use ndarray::{Array, Array1, Array2, array};
6
7use crate::calc::{ColVec};
8use crate::linalg::{LinalgError, solve_gauss};
9
10
11/// Temperature-dependent material properties must implement [`RealValuedFunction`] trait.
12pub trait RealValuedFunction {
13    /// Evaluate the function values at the given *increasing* points `incr_xs`.
14    fn ats_incr(&self, incr_xs: &ColVec) -> ColVec;
15
16    /// Evaluate the function value at a given point `x`.
17    #[inline]
18    fn at(&self, x: f64) -> f64 {
19        let fs = self.ats_incr(&array![x]);
20        fs[0]
21    }
22
23    /// Evaluate the function values at the given points `xs`.
24    #[inline]
25    fn ats(&self, xs: &ColVec) -> ColVec {
26        xs.mapv(|e| { self.at(e) })
27    }
28}
29
30/// Piecewise functions must implement [`RealValuedFunction`] trait and [`PiecewiseFunction`] trait.
31pub trait PiecewiseFunction: RealValuedFunction {
32    /// Evaluate the function value in a specific segment.
33    fn at_segment(&self, segment_idx: usize, x: f64) -> f64;
34
35    /// Return the function value at a beyond-left-end point.
36    fn at_left(&self) -> f64;
37
38    /// Return the function value at a beyond-right-end point.
39    fn at_right(&self) -> f64;
40
41    /// Evaluate function values at the given *increasing* points `input_incr_xs`.
42    /// * `input_incr_xs` must be increasing.
43    /// * `data_strict_incr_xs` must be *strictly increasing*.
44    fn piecewise_ats_incr(&self, input_incr_xs: &ColVec, data_strict_incr_xs: &ColVec, data_fs: &ColVec) -> ColVec {
45        let data_xs = data_strict_incr_xs;
46        let input_xs = input_incr_xs;
47        let n = data_xs.len();
48        let (xmin, xmax): (f64, f64) = (data_xs[0], data_xs[n-1]);
49        let mut result: ColVec = input_xs.to_owned();
50        let mut x_prev: f64 = input_xs[0] - 1.0;
51        let mut x_prev_segment_idx: usize = 0;
52        let last_segment_idx: usize = data_xs.len()-2;
53
54        for (i, &x) in input_xs.iter().enumerate() {
55            if x < x_prev {
56                panic!("`input_incr_xs` must be increasing!");
57            }
58            if x < xmin {
59                result[i] = self.at_left();
60                x_prev = x;
61                continue;
62            }
63            if x > xmax {
64                result[i] = self.at_right();
65                x_prev = x;
66                continue;
67            }
68            let x_segment_idx: usize = self.search_segment(data_xs, x, x_prev_segment_idx, last_segment_idx);
69            let xl: f64 = data_xs[x_segment_idx];
70            let xr: f64 = data_xs[x_segment_idx+1];
71            if (x < xl) || (x > xr) {
72                panic!("Impossible: check if `data_strict_incr_xs` is strictly increasing!");
73            }
74
75            if x == xl {
76                result[i] = data_fs[x_segment_idx];
77            }
78            if x == xr {
79                result[i] = data_fs[x_segment_idx+1];
80            } else {
81                result[i] = self.at_segment(x_segment_idx, x);
82            }
83            x_prev = x;
84            x_prev_segment_idx = x_segment_idx;
85        }
86
87        result
88    }
89
90    /// Finds the segment in which the given number is contained.
91    /// * Binary search is used.
92    /// * `start_segment_idx` and `end_segment_idx` are the lowest and largest index of segment to be returned.
93    #[inline]
94    fn search_segment(&self, data_strict_incr_xs: &ColVec, x: f64, start_segment_idx: usize, end_segment_idx: usize) -> usize {
95        let mut left_idx: usize = start_segment_idx;
96        let mut right_idx: usize = end_segment_idx;
97        let mut mid_idx: usize = (left_idx + right_idx)/2;
98
99        for _ in 0..(end_segment_idx-start_segment_idx)+1 {
100            mid_idx = (left_idx + right_idx)/2;
101            if left_idx >= right_idx {
102                break;
103            }
104            if x < data_strict_incr_xs[mid_idx] {
105                right_idx = mid_idx-1;
106            } else if x > data_strict_incr_xs[mid_idx+1] {
107                left_idx = mid_idx+1;
108            } else {
109                break;
110            }
111        }
112
113        mid_idx
114    }
115
116}
117
118
119/// Use [`Polynomial`] to implement [`RealValuedFunction`] when the *coefficients* of a polynomial is given.
120/// * There is no performance difference between [`Polynomial::ats_incr()`] and [`Polynomial::ats()`].
121/// * The `coefs` is the coefficients of a polynomial. For example, `coefs=[a2, a1, a0]` means the quadratic polynomial `a2 x^2 + a1 x + a0`.
122/// * [`Polynomial::deriv()`] gives the derivative of the polynomial.
123/// * [`Polynomial::from_interp()`] interpolates data and return a polynomial. **Do not use** it for 7-or-more-degree polynomials. It is numerically unstable. Use [`BarycentricPolynomial`](crate::interp::BarycentricPolynomial) whenever possible.
124/// 
125/// # Examples
126/// ```
127/// use ndarray::prelude::*;
128/// use te1d::prelude::*;
129/// use te1d::func::Polynomial;
130/// 
131/// // create a new polynomial
132/// let f = Polynomial::new(&array![1.5, -1.2, 0.5, 0.7]);
133/// let result = f.ats(&array![-1.0, -0.5, 0.0, 0.3, 0.9]);
134/// let sol = array![-2.5, -0.0375, 0.7, 0.7825, 1.2715];
135/// assert!(all_close(&result, &sol, 1e-05, 1e-08));
136/// assert!(is_close(f.at(0.75), 1.0328125, 1e-05, 1e-08));
137/// assert_eq!(&result, &f.ats_incr(&array![-1.0, -0.5, 0.0, 0.3, 0.9]));
138/// 
139/// // find the derivative
140/// let deriv = f.deriv();
141/// assert_eq!(&deriv, &Polynomial::new(&array![4.5, -2.4, 0.5]));
142/// 
143/// // polynomial interpolation
144/// let xs: ColVec = array![0.0, 0.5, 2.5, 3.0];
145/// let fs: ColVec = array![1.0, 0.0, 2.0, 3.0];
146/// let f = Polynomial::from_interp(&xs, &fs).unwrap();
147/// assert!(all_close(&f.ats(&xs), &fs, 1e-05, 1e-08));
148/// assert_eq!(f.deg, 3);
149/// ```
150#[derive(Debug, PartialEq, Clone)]
151pub struct Polynomial {
152    /// The coefficients of the polynomial.
153    pub coefs: ColVec,
154    /// The degree of the polynomial.
155    pub deg: usize,
156}
157
158impl RealValuedFunction for Polynomial {
159    fn at(&self, x: f64) -> f64 {
160        let mut result: f64 = 0.0;
161        for coef in self.coefs.iter() {
162            result *= x;
163            result += coef;
164        }
165
166        result
167    }
168
169    #[inline]
170    fn ats_incr(&self, incr_xs: &ColVec) -> ColVec {
171        self.ats(incr_xs)
172    }
173}
174
175impl Polynomial {
176    /// Construct a new polynomial by giving its coefficients.
177    pub fn new(coefs: &ColVec) -> Self {
178        Polynomial {
179            coefs: coefs.to_owned(),
180            deg: coefs.len()-1,
181        }
182    }
183
184    /// Compute the derivative of the polynomial.
185    pub fn deriv(&self) -> Self {
186        let deg = self.deg;
187        let mut deriv_coefs: ColVec = Array1::default(deg);
188        for i in 0..deg {
189            deriv_coefs[i] = (deg - i) as f64 * self.coefs[i];
190        }
191
192        Polynomial::new(&deriv_coefs)
193    }
194
195    /// Perform the polynomial interpolation.
196    pub fn from_interp(xs: &ColVec, fs: &ColVec) -> Result<Self, LinalgError> {
197        let num_data = xs.len();
198        if num_data != fs.len() {
199            panic!("The lengths of `xs` and `fs` should be equal!");
200        }
201        if num_data < 2 {
202            panic!("At least two points are required!");
203        }
204        
205        // construct Vandermonde-like matrix.
206        let mut mat: Array2<f64> = Array::default((num_data, num_data));
207        for row in 0..num_data {
208            for col in 0..num_data {
209                mat[[row, col]] = xs[row].powi((num_data-1-col) as i32);
210            }
211        }
212        // solve the matrix
213        match solve_gauss(&mat, &fs) {
214            Ok(b) => return Ok(Polynomial::new(&b)),
215            Err(err) => return Err(err),
216        }
217    }
218}
219
220
221/// Use [`CustomPiecewiseFunctionConstExt`] to construct a piecewise function with *constant extrapolation*, implementing [`RealValuedFunction`].
222/// * [`CustomPiecewiseFunctionConstExt::ats_incr()`] is faster than [`CustomPiecewiseFunctionConstExt::ats()`] if the input argument is increasing.
223/// 
224/// # Examples
225/// ```
226/// use std::rc::Rc;
227/// use ndarray::prelude::*;
228/// use te1d::prelude::*;
229/// use te1d::func::{CustomPiecewiseFunctionConstExt, ExplicitFunction};
230/// 
231/// let incr_xs: ColVec = array![0.0, 1.0, 2.0];
232/// let fs: ColVec = array![0.0, 1.0, 2.0];
233/// let func1 = ExplicitFunction::new(Rc::new(|x: f64| { x }));
234/// let func2 = ExplicitFunction::new(Rc::new(|x: f64| { x.powi(2) }));
235/// let f = CustomPiecewiseFunctionConstExt::new(&incr_xs, &fs, &vec![Rc::new(func1), Rc::new(func2)], 0.0, 3.0);
236/// let result = f.ats_incr(&array![-1.0, 0.0, 0.5, 1.0, 1.5, 2.0, 2.5]);
237/// let sol: ColVec = array![0.0, 0.0, 0.5, 1.0, 2.25, 2.0, 3.0];
238/// println!("{}, {}", result, sol);
239/// assert!(all_close(&result, &sol, 1e-05, 1e-08));
240/// ```
241#[derive(Clone)]
242pub struct CustomPiecewiseFunctionConstExt {
243    /// `x`-coordinates of the break points, must be strictly increasing.
244    pub xs: ColVec,
245    /// `y`-coordinates of the break points, same length as `xs`.
246    pub fs: ColVec,
247    /// The functions in each piece.
248    funcs: Vec<Rc<dyn RealValuedFunction>>,
249    /// Value to return for `x < xs[0]`.
250    pub left: f64,
251    /// Value to return for `x > xs[xs.len()-1]`.
252    pub right: f64,
253    /// The number of raw data points.
254    pub num_data: usize,
255}
256
257impl PiecewiseFunction for CustomPiecewiseFunctionConstExt {
258    #[inline]
259    fn at_segment(&self, segment_idx: usize, x: f64) -> f64 {
260        self.funcs[segment_idx].at(x)
261    }
262
263    #[inline]
264    fn at_left(&self) -> f64 {
265        self.left
266    }
267
268    #[inline]
269    fn at_right(&self) -> f64 {
270        self.right
271    }
272}
273
274impl RealValuedFunction for CustomPiecewiseFunctionConstExt {
275    /// `incr_xs` must be increasing.
276    #[inline]
277    fn ats_incr(&self, incr_xs: &ColVec) -> ColVec {
278        self.piecewise_ats_incr(incr_xs, &self.xs, &self.fs)
279    }
280}
281
282impl CustomPiecewiseFunctionConstExt {
283    /// Construct a new piecewise function.
284    /// 
285    /// # Arguments
286    /// - `strict_incr_xs`: `x`-coordinates of the break points, must be strictly increasing
287    /// - `fs`: `y`-coordinates of the break points, same length as `xs`
288    /// - `left`: Value to return for `x < strict_incr_xs[0]`
289    /// - `right`: Value to return for `x > strict_incr_xs[xs.len()-1]`
290    /// 
291    /// # Panics
292    /// * when the lengths of `incr_xs` and `fs` are different.
293    /// * when the length of `incr_xs` is less than two.
294    /// * when the lengths of `incr_xs` is not the length of `funcs` plus one.
295    pub fn new(strict_incr_xs: &ColVec, fs: &ColVec, funcs: &Vec<Rc<dyn RealValuedFunction>>, left: f64, right: f64) -> Self {
296        let num_data: usize = strict_incr_xs.len();
297        if num_data != fs.len() {
298            panic!("The lengths of `strict_incr_xs` and `fs` should be equal!");
299        }
300        if num_data < 2 {
301            panic!("At least two points are required!");
302        }
303        if num_data != funcs.len()+1 {
304            panic!("The lengths of `strict_incr_xs` must be the length of `funcs` plus one!");
305        }
306
307        CustomPiecewiseFunctionConstExt {
308            xs: strict_incr_xs.to_owned(), fs: fs.to_owned(), funcs: funcs.clone(), left, right, num_data
309        }
310    }
311}
312
313
314/// Use [`ExplicitFunction`] to implement [`RealValuedFunction`] when an explicit formula of a real-valued function is known.
315/// * There is no performance difference between [`ExplicitFunction::ats_incr()`] and [`ExplicitFunction::ats()`].
316/// 
317/// # Examples
318/// ```
319/// use std::rc::Rc;
320/// use ndarray::prelude::*;
321/// use te1d::prelude::*;
322/// use te1d::func::ExplicitFunction;
323/// 
324/// let f = ExplicitFunction::new(Rc::new(|x: f64| {x.powi(2)}));
325/// 
326/// let result = f.ats(&array![-1.0, -0.5, 0.0, 0.3, 0.9]);
327/// let sol = array![1.0, 0.25, 0.0, 0.09, 0.81];
328/// assert_eq!(result, sol);
329/// assert_eq!(f.at(0.75), 0.5625);
330/// assert_eq!(&result, &f.ats_incr(&array![-1.0, -0.5, 0.0, 0.3, 0.9]));
331/// ```
332#[derive(Clone)]
333pub struct ExplicitFunction {
334    f: Rc<dyn Fn(f64) -> f64>,
335}
336
337impl RealValuedFunction for ExplicitFunction {
338    #[inline]
339    fn at(&self, x: f64) -> f64 {
340        (self.f)(x)
341    }
342
343    #[inline]
344    fn ats_incr(&self, incr_xs: &ColVec) -> ColVec {
345        self.ats(incr_xs)
346    }
347}
348
349impl ExplicitFunction {
350    /// # Arguments
351    /// * `f`: an explicit function
352    pub fn new(f: Rc<dyn Fn(f64) -> f64>) -> Self {
353        ExplicitFunction { f: f.clone() }
354    }
355}
356
357
358/// Use [`ConstantFunction`] to implement [`RealValuedFunction`] when a constant function is known.
359/// * There is no performance difference between [`ConstantFunction::ats_incr()`] and [`ConstantFunction::ats()`].
360/// 
361/// # Examples
362/// ```
363/// use std::rc::Rc;
364/// use ndarray::prelude::*;
365/// use te1d::prelude::*;
366/// use te1d::func::ConstantFunction;
367/// 
368/// let f = ConstantFunction::new(2.0);
369/// 
370/// let result = f.ats(&array![-1.0, -0.5, 0.0, 0.3, 0.9]);
371/// let sol = array![2.0, 2.0, 2.0, 2.0, 2.0];
372/// assert_eq!(result, sol);
373/// assert_eq!(f.at(0.75), 2.0);
374/// assert_eq!(&result, &f.ats_incr(&array![-1.0, -0.5, 0.0, 0.3, 0.9]));
375/// ```
376#[derive(Debug, PartialEq, Clone)]
377pub struct ConstantFunction {
378    const_value: f64,
379}
380
381impl RealValuedFunction for ConstantFunction {
382    #[inline]
383    fn at(&self, _x: f64) -> f64 {
384        self.const_value
385    }
386
387    #[inline]
388    fn ats_incr(&self, incr_xs: &ColVec) -> ColVec {
389        self.ats(incr_xs)
390    }
391}
392
393impl ConstantFunction {
394    /// # Arguments
395    /// * `f`: an explicit function
396    pub fn new(const_value: f64) -> Self {
397        ConstantFunction { const_value }
398    }
399}
400
401
402#[cfg(test)]
403mod tests {
404    use super::*;
405
406    #[test]
407    #[should_panic]
408    fn test_panic_piecewise_function_const_ext_new() {
409        CustomPiecewiseFunctionConstExt::new(&array![0.0, 0.5, 1.0], &array![0.0, 1.0], &vec![], 0.0, 1.0);  // must panic; different length
410        CustomPiecewiseFunctionConstExt::new(&array![0.0], &array![1.0], &vec![], 0.0, 1.0);  // must panic; too few points
411        let f = ExplicitFunction::new(Rc::new(|x: f64| {x.powi(2)}));
412        CustomPiecewiseFunctionConstExt::new(&array![0.0, 1.0, 2.0], &array![0.0, 1.0, 2.0], &vec![Rc::new(f)], 0.0, 1.0);  // must panic; number of break points and the number of functions do not match
413    }
414}