vspline 1.1.0
Generic C++11 Code for Uniform B-Splines
bootstrap.cc
Go to the documentation of this file.
1/************************************************************************/
2/* */
3/* vspline - a set of generic tools for creation and evaluation */
4/* of uniform b-splines */
5/* */
6/* Copyright 2015 - 2023 by Kay F. Jahnke */
7/* */
8/* The git repository for this software is at */
9/* */
10/* https://bitbucket.org/kfj/vspline */
11/* */
12/* Please direct questions, bug reports, and contributions to */
13/* */
14/* kfjahnke+vspline@gmail.com */
15/* */
16/* Permission is hereby granted, free of charge, to any person */
17/* obtaining a copy of this software and associated documentation */
18/* files (the "Software"), to deal in the Software without */
19/* restriction, including without limitation the rights to use, */
20/* copy, modify, merge, publish, distribute, sublicense, and/or */
21/* sell copies of the Software, and to permit persons to whom the */
22/* Software is furnished to do so, subject to the following */
23/* conditions: */
24/* */
25/* The above copyright notice and this permission notice shall be */
26/* included in all copies or substantial portions of the */
27/* Software. */
28/* */
29/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND */
30/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES */
31/* OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND */
32/* NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT */
33/* HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, */
34/* WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING */
35/* FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR */
36/* OTHER DEALINGS IN THE SOFTWARE. */
37/* */
38/************************************************************************/
39
40/// \file bootstrap.cc
41///
42/// \brief Code to calculate b-spline basis function values at half
43/// unit steps, and prefilter poles.
44///
45/// These calculations are done using GNU GSL, BLAS and GNU GMP
46/// which are not used in other parts of vspline. this file also has
47/// code to verify that the precomputed data in <vspline/poles.h>
48/// are valid. TODO make that a separate program
49///
50/// In contrast to my previous implementation of generating the
51/// precomputed values (prefilter_poles.cc, now gone), I don't
52/// compute the values one-by-one using recursive functions, but
53/// instead build them layer by layer, each layer using it's
54/// predecessor. This makes the process very fast, even for
55/// large spline degrees.
56///
57/// In my tests, I found that with the very precise basis function
58/// values obtained with GMP, I could get GSL to provide raw values
59/// for the prefilter poles up to degree 45, above which it would
60/// miss roots. So this is where I set the limit, and I think it's
61/// unlikely anyone would ever want more than degree 45. If they
62/// do, they'll have to find other polynomial root code.
63///
64/// compile: g++ -O3 -std=gnu++11 bootstrap.cc -obootstrap \
65/// -lgmpxx -lgmp -lgsl -lblas -pthread
66///
67/// run by simply issuing './bootstrap' on the command line. you may
68/// want to redirect output to a file. The output should be equal
69/// to the values in vspline/poles.h
70
71#include <stdio.h>
72#include <iostream>
73#include <iomanip>
74#include <cmath>
75#include <limits>
76#include <numeric>
77#include <assert.h>
78#include <gsl/gsl_poly.h>
79#include <gmpxx.h>
80
81// #include <eigen3/unsupported/Eigen/Polynomials>
82
83#include "vspline/vspline.h"
84
85// In my trials, I could take the spline degree up to 45 when calculating
86// the prefilter poles. The basis function value can be found for arbitrary
87// degrees.
88
89#define degree_limit 46
90
91// when using mpf_class from GMP, we use 512 bits of precision.
92
93#define mpf_bits 512
94
95// we want to build a table of basis function values at multiples
96// of 1/2 mimicking the Cox-de Boor recursion. We only need one half
97// of the table since the basis function is symmetric (b(x) == b(-x))
98// - except for degree 0, which needs special treatment.
99// Note that we 'invert' the Cox-de Boor recursion and generate
100// the result iteratively, starting with degree 0. This results in
101// every single difference relation being executed excactly once,
102// which makes the iterative solution very fast.
103// Note the use of x2 throughout, this is the doubled argument,
104// so that we can operate in integral maths throughout the basis
105// function calculations (we use mpq_class for arbitrary-sized
106// fractions, so there is no error at all)
107// We make the table one column wider than strictly necessary to
108// allow access to x2 outside the range, in which case we want to
109// access a location containing 0.
110
111mpq_class basis_table [ degree_limit ] [ degree_limit + 1 ] ;
112
113// we want to access the table both for reading and writing, mapping
114// negative x2 to positive ones and treating access to the first row
115// (for degree 0) specially. Once the table is filled, we can access
116// the table at arbitrary x2.
117
118mpq_class & access_table ( int degree , int x2 )
119{
120 if ( degree >= degree_limit || degree < 0 )
121 throw std::invalid_argument ( "access with degree out of bounds" ) ;
122
123 int column ;
124
125 // at degree 0, we have no symmetry: the basis function is
126 // 1 for x2 == -1 and x2 == 0.
127 if ( degree == 0 )
128 {
129 if ( x2 == -1 || x2 == 0 )
130 column = 0 ;
131 else
132 column = 1 ;
133 }
134 // for all other degrees the basis function is symmetric
135 else
136 column = x2 >= 0 ? x2 : -x2 ;
137
138 // for out-of-range x2, land in the last column, which is 0.
139 column = std::min ( column , degree_limit ) ;
140
141 return basis_table [ degree ] [ column ] ;
142}
143
144// subroutine filling the table at a specific x2 and degree.
145// we rely on the table being filled correctly up to degree - 1
146
147void _fill_basis_table ( int x2 , int degree )
148{
149 // look up the neighbours one degree down and one to the
150 // left/right
151
152 mpq_class & f1 ( access_table ( degree - 1 , x2 + 1 ) ) ;
153 mpq_class & f2 ( access_table ( degree - 1 , x2 - 1 ) ) ;
154
155 // use the recursion to calculate the current value and store
156 // that value at the appropriate location in the table
157
158 access_table ( degree , x2 )
159 = ( f1 * ( degree + 1 + x2 )
160 + f2 * ( degree + 1 - x2 ) )
161 / ( 2 * degree ) ;
162}
163
164// to fill the whole table, we initialize the single value
165// in the first row, then fill the remainder by calling the
166// single-value subroutine for all non-zero locations
167
169{
170 access_table ( 0 , 0 ) = 1 ;
171 for ( int degree = 1 ; degree < degree_limit ; degree++ )
172 {
173 for ( int x2 = 0 ; x2 <= degree ; x2++ )
174 _fill_basis_table ( x2 , degree ) ;
175 }
176}
177
178// routine for one iteration of Newton's method applied to
179// a polynomial with ncoeff coefficients stored at pcoeff.
180// the polynomial and it's first derivative are computed
181// at x, then the quotient of function value and derivative
182// is subtracted from x, yielding the result, which is
183// closer to the zero we're looking for.
184
185template < typename dtype >
186void newton ( dtype & result , dtype x , int ncoeff , dtype * pcoeff )
187{
188 dtype * pk = pcoeff + ncoeff - 1 ; // point to last pcoefficient
189 dtype power = 1 ; // powers of x
190 int xd = 1 ; // ex derivative, n in (x^n)' = n(x^(n-1))
191 dtype fx = 0 ; // f(x)
192 dtype dfx = 0 ; // f'(x)
193
194 // we work our way back to front, starting with x^0 and raising
195 // the power with each iteration
196
197 fx += power * *pk ;
198 for ( ; ; )
199 {
200 --pk ;
201 if ( pk == pcoeff )
202 break ;
203 dfx += power * xd * *pk ;
204 xd++ ;
205 power *= x ;
206 fx += power * *pk ;
207 }
208 power *= x ;
209 fx += power * *pk ;
210 result = x - fx / dfx ;
211}
212
213// calculate_prefilter_poles relies on the table with basis function
214// values having been filled with fill_basis_table(). It extracts
215// the basis function values at even x2 (corresponding to whole x)
216// as double precision values, takes these to be coefficients of
217// a polynomial, and uses gsl and blas to obtain the roots of this
218// polynomial. The real parts of those roots which are less than one
219// are the raw values of the filter poles we're after.
220// Currently I don't have an arbitrary-precision root finder, so I
221// use the double precision one from GSL and submit the 'raw' result
222// to polishing code in high precision arithmetic. This only works
223// as far as the root finder can go, I found from degree 46 onwards
224// it fails to find some roots, so that's how far I take it for now.
225// The poles are stored in a std::vector of mpf_class and returned.
226// These values are precise to mpf_bits, since after using gsl's
227// root finder in double precision, they are polished as mpf_class
228// values with the newton method, so the very many postcomma digits
229// we print out below are all proper significant digits.
230
231void calculate_prefilter_poles ( std::vector<mpf_class> &res ,
232 int degree )
233{
234 const int r = degree / 2 ;
235
236 // we need storage for the coefficients of the polynomial
237 // first in double precision to feed gsl's root finder,
238 // then in mpf_class, to do the polishing
239
240 double coeffs [ 2 * r + 1 ] ;
241 mpf_class mpf_coeffs [ 2 * r + 1 ] ;
242
243 // here is space for the roots we want to compute
244
245 double roots [ 4 * r + 2 ] ;
246
247 // we fetch the coefficients from the table of b-spline basis
248 // function values at even x2, corresponding to whole x
249
250 double * pcoeffs = coeffs ;
251 mpf_class * pmpf_coeffs = mpf_coeffs ;
252
253 for ( int x2 = -r * 2 ;
254 x2 <= r * 2 ;
255 x2 += 2 , pcoeffs++ , pmpf_coeffs++ )
256 {
257 *pmpf_coeffs = access_table ( degree , x2 ) ;
258 *pcoeffs = access_table ( degree , x2 ) . get_d() ;
259 }
260
261 // we set up the environment gsl needs to find the roots
262
263 gsl_poly_complex_workspace * w
264 = gsl_poly_complex_workspace_alloc ( 2 * r + 1 ) ;
265
266 // now we call gsl's root finder
267
268 gsl_poly_complex_solve ( coeffs , 2 * r + 1 , w , roots ) ;
269
270 // and release it's workspace
271
272 gsl_poly_complex_workspace_free ( w ) ;
273
274 // we only look at the real parts of the roots, which are stored
275 // interleaved real/imag. And we take them back to front, even though
276 // it doesn't matter which end we start with - but conventionally
277 // Pole[0] is the root with the largest absolute, so I stick with that.
278
279 // I tried using eigen alternatively, but it found less roots
280 // than gsl/blas, so I stick with the GNU code, but for the reference,
281 // here is the code to use with eigen3 - for degrees up to 23 it was okay
282 // when I last tested. TODO: maybe the problem is that I did not get
283 // long double values to start with, which is tricky from GMP
284// {
285// using namespace Eigen ;
286//
287// Eigen::PolynomialSolver<long double, Eigen::Dynamic> solver;
288// Eigen::Matrix<long double,Dynamic, 1 > coeff(2*r+1);
289//
290// for ( int i = 0 ; i < 2*r+1 ; i++ )
291// {
292// // this is stupid: can't get a long double for an mpq_type
293// // TODO take the long route via a string (sigh...)
294// coeff[i] = mpf_coeffs[i].get_d() ;
295// }
296//
297// solver.compute(coeff);
298//
299// const Eigen::PolynomialSolver<long double, Eigen::Dynamic>::RootsType & r
300// = solver.roots();
301//
302// for ( int i = r.rows() - 1 ; i >= 0 ; i-- )
303// {
304// if ( std::fabs ( r[i].real() ) < 1 )
305// {
306// std::cout << "eigen gets " << r[i].real() << std::endl ;
307// }
308// }
309// }
310
311 for ( int i = 2 * r - 2 ; i >= 0 ; i -= 2 )
312 {
313 if ( std::abs ( roots[i] ) < 1.0 )
314 {
315 // fetch a double precision root from gsl's output
316 // converting to high-precision
317
318 mpf_class root ( roots[i] ) ;
319
320 // for the polishing process, we need a few more
321 // high-precision values
322
323 mpf_class pa , // current value of the iteration
324 pb , // previous value
325 pdelta ; // delta we want to go below
326
327 pa = root , pb = 0 ;
328 pdelta = "1e-300" ;
329
330 // while we're not yet below delta
331
332 while ( ( pa - pb ) * ( pa - pb ) >= pdelta )
333 {
334 // assign current to previous
335
336 pb = pa ;
337
338 // polish current value
339
340 newton ( pa , pa , 2*r+1 , mpf_coeffs ) ;
341 }
342
343 // polishing iteration terminated, we're content and store the value
344
345 root = pa ;
346 res.push_back ( root ) ;
347 }
348 }
349}
350
351// forward recursive filter, used for testing
352// this might be written as x[n] = x[n] + pole * x[n-1]
353// x[n] = 1 * x[n] + pole * x[n-1]
354
355void forward ( mpf_class * x , int size , mpf_class pole )
356{
357 mpf_class X = 0 ;
358
359 for ( int n = 1 ; n < size ; n++ )
360 {
361 x[n] = X = x[n] + pole * X ;
362 }
363}
364
365// backward recursive filter, used for testing
366// this might be written as x[n] = pole * ( x[n+1] - x[n] )
367// x[n] = (-pole) * x[n] + pole * x[n+1]
368
369void backward ( mpf_class * x , int size , mpf_class pole )
370{
371 mpf_class X = 0 ;
372
373 for ( int n = size - 2 ; n >= 0 ; n-- )
374 {
375 x[n] = X = pole * ( X - x[n] );
376 }
377}
378
379// to test the poles, we place the reconstruction kernel - a unit-spaced
380// sampling of the basis function at integral x - into the center of a large
381// buffer which is otherwise filled with zeroes. Then we apply all poles
382// in sequence with a forward and a backward run of the prefilter.
383// Afterwards, we expect to find a unit pulse in the center of the buffer.
384// Since we use very high precision (see mpf_bits) we can set a conservative
385// limit for the maximum error, here I use 1e-150. If this test throws up
386// warnings, there might be something wrong with the code.
387
388void test ( int size ,
389 const std::vector<mpf_class> & poles ,
390 int degree )
391{
392 mpf_class buffer [ size ] ;
393 for ( int k = 0 ; k < size ; k++ )
394 {
395 buffer[k] = 0 ;
396 }
397
398 // calculate overall gain of the filter
399
400 mpf_class lambda = 1 ;
401
402 for ( int k = 0 ; k < poles.size() ; k++ )
403
404 lambda = lambda * ( 1 - poles[k] ) * ( 1 - 1 / poles[k] ) ;
405
406 int center = size / 2 ;
407
408 // put basis function samples into the buffer, applying gain
409
410 for ( int x2 = 0 ; x2 <= degree ; x2 += 2 )
411 buffer [ center - x2/2 ]
412 = buffer [ center + x2/2 ]
413 = lambda * access_table ( degree , x2 ) ;
414
415 // filter
416
417 for ( int k = 0 ; k < poles.size() ; k++ )
418 {
419 forward ( buffer , size , poles[k] ) ;
420 backward ( buffer , size , poles[k] ) ;
421 }
422
423 // test
424
425 mpf_class error = 0 ;
426 mpf_class max_error = 0 ;
427
428 for ( int x2 = 0 ; x2 <= degree ; x2 += 2 )
429 {
430 error = abs ( buffer [ center - x2/2 ] - ( x2 == 0 ? 1 : 0 ) ) ;
431 if ( error > max_error )
432 max_error = error ;
433 error = abs ( buffer [ center + x2/2 ] - ( x2 == 0 ? 1 : 0 ) ) ;
434 if ( error > max_error )
435 max_error = error ;
436 }
437 mpf_class limit = 1e-150 ;
438 if ( max_error > limit )
439 std::cerr << "WARNING: degree " << degree
440 << " error exceeds limit 1e-150: "
441 << max_error << std::endl ;
442}
443
444// we want to do the test in long double as well, to see how much
445// downcasting the data to long double affects precision:
446
447vspline::xlf_type get_xlf ( const mpf_class & _x )
448{
449 long exp ;
450 std::string sign ( "+" ) ;
451 mpf_class x ( _x ) ;
452
453 if ( x < 0 )
454 {
455 x = -x ;
456 sign = std::string ( "-" ) ;
457 }
458
459 auto str = x.get_str ( exp , 10 , 64 ) ;
460
461 std::string res = sign + std::string ( "." ) + str
462 + std::string ( "e" ) + std::to_string ( exp )
463 + std::string ( "l" ) ;
464
465 vspline::xlf_type ld = std::stold ( res ) ;
466
467 return ld ;
468} ;
469
470// forward recursive filter, used for testing
471
472void ldforward ( vspline::xlf_type * x , int size , vspline::xlf_type pole )
473{
474 vspline::xlf_type X = 0 ;
475
476 for ( int n = 1 ; n < size ; n++ )
477 {
478 x[n] = X = x[n] + pole * X ;
479 }
480}
481
482// backward recursive filter, used for testing
483
484void ldbackward ( vspline::xlf_type * x , int size , vspline::xlf_type pole )
485{
486 vspline::xlf_type X = 0 ;
487
488 for ( int n = size - 2 ; n >= 0 ; n-- )
489 {
490 x[n] = X = pole * ( X - x[n] );
491 }
492}
493
494// test with values cast down to long double
495
496void ldtest ( int size ,
497 const std::vector<mpf_class> & poles ,
498 int degree )
499{
500 int nbpoles = degree / 2 ;
501
502 vspline::xlf_type buffer [ size ] ;
503 for ( int k = 0 ; k < size ; k++ )
504 {
505 buffer[k] = 0 ;
506 }
507
508 // calculate overall gain of the filter
509
510 vspline::xlf_type lambda = 1 ;
511
512 for ( int k = 0 ; k < nbpoles ; k++ )
513 {
514 auto ldp = get_xlf ( poles[k] ) ;
515 lambda = lambda * ( 1 - ldp ) * ( 1 - 1 / ldp ) ;
516 }
517
518 int center = size / 2 ;
519
520 // put basis function samples into the buffer, applying gain
521
522 for ( int x2 = 0 ; x2 <= degree ; x2 += 2 )
523 {
524 vspline::xlf_type bf = get_xlf ( access_table ( degree , x2 ) ) ;
525
526 buffer [ center - x2/2 ]
527 = buffer [ center + x2/2 ]
528 = lambda * bf ;
529 }
530
531 // filter
532
533 for ( int k = 0 ; k < nbpoles ; k++ )
534 {
535 auto ldp = get_xlf ( poles[k] ) ;
536 ldforward ( buffer , size , ldp ) ;
537 ldbackward ( buffer , size , ldp ) ;
538 }
539
540 // test
541
542 vspline::xlf_type error = 0 ;
543 vspline::xlf_type max_error = 0 ;
544
545 for ( int x2 = 0 ; x2 <= degree ; x2 += 2 )
546 {
547 error = std::abs ( buffer [ center - x2/2 ] - ( x2 == 0 ? 1 : 0 ) ) ;
548 if ( error > max_error )
549 max_error = error ;
550 error = std::abs ( buffer [ center + x2/2 ] - ( x2 == 0 ? 1 : 0 ) ) ;
551 if ( error > max_error )
552 max_error = error ;
553 }
554 vspline::xlf_type limit = 1e-12 ;
555
556 if ( max_error > limit )
557 std::cerr << "WARNING: degree " << degree
558 << " ld error exceeds limit 1e-12: "
559 << max_error << std::endl ;
560}
561
562#include <random>
563
564int main ( int argc , char * argv[] )
565{
566 mpf_set_default_prec ( mpf_bits ) ;
567
568 bool print_basis_function = true ;
569 bool print_prefilter_poles = true ;
570 bool print_umbrella_structures = true ;
571
572 std::cout << std::setprecision ( 64 ) ;
573
575 mpf_class value ;
576
577 if ( print_basis_function )
578 {
579 for ( int degree = 0 ; degree < degree_limit ; degree++ )
580 {
581 std::cout << "const vspline::xlf_type K"
582 << degree
583 << "[] = {"
584 << std::endl ;
585 for ( int x2 = 0 ; x2 <= degree ; x2++ )
586 {
587 value = access_table ( degree , x2 ) ;
588 std::cout << " XLF("
589 << value
590 << ") ,"
591 << std::endl ;
592 get_xlf ( value ) ;
593 }
594 std::cout << "} ;"
595 << std::endl ;
596 }
597 }
598
599 for ( int degree = 2 ; degree < degree_limit ; degree++ )
600 {
601 std::vector<mpf_class> res ;
602 calculate_prefilter_poles ( res , degree ) ;
603
604 if ( res.size() != degree / 2 )
605 {
606 std::cerr << "not enough poles for degree " << degree << std::endl ;
607 continue ;
608 }
609
610 if ( print_prefilter_poles )
611 {
612 std::cout << "const vspline::xlf_type Poles_"
613 << degree
614 << "[] = {"
615 << std::endl ;
616
617 for ( int p = 0 ; p < degree / 2 ; p++ )
618 {
619 value = res[p] ;
620 std::cout << " XLF("
621 << value
622 << ") ,"
623 << std::endl ;
624 }
625 std::cout << "} ;"
626 << std::endl ;
627 }
628 // test if the roots are good
629 test ( 10000 , res , degree ) ;
630 // optional, to see what happens when data are cast down to long double
631 ldtest ( 10000 , res , degree ) ;
632 }
633
634 if ( print_umbrella_structures )
635 {
636 std::cout << std::noshowpos ;
637 std::cout << "const vspline::xlf_type* const precomputed_poles[] = {"
638 << std::endl ;
639 std::cout << " 0, " << std::endl ;
640 std::cout << " 0, " << std::endl ;
641 for ( int i = 2 ; i < degree_limit ; i++ )
642 std::cout << " Poles_" << i << ", " << std::endl ;
643 std::cout << "} ;" << std::endl ;
644 std::cout << "const vspline::xlf_type* const precomputed_basis_function_values[] = {"
645 << std::endl ;
646 for ( int i = 0 ; i < degree_limit ; i++ )
647 std::cout << " K" << i << ", " << std::endl ;
648 std::cout << "} ;" << std::endl ;
649 }
650}
#define mpf_bits
Definition: bootstrap.cc:93
int main(int argc, char *argv[])
Definition: bootstrap.cc:564
void _fill_basis_table(int x2, int degree)
Definition: bootstrap.cc:147
void ldbackward(vspline::xlf_type *x, int size, vspline::xlf_type pole)
Definition: bootstrap.cc:484
mpq_class & access_table(int degree, int x2)
Definition: bootstrap.cc:118
void forward(mpf_class *x, int size, mpf_class pole)
Definition: bootstrap.cc:355
void ldtest(int size, const std::vector< mpf_class > &poles, int degree)
Definition: bootstrap.cc:496
mpq_class basis_table[degree_limit][degree_limit+1]
Definition: bootstrap.cc:111
void fill_basis_table()
Definition: bootstrap.cc:168
void backward(mpf_class *x, int size, mpf_class pole)
Definition: bootstrap.cc:369
#define degree_limit
Definition: bootstrap.cc:89
void ldforward(vspline::xlf_type *x, int size, vspline::xlf_type pole)
Definition: bootstrap.cc:472
void calculate_prefilter_poles(std::vector< mpf_class > &res, int degree)
Definition: bootstrap.cc:231
void newton(dtype &result, dtype x, int ncoeff, dtype *pcoeff)
Definition: bootstrap.cc:186
void test(int size, const std::vector< mpf_class > &poles, int degree)
Definition: bootstrap.cc:388
vspline::xlf_type get_xlf(const mpf_class &_x)
Definition: bootstrap.cc:447
double dtype
Definition: eval.cc:93
long double xlf_type
Definition: common.h:102
includes all headers from vspline (most of them indirectly)