Monotone cubic interpolation

From HandWiki
Short description: Variant of cubic interpolation that preserves monotonicity

In the mathematical field of numerical analysis, monotone cubic interpolation is a variant of cubic interpolation that preserves monotonicity of the data set being interpolated.

Monotonicity is preserved by linear interpolation but not guaranteed by cubic interpolation.

Monotone cubic Hermite interpolation

Example showing non-monotone cubic interpolation (in red) and monotone cubic interpolation (in blue) of a monotone data set.

Monotone interpolation can be accomplished using cubic Hermite spline with the tangents [math]\displaystyle{ m_i }[/math] modified to ensure the monotonicity of the resulting Hermite spline.

An algorithm is also available for monotone quintic Hermite interpolation.

Interpolant selection

There are several ways of selecting interpolating tangents for each data point. This section will outline the use of the Fritsch–Carlson method. Note that only one pass of the algorithm is required.

Let the data points be [math]\displaystyle{ (x_k,y_k) }[/math] indexed in sorted order for [math]\displaystyle{ k=1,\,\dots\,n }[/math].

  1. Compute the slopes of the secant lines between successive points:

    [math]\displaystyle{ \delta_k =\frac{y_{k+1}-y_k}{x_{k+1}-x_k} }[/math]

    for [math]\displaystyle{ k=1,\,\dots\,n-1 }[/math].

  2. These assignments are provisional, and may be superseded in the remaining steps. Initialize the tangents at every interior data point as the average of the secants,

    [math]\displaystyle{ m_k = \frac{\delta_{k-1}+\delta_k}{2} }[/math]

    for [math]\displaystyle{ k=2,\,\dots\,n-1 }[/math].

    For the endpoints, use one-sided differences:

    [math]\displaystyle{ m_1 = \delta_1 \quad \text{ and } \quad m_n = \delta_{n-1}\, }[/math].

    If [math]\displaystyle{ \delta_{k-1} }[/math] and [math]\displaystyle{ \delta_k }[/math] have opposite signs, set [math]\displaystyle{ m_k = 0 }[/math].

  3. For [math]\displaystyle{ k=1,\,\dots\,n-1 }[/math], where ever [math]\displaystyle{ \delta_k = 0 }[/math] (where ever two successive [math]\displaystyle{ y_k=y_{k+1} }[/math] are equal),
    set [math]\displaystyle{ m_k = m_{k+1} = 0, }[/math] as the spline connecting these points must be flat to preserve monotonicity.
    Ignore steps 4 and 5 for those [math]\displaystyle{ k\, }[/math].

  4. Let

    [math]\displaystyle{ \alpha_k = m_k/\delta_k \quad \text{ and } \quad \beta_k = m_{k+1}/\delta_k }[/math].

    If either [math]\displaystyle{ \alpha_k }[/math] or [math]\displaystyle{ \beta_k }[/math] is negative, then the input data points are not strictly monotone, and [math]\displaystyle{ (x_k,\,y_k) }[/math] is a local extremum. In such cases, piecewise monotone curves can still be generated by choosing [math]\displaystyle{ m_{k}=0\, }[/math] if [math]\displaystyle{ \alpha_k \lt 0 }[/math] or [math]\displaystyle{ m_{k+1}=0\, }[/math] if [math]\displaystyle{ \beta_k \lt 0 }[/math], although strict monotonicity is not possible globally.

  5. To prevent overshoot and ensure monotonicity, at least one of the following three conditions must be met:
(a) the function

[math]\displaystyle{ \phi_k = \alpha_k - \frac{(2 \alpha_k + \beta_k - 3)^2}{3(\alpha_k + \beta_k - 2)} \gt 0\, }[/math], or

(b) [math]\displaystyle{ \alpha_k + 2\beta_k - 3 \le 0\, }[/math], or
(c) [math]\displaystyle{ 2\alpha_k + \beta_k - 3 \le 0\, }[/math].
Only condition (a) is sufficient to ensure strict monotonicity: [math]\displaystyle{ \phi_k }[/math] must be positive.

One simple way to satisfy this constraint is to restrict the vector [math]\displaystyle{ (\alpha_k,\,\beta_k) }[/math] to a circle of radius 3. That is, if [math]\displaystyle{ \alpha_k^2 + \beta_k^2 \gt 9\, }[/math], then set

