File: self_test.cc

package info (click to toggle)
vspline 1.1.7-1
  • links: PTS, VCS
  • area: main
  • in suites: forky, sid, trixie
  • size: 1,708 kB
  • sloc: cpp: 15,905; ansic: 443; sh: 17; makefile: 2
file content (467 lines) | stat: -rw-r--r-- 17,689 bytes parent folder | download
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
/************************************************************************/
/*                                                                      */
/*    vspline - a set of generic tools for creation and evaluation      */
/*              of uniform b-splines                                    */
/*                                                                      */
/*            Copyright 2017 - 2023 by Kay F. Jahnke                    */
/*                                                                      */
/*    The git repository for this software is at                        */
/*                                                                      */
/*    https://bitbucket.org/kfj/vspline                                 */
/*                                                                      */
/*    Please direct questions, bug reports, and contributions to        */
/*                                                                      */
/*    kfjahnke+vspline@gmail.com                                        */
/*                                                                      */
/*    Permission is hereby granted, free of charge, to any person       */
/*    obtaining a copy of this software and associated documentation    */
/*    files (the "Software"), to deal in the Software without           */
/*    restriction, including without limitation the rights to use,      */
/*    copy, modify, merge, publish, distribute, sublicense, and/or      */
/*    sell copies of the Software, and to permit persons to whom the    */
/*    Software is furnished to do so, subject to the following          */
/*    conditions:                                                       */
/*                                                                      */
/*    The above copyright notice and this permission notice shall be    */
/*    included in all copies or substantial portions of the             */
/*    Software.                                                         */
/*                                                                      */
/*    THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND    */
/*    EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES   */
/*    OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND          */
/*    NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT       */
/*    HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,      */
/*    WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING      */
/*    FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR     */
/*    OTHER DEALINGS IN THE SOFTWARE.                                   */
/*                                                                      */
/************************************************************************/

/// \file self_test.cc
///
/// \brief test consistency of precomputed data, prefiltering and evaluation
///
/// the self-test uses three entities: a unit pulse, the reconstruction
/// kernel (which is a unit-spaced sampling of the b-spline basis function's
/// values at integer arguments) and the prefilter. These data have a few
/// fundamental relations we can test:
/// - prefiltering the reconstruction kernel results in a unit pulse
/// - producing a unit-spaced sampling of a spline with only one single
///   unit-valued coefficient produces the reconstruction kernel
///
/// Performing the tests also assures us that the evaluation machinery with
/// it's 'weight matrix' does what's intended, and that access to the basis
/// function and it's derivatives (see basis_functor) functions correctly.
///   
/// With rising spline degree, the test is ever more demanding. this is
/// reflected in the maximum error returned for every degree: it rises
/// with the spline degree. With the complex operations involved, seeing
/// a maximal error in the order of magnitude of 1e-12 for working with
/// long doubles seems reasonable enough (on my system).
///
/// I assume that the loss of precision with high degrees is mainly due to
/// the filter's overall gain becoming very large. Since the gain is 
/// applied as a factor before or during prefiltering and prefiltering
/// has the reverse effect, in the sum we end up having the effect of first
/// multiplying with, then dividing by a very large number, 'crushing'
/// the values to less precision. In bootstrap.cc, I perfom the test
/// with GMP high precision floats and there I can avoid the problem, since
/// the magnitude of the numbers I use there is well beyond the magnitude
/// of the gain occuring with high spline degrees. So the conclusion is that
/// high spline degrees can be used, but may not produce very precise results,
/// since the normal C++ types are hard pressed to cope with the dynamic
/// range covered by the filter.
///
/// The most time is spent calculating the basis function values recursively
/// using cdb_bspline_basis, for cross-reference.
///
/// compile: clang++ -O3 -std=c++11 self_test.cc -o self_test -pthread

#include <stdio.h>
#include <iostream>
#include <iomanip>
#include <cmath>
#include <limits>
#include <numeric>
#include <assert.h>
#include <random>
#include <vspline/vspline.h>

long double circular_test_previous ;

