mirror of
				https://github.com/saitohirga/WSJT-X.git
				synced 2025-10-31 04:50:34 -04:00 
			
		
		
		
	
		
			
				
	
	
		
			447 lines
		
	
	
		
			16 KiB
		
	
	
	
		
			Plaintext
		
	
	
	
	
	
			
		
		
	
	
			447 lines
		
	
	
		
			16 KiB
		
	
	
	
		
			Plaintext
		
	
	
	
	
	
| [section:test_data Graphing, Profiling, and Generating Test Data for Special Functions]
 | |
| 
 | |
| The class `test_data` and associated helper functions are designed so that in just
 | |
| a few lines of code you should be able to:
 | |
| 
 | |
| * Profile a continued fraction, or infinite series for convergence and accuracy.
 | |
| * Generate csv data from a special function that can be imported into your favorite
 | |
| graphing program (or spreadsheet) for further analysis.
 | |
| * Generate high precision test data.
 | |
| 
 | |
| [h4 Synopsis]
 | |
| 
 | |
|    #include <boost/math/tools/test_data.hpp>
 | |
| 
 | |
| [important
 | |
| This is a non-core Boost.Math header that is predominantly used for internal
 | |
| maintenance of the library: as a result the library is located under 
 | |
| `libs/math/include_private` and you will need to add that directory to
 | |
| your include path in order to use this feature.
 | |
| ]
 | |
| 
 | |
|    namespace boost{ namespace math{ namespace tools{
 | |
| 
 | |
|    enum parameter_type
 | |
|    {
 | |
|       random_in_range = 0,
 | |
|       periodic_in_range = 1,
 | |
|       power_series = 2,
 | |
|       dummy_param = 0x80,
 | |
|    };
 | |
| 
 | |
|    template <class T>
 | |
|    struct parameter_info;
 | |
| 
 | |
|    template <class T>
 | |
|    parameter_info<T> make_random_param(T start_range, T end_range, int n_points);
 | |
| 
 | |
|    template <class T>
 | |
|    parameter_info<T> make_periodic_param(T start_range, T end_range, int n_points);
 | |
| 
 | |
|    template <class T>
 | |
|    parameter_info<T> make_power_param(T basis, int start_exponent, int end_exponent);
 | |
| 
 | |
|    template <class T>
 | |
|    bool get_user_parameter_info(parameter_info<T>& info, const char* param_name);
 | |
| 
 | |
|    template <class T>
 | |
|    class test_data
 | |
|    {
 | |
|    public:
 | |
|       typedef std::vector<T> row_type;
 | |
|       typedef row_type value_type;
 | |
|    private:
 | |
|       typedef std::set<row_type> container_type;
 | |
|    public:
 | |
|       typedef typename container_type::reference reference;
 | |
|       typedef typename container_type::const_reference const_reference;
 | |
|       typedef typename container_type::iterator iterator;
 | |
|       typedef typename container_type::const_iterator const_iterator;
 | |
|       typedef typename container_type::difference_type difference_type;
 | |
|       typedef typename container_type::size_type size_type;
 | |
| 
 | |
|       // creation:
 | |
|       test_data(){}
 | |
|       template <class F>
 | |
|       test_data(F func, const parameter_info<T>& arg1);
 | |
| 
 | |
|       // insertion:
 | |
|       template <class F>
 | |
|       test_data& insert(F func, const parameter_info<T>& arg1);
 | |
| 
 | |
|       template <class F>
 | |
|       test_data& insert(F func, const parameter_info<T>& arg1, 
 | |
|                         const parameter_info<T>& arg2);
 | |
| 
 | |
|       template <class F>
 | |
|       test_data& insert(F func, const parameter_info<T>& arg1, 
 | |
|                         const parameter_info<T>& arg2, 
 | |
|                         const parameter_info<T>& arg3);
 | |
| 
 | |
|       void clear();
 | |
| 
 | |
|       // access:
 | |
|       iterator begin();
 | |
|       iterator end();
 | |
|       const_iterator begin()const;
 | |
|       const_iterator end()const;
 | |
|       bool operator==(const test_data& d)const;
 | |
|       bool operator!=(const test_data& d)const;
 | |
|       void swap(test_data& other);
 | |
|       size_type size()const;
 | |
|       size_type max_size()const;
 | |
|       bool empty()const;
 | |
| 
 | |
|       bool operator < (const test_data& dat)const;
 | |
|       bool operator <= (const test_data& dat)const;
 | |
|       bool operator > (const test_data& dat)const;
 | |
|       bool operator >= (const test_data& dat)const;
 | |
|    };
 | |
| 
 | |
|    template <class charT, class traits, class T>
 | |
|    std::basic_ostream<charT, traits>& write_csv(
 | |
|                std::basic_ostream<charT, traits>& os,
 | |
|                const test_data<T>& data);
 | |
| 
 | |
|    template <class charT, class traits, class T>
 | |
|    std::basic_ostream<charT, traits>& write_csv(
 | |
|                std::basic_ostream<charT, traits>& os,
 | |
|                const test_data<T>& data,
 | |
|                const charT* separator);
 | |
| 
 | |
|    template <class T>
 | |
|    std::ostream& write_code(std::ostream& os,
 | |
|                             const test_data<T>& data, 
 | |
|                             const char* name);
 | |
|                             
 | |
|    }}} // namespaces
 | |