[math]\displaystyle{ \tau_k = \frac{3}{\sqrt{\alpha_k^2 + \beta_k^2}}\, }[/math],

and rescale the tangents via

[math]\displaystyle{ m_k = \tau_k\, \alpha_k \,\delta_k \quad \text{ and } \quad m_{k+1} = \tau_k\, \beta_k\, \delta_k\, }[/math].

Alternatively it is sufficient to restrict [math]\displaystyle{ \alpha_k \le 3 }[/math] and [math]\displaystyle{ \beta_k \le 3\, }[/math]. To accomplish this if [math]\displaystyle{ \alpha_k \gt 3\text{ or }\beta_k \gt 3\, }[/math], then set [math]\displaystyle{ m_k = 3 \, \delta_k\, }[/math].

Cubic interpolation

After the preprocessing above, evaluation of the interpolated spline is equivalent to cubic Hermite spline, using the data [math]\displaystyle{ x_k }[/math], [math]\displaystyle{ y_k }[/math], and [math]\displaystyle{ m_k }[/math] for [math]\displaystyle{ k=1,\,\dots\,n }[/math].

To evaluate at [math]\displaystyle{ x }[/math], find the index [math]\displaystyle{ k }[/math] in the sequence where [math]\displaystyle{ x }[/math], lies between [math]\displaystyle{ x_k }[/math], and [math]\displaystyle{ x_{k+1} }[/math], that is: [math]\displaystyle{ x_k \leq x \leq x_{k+1} }[/math]. Calculate

[math]\displaystyle{ \Delta = x_{k+1}-x_k \quad \text{ and } \quad t = \frac{x - x_k}{\Delta} }[/math]

then the interpolated value is

[math]\displaystyle{ f_\text{interpolated}(x) = y_k\cdot h_{00}(t) + \Delta\cdot m_k\cdot h_{10}(t) + y_{k+1}\cdot h_{01}(t) + \Delta\cdot m_{k+1}\cdot h_{11}(t) }[/math]

where [math]\displaystyle{ h_{ii} }[/math] are the basis functions for the cubic Hermite spline.

Example implementation

The following JavaScript implementation takes a data set and produces a monotone cubic spline interpolant function:

/*
 * Monotone cubic spline interpolation
 * Usage example listed at bottom; this is a fully-functional package. For
 * example, this can be executed either at sites like
 * https://www.programiz.com/javascript/online-compiler/
 * or using nodeJS.
 */