template < typename dtype >
dtype self_test ( int degree , dtype threshold , dtype strict_threshold )
{
  if ( degree == 0 )
    circular_test_previous = 1 ;
  
  dtype max_error = 0 ;
  
  // self-test for plausibility. we know that using the b-spline
  // prefilter of given degree on a set of unit-spaced samples of
  // the basis function (at 0, +/-k) should yield a unit pulse.
  
  // we create a bspline object for 100 core coefficients
  
  typedef vspline::bspline < dtype , 1 > spline_type ;
  spline_type bspl ( 100 , degree ) ;
            
  // next we create an evaluator for this spline.
  // Note how, to be as precise as possible for this test,
  // we specify 'double' as elementary type for coordinates.
  // This is overkill, but so what...
  
  auto ev = vspline::make_evaluator < spline_type , double >
                    ( bspl ) ;
  
  // and two arrays with the same size as the spline's 'core'.
            
  vigra::MultiArray<1,dtype> result ( 100 ) ;
  vigra::MultiArray<1,dtype> reference ( 100 ) ;
  
  // we obtain a pointer to the reference array's center
  
  dtype * p_center = &(reference[50]) ;

  // we can obtain the reconstruction kernel by accessing precomputed
  // basis function values via bspline_basis_2()
  
  for ( int x = - degree / 2 ; x <= degree / 2 ; x++ )
  {
    p_center[x] = vspline::bspline_basis_2<dtype> ( x+x , degree ) ;
  }
  
  // alternatively we can put a unit pulse into the center of the
  // coefficient array, transform and assign back. Transforming
  // the unit pulse produces the reconstruction kernel. Doing so
  // additionally assures us that the evaluation machinery with
  // it's 'weight matrix' is functioning correctly.
  
  // we obtain a pointer to the coefficient array's center
  
  p_center = &(bspl.core[50]) ;
  
  *p_center = 1 ;
  
  vspline::transform ( ev , result ) ;

  bspl.core = result ;
  
  // we compare the two versions of the reconstruction kernel we
  // have to make sure they agree. the data should be identical.
  // we also compare with the result of a vspline::basis_functor,
  // which uses the same method of evaluating a b-spline with a
  // single unit-valued coefficient.
  // Here we expect complete agreement.
  
  vspline::basis_functor<dtype> bf ( degree ) ;
  
  for ( int x = 50 - degree / 2 ; x <= 50 + degree / 2 ; x++ )
  {
    assert ( result[x] == reference[x] ) ;
    assert ( bf ( x - 50 ) == reference[x] ) ;
  }
  
  // now we apply the prefilter, expecting that afterwards we have
  // a single unit pulse coinciding with the location where we
  // put the center of the kernel. This test will exceed the strict
  // threshold, but the ordinary one will hold.
  
  bspl.prefilter() ;
  
  // we test our predicate
  
  for ( int x = - degree / 2 ; x <= degree / 2 ; x++ )
  {
    dtype error ;
    if ( x == 0 )
    {
      // at the origin we expect a value of 1.0
      error = std::fabs ( p_center [ x ] - 1.0 ) ;
    }
    else
    {
      // off the origin we expect a value of 0.0
      error = std::fabs ( p_center [ x ] ) ;
    }
    if ( error > threshold )
      std::cout << "unit pulse test, x = " << x << ", error = "
                << error << std::endl ;
    max_error = std::max ( max_error , error ) ;
  }
  
  // test bspline_basis() at k/2, k E N against precomputed values
  // while bspline_basis at whole arguments has delta == 0 and hence
  // makes no use of rows beyond row 0 of the weight matrix, arguments
  // at X.5 use all these rows. We can test against bspline_basis_2,
  // which provides precomputed values for half unit steps.
  // we run this test with strict_threshold.
  
  int xmin = - degree - 1 ;
  int xmax = degree + 1 ;
  
  for ( int x2 = xmin ; x2 <= xmax ; x2++ )
  {
    auto a = bf ( x2 / 2.0L ) ;
    auto b = vspline::bspline_basis_2<dtype> ( x2 , degree ) ;
    auto error = std::abs ( a - b ) ;
    if ( error > strict_threshold )
      std::cout << "bfx2: " << x2 / 2.0 << " : "
                << a << " <--> " << b
                << " error " << error << std::endl << std::endl ;
    max_error = std::max ( max_error , error ) ;
  }
  
  // set all coefficients to 1, evaluate. result should be 1,
  // because every set of weights is a partition of unity
  // this is a nice test, because it requires participation of
  // all rows in the weight matrix, since the random arguments
  // produce arbitrary delta. we run this test with strict_threshold.
  {
    std::random_device rd ;
    std::mt19937 gen ( rd() ) ;
    std::uniform_real_distribution<>
       dis ( 50 - degree -1 , 50 + degree + 1 ) ;
       
    bspl.container = 1 ;
    for ( int k = 0 ; k < 1000 ; k++ )
    {
      double x = dis ( gen ) ;
      dtype y = ev ( x ) ;
      dtype error = std::abs ( y - 1 ) ;
      if ( error > strict_threshold )
        std::cout << "partition of unity test, d0: " << x << " : "
                  << y << " <--> " << 1
                  << " error " << error << std::endl << std::endl ;
      max_error = std::max ( max_error , error ) ;
    }
    
    vigra::TinyVector < int , 1 > deriv_spec ;
    
    // we also test evaluation of derivatives up to 2.
    // Here, with the coefficients all equal, we expect 0 as a result.
    
    if ( degree > 1 )
    {
      deriv_spec[0] = 1 ;
      auto dev = vspline::make_evaluator < spline_type , double >
                          ( bspl , deriv_spec ) ;
      for ( int k = 0 ; k < 1000 ; k++ )
      {
        double x = dis ( gen ) ;
        dtype y = dev ( x ) ;
        dtype error = std::abs ( y ) ;
        if ( error > strict_threshold )
          std::cout << "partition of unity test, d1: " << x << " : "
                    << y << " <--> " << 0
                    << " error " << error << std::endl << std::endl ;
        max_error = std::max ( max_error , error ) ;
      }
    }
    
    if ( degree > 2 )
    {
      deriv_spec[0] = 2 ;
      auto ddev = vspline::make_evaluator< spline_type , double >
                           ( bspl , deriv_spec ) ;
      for ( int k = 0 ; k < 1000 ; k++ )
      {
        double x = dis ( gen ) ;
        dtype y = ddev ( x ) ;
        dtype error = std::abs ( y ) ;
        if ( error > strict_threshold )
          std::cout << "partition of unity test, d2: " << x << " : "
                    << y << " <--> " << 0
                    << " error " << error << std::endl << std::endl ;
        max_error = std::max ( max_error , error ) ;
      }
    }
  }
  
  // for smaller degrees, the cdb recursion is usable, so we can
  // doublecheck basis_functor for a few sample x. The results here
  // are also very precise, so we use strict_threshold. Initially
  // I took this up to degree 19, but now it's only up to 13, which
  // should be convincing enough and is much faster.
  
  if ( degree < 13 ) // was: 19
  {
    std::random_device rd ;
    std::mt19937 gen ( rd() ) ;
    std::uniform_real_distribution<> dis ( - degree -1 , degree + 1 ) ;
    for ( int k = 0 ; k < 1000 ; k++ )
    {
      dtype x = dis ( gen ) ;
      dtype a = bf ( x ) ;
      dtype b = vspline::cdb_bspline_basis ( x , degree ) ;
      dtype error = std::abs ( a - b ) ;
      if ( error > strict_threshold )
        std::cout << "bf: " << x << " : "
                  << a << " <--> " << b
                  << " error " << error << std::endl << std::endl ;
      max_error = std::max ( max_error , error ) ;
    }
  }
  
  if ( degree > 1 && degree < 13 )
  {
    vspline::basis_functor<dtype> dbf ( degree , 1 ) ;
    std::random_device rd ;
    std::mt19937 gen ( rd() ) ;
    std::uniform_real_distribution<> dis ( - degree -1 , degree + 1 ) ;
    for ( int k = 0 ; k < 1000 ; k++ )
    {
      dtype x = dis ( gen ) ;
      dtype a = dbf ( x ) ;
      dtype b = vspline::cdb_bspline_basis ( x , degree , 1 ) ;
      dtype error = std::abs ( a - b ) ;
      if ( error > strict_threshold )
        std::cout << "dbf: " << x << " : "
                  << a << " <--> " << b
                  << " error " << error << std::endl << std::endl ;
      max_error = std::max ( max_error , error ) ;
    }
  }
  
  if ( degree > 2 && degree < 13 )
  {
    vspline::basis_functor<dtype> ddbf ( degree , 2 ) ;
    std::random_device rd ;
    std::mt19937 gen ( rd() ) ;
    std::uniform_real_distribution<> dis ( - degree -1 , degree + 1 ) ;
    for ( int k = 0 ; k < 1000 ; k++ )
    {
      dtype x = dis ( gen ) ;
      dtype a = ddbf ( x ) ;
      dtype b = vspline::cdb_bspline_basis ( x , degree , 2 ) ;
      dtype error = std::abs ( a - b ) ;
      if ( error > strict_threshold )
        std::cout << "ddbf: " << x << " : "
                  << a << " <--> " << b
                  << " error " << error << std::endl << std::endl ;
      max_error = std::max ( max_error , error ) ;
   }
  }
  
  if ( degree > 0 )
  {
    std::random_device rd ;
    std::mt19937 gen ( rd() ) ;
    std::uniform_real_distribution<>
       dis ( -1 , 1 ) ;
    
    dtype circle_error = 0 ;
    dtype avg_circle_error = 0 ;
    
    // consider a spline with a single 1.0 coefficient at the origin

    // reference is the spline's value at ( 1 , 0 ), which is
    // certainly on the unit circle
    
    dtype v2 = bf ( 1 ) * bf ( 0 ) ;
    
    // let's assume 10000 evaluations is a good enough
    // statistical base

    for ( int k = 0 ; k < 10000 ; k++ )
    {
      // take a random x and y coordinate
      
      double x = dis ( gen ) ;
      double y = dis ( gen ) ;
      
      // normalize to unit circle
      
      double s = sqrt ( x * x + y * y ) ;
      
      x /= s ;
      y /= s ;
      
      // and take the value of the spline there, which is
      // the product of the basis function values
      
      dtype v1 = bf ( x ) * bf ( y ) ;
    
      // we assume that with rising spline degree, the difference
      // between these two values will become ever smaller, as the
      // equipotential lines of the splines become more and
      // more circular
      
      dtype error = std::abs ( v1 - v2 ) ;
      circle_error = std::max ( circle_error , error ) ;
      avg_circle_error += error ;
    }
    
    assert ( circle_error < circular_test_previous ) ;
    circular_test_previous = circle_error ;
    
    // in my tests, circle_error goes down to ca 7.4e-7,
    // so with degree 45 evaluations on the unit circle
    // differ very little from each other.
  
//     std::cout << "unit circle test, degree " << degree
//               << " emax = " << circle_error
//               << " avg(e) = " << avg_circle_error / 10000
//               << " value: " << v2 << std::endl ;
  }
//   std::cout << "max error for degree " << degree
//             << ": " << max_error << std::endl ;
//             
  return max_error ;
}