|    
 | |
| [h4 Description]
 | |
| 
 | |
| This tool is best illustrated with the following series of examples.
 | |
| 
 | |
| The functionality of test_data is split into the following parts:
 | |
| 
 | |
| * A functor that implements the function for which data is being generated:
 | |
| this is the bit you have to write.
 | |
| * One of more parameters that are to be passed to the functor, these are
 | |
| described in fairly abstract terms: give me N points distributed like /this/ etc.
 | |
| * The class test_data, that takes the functor and descriptions of the parameters
 | |
| and computes how ever many output points have been requested, these are stored
 | |
| in a sorted container.
 | |
| * Routines to iterate over the test_data container and output the data in either
 | |
| csv format, or as C++ source code (as a table using Boost.Array).
 | |
| 
 | |
| [h5 Example 1: Output Data for Graph Plotting]
 | |
| 
 | |
| For example, lets say we want to graph the lgamma function between -3 and 100,
 | |
| one could do this like so:
 | |
| 
 | |
|    #include <boost/math/tools/test_data.hpp>
 | |
|    #include <boost/math/special_functions/gamma.hpp>
 | |
|    
 | |
|    int main()
 | |
|    {
 | |
|       using namespace boost::math::tools;
 | |
|       
 | |
|       // create an object to hold the data:
 | |
|       test_data<double> data;
 | |
|       
 | |
|       // insert 500 points at uniform intervals between just after -3 and 100:
 | |
|       double (*pf)(double) = boost::math::lgamma;
 | |
|       data.insert(pf, make_periodic_param(-3.0 + 0.00001, 100.0, 500));
 | |
|       
 | |
|       // print out in csv format:
 | |
|       write_csv(std::cout, data, ", ");
 | |
|       return 0;
 | |
|    }
 | |
|    
 | |
| Which, when plotted, results in:
 | |
| 
 | |
| [graph lgamma]
 | |
| 
 | |
| [h5 Example 2: Creating Test Data]
 | |
| 
 | |
| As a second example, let's suppose we want to create highly accurate test
 | |
| data for a special function.  Since many special functions have two or
 | |
| more independent parameters, it's very hard to effectively cover all of
 | |
| the possible parameter space without generating gigabytes of data at
 | |
| great computational expense.  A second best approach is to provide the tools
 | |
| by which a user (or the library maintainer) can quickly generate more data
 | |
| on demand to probe the function over a particular domain of interest.
 | |
| 
 | |
| In this example we'll generate test data for the beta function using
 | |
