Loading...
Searching...
No Matches
bsplinebuilder.C
1/*
2 * This file is part of the SPLINTER library.
3 * Copyright (C) 2012 Bjarne Grimstad (bjarne.grimstad@gmail.com).
4 *
5 * This Source Code Form is subject to the terms of the Mozilla Public
6 * License, v. 2.0. If a copy of the MPL was not distributed with this
7 * file, You can obtain one at http://mozilla.org/MPL/2.0/.
8*/
9
10#include "bsplinebuilder.h"
11#include "mykroneckerproduct.h"
12#include <unsupported/Eigen/KroneckerProduct>
13#include <linearsolvers.h>
14#include <serializer.h>
15#include <iostream>
16#include <utilities.h>
17#include "IOstreams.H"
18
19namespace SPLINTER
20{
21// Default constructor
22BSpline::Builder::Builder(const DataTable& data)
23 :
24 _data(data),
25 _degrees(getBSplineDegrees(data.getNumVariables(), 3)),
26 _numBasisFunctions(std::vector<unsigned int>(data.getNumVariables(), 0)),
27 _knotSpacing(KnotSpacing::AS_SAMPLED),
28 _smoothing(Smoothing::NONE),
29 _alpha(0.1)
30{
31}
32
33/*
34 * Build B-spline
35 */
36BSpline BSpline::Builder::build() const
37{
38 // Check data
39 // TODO: Remove this test
40 if (!_data.isGridComplete())
41 {
42 throw Exception("BSpline::Builder::build: Cannot create B-spline from irregular (incomplete) grid.");
43 }
44
45 // Build knot vectors
46 auto knotVectors = computeKnotVectors();
47 // Build B-spline (with default coefficients)
48 auto bspline = BSpline(knotVectors, _degrees);
49 // Compute coefficients from samples and update B-spline
50 auto coefficients = computeCoefficients(bspline);
51 bspline.setCoefficients(coefficients);
52 return bspline;
53}
54
55/*
56 * Find coefficients of B-spline by solving:
57 * min ||A*x - b||^2 + alpha*||R||^2,
58 * where
59 * A = mxn matrix of n basis functions evaluated at m sample points,
60 * b = vector of m sample points y-values (or x-values when calculating knot averages),
61 * x = B-spline coefficients (or knot averages),
62 * R = Regularization matrix,
63 * alpha = regularization parameter.
64 */
65DenseVector BSpline::Builder::computeCoefficients(const BSpline& bspline) const
66{
67 SparseMatrix B = computeBasisFunctionMatrix(bspline);
68 SparseMatrix A = B;
69 DenseVector b = getSamplePointValues();
70
71 if (_smoothing == Smoothing::IDENTITY)
72 {
73 /*
74 * Computing B-spline coefficients with a regularization term
75 * ||Ax-b||^2 + alpha*x^T*x
76 *
77 * NOTE: This corresponds to a Tikhonov regularization (or ridge regression) with the Identity matrix.
78 * See: https://en.wikipedia.org/wiki/Tikhonov_regularization
79 *
80 * NOTE2: consider changing regularization factor to (alpha/numSample)
81 */
82 SparseMatrix Bt = B.transpose();
83 A = Bt * B;
84 b = Bt * b;
85 auto I = SparseMatrix(A.cols(), A.cols());
86 I.setIdentity();
87 A += _alpha * I;
88 }
89 else if (_smoothing == Smoothing::PSPLINE)
90 {
91 /*
92 * The P-Spline is a smooting B-spline which relaxes the interpolation constraints on the control points to allow
93 * smoother spline curves. It minimizes an objective which penalizes both deviation from sample points (to lower bias)
94 * and the magnitude of second derivatives (to lower variance).
95 *
96 * Setup and solve equations Ax = b,
97 * A = B'*W*B + l*D'*D
98 * b = B'*W*y
99 * x = control coefficients or knot averages.
100 * B = basis functions at sample x-values,
101 * W = weighting matrix for interpolating specific points
102 * D = second-order finite difference matrix
103 * l = penalizing parameter (increase for more smoothing)
104 * y = sample y-values when calculating control coefficients,
105 * y = sample x-values when calculating knot averages
106 */
107 // Assuming regular grid
108 unsigned int numSamples = _data.getNumSamples();
109 SparseMatrix Bt = B.transpose();
110 // Weight matrix
111 SparseMatrix W;
112 W.resize(numSamples, numSamples);
113 W.setIdentity();
114 // Second order finite difference matrix
115 SparseMatrix D = getSecondOrderFiniteDifferenceMatrix(bspline);
116 // Left-hand side matrix
117 A = Bt * W * B + _alpha * D.transpose() * D;
118 // Compute right-hand side matrices
119 b = Bt * W * b;
120 }
121
122 DenseVector x;
123 int numEquations = A.rows();
124 int maxNumEquations = 100;
125 bool solveAsDense = (numEquations < maxNumEquations);
126
127 if (!solveAsDense)
128 {
129#ifndef NDEBUG
130 Foam::Info <<
131 "BSpline::Builder::computeBSplineCoefficients: Computing B-spline control points using sparse solver."
132 << Foam::endl;
133#endif // NDEBUG
134 SparseLU<> s;
135 //bool successfulSolve = (s.solve(A,Bx,Cx) && s.solve(A,By,Cy));
136 solveAsDense = !s.solve(A, b, x);
137 }
138
139 if (solveAsDense)
140 {
141#ifndef NDEBUG
142 Foam::Info <<
143 "BSpline::Builder::computeBSplineCoefficients: Computing B-spline control points using dense solver."
144 << Foam::endl;
145#endif // NDEBUG
146 DenseMatrix Ad = A.toDense();
147 DenseQR<DenseVector> s;
148
149 // DenseSVD<DenseVector> s;
150 //bool successfulSolve = (s.solve(Ad,Bx,Cx) && s.solve(Ad,By,Cy));
151 if (!s.solve(Ad, b, x))
152 {
153 throw Exception("BSpline::Builder::computeBSplineCoefficients: Failed to solve for B-spline coefficients.");
154 }
155 }
156
157 return x;
158}
159
160SparseMatrix BSpline::Builder::computeBasisFunctionMatrix(
161 const BSpline& bspline) const
162{
163 unsigned int numVariables = _data.getNumVariables();
164 unsigned int numSamples = _data.getNumSamples();
165 // TODO: Reserve nnz per row (degree+1)
166 //int nnzPrCol = bspline.basis.supportedPrInterval();
167 SparseMatrix A(numSamples, bspline.getNumBasisFunctions());
168 //A.reserve(DenseVector::Constant(numSamples, nnzPrCol)); // TODO: should reserve nnz per row!
169 int i = 0;
170
171 for (auto it = _data.cbegin(); it != _data.cend(); ++it, ++i)
172 {
173 DenseVector xi(numVariables);
174 xi.setZero();
175 std::vector<double> xv = it->getX();
176
177 for (unsigned int j = 0; j < numVariables; ++j)
178 {
179 xi(j) = xv.at(j);
180 }
181
182 SparseVector basisValues = bspline.evalBasis(xi);
183
184 for (SparseVector::InnerIterator it2(basisValues); it2; ++it2)
185 {
186 A.insert(i, it2.index()) = it2.value();
187 }
188 }
189
190 A.makeCompressed();
191 return A;
192}
193
194DenseVector BSpline::Builder::getSamplePointValues() const
195{
196 DenseVector B = DenseVector::Zero(_data.getNumSamples());
197 int i = 0;
198
199 for (auto it = _data.cbegin(); it != _data.cend(); ++it, ++i)
200 {
201 B(i) = it->getY();
202 }
203
204 return B;
205}
206
207/*
208* Function for generating second order finite-difference matrix, which is used for penalizing the
209* (approximate) second derivative in control point calculation for P-splines.
210*/
211SparseMatrix BSpline::Builder::getSecondOrderFiniteDifferenceMatrix(
212 const BSpline& bspline) const
213{
214 unsigned int numVariables = bspline.getNumVariables();
215 // Number of (total) basis functions - defines the number of columns in D
216 unsigned int numCols = bspline.getNumBasisFunctions();
217 std::vector<unsigned int> numBasisFunctions =
218 bspline.getNumBasisFunctionsPerVariable();
219 // Number of basis functions (and coefficients) in each variable
220 std::vector<unsigned int> dims;
221
222 for (unsigned int i = 0; i < numVariables; i++)
223 {
224 dims.push_back(numBasisFunctions.at(i));
225 }
226
227 std::reverse(dims.begin(), dims.end());
228
229 for (unsigned int i = 0; i < numVariables; ++i)
230 if (numBasisFunctions.at(i) < 3)
231 {
232 throw Exception("BSpline::Builder::getSecondOrderDifferenceMatrix: Need at least three coefficients/basis function per variable.");
233 }
234
235 // Number of rows in D and in each block
236 int numRows = 0;
237 std::vector< int > numBlkRows;
238
239 for (unsigned int i = 0; i < numVariables; i++)
240 {
241 int prod = 1;
242
243 for (unsigned int j = 0; j < numVariables; j++)
244 {
245 if (i == j)
246 {
247 prod *= (dims[j] - 2);
248 }
249 else
250 {
251 prod *= dims[j];
252 }
253 }
254
255 numRows += prod;
256 numBlkRows.push_back(prod);
257 }
258
259 // Resize and initialize D
260 SparseMatrix D(numRows, numCols);
261 D.reserve(DenseVector::Constant(numCols,
262 2 * numVariables)); // D has no more than two elems per col per dim
263 int i = 0; // Row index
264
265 // Loop though each dimension (each dimension has its own block)
266 for (unsigned int d = 0; d < numVariables; d++)
267 {
268 // Calculate left and right products
269 int leftProd = 1;
270 int rightProd = 1;
271
272 for (unsigned int k = 0; k < d; k++)
273 {
274 leftProd *= dims[k];
275 }
276
277 for (unsigned int k = d + 1; k < numVariables; k++)
278 {
279 rightProd *= dims[k];
280 }
281
282 // Loop through subblocks on the block diagonal
283 for (int j = 0; j < rightProd; j++)
284 {
285 // Start column of current subblock
286 int blkBaseCol = j * leftProd * dims[d];
287
288 // Block rows [I -2I I] of subblock
289 for (unsigned int l = 0; l < (dims[d] - 2); l++)
290 {
291 // Special case for first dimension
292 if (d == 0)
293 {
294 int k = j * leftProd * dims[d] + l;
295 D.insert(i, k) = 1;
296 k += leftProd;
297 D.insert(i, k) = -2;
298 k += leftProd;
299 D.insert(i, k) = 1;
300 i++;
301 }
302 else
303 {
304 // Loop for identity matrix
305 for (int n = 0; n < leftProd; n++)
306 {
307 int k = blkBaseCol + l * leftProd + n;
308 D.insert(i, k) = 1;
309 k += leftProd;
310 D.insert(i, k) = -2;
311 k += leftProd;
312 D.insert(i, k) = 1;
313 i++;
314 }
315 }
316 }
317 }
318 }
319
320 D.makeCompressed();
321 return D;
322}
323
324// Compute all knot vectors from sample data
325std::vector<std::vector<double >> BSpline::Builder::computeKnotVectors() const
326{
327 if (_data.getNumVariables() != _degrees.size())
328 {
329 throw Exception("BSpline::Builder::computeKnotVectors: Inconsistent sizes on input vectors.");
330 }
331
332 std::vector<std::vector<double >> grid = _data.getTableX();
333 std::vector<std::vector<double >> knotVectors;
334
335 for (unsigned int i = 0; i < _data.getNumVariables(); ++i)
336 {
337 // Compute knot vector
338 auto knotVec = computeKnotVector(grid.at(i), _degrees.at(i),
339 _numBasisFunctions.at(i));
340 knotVectors.push_back(knotVec);
341 }
342
343 return knotVectors;
344}
345
346// Compute a single knot vector from sample grid and degree
347std::vector<double> BSpline::Builder::computeKnotVector(
348 const std::vector<double>& values,
349 unsigned int degree,
350 unsigned int numBasisFunctions) const
351{
352 switch (_knotSpacing)
353 {
354 case KnotSpacing::AS_SAMPLED:
355 return knotVectorMovingAverage(values, degree);
356
357 case KnotSpacing::EQUIDISTANT:
358 return knotVectorEquidistant(values, degree, numBasisFunctions);
359
360 case KnotSpacing::EXPERIMENTAL:
361 return knotVectorBuckets(values, degree);
362
363 default:
364 return knotVectorMovingAverage(values, degree);
365 }
366}
367
368/*
369* Automatic construction of (p+1)-regular knot vector
370* using moving average.
371*
372* Requirement:
373* Knot vector should be of size n+p+1.
374* End knots are should be repeated p+1 times.
375*
376* Computed sizes:
377* n+2*(p) = n + p + 1 + (p - 1)
378* k = (p - 1) values must be removed from sample vector.
379* w = k + 3 window size in moving average
380*
381* Algorithm:
382* 1) compute n - k values using moving average with window size w
383* 2) repeat first and last value p + 1 times
384*
385* The resulting knot vector has n - k + 2*p = n + p + 1 knots.
386*
387* NOTE:
388* For equidistant samples, the resulting knot vector is identically to
389* the free end conditions knot vector used in cubic interpolation.
390* That is, samples (a,b,c,d,e,f) produces the knot vector (a,a,a,a,c,d,f,f,f,f) for p = 3.
391* For p = 1, (a,b,c,d,e,f) becomes (a,a,b,c,d,e,f,f).
392*
393* TODO:
394* Does not work well when number of knots is << number of samples! For such cases
395* almost all knots will lie close to the left samples. Try a bucket approach, where the
396* samples are added to buckets and the knots computed as the average of these.
397*/
398std::vector<double> BSpline::Builder::knotVectorMovingAverage(
399 const std::vector<double>& values,
400 unsigned int degree) const
401{
402 // Sort and remove duplicates
403 std::vector<double> unique = extractUniqueSorted(values);
404 // Compute sizes
405 unsigned int n = unique.size();
406 unsigned int k = degree - 1; // knots to remove
407 unsigned int w = k + 3; // Window size
408
409 // The minimum number of samples from which a free knot vector can be created
410 if (n < degree + 1)
411 {
412 std::ostringstream e;
413 e << "knotVectorMovingAverage: Only " << n
414 << " unique interpolation points are given. A minimum of degree+1 = " << degree
415 + 1
416 << " unique points are required to build a B-spline basis of degree " << degree
417 << ".";
418 throw Exception(e.str());
419 }
420
421 std::vector<double> knots(n - k - 2, 0);
422
423 // Compute (n-k-2) interior knots using moving average
424 for (unsigned int i = 0; i < n - k - 2; ++i)
425 {
426 double ma = 0;
427
428 for (unsigned int j = 0; j < w; ++j)
429 {
430 ma += unique.at(i + j);
431 }
432
433 knots.at(i) = ma / w;
434 }
435
436 // Repeat first knot p + 1 times (for interpolation of start point)
437 for (unsigned int i = 0; i < degree + 1; ++i)
438 {
439 knots.insert(knots.begin(), unique.front());
440 }
441
442 // Repeat last knot p + 1 times (for interpolation of end point)
443 for (unsigned int i = 0; i < degree + 1; ++i)
444 {
445 knots.insert(knots.end(), unique.back());
446 }
447
448 // Number of knots in a (p+1)-regular knot vector
449 //assert(knots.size() == uniqueX.size() + degree + 1);
450 return knots;
451}
452
453std::vector<double> BSpline::Builder::knotVectorEquidistant(
454 const std::vector<double>& values,
455 unsigned int degree,
456 unsigned int numBasisFunctions = 0) const
457{
458 // Sort and remove duplicates
459 std::vector<double> unique = extractUniqueSorted(values);
460 // Compute sizes
461 unsigned int n = unique.size();
462
463 if (numBasisFunctions > 0)
464 {
465 n = numBasisFunctions;
466 }
467
468 unsigned int k = degree - 1; // knots to remove
469
470 // The minimum number of samples from which a free knot vector can be created
471 if (n < degree + 1)
472 {
473 std::ostringstream e;
474 e << "knotVectorMovingAverage: Only " << n
475 << " unique interpolation points are given. A minimum of degree+1 = " << degree
476 + 1
477 << " unique points are required to build a B-spline basis of degree " << degree
478 << ".";
479 throw Exception(e.str());
480 }
481
482 // Compute (n-k-2) equidistant interior knots
483 unsigned int numIntKnots = std::max(n - k - 2, (unsigned int)0);
484 numIntKnots = std::min((unsigned int)10, numIntKnots);
485 std::vector<double> knots = linspace(unique.front(), unique.back(),
486 numIntKnots);
487
488 // Repeat first knot p + 1 times (for interpolation of start point)
489 for (unsigned int i = 0; i < degree; ++i)
490 {
491 knots.insert(knots.begin(), unique.front());
492 }
493
494 // Repeat last knot p + 1 times (for interpolation of end point)
495 for (unsigned int i = 0; i < degree; ++i)
496 {
497 knots.insert(knots.end(), unique.back());
498 }
499
500 // Number of knots in a (p+1)-regular knot vector
501 //assert(knots.size() == uniqueX.size() + degree + 1);
502 return knots;
503}
504
505std::vector<double> BSpline::Builder::knotVectorBuckets(
506 const std::vector<double>& values, unsigned int degree,
507 unsigned int maxSegments) const
508{
509 // Sort and remove duplicates
510 std::vector<double> unique = extractUniqueSorted(values);
511
512 // The minimum number of samples from which a free knot vector can be created
513 if (unique.size() < degree + 1)
514 {
515 std::ostringstream e;
516 e << "BSpline::Builder::knotVectorBuckets: Only " << unique.size()
517 << " unique sample points are given. A minimum of degree+1 = " << degree + 1
518 << " unique points are required to build a B-spline basis of degree " << degree
519 << ".";
520 throw Exception(e.str());
521 }
522
523 // Num internal knots (0 <= ni <= unique.size() - degree - 1)
524 unsigned int ni = unique.size() - degree - 1;
525 // Num segments
526 unsigned int ns = ni + degree + 1;
527
528 // Limit number of segments
529 if (ns > maxSegments && maxSegments >= degree + 1)
530 {
531 ns = maxSegments;
532 ni = ns - degree - 1;
533 }
534
535 // Num knots
536 // unsigned int nk = ns + degree + 1;
537
538 // Check numbers
539 if (ni > unique.size() - degree - 1)
540 {
541 throw Exception("BSpline::Builder::knotVectorBuckets: Invalid number of internal knots!");
542 }
543
544 // Compute window sizes
545 unsigned int w = 0;
546
547 if (ni > 0)
548 {
549 w = std::floor(unique.size() / ni);
550 }
551
552 // Residual
553 unsigned int res = unique.size() - w * ni;
554 // Create array with window sizes
555 std::vector<unsigned int> windows(ni, w);
556
557 // Add residual
558 for (unsigned int i = 0; i < res; ++i)
559 {
560 windows.at(i) += 1;
561 }
562
563 // Compute internal knots
564 std::vector<double> knots(ni, 0);
565 // Compute (n-k-2) interior knots using moving average
566 unsigned int index = 0;
567
568 for (unsigned int i = 0; i < ni; ++i)
569 {
570 for (unsigned int j = 0; j < windows.at(i); ++j)
571 {
572 knots.at(i) += unique.at(index + j);
573 }
574
575 knots.at(i) /= windows.at(i);
576 index += windows.at(i);
577 }
578
579 // Repeat first knot p + 1 times (for interpolation of start point)
580 for (unsigned int i = 0; i < degree + 1; ++i)
581 {
582 knots.insert(knots.begin(), unique.front());
583 }
584
585 // Repeat last knot p + 1 times (for interpolation of end point)
586 for (unsigned int i = 0; i < degree + 1; ++i)
587 {
588 knots.insert(knots.end(), unique.back());
589 }
590
591 return knots;
592}
593
594std::vector<double> BSpline::Builder::extractUniqueSorted(
595 const std::vector<double>& values) const
596{
597 // Sort and remove duplicates
598 std::vector<double> unique(values);
599 std::sort(unique.begin(), unique.end());
600 std::vector<double>::iterator it = unique_copy(unique.begin(), unique.end(),
601 unique.begin());
602 unique.resize(distance(unique.begin(), it));
603 return unique;
604}
605
606} // namespace SPLINTER