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}