| [@http://shoup.net/ntl/doc/RR.txt NTL::RR] at 1000 bit precision.
 | |
| Rather than call our generic
 | |
| version of the beta function, we'll implement a deliberately naive version
 | |
| of the beta function using lgamma, and rely on the high precision of the 
 | |
| data type used to get results accurate to at least 128-bit precision. In this
 | |
| way our test data is independent of whatever clever tricks we may wish to
 | |
| use inside the our beta function.
 | |
| 
 | |
| To start with then, here's the function object that creates the test data:
 | |
| 
 | |
|    #include <boost/math/tools/ntl.hpp>
 | |
|    #include <boost/math/special_functions/gamma.hpp>
 | |
|    #include <boost/math/tools/test_data.hpp>
 | |
|    #include <fstream>
 | |
| 
 | |
|    #include <boost/math/tools/test_data.hpp>
 | |
| 
 | |
|    using namespace boost::math::tools;
 | |
| 
 | |
|    struct beta_data_generator
 | |
|    {
 | |
|       NTL::RR operator()(NTL::RR a, NTL::RR b)
 | |
|       {
 | |
|          //
 | |
|          // If we throw a domain error then test_data will
 | |
|          // ignore this input point. We'll use this to filter
 | |
|          // out all cases where a < b since the beta function
 | |
|          // is symmetrical in a and b:
 | |
|          //
 | |
|          if(a < b)
 | |
|             throw std::domain_error("");
 | |
|             
 | |
|          // very naively calculate spots with lgamma:
 | |
|          NTL::RR g1, g2, g3;
 | |
|          int s1, s2, s3;
 | |
|          g1 = boost::math::lgamma(a, &s1);
 | |
|          g2 = boost::math::lgamma(b, &s2);
 | |
|          g3 = boost::math::lgamma(a+b, &s3);
 | |
|          g1 += g2 - g3;
 | |
|          g1 = exp(g1);
 | |
|          g1 *= s1 * s2 * s3;
 | |
|          return g1;
 | |
|       }
 | |
|    };
 | |
| 
 | |
| To create the data, we'll need to input the domains for a and b
 | |
| for which the function will be tested: the function `get_user_parameter_info`
 | |
| is designed for just that purpose.  The start of main will look something like:
 | |
| 
 | |
|    // Set the precision on RR:
 | |
|    NTL::RR::SetPrecision(1000); // bits.
 | |
|    NTL::RR::SetOutputPrecision(40); // decimal digits.
 | |
| 
 | |
|    parameter_info<NTL::RR> arg1, arg2;
 | |
|    test_data<NTL::RR> data;
 | |
| 
 | |
|    std::cout << "Welcome.\n"
 | |
|       "This program will generate spot tests for the beta function:\n"
 | |
|       "  beta(a, b)\n\n";
 | |
| 
 | |
|    bool cont;
 | |
|    std::string line;
 | |
| 
 | |
|    do{
 | |
|       // prompt the user for the domain of a and b to test:
 | |
|       get_user_parameter_info(arg1, "a");
 | |
|       get_user_parameter_info(arg2, "b");
 | |
|       
 | |
|       // create the data:
 | |
|       data.insert(beta_data_generator(), arg1, arg2);
 | |
|       
 | |
|       // see if the user want's any more domains tested:
 | |
|       std::cout << "Any more data [y/n]?";
 | |
|       std::getline(std::cin, line);
 | |
|       boost::algorithm::trim(line);
 | |
|       cont = (line == "y");
 | |
|    }while(cont);
 | |
| 
 | |
| [caution At this point one potential stumbling block should be mentioned:
 | |
| test_data<>::insert will create a matrix of test data when there are two
 | |
| or more parameters, so if we have two parameters and we're asked for
 | |
| a thousand points on each, that's a ['million test points in total].
 | |
| Don't say you weren't warned!]
 | |
| 
 | |
| There's just one final step now, and that's to write the test data to file:
 | |
| 
 | |
|    std::cout << "Enter name of test data file [default=beta_data.ipp]";
 | |
|    std::getline(std::cin, line);
 | |
|    boost::algorithm::trim(line);
 | |
|    if(line == "")
 | |
|       line = "beta_data.ipp";
 | |
|    std::ofstream ofs(line.c_str());
 | |
|    write_code(ofs, data, "beta_data");
 | |
| 
 | |
| The format of the test data looks something like:
 | |
| 
 | |
|    #define SC_(x) static_cast<T>(BOOST_JOIN(x, L))
 | |
|       static const boost::array<boost::array<T, 3>, 1830>
 | |
