|
| 1 | +import numpy as np |
| 2 | + |
| 3 | + |
| 4 | +def schumaker_qspline(x, y): |
| 5 | + """ |
| 6 | + Schumaker_QSpline fits a quadratic spline which preserves monotonicity and |
| 7 | + convexity in the data. |
| 8 | +
|
| 9 | + Syntax |
| 10 | + ------ |
| 11 | + outa, outxk, outy, kflag = schumaker_qspline(x, y) |
| 12 | +
|
| 13 | + Description |
| 14 | + ----------- |
| 15 | + Calculates coefficients for C1 quadratic spline interpolating data X, Y |
| 16 | + where length(x) = N and length(y) = N, which preserves monotonicity and |
| 17 | + convexity in the data. |
| 18 | +
|
| 19 | + Parameters |
| 20 | + ---------- |
| 21 | + x, y: numpy arrays of length N containing (x, y) points between which the |
| 22 | + spline will interpolate. |
| 23 | +
|
| 24 | + Returns |
| 25 | + ------- |
| 26 | + outa: a Nx3 matrix of coefficients where the ith row defines the quadratic |
| 27 | + interpolant between xk_i to xk_(i+1), i.e., y = A[i, 0] * |
| 28 | + (x - xk[i]] ** 2 + A[i, 1] * (x - xk[i]) + A[i, 2] |
| 29 | + outxk: an ordered vector of knots, i.e., values xk_i where the spline |
| 30 | + changes coefficients. All values in x are used as knots. However |
| 31 | + the algorithm may insert additional knots between data points in x |
| 32 | + where changes in convexity are indicated by the (numerical) |
| 33 | + derivative. Consequently output outxk has length >= length(x). |
| 34 | + outy: y values corresponding to the knots in outxk. Contains the original |
| 35 | + data points, y, and also y-values estimated from the spline at the |
| 36 | + inserted knots. |
| 37 | + kflag: a vector of length(outxk) of logicals, which are set to true for |
| 38 | + elements of outxk that are knots inserted by the algorithm. |
| 39 | +
|
| 40 | + References |
| 41 | + ---------- |
| 42 | + [1] PVLib MATLAB |
| 43 | + [2] L. L. Schumaker, "On Shape Preserving Quadratic Spline Interpolation", |
| 44 | + SIAM Journal on Numerical Analysis 20(4), August 1983, pp 854 - 864 |
| 45 | + [3] M. H. Lam, "Monotone and Convex Quadratic Spline Interpolation", |
| 46 | + Virginia Journal of Science 41(1), Spring 1990 |
| 47 | + """ |
| 48 | + |
| 49 | + # A small number used to decide when a slope is equivalent to zero |
| 50 | + eps = 1e-6 |
| 51 | + |
| 52 | + # Make sure vectors are 1D arrays |
| 53 | + if x.ndim != 1.: |
| 54 | + x = x.flatten([range(x.size)]) |
| 55 | + if y.ndim != 1.: |
| 56 | + y = y.flatten([range(y.size)]) |
| 57 | + |
| 58 | + n = len(x) |
| 59 | + |
| 60 | + # compute various values used by the algorithm: differences, length of line |
| 61 | + # segments between data points, and ratios of differences. |
| 62 | + delx = np.diff(x) # delx[i] = x[i + 1] - x[i] |
| 63 | + dely = np.diff(y) |
| 64 | + |
| 65 | + delta = dely / delx |
| 66 | + |
| 67 | + # Calculate first derivative at each x value per [3] |
| 68 | + |
| 69 | + s = np.zeros(x.shape) |
| 70 | + |
| 71 | + left = np.append(0., delta) |
| 72 | + right = np.append(delta, 0.) |
| 73 | + |
| 74 | + pdelta = left * right |
| 75 | + |
| 76 | + u = pdelta > 0. |
| 77 | + |
| 78 | + # [3], Eq. 9 for interior points |
| 79 | + # fix tuning parameters in [2], Eq 9 at chi = .5 and eta = .5 |
| 80 | + s[u] = pdelta[u] / (.5 * left[u] + .5 * right[u]) |
| 81 | + |
| 82 | + # [3], Eq. 7 for left endpoint |
| 83 | + if delta[0] * (2. * delta[0] - s[1]) > 0.: |
| 84 | + s[0] = 2. * delta[0] - s[1] |
| 85 | + |
| 86 | + # [3], Eq. 8 for right endpoint |
| 87 | + if delta[n - 2] * (2. * delta[n - 2] - s[n - 2]) > 0.: |
| 88 | + s[n - 1] = 2. * delta[n - 2] - s[n - 2] |
| 89 | + |
| 90 | + # determine knots. Start with initial pointsx |
| 91 | + # [2], Algorithm 4.1 first 'if' condition of step 5 defines intervals |
| 92 | + # which won't get internal knots |
| 93 | + tests = s[0.:(n - 1)] + s[1:n] |
| 94 | + u = np.abs(tests - 2. * delta[0:(n - 1)]) <= eps |
| 95 | + # u = true for an interval which will not get an internal knot |
| 96 | + |
| 97 | + k = n + sum(~u) # total number of knots = original data + inserted knots |
| 98 | + |
| 99 | + # set up output arrays |
| 100 | + # knot locations, first n - 1 and very last (n + k) are original data |
| 101 | + xk = np.zeros(k) |
| 102 | + yk = np.zeros(k) # function values at knot locations |
| 103 | + # logicals that will indicate where additional knots are inserted |
| 104 | + flag = np.zeros(k, dtype=bool) |
| 105 | + a = np.zeros((k, 3.)) |
| 106 | + |
| 107 | + # structures needed to compute coefficients, have to be maintained in |
| 108 | + # association with each knot |
| 109 | + |
| 110 | + tmpx = x[0:(n - 1)] |
| 111 | + tmpy = y[0:(n - 1)] |
| 112 | + tmpx2 = x[1:n] |
| 113 | + tmps = s[0.:(n - 1)] |
| 114 | + tmps2 = s[1:n] |
| 115 | + diffs = np.diff(s) |
| 116 | + |
| 117 | + # structure to contain information associated with each knot, used to |
| 118 | + # calculate coefficients |
| 119 | + uu = np.zeros((k, 6.)) |
| 120 | + |
| 121 | + uu[0:(n - 1), :] = np.array([tmpx, tmpx2, tmpy, tmps, tmps2, delta]).T |
| 122 | + |
| 123 | + # [2], Algorithm 4.1 subpart 1 of Step 5 |
| 124 | + # original x values that are left points of intervals without internal |
| 125 | + # knots |
| 126 | + xk[u] = tmpx[u] |
| 127 | + yk[u] = tmpy[u] |
| 128 | + # constant term for each polynomial for intervals without knots |
| 129 | + a[u, 2] = tmpy[u] |
| 130 | + a[u, 1] = s[u] |
| 131 | + a[u, 0] = .5 * diffs[u] / delx[u] # leading coefficients |
| 132 | + |
| 133 | + # [2], Algorithm 4.1 subpart 2 of Step 5 |
| 134 | + # original x values that are left points of intervals with internal knots |
| 135 | + xk[~u] = tmpx[~u] |
| 136 | + yk[~u] = tmpy[~u] |
| 137 | + |
| 138 | + aa = s[0:(n - 1)] - delta[0:(n - 1)] |
| 139 | + b = s[1:n] - delta[0:(n - 1)] |
| 140 | + |
| 141 | + sbar = np.zeros(k) |
| 142 | + eta = np.zeros(k) |
| 143 | + # will contain mapping from the left points of intervals containing an |
| 144 | + # added knot to each inverval's internal knot value |
| 145 | + xi = np.zeros(k) |
| 146 | + |
| 147 | + t0 = aa * b >= 0 |
| 148 | + # first 'else' in Algorithm 4.1 Step 5 |
| 149 | + v = np.logical_and(~u, t0[0:len(u)]) |
| 150 | + q = np.sum(v) # number of this type of knot to add |
| 151 | + |
| 152 | + if q > 0.: |
| 153 | + xk[(n - 1):(n + q - 1)] = .5 * (tmpx[v] + tmpx2[v]) # knot location |
| 154 | + uu[(n - 1):(n + q - 1), :] = np.array([tmpx[v], tmpx2[v], tmpy[v], |
| 155 | + tmps[v], tmps2[v], delta[v]]).T |
| 156 | + xi[v] = xk[(n - 1):(n + q - 1)] |
| 157 | + |
| 158 | + t1 = np.abs(aa) > np.abs(b) |
| 159 | + w = np.logical_and(~u, ~v) # second 'else' in Algorithm 4.1 Step 5 |
| 160 | + w = np.logical_and(w, t1) |
| 161 | + r = np.sum(w) |
| 162 | + |
| 163 | + if r > 0.: |
| 164 | + xk[(n + q - 1):(n + q + r - 1)] = tmpx2[w] + aa[w] * delx[w] / diffs[w] |
| 165 | + uu[(n + q - 1):(n + q + r - 1), :] = np.array([tmpx[w], tmpx2[w], |
| 166 | + tmpy[w], tmps[w], |
| 167 | + tmps2[w], delta[w]]).T |
| 168 | + xi[w] = xk[(n + q - 1):(n + q + r - 1)] |
| 169 | + |
| 170 | + z = np.logical_and(~u, ~v) # last 'else' in Algorithm 4.1 Step 5 |
| 171 | + z = np.logical_and(z, ~w) |
| 172 | + ss = np.sum(z) |
| 173 | + |
| 174 | + if ss > 0.: |
| 175 | + xk[(n + q + r - 1):(n + q + r + ss - 1)] = tmpx[z] + b[z] * delx[z] / \ |
| 176 | + diffs[z] |
| 177 | + uu[(n + q + r - 1):(n + q + r + ss - 1), :] = \ |
| 178 | + np.array([tmpx[z], tmpx2[z], tmpy[z], tmps[z], tmps2[z], |
| 179 | + delta[z]]).T |
| 180 | + xi[z] = xk[(n + q + r - 1):(n + q + r + ss - 1)] |
| 181 | + |
| 182 | + # define polynomial coefficients for intervals with added knots |
| 183 | + ff = ~u |
| 184 | + sbar[ff] = (2 * uu[ff, 5] - uu[ff, 4]) + \ |
| 185 | + (uu[ff, 4] - uu[ff, 3]) * (xi[ff] - uu[ff, 0]) / (uu[ff, 1] - |
| 186 | + uu[ff, 0]) |
| 187 | + eta[ff] = (sbar[ff] - uu[ff, 3]) / (xi[ff] - uu[ff, 0]) |
| 188 | + |
| 189 | + sbar[(n - 1):(n + q + r + ss - 1)] = \ |
| 190 | + (2 * uu[(n - 1):(n + q + r + ss - 1), 5] - |
| 191 | + uu[(n - 1):(n + q + r + ss - 1), 4]) + \ |
| 192 | + (uu[(n - 1):(n + q + r + ss - 1), 4] - |
| 193 | + uu[(n - 1):(n + q + r + ss - 1), 3]) * \ |
| 194 | + (xk[(n - 1):(n + q + r + ss - 1)] - |
| 195 | + uu[(n - 1):(n + q + r + ss - 1), 0]) / \ |
| 196 | + (uu[(n - 1):(n + q + r + ss - 1), 1] - |
| 197 | + uu[(n - 1):(n + q + r + ss - 1), 0]) |
| 198 | + eta[(n - 1):(n + q + r + ss - 1)] = \ |
| 199 | + (sbar[(n - 1):(n + q + r + ss - 1)] - |
| 200 | + uu[(n - 1):(n + q + r + ss - 1), 3]) / \ |
| 201 | + (xk[(n - 1):(n + q + r + ss - 1)] - |
| 202 | + uu[(n - 1):(n + q + r + ss - 1), 0]) |
| 203 | + |
| 204 | + # constant term for polynomial for intervals with internal knots |
| 205 | + a[~u, 2] = uu[~u, 2] |
| 206 | + a[~u, 1] = uu[~u, 3] |
| 207 | + a[~u, 0] = .5 * eta[~u] # leading coefficient |
| 208 | + |
| 209 | + a[(n - 1):(n + q + r + ss - 1), 2] = \ |
| 210 | + uu[(n - 1):(n + q + r + ss - 1), 2] + \ |
| 211 | + uu[(n - 1):(n + q + r + ss - 1), 3] * \ |
| 212 | + (xk[(n - 1):(n + q + r + ss - 1)] - |
| 213 | + uu[(n - 1):(n + q + r + ss - 1), 0]) + \ |
| 214 | + .5 * eta[(n - 1):(n + q + r + ss - 1)] * \ |
| 215 | + (xk[(n - 1):(n + q + r + ss - 1)] - |
| 216 | + uu[(n - 1):(n + q + r + ss - 1), 0]) ** 2. |
| 217 | + a[(n - 1):(n + q + r + ss - 1), 1] = sbar[(n - 1):(n + q + r + ss - 1)] |
| 218 | + a[(n - 1):(n + q + r + ss - 1), 0] = \ |
| 219 | + .5 * (uu[(n - 1):(n + q + r + ss - 1), 4] - |
| 220 | + sbar[(n - 1):(n + q + r + ss - 1)]) / \ |
| 221 | + (uu[(n - 1):(n + q + r + ss - 1), 1] - |
| 222 | + uu[(n - 1):(n + q + r + ss - 1), 0]) |
| 223 | + |
| 224 | + yk[(n - 1):(n + q + r + ss - 1)] = a[(n - 1):(n + q + r + ss - 1), 2] |
| 225 | + |
| 226 | + xk[n + q + r + ss - 1] = x[n - 1] |
| 227 | + yk[n + q + r + ss - 1] = y[n - 1] |
| 228 | + flag[(n - 1):(n + q + r + ss - 1)] = True # these are all inserted knots |
| 229 | + |
| 230 | + tmp = np.vstack((xk, a.T, yk, flag)).T |
| 231 | + # sort output in terms of increasing x (original plus added knots) |
| 232 | + tmp2 = tmp[tmp[:, 0].argsort(kind='mergesort')] |
| 233 | + outxk = tmp2[:, 0] |
| 234 | + outn = len(outxk) |
| 235 | + outa = tmp2[0:(outn - 1), 1:4] |
| 236 | + outy = tmp2[:, 4] |
| 237 | + kflag = tmp2[:, 5] |
| 238 | + return outa, outxk, outy, kflag |
0 commit comments