// run a self test of vspline's constants, prefiltering and evaluation.
// This tests if a set of common operations produces larger errors than
// anticipated, to alert us if something has gone amiss.
// The thresholds are fixed heuristically to be quite close to the actually
// occuring maximum error.

int main ( int argc , char * argv[] )
{
  long double max_error_l = 0 ;
  
  for ( int degree = 0 ; degree <= vspline_constants::max_degree ; degree++ )
  {
    max_error_l = std::max ( max_error_l ,
                             self_test<long double>
                             ( degree , 4e-13l , 1e-18 ) ) ;
  }
  
  std::cout << "maximal error of tests with long double precision: "
  
  << max_error_l << std::endl ;
  
  double max_error_d = 0 ;
  
  for ( int degree = 0 ; degree <= vspline_constants::max_degree ; degree++ )
  {
    max_error_d = std::max ( max_error_d ,
                             self_test<double>
                             ( degree , 1e-9 , 7e-16 ) ) ;
  }
  
  std::cout << "maximal error of tests with double precision: "
  << max_error_d << std::endl ;
  
  float max_error_f = 0 ;
  
  // test float only up to degree 15.

  for ( int degree = 0 ; degree < 16 ; degree++ )
  {
    max_error_f = std::max ( max_error_f ,
                             self_test<float>
                             ( degree , 3e-6 , 4e-7 ) ) ;
  }
  
  std::cout << "maximal error of tests with float precision: "
  << max_error_f << std::endl ;
  
  if (    max_error_l < 4e-13
       && max_error_d < 1e9
       && max_error_f < 3e-6 )
    
    std::cout << "test passed" << std::endl ;
  else
    std::cout << "test failed" << std::endl ;
}