function DEBUG(s) {
    /* Uncomment the following to enable verbose output of the solver: */
    //console.log(s);
}
var outputCounter = 0;
var createInterpolant = function(xs, ys) {
    var i, length = xs.length;
    
    // Deal with length issues
    if (length != ys.length) { throw 'Need an equal count of xs and ys.'; }
    if (length === 0) { return function(x) { return 0; }; }
    if (length === 1) {
        // Impl: Precomputing the result prevents problems if ys is mutated later and allows garbage collection of ys
        // Impl: Unary plus properly converts values to numbers
        var result = +ys[0];
        return function(x) { return result; };
    }
    
    // Rearrange xs and ys so that xs is sorted
    var indexes = [];
    for (i = 0; i < length; i++) { indexes.push(i); }
    indexes.sort(function(a, b) { return xs[a] < xs[b] ? -1 : 1; });
    var oldXs = xs, oldYs = ys;
    // Impl: Creating new arrays also prevents problems if the input arrays are mutated later
    xs = []; ys = [];
    // Impl: Unary plus properly converts values to numbers
    for (i = 0; i < length; i++) { 
        xs[i] = +oldXs[indexes[i]];
        ys[i] = +oldYs[indexes[i]];
    }

    DEBUG("debug: xs = [ " + xs + " ]")
    DEBUG("debug: ys = [ " + ys + " ]")
    
    // Get consecutive differences and slopes
    var dys = [], dxs = [], ms = [];
    for (i = 0; i < length - 1; i++) {
        var dx = xs[i + 1] - xs[i], dy = ys[i + 1] - ys[i];
        dxs[i] = dx;
        dys[i] = dy;
        ms[i] = dy/dx;
    }
    // Get degree-1 coefficients
    var c1s = [ms[0]];
    for (i = 0; i < dxs.length - 1; i++) {
        var m = ms[i], mNext = ms[i + 1];
        if (m*mNext <= 0) {
            c1s[i] = 0;
        } else {
            var dx_ = dxs[i], dxNext = dxs[i + 1], common = dx_ + dxNext;
            c1s[i] = 3*common/((common + dxNext)/m + (common + dx_)/mNext);
        }
    }
    c1s.push(ms[ms.length - 1]);

    DEBUG("debug: dxs = [ " + dxs + " ]")
    DEBUG("debug: ms = [ " + ms + " ]")
    DEBUG("debug: c1s.length = " + c1s.length)
    DEBUG("debug: c1s = [ " + c1s + " ]")
    
    // Get degree-2 and degree-3 coefficients
    var c2s = [], c3s = [];
    for (i = 0; i < c1s.length - 1; i++) {
        var c1 = c1s[i];
        var m_ = ms[i];
        var invDx = 1/dxs[i];
        var common_ = c1 + c1s[i + 1] - m_ - m_;
        DEBUG("debug: " + i + ". c1 = " + c1);
        DEBUG("debug: " + i + ". m_ = " + m_);
        DEBUG("debug: " + i + ". invDx = " + invDx);
        DEBUG("debug: " + i + ". common_ = " + common_);
        c2s[i] = (m_ - c1 - common_)*invDx;
        c3s[i] = common_*invDx*invDx;
    }
    DEBUG("debug: c2s = [ " + c2s + " ]")
    DEBUG("debug: c3s = [ " + c3s + " ]")

    // Return interpolant function
    return function(x) {
        // The rightmost point in the dataset should give an exact result
        var i = xs.length - 1;
        // Uncomment the following to return only the interpolated value.
        //if (x == xs[i]) { return ys[i]; }
        
        // Search for the interval x is in, returning the corresponding y if x is one of the original xs
        var low = 0, mid, high = c3s.length - 1;
        while (low <= high) {
            mid = Math.floor(0.5*(low + high));
            var xHere = xs[mid];
            if (xHere < x) { low = mid + 1; }
            else if (xHere > x) { high = mid - 1; }
            else {
                // Uncomment the following to return only the interpolated value.
                //return ys[mid];
                low = c3s.length - 1;
                high = mid;
                break;
            }
        }
        i = Math.max(0, high);

        // Interpolate
        var diff = x - xs[i];
        outputCounter++;
        var interpolatedValue = ys[i] + diff * (c1s[i] + diff *  (c2s[i] + diff * c3s[i]));
        // The value of the interpolator's derivative at this point.
        var derivativeValue = c1s[i] + diff * (2*c2s[i] + diff * 3*c3s[i]);
        DEBUG("debug: #" + outputCounter + ". x = " + x + ". i = " + i + ", diff = " + diff + ", interpolatedValue = " + interpolatedValue + ", derivativeValue = " + derivativeValue);
        // Uncomment the following to return only the interpolated value.
        // return interpolatedValue;
        return [ interpolatedValue, derivativeValue ];
    };
};

/*
   Usage example below will approximate x^2 for 0 <= x <= 4.

   Command line usage example (requires installation of nodejs):
   node monotone-cubic-spline.js
*/

var X = [0, 1, 2, 3, 4];
var F = [0, 1, 4, 9, 16];
var f = createInterpolant(X,F);
var N = X.length;
console.log("# BLOCK 0 :: Data for monotone-cubic-spline.js");
console.log("X" + "\t" + "F");
for (var i = 0; i < N; i += 1) {
    console.log(X[i] + '\t' + F[i]);
}
console.log(" ");
console.log(" ");
console.log("# BLOCK 1 :: Interpolated data for monotone-cubic-spline.js");
console.log("      x       " + "\t\t" + "     P(x)      " + "\t\t" + "    dP(x)/dx     ");
var message = '';
var M = 25;
for (var i = 0; i <= M; i += 1) {
    var x = X[0] + (X[N-1]-X[0])*i/M;
    var rvals = f(x);
    var P = rvals[0];
    var D = rvals[1];
    message += x.toPrecision(15) + '\t' + P.toPrecision(15) + '\t' + D.toPrecision(15) + '\n';
}
console.log(message);

References

External links