• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1[section:test_data Graphing, Profiling, and Generating Test Data for Special Functions]
2
3The class `test_data` and associated helper functions are designed so that in just
4a few lines of code you should be able to:
5
6* Profile a continued fraction, or infinite series for convergence and accuracy.
7* Generate csv data from a special function that can be imported into your favorite
8graphing program (or spreadsheet) for further analysis.
9* Generate high precision test data.
10
11[h4 Synopsis]
12
13   #include <boost/math/tools/test_data.hpp>
14
15[important
16This is a non-core Boost.Math header that is predominantly used for internal
17maintenance of the library: as a result the library is located under
18`libs/math/include_private` and you will need to add that directory to
19your include path in order to use this feature.
20]
21
22   namespace boost{ namespace math{ namespace tools{
23
24   enum parameter_type
25   {
26      random_in_range = 0,
27      periodic_in_range = 1,
28      power_series = 2,
29      dummy_param = 0x80,
30   };
31
32   template <class T>
33   struct parameter_info;
34
35   template <class T>
36   parameter_info<T> make_random_param(T start_range, T end_range, int n_points);
37
38   template <class T>
39   parameter_info<T> make_periodic_param(T start_range, T end_range, int n_points);
40
41   template <class T>
42   parameter_info<T> make_power_param(T basis, int start_exponent, int end_exponent);
43
44   template <class T>
45   bool get_user_parameter_info(parameter_info<T>& info, const char* param_name);
46
47   template <class T>
48   class test_data
49   {
50   public:
51      typedef std::vector<T> row_type;
52      typedef row_type value_type;
53   private:
54      typedef std::set<row_type> container_type;
55   public:
56      typedef typename container_type::reference reference;
57      typedef typename container_type::const_reference const_reference;
58      typedef typename container_type::iterator iterator;
59      typedef typename container_type::const_iterator const_iterator;
60      typedef typename container_type::difference_type difference_type;
61      typedef typename container_type::size_type size_type;
62
63      // creation:
64      test_data(){}
65      template <class F>
66      test_data(F func, const parameter_info<T>& arg1);
67
68      // insertion:
69      template <class F>
70      test_data& insert(F func, const parameter_info<T>& arg1);
71
72      template <class F>
73      test_data& insert(F func, const parameter_info<T>& arg1,
74                        const parameter_info<T>& arg2);
75
76      template <class F>
77      test_data& insert(F func, const parameter_info<T>& arg1,
78                        const parameter_info<T>& arg2,
79                        const parameter_info<T>& arg3);
80
81      void clear();
82
83      // access:
84      iterator begin();
85      iterator end();
86      const_iterator begin()const;
87      const_iterator end()const;
88      bool operator==(const test_data& d)const;
89      bool operator!=(const test_data& d)const;
90      void swap(test_data& other);
91      size_type size()const;
92      size_type max_size()const;
93      bool empty()const;
94
95      bool operator < (const test_data& dat)const;
96      bool operator <= (const test_data& dat)const;
97      bool operator > (const test_data& dat)const;
98      bool operator >= (const test_data& dat)const;
99   };
100
101   template <class charT, class traits, class T>
102   std::basic_ostream<charT, traits>& write_csv(
103               std::basic_ostream<charT, traits>& os,
104               const test_data<T>& data);
105
106   template <class charT, class traits, class T>
107   std::basic_ostream<charT, traits>& write_csv(
108               std::basic_ostream<charT, traits>& os,
109               const test_data<T>& data,
110               const charT* separator);
111
112   template <class T>
113   std::ostream& write_code(std::ostream& os,
114                            const test_data<T>& data,
115                            const char* name);
116
117   }}} // namespaces
118
119[h4 Description]
120
121This tool is best illustrated with the following series of examples.
122
123The functionality of test_data is split into the following parts:
124
125* A functor that implements the function for which data is being generated:
126this is the bit you have to write.
127* One of more parameters that are to be passed to the functor, these are
128described in fairly abstract terms: give me N points distributed like /this/ etc.
129* The class test_data, that takes the functor and descriptions of the parameters
130and computes how ever many output points have been requested, these are stored
131in a sorted container.
132* Routines to iterate over the test_data container and output the data in either
133csv format, or as C++ source code (as a table using Boost.Array).
134
135[h5 Example 1: Output Data for Graph Plotting]
136
137For example, lets say we want to graph the lgamma function between -3 and 100,
138one could do this like so:
139
140   #include <boost/math/tools/test_data.hpp>
141   #include <boost/math/special_functions/gamma.hpp>
142
143   int main()
144   {
145      using namespace boost::math::tools;
146
147      // create an object to hold the data:
148      test_data<double> data;
149
150      // insert 500 points at uniform intervals between just after -3 and 100:
151      double (*pf)(double) = boost::math::lgamma;
152      data.insert(pf, make_periodic_param(-3.0 + 0.00001, 100.0, 500));
153
154      // print out in csv format:
155      write_csv(std::cout, data, ", ");
156      return 0;
157   }
158
159Which, when plotted, results in:
160
161[graph lgamma]
162
163[h5 Example 2: Creating Test Data]
164
165As a second example, let's suppose we want to create highly accurate test
166data for a special function.  Since many special functions have two or
167more independent parameters, it's very hard to effectively cover all of
168the possible parameter space without generating gigabytes of data at
169great computational expense.  A second best approach is to provide the tools
170by which a user (or the library maintainer) can quickly generate more data
171on demand to probe the function over a particular domain of interest.
172
173In this example we'll generate test data for the beta function using
174[@http://shoup.net/ntl/doc/RR.txt NTL::RR] at 1000 bit precision.
175Rather than call our generic
176version of the beta function, we'll implement a deliberately naive version
177of the beta function using lgamma, and rely on the high precision of the
178data type used to get results accurate to at least 128-bit precision. In this
179way our test data is independent of whatever clever tricks we may wish to
180use inside the our beta function.
181
182To start with then, here's the function object that creates the test data:
183
184   #include <boost/math/tools/ntl.hpp>
185   #include <boost/math/special_functions/gamma.hpp>
186   #include <boost/math/tools/test_data.hpp>
187   #include <fstream>
188
189   #include <boost/math/tools/test_data.hpp>
190
191   using namespace boost::math::tools;
192
193   struct beta_data_generator
194   {
195      NTL::RR operator()(NTL::RR a, NTL::RR b)
196      {
197         //
198         // If we throw a domain error then test_data will
199         // ignore this input point. We'll use this to filter
200         // out all cases where a < b since the beta function
201         // is symmetrical in a and b:
202         //
203         if(a < b)
204            throw std::domain_error("");
205
206         // very naively calculate spots with lgamma:
207         NTL::RR g1, g2, g3;
208         int s1, s2, s3;
209         g1 = boost::math::lgamma(a, &s1);
210         g2 = boost::math::lgamma(b, &s2);
211         g3 = boost::math::lgamma(a+b, &s3);
212         g1 += g2 - g3;
213         g1 = exp(g1);
214         g1 *= s1 * s2 * s3;
215         return g1;
216      }
217   };
218
219To create the data, we'll need to input the domains for a and b
220for which the function will be tested: the function `get_user_parameter_info`
221is designed for just that purpose.  The start of main will look something like:
222
223   // Set the precision on RR:
224   NTL::RR::SetPrecision(1000); // bits.
225   NTL::RR::SetOutputPrecision(40); // decimal digits.
226
227   parameter_info<NTL::RR> arg1, arg2;
228   test_data<NTL::RR> data;
229
230   std::cout << "Welcome.\n"
231      "This program will generate spot tests for the beta function:\n"
232      "  beta(a, b)\n\n";
233
234   bool cont;
235   std::string line;
236
237   do{
238      // prompt the user for the domain of a and b to test:
239      get_user_parameter_info(arg1, "a");
240      get_user_parameter_info(arg2, "b");
241
242      // create the data:
243      data.insert(beta_data_generator(), arg1, arg2);
244
245      // see if the user want's any more domains tested:
246      std::cout << "Any more data [y/n]?";
247      std::getline(std::cin, line);
248      boost::algorithm::trim(line);
249      cont = (line == "y");
250   }while(cont);
251
252[caution At this point one potential stumbling block should be mentioned:
253test_data<>::insert will create a matrix of test data when there are two
254or more parameters, so if we have two parameters and we're asked for
255a thousand points on each, that's a ['million test points in total].
256Don't say you weren't warned!]
257
258There's just one final step now, and that's to write the test data to file:
259
260   std::cout << "Enter name of test data file [default=beta_data.ipp]";
261   std::getline(std::cin, line);
262   boost::algorithm::trim(line);
263   if(line == "")
264      line = "beta_data.ipp";
265   std::ofstream ofs(line.c_str());
266   write_code(ofs, data, "beta_data");
267
268The format of the test data looks something like:
269
270   #define SC_(x) static_cast<T>(BOOST_JOIN(x, L))
271      static const boost::array<boost::array<T, 3>, 1830>
272      beta_med_data = {
273         SC_(0.4883005917072296142578125),
274         SC_(0.4883005917072296142578125),
275         SC_(3.245912809500479157065104747353807392371),
276         SC_(3.5808107852935791015625),
277         SC_(0.4883005917072296142578125),
278         SC_(1.007653173802923954909901438393379243537),
279         /* ... lots of rows skipped */
280   };
281
282The first two values in each row are the input parameters that were passed
283to our functor and the last value is the return value from the functor.
284Had our functor returned a __tuple rather than a value, then we would have had
285one entry for each element in the tuple in addition to the input parameters.
286
287The first #define serves two purposes:
288
289* It reduces the file sizes considerably: all those `static_cast`'s add up to a lot
290of bytes otherwise (they are needed to suppress compiler warnings when `T` is
291narrower than a `long double`).
292* It provides a useful customisation point: for example if we were testing
293a user-defined type that has more precision than a `long double` we could change
294it to:
295
296[^#define SC_(x) lexical_cast<T>(BOOST_STRINGIZE(x))]
297
298in order to ensure that no truncation of the values occurs prior to conversion
299to `T`.  Note that this isn't used by default as it's rather hard on the compiler
300when the table is large.
301
302[h5 Example 3: Profiling a Continued Fraction for Convergence and Accuracy]
303
304Alternatively, lets say we want to profile a continued fraction for
305convergence and error.  As an example, we'll use the continued fraction
306for the upper incomplete gamma function, the following function object
307returns the next a[sub N ] and b[sub N ] of the continued fraction
308each time it's invoked:
309
310   template <class T>
311   struct upper_incomplete_gamma_fract
312   {
313   private:
314      T z, a;
315      int k;
316   public:
317      typedef std::pair<T,T> result_type;
318
319      upper_incomplete_gamma_fract(T a1, T z1)
320         : z(z1-a1+1), a(a1), k(0)
321      {
322      }
323
324      result_type operator()()
325      {
326         ++k;
327         z += 2;
328         return result_type(k * (a - k), z);
329      }
330   };
331
332We want to measure both the relative error, and the rate of convergence
333of this fraction, so we'll write a functor that returns both as a __tuple:
334class test_data will unpack the tuple for us, and create one column of data
335for each element in the tuple (in addition to the input parameters):
336
337   #include <boost/math/tools/test_data.hpp>
338   #include <boost/math/tools/test.hpp>
339   #include <boost/math/special_functions/gamma.hpp>
340   #include <boost/math/tools/ntl.hpp>
341   #include <boost/math/tools/tuple.hpp>
342
343   template <class T>
344   struct profile_gamma_fraction
345   {
346      typedef ``__tuple``<T, T> result_type;
347
348      result_type operator()(T val)
349      {
350         using namespace boost::math::tools;
351         // estimate the true value, using arbitrary precision
352         // arithmetic and NTL::RR:
353         NTL::RR rval(val);
354         upper_incomplete_gamma_fract<NTL::RR> f1(rval, rval);
355         NTL::RR true_val = continued_fraction_a(f1, 1000);
356         //
357         // Now get the approximation at double precision, along with the number of
358         // iterations required:
359         boost::uintmax_t iters = std::numeric_limits<boost::uintmax_t>::max();
360         upper_incomplete_gamma_fract<T> f2(val, val);
361         T found_val = continued_fraction_a(f2, std::numeric_limits<T>::digits, iters);
362         //
363         // Work out the relative error, as measured in units of epsilon:
364         T err = real_cast<T>(relative_error(true_val, NTL::RR(found_val)) / std::numeric_limits<T>::epsilon());
365         //
366         // now just return the results as a tuple:
367         return boost::math::make_tuple(err, iters);
368      }
369   };
370
371Feeding that functor into test_data allows rapid output of csv data,
372for whatever type `T` we may be interested in:
373
374   int main()
375   {
376      using namespace boost::math::tools;
377      // create an object to hold the data:
378      test_data<double> data;
379      // insert 500 points at uniform intervals between just after 0 and 100:
380      data.insert(profile_gamma_fraction<double>(), make_periodic_param(0.01, 100.0, 100));
381      // print out in csv format:
382      write_csv(std::cout, data, ", ");
383      return 0;
384   }
385
386This time there's no need to plot a graph, the first few rows are:
387
388   a and z,  Error/epsilon,  Iterations required
389
390   0.01,     9723.14,        4726
391   1.0099,   9.54818,        87
392   2.0098,   3.84777,        40
393   3.0097,   0.728358,       25
394   4.0096,   2.39712,        21
395   5.0095,   0.233263,       16
396
397So it's pretty clear that this fraction shouldn't be used for small values of /a/ and /z/.
398
399[h4 reference]
400
401Most of this tool has been described already in the examples above, we'll
402just add the following notes on the non-member functions:
403
404   template <class T>
405   parameter_info<T> make_random_param(T start_range, T end_range, int n_points);
406
407Tells class test_data to test /n_points/ random values in the range
408[start_range,end_range].
409
410   template <class T>
411   parameter_info<T> make_periodic_param(T start_range, T end_range, int n_points);
412
413Tells class test_data to test /n_points/ evenly spaced values in the range
414[start_range,end_range].
415
416   template <class T>
417   parameter_info<T> make_power_param(T basis, int start_exponent, int end_exponent);
418
419Tells class test_data to test points of the form ['basis + R * 2[super expon]] for each
420/expon/ in the range [start_exponent, end_exponent], and /R/ a random number in \[0.5, 1\].
421
422   template <class T>
423   bool get_user_parameter_info(parameter_info<T>& info, const char* param_name);
424
425Prompts the user for the parameter range and form to use.
426
427Finally, if we don't want the parameter to be included in the output, we can tell
428test_data by setting it a "dummy parameter":
429
430   parameter_info<double> p = make_random_param(2.0, 5.0, 10);
431   p.type |= dummy_param;
432
433This is useful when the functor used transforms the parameter in some way
434before passing it to the function under test, usually the functor will then
435return both the transformed input and the result in a tuple, so there's no
436need for the original pseudo-parameter to be included in program output.
437
438[endsect] [/section:test_data Graphing, Profiling, and Generating Test Data for Special Functions]
439
440[/
441  Copyright 2006 John Maddock and Paul A. Bristow.
442  Distributed under the Boost Software License, Version 1.0.
443  (See accompanying file LICENSE_1_0.txt or copy at
444  http://www.boost.org/LICENSE_1_0.txt).
445]
446