Skip to main content

resid/
spline.rs

1// This file is part of resid-rs.
2// Copyright (c) 2017-2019 Sebastian Jastrzebski <sebby2k@gmail.com>. All rights reserved.
3// Portions (c) 2004 Dag Lem <resid@nimrod.no>
4// Licensed under the GPLv3. See LICENSE file in the project root for full license text.
5
6//! Our objective is to construct a smooth interpolating single-valued function
7//! y = f(x).
8//!
9//! Catmull-Rom splines are widely used for interpolation, however these are
10//! parametric curves [x(t) y(t) ...] and can not be used to directly calculate
11//! y = f(x).
12//! For a discussion of Catmull-Rom splines see Catmull, E., and R. Rom,
13//! "A Class of Local Interpolating Splines", Computer Aided Geometric Design.
14//!
15//! Natural cubic splines are single-valued functions, and have been used in
16//! several applications e.g. to specify gamma curves for image display.
17//! These splines do not afford local control, and a set of linear equations
18//! including all interpolation points must be solved before any point on the
19//! curve can be calculated. The lack of local control makes the splines
20//! more difficult to handle than e.g. Catmull-Rom splines, and real-time
21//! interpolation of a stream of data points is not possible.
22//! For a discussion of natural cubic splines, see e.g. Kreyszig, E., "Advanced
23//! Engineering Mathematics".
24//!
25//! Our approach is to approximate the properties of Catmull-Rom splines for
26//! piecewice cubic polynomials f(x) = ax^3 + bx^2 + cx + d as follows:
27//! Each curve segment is specified by four interpolation points,
28//! p0, p1, p2, p3.
29//! The curve between p1 and p2 must interpolate both p1 and p2, and in addition
30//! f'(p1.x) = k1 = (p2.y - p0.y)/(p2.x - p0.x) and
31//! f'(p2.x) = k2 = (p3.y - p1.y)/(p3.x - p1.x).
32//!
33//! The constraints are expressed by the following system of linear equations
34//! ``` ignore,
35//! [ 1  xi    xi^2    xi^3 ]   [ d ]    [ yi ]
36//! [     1  2*xi    3*xi^2 ] * [ c ] =  [ ki ]
37//! [ 1  xj    xj^2    xj^3 ]   [ b ]    [ yj ]
38//! [     1  2*xj    3*xj^2 ]   [ a ]    [ kj ]
39//! ```
40//! Solving using Gaussian elimination and back substitution, setting
41//! dy = yj - yi, dx = xj - xi, we get
42//! ``` ignore,
43//! a = ((ki + kj) - 2*dy/dx)/(dx*dx);
44//! b = ((kj - ki)/dx - 3*(xi + xj)*a)/2;
45//! c = ki - (3*xi*a + 2*b)*xi;
46//! d = yi - ((xi*a + b)*xi + c)*xi;
47//! ```
48//! Having calculated the coefficients of the cubic polynomial we have the
49//! choice of evaluation by brute force
50//! ``` ignore,
51//! for (x = x1; x <= x2; x += res) {
52//!   y = ((a*x + b)*x + c)*x + d;
53//!   plot(x, y);
54//! }
55//! ```
56//! or by forward differencing
57//! ``` ignore,
58//! y = ((a*x1 + b)*x1 + c)*x1 + d;
59//! dy = (3*a*(x1 + res) + 2*b)*x1*res + ((a*res + b)*res + c)*res;
60//! d2y = (6*a*(x1 + res) + 2*b)*res*res;
61//! d3y = 6*a*res*res*res;
62//!
63//! for (x = x1; x <= x2; x += res) {
64//!   plot(x, y);
65//!   y += dy; dy += d2y; d2y += d3y;
66//! }
67//! ```
68//! See Foley, Van Dam, Feiner, Hughes, "Computer Graphics, Principles and
69//! Practice" for a discussion of forward differencing.
70//!
71//! If we have a set of interpolation points p0, ..., pn, we may specify
72//! curve segments between p0 and p1, and between pn-1 and pn by using the
73//! following constraints:
74//! f''(p0.x) = 0 and
75//! f''(pn.x) = 0.
76//!
77//! Substituting the results for a and b in
78//!
79//! 2*b + 6*a*xi = 0
80//!
81//! we get
82//!
83//! ki = (3*dy/dx - kj)/2;
84//!
85//! or by substituting the results for a and b in
86//!
87//! 2*b + 6*a*xj = 0
88//!
89//! we get
90//!
91//! kj = (3*dy/dx - ki)/2;
92//!
93//! Finally, if we have only two interpolation points, the cubic polynomial
94//! will degenerate to a straight line if we set
95//!
96//! ki = kj = dy/dx;
97//!
98
99#![cfg_attr(feature = "cargo-clippy", allow(clippy::float_cmp))]
100#![cfg_attr(feature = "cargo-clippy", allow(clippy::too_many_arguments))]
101
102#[derive(Clone, Copy, PartialEq)]
103pub struct Point {
104    pub x: f64,
105    pub y: f64,
106}
107
108impl From<(i32, i32)> for Point {
109    fn from((x, y): (i32, i32)) -> Point {
110        Point {
111            x: x as f64,
112            y: y as f64,
113        }
114    }
115}
116
117pub struct PointPlotter<'a> {
118    output: &'a mut [i32],
119}
120
121impl<'a> PointPlotter<'a> {
122    pub fn new(output: &'a mut [i32]) -> Self {
123        PointPlotter { output }
124    }
125
126    pub fn plot(&mut self, x: f64, y: f64) {
127        let value = if y > 0.0 { y as i32 } else { 0 };
128        self.output[x as usize] = value;
129    }
130}
131
132/// Calculation of coefficients.
133fn cubic_coefficients(
134    x1: f64,
135    y1: f64,
136    x2: f64,
137    y2: f64,
138    k1: f64,
139    k2: f64,
140) -> (f64, f64, f64, f64) {
141    let dx = x2 - x1;
142    let dy = y2 - y1;
143    let a = ((k1 + k2) - 2.0 * dy / dx) / (dx * dx);
144    let b = ((k2 - k1) / dx - 3.0 * (x1 + x2) * a) / 2.0;
145    let c = k1 - (3.0 * x1 * a + 2.0 * b) * x1;
146    let d = y1 - ((x1 * a + b) * x1 + c) * x1;
147    (a, b, c, d)
148}
149
150/// Evaluation of cubic polynomial by brute force.
151#[allow(dead_code)]
152fn interpolate_brute_force(
153    x1: f64,
154    y1: f64,
155    x2: f64,
156    y2: f64,
157    k1: f64,
158    k2: f64,
159    plotter: &mut PointPlotter,
160    res: f64,
161) {
162    let (a, b, c, d) = cubic_coefficients(x1, y1, x2, y2, k1, k2);
163    // Calculate each point.
164    let mut xi = x1;
165    while xi <= x2 {
166        let yi = ((a * xi + b) * xi + c) * xi + d;
167        plotter.plot(xi, yi);
168        xi += res;
169    }
170}
171
172/// Evaluation of cubic polynomial by forward differencing.
173fn interpolate_forward_difference(
174    x1: f64,
175    y1: f64,
176    x2: f64,
177    y2: f64,
178    k1: f64,
179    k2: f64,
180    plotter: &mut PointPlotter,
181    res: f64,
182) {
183    let (a, b, c, d) = cubic_coefficients(x1, y1, x2, y2, k1, k2);
184    let mut yi = ((a * x1 + b) * x1 + c) * x1 + d;
185    let mut dy = (3.0 * a * (x1 + res) + 2.0 * b) * x1 * res + ((a * res + b) * res + c) * res;
186    let mut d2y = (6.0 * a * (x1 + res) + 2.0 * b) * res * res;
187    let d3y = 6.0 * a * res * res * res;
188    // Calculate each point.
189    let mut xi = x1;
190    while xi <= x2 {
191        plotter.plot(xi, yi);
192        yi += dy;
193        dy += d2y;
194        d2y += d3y;
195        xi += res;
196    }
197}
198
199/// Evaluation of complete interpolating function.
200/// Note that since each curve segment is controlled by four points, the
201/// end points will not be interpolated. If extra control points are not
202/// desirable, the end points can simply be repeated to ensure interpolation.
203/// Note also that points of non-differentiability and discontinuity can be
204/// introduced by repeating points.
205pub fn interpolate<P: Into<Point> + Copy>(points: &[P], plotter: &mut PointPlotter, res: f64) {
206    let last_index = points.len() - 4;
207    let mut i = 0;
208    while i <= last_index {
209        let p0 = points[i].into();
210        let p1 = points[i + 1].into();
211        let p2 = points[i + 2].into();
212        let p3 = points[i + 3].into();
213        // p1 and p2 equal; single point.
214        if p1.x != p2.x {
215            let k1;
216            let k2;
217            if p0.x == p1.x && p2.x == p3.x {
218                // Both end points repeated; straight line.
219                k1 = (p2.y - p1.y) / (p2.x - p1.x);
220                k2 = k1;
221            } else if p0.x == p1.x {
222                // p0 and p1 equal; use f''(x1) = 0.
223                k2 = (p3.y - p1.y) / (p3.x - p1.x);
224                k1 = (3.0 * (p2.y - p1.y) / (p2.x - p1.x) - k2) / 2.0;
225            } else if p2.x == p3.x {
226                // p2 and p3 equal; use f''(x2) = 0.
227                k1 = (p2.y - p0.y) / (p2.x - p0.x);
228                k2 = (3.0 * (p2.y - p1.y) / (p2.x - p1.x) - k1) / 2.0;
229            } else {
230                // Normal curve.
231                k1 = (p2.y - p0.y) / (p2.x - p0.x);
232                k2 = (p3.y - p1.y) / (p3.x - p1.x);
233            }
234            interpolate_forward_difference(p1.x, p1.y, p2.x, p2.y, k1, k2, plotter, res);
235        }
236        i += 1;
237    }
238}