vspline 1.1.0
Generic C++11 Code for Uniform B-Splines
self_test.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 2017 - 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 self_test.cc
41///
42/// \brief test consistency of precomputed data, prefiltering and evaluation
43///
44/// the self-test uses three entities: a unit pulse, the reconstruction
45/// kernel (which is a unit-spaced sampling of the b-spline basis function's
46/// values at integer arguments) and the prefilter. These data have a few
47/// fundamental relations we can test:
48/// - prefiltering the reconstruction kernel results in a unit pulse
49/// - producing a unit-spaced sampling of a spline with only one single
50/// unit-valued coefficient produces the reconstruction kernel
51///
52/// Performing the tests also assures us that the evaluation machinery with
53/// it's 'weight matrix' does what's intended, and that access to the basis
54/// function and it's derivatives (see basis_functor) functions correctly.
55///
56/// With rising spline degree, the test is ever more demanding. this is
57/// reflected in the maximum error returned for every degree: it rises
58/// with the spline degree. With the complex operations involved, seeing
59/// a maximal error in the order of magnitude of 1e-12 for working with
60/// long doubles seems reasonable enough (on my system).
61///
62/// I assume that the loss of precision with high degrees is mainly due to
63/// the filter's overall gain becoming very large. Since the gain is
64/// applied as a factor before or during prefiltering and prefiltering
65/// has the reverse effect, in the sum we end up having the effect of first
66/// multiplying with, then dividing by a very large number, 'crushing'
67/// the values to less precision. In bootstrap.cc, I perfom the test
68/// with GMP high precision floats and there I can avoid the problem, since
69/// the magnitude of the numbers I use there is well beyond the magnitude
70/// of the gain occuring with high spline degrees. So the conclusion is that
71/// high spline degrees can be used, but may not produce very precise results,
72/// since the normal C++ types are hard pressed to cope with the dynamic
73/// range covered by the filter.
74///
75/// The most time is spent calculating the basis function values recursively
76/// using cdb_bspline_basis, for cross-reference.
77///
78/// compile: clang++ -O3 -std=c++11 self_test.cc -o self_test -pthread
79
80#include <stdio.h>
81#include <iostream>
82#include <iomanip>
83#include <cmath>
84#include <limits>
85#include <numeric>
86#include <assert.h>
87#include <random>
88#include <vspline/vspline.h>
89
91
92template < typename dtype >
93dtype self_test ( int degree , dtype threshold , dtype strict_threshold )
94{
95 if ( degree == 0 )
97
98 dtype max_error = 0 ;
99
100 // self-test for plausibility. we know that using the b-spline
101 // prefilter of given degree on a set of unit-spaced samples of
102 // the basis function (at 0, +/-k) should yield a unit pulse.
103
104 // we create a bspline object for 100 core coefficients
105
107 spline_type bspl ( 100 , degree ) ;
108
109 // next we create an evaluator for this spline.
110 // Note how, to be as precise as possible for this test,
111 // we specify 'double' as elementary type for coordinates.
112 // This is overkill, but so what...
113
114 auto ev = vspline::make_evaluator < spline_type , double >
115 ( bspl ) ;
116
117 // and two arrays with the same size as the spline's 'core'.
118
119 vigra::MultiArray<1,dtype> result ( 100 ) ;
120 vigra::MultiArray<1,dtype> reference ( 100 ) ;
121
122 // we obtain a pointer to the reference array's center
123
124 dtype * p_center = &(reference[50]) ;
125
126 // we can obtain the reconstruction kernel by accessing precomputed
127 // basis function values via bspline_basis_2()
128
129 for ( int x = - degree / 2 ; x <= degree / 2 ; x++ )
130 {
131 p_center[x] = vspline::bspline_basis_2<dtype> ( x+x , degree ) ;
132 }
133
134 // alternatively we can put a unit pulse into the center of the
135 // coefficient array, transform and assign back. Transforming
136 // the unit pulse produces the reconstruction kernel. Doing so
137 // additionally assures us that the evaluation machinery with
138 // it's 'weight matrix' is functioning correctly.
139
140 // we obtain a pointer to the coefficient array's center
141
142 p_center = &(bspl.core[50]) ;
143
144 *p_center = 1 ;
145
146 vspline::transform ( ev , result ) ;
147
148 bspl.core = result ;
149
150 // we compare the two versions of the reconstruction kernel we
151 // have to make sure they agree. the data should be identical.
152 // we also compare with the result of a vspline::basis_functor,
153 // which uses the same method of evaluating a b-spline with a
154 // single unit-valued coefficient.
155 // Here we expect complete agreement.
156
157 vspline::basis_functor<dtype> bf ( degree ) ;
158
159 for ( int x = 50 - degree / 2 ; x <= 50 + degree / 2 ; x++ )
160 {
161 assert ( result[x] == reference[x] ) ;
162 assert ( bf ( x - 50 ) == reference[x] ) ;
163 }
164
165 // now we apply the prefilter, expecting that afterwards we have
166 // a single unit pulse coinciding with the location where we
167 // put the center of the kernel. This test will exceed the strict
168 // threshold, but the ordinary one will hold.
169
170 bspl.prefilter() ;
171
172 // we test our predicate
173
174 for ( int x = - degree / 2 ; x <= degree / 2 ; x++ )
175 {
176 dtype error ;
177 if ( x == 0 )
178 {
179 // at the origin we expect a value of 1.0
180 error = std::fabs ( p_center [ x ] - 1.0 ) ;
181 }
182 else
183 {
184 // off the origin we expect a value of 0.0
185 error = std::fabs ( p_center [ x ] ) ;
186 }
187 if ( error > threshold )
188 std::cout << "unit pulse test, x = " << x << ", error = "
189 << error << std::endl ;
190 max_error = std::max ( max_error , error ) ;
191 }
192
193 // test bspline_basis() at k/2, k E N against precomputed values
194 // while bspline_basis at whole arguments has delta == 0 and hence
195 // makes no use of rows beyond row 0 of the weight matrix, arguments
196 // at X.5 use all these rows. We can test against bspline_basis_2,
197 // which provides precomputed values for half unit steps.
198 // we run this test with strict_threshold.
199
200 int xmin = - degree - 1 ;
201 int xmax = degree + 1 ;
202
203 for ( int x2 = xmin ; x2 <= xmax ; x2++ )
204 {
205 auto a = bf ( x2 / 2.0L ) ;
206 auto b = vspline::bspline_basis_2<dtype> ( x2 , degree ) ;
207 auto error = std::abs ( a - b ) ;
208 if ( error > strict_threshold )
209 std::cout << "bfx2: " << x2 / 2.0 << " : "
210 << a << " <--> " << b
211 << " error " << error << std::endl << std::endl ;
212 max_error = std::max ( max_error , error ) ;
213 }
214
215 // set all coefficients to 1, evaluate. result should be 1,
216 // because every set of weights is a partition of unity
217 // this is a nice test, because it requires participation of
218 // all rows in the weight matrix, since the random arguments
219 // produce arbitrary delta. we run this test with strict_threshold.
220 {
221 std::random_device rd ;
222 std::mt19937 gen ( rd() ) ;
223 std::uniform_real_distribution<>
224 dis ( 50 - degree -1 , 50 + degree + 1 ) ;
225
226 bspl.container = 1 ;
227 for ( int k = 0 ; k < 1000 ; k++ )
228 {
229 double x = dis ( gen ) ;
230 dtype y = ev ( x ) ;
231 dtype error = std::abs ( y - 1 ) ;
232 if ( error > strict_threshold )
233 std::cout << "partition of unity test, d0: " << x << " : "
234 << y << " <--> " << 1
235 << " error " << error << std::endl << std::endl ;
236 max_error = std::max ( max_error , error ) ;
237 }
238
239 vigra::TinyVector < int , 1 > deriv_spec ;
240
241 // we also test evaluation of derivatives up to 2.
242 // Here, with the coefficients all equal, we expect 0 as a result.
243
244 if ( degree > 1 )
245 {
246 deriv_spec[0] = 1 ;
247 auto dev = vspline::make_evaluator < spline_type , double >
248 ( bspl , deriv_spec ) ;
249 for ( int k = 0 ; k < 1000 ; k++ )
250 {
251 double x = dis ( gen ) ;
252 dtype y = dev ( x ) ;
253 dtype error = std::abs ( y ) ;
254 if ( error > strict_threshold )
255 std::cout << "partition of unity test, d1: " << x << " : "
256 << y << " <--> " << 0
257 << " error " << error << std::endl << std::endl ;
258 max_error = std::max ( max_error , error ) ;
259 }
260 }
261
262 if ( degree > 2 )
263 {
264 deriv_spec[0] = 2 ;
265 auto ddev = vspline::make_evaluator< spline_type , double >
266 ( bspl , deriv_spec ) ;
267 for ( int k = 0 ; k < 1000 ; k++ )
268 {
269 double x = dis ( gen ) ;
270 dtype y = ddev ( x ) ;
271 dtype error = std::abs ( y ) ;
272 if ( error > strict_threshold )
273 std::cout << "partition of unity test, d2: " << x << " : "
274 << y << " <--> " << 0
275 << " error " << error << std::endl << std::endl ;
276 max_error = std::max ( max_error , error ) ;
277 }
278 }
279 }
280
281 // for smaller degrees, the cdb recursion is usable, so we can
282 // doublecheck basis_functor for a few sample x. The results here
283 // are also very precise, so we use strict_threshold. Initially
284 // I took this up to degree 19, but now it's only up to 13, which
285 // should be convincing enough and is much faster.
286
287 if ( degree < 13 ) // was: 19
288 {
289 std::random_device rd ;
290 std::mt19937 gen ( rd() ) ;
291 std::uniform_real_distribution<> dis ( - degree -1 , degree + 1 ) ;
292 for ( int k = 0 ; k < 1000 ; k++ )
293 {
294 dtype x = dis ( gen ) ;
295 dtype a = bf ( x ) ;
296 dtype b = vspline::cdb_bspline_basis ( x , degree ) ;
297 dtype error = std::abs ( a - b ) ;
298 if ( error > strict_threshold )
299 std::cout << "bf: " << x << " : "
300 << a << " <--> " << b
301 << " error " << error << std::endl << std::endl ;
302 max_error = std::max ( max_error , error ) ;
303 }
304 }
305
306 if ( degree > 1 && degree < 13 )
307 {
308 vspline::basis_functor<dtype> dbf ( degree , 1 ) ;
309 std::random_device rd ;
310 std::mt19937 gen ( rd() ) ;
311 std::uniform_real_distribution<> dis ( - degree -1 , degree + 1 ) ;
312 for ( int k = 0 ; k < 1000 ; k++ )
313 {
314 dtype x = dis ( gen ) ;
315 dtype a = dbf ( x ) ;
316 dtype b = vspline::cdb_bspline_basis ( x , degree , 1 ) ;
317 dtype error = std::abs ( a - b ) ;
318 if ( error > strict_threshold )
319 std::cout << "dbf: " << x << " : "
320 << a << " <--> " << b
321 << " error " << error << std::endl << std::endl ;
322 max_error = std::max ( max_error , error ) ;
323 }
324 }
325
326 if ( degree > 2 && degree < 13 )
327 {
328 vspline::basis_functor<dtype> ddbf ( degree , 2 ) ;
329 std::random_device rd ;
330 std::mt19937 gen ( rd() ) ;
331 std::uniform_real_distribution<> dis ( - degree -1 , degree + 1 ) ;
332 for ( int k = 0 ; k < 1000 ; k++ )
333 {
334 dtype x = dis ( gen ) ;
335 dtype a = ddbf ( x ) ;
336 dtype b = vspline::cdb_bspline_basis ( x , degree , 2 ) ;
337 dtype error = std::abs ( a - b ) ;
338 if ( error > strict_threshold )
339 std::cout << "ddbf: " << x << " : "
340 << a << " <--> " << b
341 << " error " << error << std::endl << std::endl ;
342 max_error = std::max ( max_error , error ) ;
343 }
344 }
345
346 if ( degree > 0 )
347 {
348 std::random_device rd ;
349 std::mt19937 gen ( rd() ) ;
350 std::uniform_real_distribution<>
351 dis ( -1 , 1 ) ;
352
353 dtype circle_error = 0 ;
354 dtype avg_circle_error = 0 ;
355
356 // consider a spline with a single 1.0 coefficient at the origin
357
358 // reference is the spline's value at ( 1 , 0 ), which is
359 // certainly on the unit circle
360
361 dtype v2 = bf ( 1 ) * bf ( 0 ) ;
362
363 // let's assume 10000 evaluations is a good enough
364 // statistical base
365
366 for ( int k = 0 ; k < 10000 ; k++ )
367 {
368 // take a random x and y coordinate
369
370 double x = dis ( gen ) ;
371 double y = dis ( gen ) ;
372
373 // normalize to unit circle
374
375 double s = sqrt ( x * x + y * y ) ;
376
377 x /= s ;
378 y /= s ;
379
380 // and take the value of the spline there, which is
381 // the product of the basis function values
382
383 dtype v1 = bf ( x ) * bf ( y ) ;
384
385 // we assume that with rising spline degree, the difference
386 // between these two values will become ever smaller, as the
387 // equipotential lines of the splines become more and
388 // more circular
389
390 dtype error = std::abs ( v1 - v2 ) ;
391 circle_error = std::max ( circle_error , error ) ;
392 avg_circle_error += error ;
393 }
394
395 assert ( circle_error < circular_test_previous ) ;
396 circular_test_previous = circle_error ;
397
398 // in my tests, circle_error goes down to ca 7.4e-7,
399 // so with degree 45 evaluations on the unit circle
400 // differ very little from each other.
401
402// std::cout << "unit circle test, degree " << degree
403// << " emax = " << circle_error
404// << " avg(e) = " << avg_circle_error / 10000
405// << " value: " << v2 << std::endl ;
406 }
407// std::cout << "max error for degree " << degree
408// << ": " << max_error << std::endl ;
409//
410 return max_error ;
411}
412
413// run a self test of vspline's constants, prefiltering and evaluation.
414// This tests if a set of common operations produces larger errors than
415// anticipated, to alert us if something has gone amiss.
416// The thresholds are fixed heuristically to be quite close to the actually
417// occuring maximum error.
418
419int main ( int argc , char * argv[] )
420{
421 long double max_error_l = 0 ;
422
423 for ( int degree = 0 ; degree <= vspline_constants::max_degree ; degree++ )
424 {
425 max_error_l = std::max ( max_error_l ,
426 self_test<long double>
427 ( degree , 4e-13l , 1e-18 ) ) ;
428 }
429
430 std::cout << "maximal error of tests with long double precision: "
431
432 << max_error_l << std::endl ;
433
434 double max_error_d = 0 ;
435
436 for ( int degree = 0 ; degree <= vspline_constants::max_degree ; degree++ )
437 {
438 max_error_d = std::max ( max_error_d ,
439 self_test<double>
440 ( degree , 1e-9 , 7e-16 ) ) ;
441 }
442
443 std::cout << "maximal error of tests with double precision: "
444 << max_error_d << std::endl ;
445
446 float max_error_f = 0 ;
447
448 // test float only up to degree 15.
449
450 for ( int degree = 0 ; degree < 16 ; degree++ )
451 {
452 max_error_f = std::max ( max_error_f ,
453 self_test<float>
454 ( degree , 3e-6 , 4e-7 ) ) ;
455 }
456
457 std::cout << "maximal error of tests with float precision: "
458 << max_error_f << std::endl ;
459
460 if ( max_error_l < 4e-13
461 && max_error_d < 1e9
462 && max_error_f < 3e-6 )
463
464 std::cout << "test passed" << std::endl ;
465 else
466 std::cout << "test failed" << std::endl ;
467}
vspline::bspline< pixel_type, 2 > spline_type
Definition: ca_correct.cc:111
double dtype
Definition: eval.cc:93
void transform(const unary_functor_type &functor, const vigra::MultiArrayView< dimension, typename unary_functor_type::in_type > &input, vigra::MultiArrayView< dimension, typename unary_functor_type::out_type > &output, int njobs=vspline::default_njobs, vspline::atomic< bool > *p_cancel=0)
implementation of two-array transform using wielding::coupled_wield.
Definition: transform.h:211
real_type cdb_bspline_basis(real_type x, int degree, int derivative=0)
Implementation of the Cox-de Boor recursion formula to calculate the value of the bspline basis funct...
Definition: basis.h:655
int main(int argc, char *argv[])
Definition: self_test.cc:419
dtype self_test(int degree, dtype threshold, dtype strict_threshold)
Definition: self_test.cc:93
long double circular_test_previous
Definition: self_test.cc:90
basis_functor is an object producing the b-spline basis function value for given arguments,...
Definition: basis.h:402
class bspline now builds on class bspline_base, adding coefficient storage, while bspline_base provid...
Definition: bspline.h:499
view_type core
Definition: bspline.h:566
void prefilter(vspline::xlf_type boost=vspline::xlf_type(1), int njobs=vspline::default_njobs)
prefilter converts the knot point data in the 'core' area into b-spline coefficients....
Definition: bspline.h:815
view_type container
Definition: bspline.h:565
includes all headers from vspline (most of them indirectly)