-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathInterpolation.hpp
More file actions
1532 lines (1228 loc) · 54.8 KB
/
Interpolation.hpp
File metadata and controls
1532 lines (1228 loc) · 54.8 KB
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
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
/*==============================================================================
Interpolation
There are many situations where one would like to represent a function based
on sampled data points. However, most optimization algorithms assume that the
functions to be optimized are continuous. The solution is then to represent
the function as an interpolation of the sample data points.
This file provides a homogeneous interface to several interpolation algorithms
from the GNU Scientific Library (GSL) [1] and Boost::Math [2]. Currently only
univariate interpolation has been defined.
The sampled data points can be given through a pre-defined container, either a
map or as some kind of linear structure with an iterator, or it can be given
in a comma separated file (CSV) assuming that each row contains <x, f(x)>
value pairs. Ben Strasser's fast C++ CSV Reader class [3] is used for parsing
the file.
Following the principle of using the newest C++ standard, this implementation
requires a compiler that is capable of handling C++20, in particular the
constexpr virtual functions.
References:
[1] https://www.gnu.org/software/gsl/
[2] https://www.boost.org/doc/libs/1_72_0/libs/math/doc/html/interpolation.html
[3] https://github.com/ben-strasser/fast-cpp-csv-parser
[4] M. Steffen: "A Simple Method for Monotonic Interpolation in One Dimension",
Astronomy & Astrophysics, Vol. 239, No. 1-2, pp. 443-450, November 1990
[4] Jerrold Fried and Stanley Zietz: "Curve fitting by spline and Akima
methods: possibility of interpolation error and its suppression",
Physics in Medicine and Biology, Vol. 18, No. 4, July 1973
[5] Hiroshi Akima: "A New Method of Interpolation and Smooth Curve Fitting
Based on Local Procedures", Journal of the ACM, Vol. 17, No. 4, pp.
589–602, October 1970
[5] Michael S. Floater and Kai Hormann: "Barycentric rational interpolation
with no poles and high rates of approximation", Numerische Mathematik,
Vol. 107, pp. 315–331, 2007
[6] Claus Schneider and Wilhelm Werner: "Some New Aspects of Rational
Interpolation", Mathematics of Computation, Vol 47, No. 175, pp. 285-299,
July 1986
[7] Giesela Engeln-Müllges and Frank Uhlig: Numerical Algorithms with C,
Springer-Verlag Berlin Heidelberg, ISBN 978-3-642-61074-5, 1996
Author and Copyright: Geir Horn, University of Oslo 2020
License: European Union Public Licence 1.2 (EUPL-1.2)
https://ec.europa.eu/info/european-union-public-licence_en
==============================================================================*/
#ifndef OPTIMIZATION_INTERPOLATION
#define OPTIMIZATION_INTERPOLATION
#include <string> // Strings
#include <sstream> // For error messages
#include <stdexcept> // For standard exceptions
#include <type_traits> // Type checking
#include <iterator> // Iterating over data
#include <map> // Storing sample data
#include <vector> // C-style vectors for GSL
#include <filesystem> // File names
#include <mutex> // Protecting GSL accelerator
#include <optional> // f(x) when defined or not
#include <algorithm> // STL algorithms
#include <numeric> // Numerical STL algorithms
#include <limits> // Numeric limits of types
#include <gsl/gsl_interp.h> // GSL algorithms
#include <gsl/gsl_errno.h> // GSL error reporting
#include <boost/range/adaptor/map.hpp> // Range adaptors
#include <boost/numeric/conversion/cast.hpp> // Safe numeric casts
#include <boost/math/interpolators/barycentric_rational.hpp>
#include <boost/math/quadrature/gauss_kronrod.hpp>
#include "csv.h" // The CSV parser
namespace Interpolation
{
/*==============================================================================
Naming algorithms
==============================================================================*/
class Algorithm
{
private:
// In order to ensure the strict typing of algorithms, they are all
// defined in a flat scoped enumeration list. U means uniformly sampled
// abscissa whereas NU means non-uniformly sampled abscissa. The algorithms
// are given in preference order so that non-uniform algorithms are generally
// preferred over uniformly sampled, and so that the periodic algorithms are
// the least preferred.
enum class Type
{
Steffen, // GSL NU: Monotonous piecewise cubic
Akima, // GSL NU: Natural endpoints
Barycentric, // Boost NU: O(n) evaluation time
Spline, // GSL NU: Piecewise cubic
Linear, // GSL NU: Simple
Polynomial, // GSL NU: Easily introduces oscillations
BSpline, // Boost U: cubic version is used
WhittakerShannon, // Boost U: compactly supported functions
PeriodicSpline, // GSL NU: Cubic spline with periodic boundaries
PeriodicAkima, // GSL NU: Periodic endpoints
Fourier // Boost U: Periodic function interpolation
};
public:
// The next declaration is a trick to give other classes the possibility
// to define arguments and return values of the enumeration type but forcing
// the use of the below constants to actually set these values since the
// enumeration itself is private.
using InterpolationType = Type;
// For error reporting it is useful to have a textual string representation
// of the various algorithm types, and there is a simple function to produce
// the required text for a given type.
static constexpr const char * Description( Type Method )
{
switch( Method )
{
case Type::Steffen:
return "Steffen's method";
break;
case Type::Akima:
return "Akima spline";
break;
case Type::Barycentric:
return "Barycentric rational";
break;
case Type::Spline:
return "Cubic spline";
break;
case Type::Linear:
return "Linear interpolation";
break;
case Type::Polynomial:
return "Polynomial interpolation";
break;
case Type::BSpline:
return "B-Spline";
break;
case Type::WhittakerShannon:
return "Whittaker-Shannon method";
break;
case Type::PeriodicSpline:
return "Periodic spline";
break;
case Type::PeriodicAkima:
return "Periodic Akima spline";
break;
case Type::Fourier:
return "Periodic cardinal trigonometric interpolation";
break;
}
return "Unknown method";
}
// The actual definitions are made in terms of the types of algorithms
// to improve readability of the subsequent implementations. Some algorithms
// requiring an uniformly sampled abscissa, i.e. the function values are only
// known at equally spaced points. For these algorithms, it is necessary to
// check this condition prior to constructing the interpolation class. To
// be able to inherit the interpolation class, or for interpolation methods
// implemented without a default constructor, there is a small class to
// check this condition, and it will throw an invalid argument exception if
// the condition of a uniformly sampled abscissa is not met.
struct Univariate
{
static constexpr Type
Linear = Type::Linear,
Polynomial = Type::Polynomial,
Spline = Type::Spline,
Akima = Type::Akima,
Steffen = Type::Steffen,
Barycentric = Type::Barycentric;
struct Periodic
{
static constexpr Type
Akima = Type::PeriodicAkima,
Spline = Type::PeriodicSpline;
};
struct Uniform
{
static constexpr Type
BSpline = Type::BSpline,
WhittakerShannon = Type::WhittakerShannon;
struct Periodic
{
static constexpr Type
Fourier = Type::Fourier;
};
// The test class will first create a vector of differences between the
// abscissa values and then check if all of these are equal. If not then
// the invalid argument exception error will be thrown. Note that the
// per element test is done against the precision of the real type to
// avoid a test for exact zero.
class AbscissaTest
{
public:
template< class AbscissaIterator >
AbscissaTest( AbscissaIterator xBegin, AbscissaIterator xEnd,
Type Method )
{
using RealType =
typename std::iterator_traits< AbscissaIterator >::value_type;
static_assert( std::is_arithmetic< RealType >::value,
"The abscissa type must be a numeric type!" );
std::vector< RealType > Delta;
std::adjacent_difference( xBegin, xEnd, std::back_inserter( Delta ) );
if( !std::all_of( Delta.begin(), Delta.end(),
[&](RealType delta)->bool{ return
( std::max( delta, Delta.front() )
- std::min( delta, Delta.front() ) ) <
2 * std::numeric_limits< RealType >::epsilon(); } ) )
{
std::ostringstream ErrorMessage;
ErrorMessage << __FILE__ << " at line " << __LINE__ << ": "
<< "The abscissa values are not equally spaced "
<< "points as required by the interpolation method"
<< Description( Method );
throw std::invalid_argument( ErrorMessage.str() );
}
}
};
};
};
// Later it is possible to add interpolations in multiple dimensions here
// ---------------------------------------------------------------------------
// Comparators
// ---------------------------------------------------------------------------
//
// The function to chose one out of two types will select the one with the
// least value based on the ordering of the different interpolation types
// in the above enumeration.
static constexpr InterpolationType Choose( InterpolationType A,
InterpolationType B )
{
if ( static_cast< unsigned int >(A) <= static_cast< unsigned int >(B) )
return A;
else
return B;
}
// There are two functions to test if a given algorithm is uniformly sampled
// or if it is periodic
static constexpr bool UniformQ( InterpolationType A )
{
switch( A )
{
case Univariate::Uniform::BSpline:
case Univariate::Uniform::WhittakerShannon:
case Univariate::Uniform::Periodic::Fourier:
return true;
default:
return false;
}
}
static constexpr bool PeriodicQ( InterpolationType A )
{
switch( A )
{
case Univariate::Periodic::Akima:
case Univariate::Periodic::Spline:
case Univariate::Uniform::Periodic::Fourier:
return true;
default:
return false;
}
}
};
/*==============================================================================
Generic interface
==============================================================================*/
//
// The generic interface is the base class of all the interpolation functions
// and it basically provides the map of the values of the function being
// interpolated by the various methods and constructors to initialize this map.
// The class is abstract and should never be used on its own.
template< class AbscissaType, class OrdinateType = AbscissaType >
class GenericFunction
{
public:
// The types are defined so that derived classes knows the double types.
using Abscissa = AbscissaType;
using Ordinate = OrdinateType;
private:
// First a check that the given types are arithmetic, i.e. integer or
// real numbers
static_assert( std::is_arithmetic< AbscissaType >::value &&
std::is_arithmetic< OrdinateType >::value,
"Interpolation data types must be arithmetic!" );
// If the given types are acceptable, then the vectors holding separately
// the data are defined. They must be kept separate because some algorithms
// may require the data for every evaluation of the interpolated function.
// Note that it is assumed that the abscissa data are sorted in ascending
// order.
std::vector< AbscissaType > AbscissaData;
std::vector< OrdinateType > OrdinateData;
// In order to ensure that the abscissa is sorted, the data is assumed to
// be initialized from a map. The basic initialization function is therefore
// based on two map iterators. It is a template since the value stored in
// the map can be arithmetic type.
template< class MapIterator >
void StoreMapData( MapIterator DataPoint, MapIterator End )
{
using MapAbcissaType =
typename std::iterator_traits< MapIterator >::value_type::first_type;
using MapOrdinateType =
typename std::iterator_traits< MapIterator >::value_type::second_type;
static_assert( std::is_arithmetic< MapAbcissaType >::value &&
std::is_arithmetic< MapOrdinateType >::value,
"The map must contain numerical values" );
// Types are numeric so we can use the safe numeric cast of boost to
// fill the values into the sample data map.
for( ; DataPoint != End; ++DataPoint )
{
AbscissaData.push_back(
boost::numeric_cast< AbscissaType >( DataPoint->first ) );
OrdinateData.push_back(
boost::numeric_cast< OrdinateType >( DataPoint->second ) );
}
// At least two data points must be given, otherwise an invalid argument
// exception will be thrown
if( AbscissaData.size() < 2 )
{
std::ostringstream ErrorMessage;
ErrorMessage << __FILE__ << " at line " << __LINE__ << ": "
<< "The given data set does not contain sufficient data";
throw std::invalid_argument( ErrorMessage.str() );
}
}
protected:
// There are functions to generate a range that can be used to iterate over
// the abscissa and ordinate values separately. The objects returned from
// these functions has a 'begin' and 'end' functions returning iterators to
// go over the data.
inline const std::vector< AbscissaType > & AbscissaValues( void )
{ return AbscissaData; }
inline const std::vector< OrdinateType > & OrdinateValues( void )
{ return OrdinateData; }
public:
// Often one would be satisfied by just knowing the number of samples. This
// is returned as the size variable supported by the map, which should be
// an integral type, usually size_t, but it is implementation dependent.
inline typename std::vector< AbscissaType >::size_type SampleSize( void )
{ return AbscissaData.size(); }
// External users can only get constant iterators to the sample data
inline const typename std::vector< AbscissaType >::const_iterator
AbscissaBegin( void )
{ return AbscissaData.cbegin(); }
inline const typename std::vector< AbscissaType >::const_iterator
AbscissaEnd( void )
{ return AbscissaData.cend(); }
inline const typename std::vector< OrdinateType >::const_iterator
OrdinateBegin( void )
{ return OrdinateData.cbegin(); }
inline const typename std::vector< OrdinateType >::const_iterator
OrdinateEnd( void )
{ return OrdinateData.cbegin(); }
// ---------------------------------------------------------------------------
// Constructors
// ---------------------------------------------------------------------------
//
// The easiest part is when the data is already available in a map, and when
// the 'begin' and 'end' iterators are given. It must be checked that the
// data type of the input is numerical, and the data will then be copied
// to the sampled data
template< class MapIterator >
GenericFunction( MapIterator DataPoint, MapIterator End )
{ StoreMapData( DataPoint, End ); }
// If the map is given directly, the handling can just be forwarded to
// the storage function.
template< typename XValueType, typename YValueType,
class Comparator, class Allocator >
GenericFunction(
const std::map< XValueType, YValueType, Comparator, Allocator > & DataPoints )
{ StoreMapData( DataPoints.cbegin(), DataPoints.cend() ); }
// It is also possible that the abscissa values and the ordinate values are
// already prepared in the separate containers. In this case the 'begin' and
// 'end' should be given for the abscissa values, and it is implicitly assumed
// that the ordinate values are at least as many. In this case it is
// necessary to first store the data in a map to ensure that the abscissa is
// correctly sorted, and then use this map to initialize the data vectors.
template< class AbscissaIterator, class OrdinateIterator >
GenericFunction( AbscissaIterator XValues, AbscissaIterator XValuesEnd,
OrdinateIterator YValues )
{
std::map< typename std::iterator_traits< AbscissaIterator >::value_type,
typename std::iterator_traits< OrdinateIterator >::value_type >
GivenData;
for( ; XValues != XValuesEnd; ++XValues, ++YValues )
GivenData.emplace(*XValues, *YValues );
StoreMapData( GivenData.cbegin(), GivenData.cend() );
}
// The last constructors will read the data from a Comma Separated File (CSV)
// assuming that there is one data point <x,y> per line (record).
GenericFunction( const std::filesystem::path & FileName )
{
// First two variable place holders are defined as the CSV library uses a
// C-style interface filling two variables with data.
AbscissaType X;
OrdinateType Y;
// Parse two columns from the file, using space as separator and ignore only
// tabs.
io::CSVReader<2, io::trim_chars<'\t'>, io::no_quote_escape<' '> >
CSVParser( FileName );
// The file is not supposed to contain a header and so it is defined to
// create an exception if already defined.
CSVParser.set_header("X", "Y");
// Then the file can be parsed and the content converted and stored in a
// temporary map that is finally stored to the data vectors.
std::map< AbscissaType, OrdinateType > GivenData;
while ( CSVParser.read_row( X, Y ) )
GivenData.emplace( X, Y );
StoreMapData( GivenData.cbegin(), GivenData.cend() );
}
// There are copy constructors and move constructors basically passing the
// stored data on to the sample data map directly.
GenericFunction(
Interpolation::GenericFunction< AbscissaType, OrdinateType > && Other )
: AbscissaData( Other.AbscissaData ), OrdinateData( Other.OrdinateData )
{};
GenericFunction(
const Interpolation::GenericFunction< AbscissaType, OrdinateType > & Other )
: AbscissaData( Other.AbscissaData ), OrdinateData( Other.OrdinateData )
{ };
// The default constructor is not possible as the class must have the data
GenericFunction( void ) = delete;
// ---------------------------------------------------------------------------
// Algorithm function
// ---------------------------------------------------------------------------
//
// There is a function for the classes in the hierarchy to inspect what kind
// of algorithm is used for the interpolation involved. It is declared
// constexpr so that it can be used at compile time to check the algorithm
// avoiding a separate type definition in the various method classes. Note
// that this is a C++20 feature and the compiler should be able to support
// this.
constexpr virtual Algorithm::InterpolationType Method( void ) const = 0;
// ---------------------------------------------------------------------------
// Value functions
// ---------------------------------------------------------------------------
//
// Evaluating the function for a given argument depends on the interpolation
// method and the functions are therefore just defined. The value version
// will not throw, but return an optional that will not have a value if the
// argument was out of bounds for the interpolating method. The operator
// version will check the optional value returned from Value, and throw
// a invalid argument exception if the value function does not return a value.
virtual std::optional< OrdinateType > Value( AbscissaType Argument ) = 0;
OrdinateType operator() ( AbscissaType Argument )
{
std::optional< OrdinateType > Result( Value( Argument ) );
if( Result ) return Result.value();
else
{
std::ostringstream ErrorMessage;
ErrorMessage << __FILE__ << " at line " << __LINE__ << ": "
<< "The argument " << Argument << " is not valid for "
<< "the interpolation defined on the closed interval ["
<< DomainLower() << ", " << DomainUpper() << "] for the "
<< " interpolation method "
<< Algorithm::Description( Method() );
throw std::invalid_argument( ErrorMessage.str() );
}
}
// ---------------------------------------------------------------------------
// Domain functions
// ---------------------------------------------------------------------------
//
inline AbscissaType DomainLower (void) const
{ return AbscissaData.front(); }
inline AbscissaType DomainUpper (void) const
{ return AbscissaData.back(); }
inline bool DomainQ (AbscissaType x) const
{ return (DomainLower() <= x) && (x <= DomainUpper()); }
// ---------------------------------------------------------------------------
// Derivatives and integrals
// ---------------------------------------------------------------------------
//
// There are functions to find the derivatives of the interpolated function
// at certain points. However it is understood that not all derivatives
// exists for all algorithms, and the default behaviour is therefore to
// return an empty value
virtual
std::optional< OrdinateType > FirstDerivative( AbscissaType Argument )
{ return std::optional< OrdinateType >(); }
virtual
std::optional< OrdinateType > SecondDerivative( AbscissaType Argument )
{ return std::optional< OrdinateType >(); }
virtual
std::optional< OrdinateType > Integral( AbscissaType From, AbscissaType To )
{ return std::optional< OrdinateType >(); }
// ---------------------------------------------------------------------------
// Destructor
// ---------------------------------------------------------------------------
//
// The destructor does nothing, but it is a virtual place holder to ensure
// that the destructor of a derived class is properly invoked.
virtual ~GenericFunction()
{ }
};
/*==============================================================================
GNU Scientific Library base
==============================================================================*/
//
// GSL is C-style object oriented meaning that a data structure will be
// allocated and then populated with algorithm specific parameters. This is
// common behaviour to all GSL interpolation algorithms and it is therefore
// managed by a common base class.
//
// This class also inherits the Generic Function object specialized for double
// precision real valued data since the GSL algorithms are hard coded for
// doubles. The generic function is inherited as virtual mainly to force the
// algorithm specific classes to explicitly call its constructor to ensure it
// initializes the sample data map before the GSL interpolation object is
// allocated as the size of the data store is needed.
class GSL : public GenericFunction< double >
{
private:
// ---------------------------------------------------------------------------
// Interpolation object and accelerator
// ---------------------------------------------------------------------------
//
// The parameter storage for GSL is done in a object that is dynamically
// allocated depending on the algorithm type to be used.
gsl_interp * InterpolationObject = nullptr;
// The GSL needs an accelerator object holding the state of searches and an
// interpolation object holding the static state (coefficients) computed from
// the data. The accelerator object contains information about the
// interpolation function that may speed up some computations.
gsl_interp_accel * AcceleratorObject = nullptr;
// There is a function to allocate the two GSL objects and initialize them
// from the data stored in the Generic Function object. It should be noted
// that when computing the parameters of the interpolation algorithm, C-style
// arrays are needed and the above access functions are constructed for this
// purpose.
void AllocateAndCompute( const gsl_interp_type * GSLAlgorithm )
{
InterpolationAlgorithm = GSLAlgorithm;
// Allocate the GSL data storage
InterpolationObject = gsl_interp_alloc( GSLAlgorithm, SampleSize() );
AcceleratorObject = gsl_interp_accel_alloc();
// Compute the interpolation object data
gsl_interp_init( InterpolationObject,
AbscissaValues().data(), OrdinateValues().data(),
SampleSize() );
}
// Various functions using the accelerator object may actually write to it,
// and therefore it must be protected in order to allow the same
// interpolation object to be used from concurrent threads. The interpolation
// structure is passed as a constant to every function, so parallelism
// should not be a problem for this object.
std::mutex AcceleratorLock;
// It stores the GSL algorithm type. The only thing this is useful for is
// to be able to manipulate interpolation objects with arithmetic operations
// and to copy one object to another.
const gsl_interp_type * InterpolationAlgorithm;
// ---------------------------------------------------------------------------
// Error handling
// ---------------------------------------------------------------------------
//
// In the same way the error code returned from some of the value producing
// functions will be stored so that when an empty optional is received it
// is possible to check the error value.
int GSLErrorCode;
public:
// This code can be reported as a string if it should be communicated to the
// user somehow.
inline bool ErrorQ( void )
{ return GSLErrorCode != GSL_SUCCESS; }
inline std::string ErrorMessage( void )
{ return gsl_strerror( GSLErrorCode ); }
// ---------------------------------------------------------------------------
// Constructors
// ---------------------------------------------------------------------------
//
// The same set of constructors as for the Generic Function is supported,
// and they all take an extra parameter being the GSL ID of the algorithm.
// This is used to allocate the GSL data structures correctly.
//
// The first constructor is used when iterators for a map of data is
// directly given. The underlying numerical type can be whatever as the
// conversion to doubles will be managed by the generic function.
template< class MapIterator >
GSL( MapIterator FirstDataPoint, MapIterator LastDataPoint,
const gsl_interp_type * GSLAlgorithm )
: GenericFunction( FirstDataPoint, LastDataPoint )
{ AllocateAndCompute( GSLAlgorithm ); }
// The second constructor assumes that the map is given, and this requires
// template arguments for all possible template arguments for the map.
template< typename XValueType, typename YValueType,
class Comparator, class Allocator >
GSL( const std::map< XValueType, YValueType, Comparator, Allocator >
& DataPoints, const gsl_interp_type * GSLAlgorithm )
: GenericFunction( DataPoints )
{ AllocateAndCompute( GSLAlgorithm );}
// The third constructor takes data already filled in separate sequence
// containers for the abscissa values and the ordinate values. As long as
// the data can be converted to a double it is OK.
template< class AbscissaIterator, class OrdinateIterator >
GSL( AbscissaIterator XValues, AbscissaIterator XValuesEnd,
OrdinateIterator YValues, const gsl_interp_type * GSLAlgorithm )
: GenericFunction( XValues, XValuesEnd, YValues )
{ AllocateAndCompute( GSLAlgorithm ); }
// The last constructor is taking a CSV file of data and parse this
// to get the sample data
GSL( const std::filesystem::path & FileName,
const gsl_interp_type * GSLAlgorithm )
: GenericFunction( FileName )
{ AllocateAndCompute( GSLAlgorithm );}
// Moving and copying is allowed. Moving means taking over the initialized
// objects from the other type, whereas copying means basically recomputing
// based on the data from the other.
GSL( GSL && Other )
: GenericFunction( Other )
{
std::lock_guard< std::mutex > Lock( Other.AcceleratorLock );
InterpolationObject = Other.InterpolationObject;
AcceleratorObject = Other.AcceleratorObject;
Other.InterpolationObject = nullptr;
Other.AcceleratorObject = nullptr;
}
GSL( const GSL & Other )
: GenericFunction( Other )
{ AllocateAndCompute( Other.InterpolationAlgorithm ); }
// The standard constructor is disallowed
GSL( void ) = delete;
// ---------------------------------------------------------------------------
// Evaluating the interpolated function
// ---------------------------------------------------------------------------
//
// The evaluation function of the GSL library is used, and if it returns a
// domain error, it means that the argument is outside of the bounds of the
// abscissa. In this case an empty optional will be returned. It seems that
// this could be readily tested by checking the domain and not try to do an
// evaluation, but for periodic interpolations one may be able to use the
// interpolation for extrapolation and as such the decision is left to the
// GSL routines.
virtual std::optional< double > Value( double Argument )
{
std::lock_guard< std::mutex > Lock( AcceleratorLock );
double InterpolatedValue;
GSLErrorCode = gsl_interp_eval_e( InterpolationObject,
AbscissaValues().data(), OrdinateValues().data(),
Argument, AcceleratorObject, &InterpolatedValue );
if ( GSLErrorCode == GSL_EDOM )
return std::optional< double >();
else
return std::optional< double >( InterpolatedValue );
}
// ---------------------------------------------------------------------------
// Derivatives and integrals
// ---------------------------------------------------------------------------
//
// There are functions to find the derivatives of the interpolated function
// at certain points. If the argument is outside of the domain for the given
// operation, an empty optional is returned. Again, the decision on what to
// return is made by the GSL.
virtual
std::optional< double > FirstDerivative( double Argument )
{
std::lock_guard< std::mutex > Lock( AcceleratorLock );
double InterpolatedValue;
GSLErrorCode = gsl_interp_eval_deriv_e( InterpolationObject,
AbscissaValues().data(), OrdinateValues().data(),
Argument, AcceleratorObject, &InterpolatedValue );
if ( ErrorQ() )
return std::optional< double >();
else
return std::optional< double >( InterpolatedValue );
}
virtual
std::optional< double > SecondDerivative( double Argument )
{
std::lock_guard< std::mutex > Lock( AcceleratorLock );
double InterpolatedValue;
GSLErrorCode = gsl_interp_eval_deriv2_e( InterpolationObject,
AbscissaValues().data(), OrdinateValues().data(),
Argument, AcceleratorObject, &InterpolatedValue );
if ( ErrorQ() )
return std::optional< double >();
else
return std::optional< double >( InterpolatedValue );
}
virtual
std::optional< double > Integral( double From, double To )
{
std::lock_guard< std::mutex > Lock( AcceleratorLock );
double InterpolatedValue;
GSLErrorCode = gsl_interp_eval_integ_e( InterpolationObject,
AbscissaValues().data(), OrdinateValues().data(),
From, To, AcceleratorObject, &InterpolatedValue );
if ( ErrorQ() )
return std::optional< double >();
else
return std::optional< double >( InterpolatedValue );
}
// ---------------------------------------------------------------------------
// Destructor
// ---------------------------------------------------------------------------
//
// The destructor ensures that the allocated objects are properly destroyed
// if they have been allocated and if they are still under the ownership of
// this object.
virtual ~GSL()
{
if ( InterpolationObject != nullptr )
gsl_interp_free( InterpolationObject );
if ( AcceleratorObject != nullptr )
{
std::lock_guard< std::mutex > Lock( AcceleratorLock );
gsl_interp_accel_free( AcceleratorObject );
}
}
};
/*==============================================================================
Interpolating functions
==============================================================================*/
//
// An interpolating function is a template specialized for each of the available
// algorithms. Fundamentally, each class needs to specify the constructors and
// the virtual functions, and the implementation of the actual calculations
// will be managed by the defined base classes. The references are at the top
// of this file. The non-uniform algorithms are defined first in alphabetical
// order, and then the uniform algorithms and finally the periodic
// interpolations
//
// The function templates for specifying the real types used can be understood
// as iterators for some algorithms
template< Algorithm::InterpolationType TheAlgorithm,
class AbscissaReal = double, class OrdinateReal = AbscissaReal >
class Function;
// ---------------------------------------------------------------------------
// Steffen's method
// ---------------------------------------------------------------------------
//
// Steffen's method [4] guarantees monotonicity in the interpolation and has
// therefore been chosen as the default method here, with the Akima spline
// as a good candidate to consider if additivity is desired. Because of the
// monotonicity Steffen's method guarantees that minima and maxima can only
// occur exactly at the data points, and there can never be spurious
// oscillations between data points. The interpolated function is piecewise
// cubic in each interval. The resulting curve and its first derivative are
// guaranteed to be continuous, but the second derivative may be discontinuous.
template<>
class Function< Algorithm::Univariate::Steffen >
: public GSL
{
public:
// Then the constructors can be defined.
template< class MapIterator >
Function( MapIterator FirstDataPoint, MapIterator LastDataPoint)
: GSL( FirstDataPoint, LastDataPoint, gsl_interp_steffen )
{}
template< typename XValueType, typename YValueType,
class Comparator, class Allocator >
Function( const std::map< XValueType, YValueType, Comparator, Allocator >
& DataPoints )
: GSL( DataPoints, gsl_interp_steffen )
{}
template< class AbscissaIterator, class OrdinateIterator >
Function( AbscissaIterator XValues, AbscissaIterator XValuesEnd,
OrdinateIterator YValues )
: GSL( XValues, XValuesEnd, YValues, gsl_interp_steffen )
{}
Function( const std::filesystem::path & FileName )
: GSL( FileName, gsl_interp_steffen )
{}
Function( Function< Algorithm::Univariate::Steffen > && Other )
: GSL( Other )
{}
Function( Function< Algorithm::Univariate::Steffen > & Other )
: GSL( Other )
{}
constexpr virtual Algorithm::InterpolationType Method( void ) const
{ return Algorithm::Univariate::Steffen; }
virtual ~Function()
{}
};
// ---------------------------------------------------------------------------
// Akima
// ---------------------------------------------------------------------------
//
// Splines generally fit cubic polynomials to each pair of points along the
// function, matching the first and second order derivatives in the given
// data points. However, these splines can oscillate near outliers [5], and a
// more robust alternative would be to use Akima splines [6].
template<>
class Function< Algorithm::Univariate::Akima >
: public GSL
{
public:
// Then the constructors can be defined.
template< class MapIterator >
Function( MapIterator FirstDataPoint, MapIterator LastDataPoint)
: GSL( FirstDataPoint, LastDataPoint, gsl_interp_akima )
{}
template< typename XValueType, typename YValueType,
class Comparator, class Allocator >
Function( const std::map< XValueType, YValueType, Comparator, Allocator >
& DataPoints )
: GSL( DataPoints, gsl_interp_akima )
{}
template< class AbscissaIterator, class OrdinateIterator >
Function( AbscissaIterator XValues, AbscissaIterator XValuesEnd,
OrdinateIterator YValues )
: GSL( XValues, XValuesEnd, YValues, gsl_interp_akima )
{}
Function( const std::filesystem::path & FileName )
: GSL( FileName, gsl_interp_akima )
{}
Function( Function< Algorithm::Univariate::Akima > && Other )
: GSL( Other )
{}
Function( Function< Algorithm::Univariate::Akima > & Other )