|
| 1 | +# bspline.py - B-spline basis functions |
| 2 | +# RMM, 2 Aug 2022 |
| 3 | +# |
| 4 | +# This class implements a set of B-spline basis functions that implement a |
| 5 | +# piecewise polynomial at a set of breakpoints t0, ..., tn with given orders |
| 6 | +# and smoothness. |
| 7 | +# |
| 8 | + |
| 9 | +importnumpyasnp |
| 10 | +from .basisimportBasisFamily |
| 11 | +fromscipy.interpolateimportBSpline,splev |
| 12 | + |
| 13 | +classBSplineFamily(BasisFamily): |
| 14 | +"""B-spline basis functions. |
| 15 | +
|
| 16 | + This class represents a B-spline basis for piecewise polynomials defined |
| 17 | + across a set of breakpoints with given degree and smoothness. On each |
| 18 | + interval between two breakpoints, we have a polynomial of a given degree |
| 19 | + and the spline is continuous up to a given smoothness at interior |
| 20 | + breakpoints. |
| 21 | +
|
| 22 | + Parameters |
| 23 | + ---------- |
| 24 | + breakpoints : 1D array or 2D array of float |
| 25 | + The breakpoints for the spline(s). |
| 26 | +
|
| 27 | + degree : int or list of ints |
| 28 | + For each spline variable, the degree of the polynomial between |
| 29 | + breakpoints. If a single number is given and more than one spline |
| 30 | + variable is specified, the same degree is used for each spline |
| 31 | + variable. |
| 32 | +
|
| 33 | + smoothness : int or list of ints |
| 34 | + For each spline variable, the smoothness at breakpoints (number of |
| 35 | + derivatives that should match). |
| 36 | +
|
| 37 | + vars : None or int, optional |
| 38 | + The number of spline variables. If specified as None (default), |
| 39 | + then the spline basis describes a single variable, with no indexing. |
| 40 | + If the number of spine variables is > 0, then the spline basis is |
| 41 | + index using the `var` keyword. |
| 42 | +
|
| 43 | + """ |
| 44 | +def__init__(self,breakpoints,degree,smoothness=None,vars=None): |
| 45 | +"""Create a B-spline basis for piecewise smooth polynomials.""" |
| 46 | +# Process the breakpoints for the spline */ |
| 47 | +breakpoints=np.array(breakpoints,dtype=float) |
| 48 | +ifbreakpoints.ndim==2: |
| 49 | +raiseNotImplementedError( |
| 50 | +"breakpoints for each spline variable not yet supported") |
| 51 | +elifbreakpoints.ndim!=1: |
| 52 | +raiseValueError("breakpoints must be convertable to a 1D array") |
| 53 | +elifbreakpoints.size<2: |
| 54 | +raiseValueError("break point vector must have at least 2 values") |
| 55 | +elifnp.any(np.diff(breakpoints)<=0): |
| 56 | +raiseValueError("break points must be strictly increasing values") |
| 57 | + |
| 58 | +# Decide on the number of spline variables |
| 59 | +ifvarsisNone: |
| 60 | +nvars=1 |
| 61 | +self.nvars=None# track as single variable |
| 62 | +elifnotisinstance(vars,int): |
| 63 | +raiseTypeError("vars must be an integer") |
| 64 | +else: |
| 65 | +nvars=vars |
| 66 | +self.nvars=nvars |
| 67 | + |
| 68 | +# |
| 69 | +# Process B-spline parameters (degree, smoothness) |
| 70 | +# |
| 71 | +# B-splines are defined on a set of intervals separated by |
| 72 | +# breakpoints. On each interval we have a polynomial of a certain |
| 73 | +# degree and the spline is continuous up to a given smoothness at |
| 74 | +# breakpoints. The code in this section allows some flexibility in |
| 75 | +# the way that all of this information is supplied, including using |
| 76 | +# scalar values for parameters (which are then broadcast to each |
| 77 | +# output) and inferring values and dimensions from other |
| 78 | +# information, when possible. |
| 79 | +# |
| 80 | + |
| 81 | +# Utility function for broadcasting spline params (degree, smoothness) |
| 82 | +defprocess_spline_parameters( |
| 83 | +values,length,allowed_types,minimum=0, |
| 84 | +default=None,name='unknown'): |
| 85 | + |
| 86 | +# Preprocessing |
| 87 | +ifvaluesisNoneanddefaultisNone: |
| 88 | +returnNone |
| 89 | +elifvaluesisNone: |
| 90 | +values=default |
| 91 | +elifisinstance(values,np.ndarray): |
| 92 | +# Convert ndarray to list |
| 93 | +values=values.tolist() |
| 94 | + |
| 95 | +# Figure out what type of object we were passed |
| 96 | +ifisinstance(values,allowed_types): |
| 97 | +# Single number of an allowed type => broadcast to list |
| 98 | +values= [valuesforiinrange(length)] |
| 99 | +elifall([isinstance(v,allowed_types)forvinvalues]): |
| 100 | +# List of values => make sure it is the right size |
| 101 | +iflen(values)!=length: |
| 102 | +raiseValueError(f"length of '{name}' does not match" |
| 103 | +f" number of variables") |
| 104 | +else: |
| 105 | +raiseValueError(f"could not parse '{name}' keyword") |
| 106 | + |
| 107 | +# Check to make sure the values are OK |
| 108 | +ifvaluesisnotNoneandany([val<minimumforvalinvalues]): |
| 109 | +raiseValueError( |
| 110 | +f"invalid value for '{name}'; must be at least{minimum}") |
| 111 | + |
| 112 | +returnvalues |
| 113 | + |
| 114 | +# Degree of polynomial |
| 115 | +degree=process_spline_parameters( |
| 116 | +degree,nvars, (int),name='degree',minimum=1) |
| 117 | + |
| 118 | +# Smoothness at breakpoints; set default to degree - 1 (max possible) |
| 119 | +smoothness=process_spline_parameters( |
| 120 | +smoothness,nvars, (int),name='smoothness',minimum=0, |
| 121 | +default=[d-1fordindegree]) |
| 122 | + |
| 123 | +# Make sure degree is sufficent for the level of smoothness |
| 124 | +ifany([degree[i]-smoothness[i]<1foriinrange(nvars)]): |
| 125 | +raiseValueError("degree must be greater than smoothness") |
| 126 | + |
| 127 | +# Store the parameters for the spline (self.nvars already stored) |
| 128 | +self.breakpoints=breakpoints |
| 129 | +self.degree=degree |
| 130 | +self.smoothness=smoothness |
| 131 | + |
| 132 | +# |
| 133 | +# Compute parameters for a SciPy BSpline object |
| 134 | +# |
| 135 | +# To create a B-spline, we need to compute the knotpoints, keeping |
| 136 | +# track of the use of repeated knotpoints at the initial knot and |
| 137 | +# final knot as well as repeated knots at intermediate points |
| 138 | +# depending on the desired smoothness. |
| 139 | +# |
| 140 | + |
| 141 | +# Store the coefficients for each output (useful later) |
| 142 | +self.coef_offset,self.coef_length,offset= [], [],0 |
| 143 | +foriinrange(nvars): |
| 144 | +# Compute number of coefficients for the piecewise polynomial |
| 145 | +ncoefs= (self.degree[i]+1)* (len(self.breakpoints)-1)- \ |
| 146 | + (self.smoothness[i]+1)* (len(self.breakpoints)-2) |
| 147 | + |
| 148 | +self.coef_offset.append(offset) |
| 149 | +self.coef_length.append(ncoefs) |
| 150 | +offset+=ncoefs |
| 151 | +self.N=offset# save the total number of coefficients |
| 152 | + |
| 153 | +# Create knotpoints for each spline variable |
| 154 | +# TODO: extend to multi-dimensional breakpoints |
| 155 | +self.knotpoints= [] |
| 156 | +foriinrange(nvars): |
| 157 | +# Allocate space for the knotpoints |
| 158 | +self.knotpoints.append(np.empty( |
| 159 | + (self.degree[i]+1)+ (len(self.breakpoints)-2)* \ |
| 160 | + (self.degree[i]-self.smoothness[i])+ (self.degree[i]+1))) |
| 161 | + |
| 162 | +# Initial knotpoints (multiplicity = order) |
| 163 | +self.knotpoints[i][0:self.degree[i]+1]=self.breakpoints[0] |
| 164 | +offset=self.degree[i]+1 |
| 165 | + |
| 166 | +# Interior knotpoints (multiplicity = degree - smoothness) |
| 167 | +nknots=self.degree[i]-self.smoothness[i] |
| 168 | +assertnknots>0# just in case |
| 169 | +forjinrange(1,self.breakpoints.size-1): |
| 170 | +self.knotpoints[i][offset:offset+nknots]=self.breakpoints[j] |
| 171 | +offset+=nknots |
| 172 | + |
| 173 | +# Final knotpoint (multiplicity = order) |
| 174 | +self.knotpoints[i][offset:offset+self.degree[i]+1]= \ |
| 175 | +self.breakpoints[-1] |
| 176 | + |
| 177 | +def__repr__(self): |
| 178 | +returnf'<{self.__class__.__name__}: nvars={self.nvars}, '+ \ |
| 179 | +f'degree={self.degree}, smoothness={self.smoothness}>' |
| 180 | + |
| 181 | +# Compute the kth derivative of the ith basis function at time t |
| 182 | +defeval_deriv(self,i,k,t,var=None): |
| 183 | +"""Evaluate the kth derivative of the ith basis function at time t.""" |
| 184 | +ifself.nvarsisNoneor (self.nvars==1andvarisNone): |
| 185 | +# Use same variable for all requests |
| 186 | +var=0 |
| 187 | +elifself.nvars>1andvarisNone: |
| 188 | +raiseSystemError( |
| 189 | +"scalar variable call to multi-variable splines") |
| 190 | + |
| 191 | +# Create a coefficient vector for this spline |
| 192 | +coefs=np.zeros(self.coef_length[var]);coefs[i]=1 |
| 193 | + |
| 194 | +# Evaluate the derivative of the spline at the desired point in time |
| 195 | +returnBSpline(self.knotpoints[var],coefs, |
| 196 | +self.degree[var]).derivative(k)(t) |