|       beta_med_data = {
 | |
|          SC_(0.4883005917072296142578125),
 | |
|          SC_(0.4883005917072296142578125),
 | |
|          SC_(3.245912809500479157065104747353807392371), 
 | |
|          SC_(3.5808107852935791015625),
 | |
|          SC_(0.4883005917072296142578125),
 | |
|          SC_(1.007653173802923954909901438393379243537), 
 | |
|          /* ... lots of rows skipped */
 | |
|    };
 | |
| 
 | |
| The first two values in each row are the input parameters that were passed
 | |
| to our functor and the last value is the return value from the functor.
 | |
| Had our functor returned a __tuple rather than a value, then we would have had
 | |
| one entry for each element in the tuple in addition to the input parameters.
 | |
| 
 | |
| The first #define serves two purposes:
 | |
| 
 | |
| * It reduces the file sizes considerably: all those `static_cast`'s add up to a lot
 | |
| of bytes otherwise (they are needed to suppress compiler warnings when `T` is 
 | |
| narrower than a `long double`).
 | |
| * It provides a useful customisation point: for example if we were testing
 | |
| a user-defined type that has more precision than a `long double` we could change
 | |
| it to:
 | |
| 
 | |
| [^#define SC_(x) lexical_cast<T>(BOOST_STRINGIZE(x))]
 | |
|    
 | |
| in order to ensure that no truncation of the values occurs prior to conversion
 | |
| to `T`.  Note that this isn't used by default as it's rather hard on the compiler
 | |
| when the table is large.
 | |
| 
 | |
| [h5 Example 3: Profiling a Continued Fraction for Convergence and Accuracy]
 | |
| 
 | |
| Alternatively, lets say we want to profile a continued fraction for 
 | |
| convergence and error.  As an example, we'll use the continued fraction
 | |
| for the upper incomplete gamma function, the following function object
 | |
| returns the next a[sub N ] and b[sub N ] of the continued fraction
 | |
| each time it's invoked:
 | |
| 
 | |
|    template <class T>
 | |
|    struct upper_incomplete_gamma_fract
 | |
|    {
 | |
|    private:
 | |
|       T z, a;
 | |
|       int k;
 | |
|    public:
 | |
|       typedef std::pair<T,T> result_type;
 | |
| 
 | |
|       upper_incomplete_gamma_fract(T a1, T z1)
 | |
|          : z(z1-a1+1), a(a1), k(0)
 | |
|       {
 | |
|       }
 | |
| 
 | |
|       result_type operator()()
 | |
|       {
 | |
|          ++k;
 | |
|          z += 2;
 | |
|          return result_type(k * (a - k), z);
 | |
|       }
 | |
|    };
 | |
| 
 | |
| We want to measure both the relative error, and the rate of convergence
 | |
| of this fraction, so we'll write a functor that returns both as a __tuple:
 | |
| class test_data will unpack the tuple for us, and create one column of data
 | |
| for each element in the tuple (in addition to the input parameters):
 | |
| 
 | |
|    #include <boost/math/tools/test_data.hpp>
 | |
|    #include <boost/math/tools/test.hpp>
 | |
|    #include <boost/math/special_functions/gamma.hpp>
 | |
|    #include <boost/math/tools/ntl.hpp>
 | |
|    #include <boost/math/tools/tuple.hpp>
 | |
| 
 | |
|    template <class T>
 | |
|    struct profile_gamma_fraction
 | |
|    {
 | |
|       typedef ``__tuple``<T, T> result_type;
 | |
| 
 | |
|       result_type operator()(T val)
 | |
|       {
 | |
|          using namespace boost::math::tools;
 | |
|          // estimate the true value, using arbitary precision
 | |
|          // arithmetic and NTL::RR:
 | |
|          NTL::RR rval(val);
 | |
|          upper_incomplete_gamma_fract<NTL::RR> f1(rval, rval);
 | |
|          NTL::RR true_val = continued_fraction_a(f1, 1000);
 | |
|          //
 | |
|          // Now get the aproximation at double precision, along with the number of
 | |
|          // iterations required:
 | |
|          boost::uintmax_t iters = std::numeric_limits<boost::uintmax_t>::max();
 | |
|          upper_incomplete_gamma_fract<T> f2(val, val);
 | |
|          T found_val = continued_fraction_a(f2, std::numeric_limits<T>::digits, iters);
 | |
|          //
 | |
|          // Work out the relative error, as measured in units of epsilon:
 | |
|          T err = real_cast<T>(relative_error(true_val, NTL::RR(found_val)) / std::numeric_limits<T>::epsilon());
 | |
|          //
 | |
|          // now just return the results as a tuple:
 | |
|          return boost::math::make_tuple(err, iters);
 | |
|       }
 | |
|    };
 | |
| 
 | |
| Feeding that functor into test_data allows rapid output of csv data,
 | |
| for whatever type `T` we may be interested in:
 | |
|    
 | |
|    int main()
 | |
|    {
 | |
|       using namespace boost::math::tools;
 | |
|       // create an object to hold the data:
 | |
|       test_data<double> data;
 | |
|       // insert 500 points at uniform intervals between just after 0 and 100:
 | |
|       data.insert(profile_gamma_fraction<double>(), make_periodic_param(0.01, 100.0, 100));
 | |
|       // print out in csv format:
 | |
|       write_csv(std::cout, data, ", ");
 | |
|       return 0;
 | |
|    }
 | |
| 
 | |
| This time there's no need to plot a graph, the first few rows are:
 | |
| 
 | |
|    a and z,  Error/epsilon,  Iterations required
 | |
|    
 | |
|    0.01,     9723.14,        4726
 | |
|    1.0099,   9.54818,        87
 | |
|    2.0098,   3.84777,        40
 | |
|    3.0097,   0.728358,       25
 | |
|    4.0096,   2.39712,        21
 | |
|    5.0095,   0.233263,       16
 | |
| 
 | |
| So it's pretty clear that this fraction shouldn't be used for small values
 | |
| of a and z.
 | |
| 
 | |
| [h4 reference]
 | |
| 
 | |
| Most of this tool has been described already in the examples above, we'll 
 | |
| just add the following notes on the non-member functions:
 | |
| 
 | |
|    template <class T>
 | |
|    parameter_info<T> make_random_param(T start_range, T end_range, int n_points);
 | |
|    
 | |
| Tells class test_data to test /n_points/ random values in the range 
 | |
| [start_range,end_range].
 | |
| 
 | |
|    template <class T>
 | |
|    parameter_info<T> make_periodic_param(T start_range, T end_range, int n_points);
 | |
|    
 | |
| Tells class test_data to test /n_points/ evenly spaced values in the range 
 | |
| [start_range,end_range].
 | |
| 
 | |
|    template <class T>
 | |
|    parameter_info<T> make_power_param(T basis, int start_exponent, int end_exponent);
 | |
| 
 | |
| Tells class test_data to test points of the form ['basis + R * 2[super expon]] for each
 | |
| /expon/ in the range [start_exponent, end_exponent], and /R/ a random number in \[0.5, 1\].
 | |
| 
 | |
|    template <class T>
 | |
|    bool get_user_parameter_info(parameter_info<T>& info, const char* param_name);
 | |
|    
 | |
| Prompts the user for the parameter range and form to use.
 | |
| 
 | |
| Finally, if we don't want the parameter to be included in the output, we can tell
 | |
| test_data by setting it a "dummy parameter":
 | |
| 
 | |
|    parameter_info<double> p = make_random_param(2.0, 5.0, 10);
 | |
|    p.type |= dummy_param;
 | |
|    
 | |
| This is useful when the functor used transforms the parameter in some way
 | |
| before passing it to the function under test, usually the functor will then
 | |
| return both the transformed input and the result in a tuple, so there's no
 | |
| need for the original pseudo-parameter to be included in program output.
 | |
| 
 | |
| [endsect][/section:test_data Graphing, Profiling, and Generating Test Data for Special Functions]
 | |
| 
 | |
| [/ 
 | |
|   Copyright 2006 John Maddock and Paul A. Bristow.
 | |
|   Distributed under the Boost Software License, Version 1.0.
 | |
|   (See accompanying file LICENSE_1_0.txt or copy at
 | |
|   http://www.boost.org/LICENSE_1_0.txt).
 | |
| ]
 |