mirror of
https://github.com/saitohirga/WSJT-X.git
synced 2024-11-07 17:46:04 -05:00
509 lines
22 KiB
Plaintext
509 lines
22 KiB
Plaintext
[section:special_tut Tutorial: How to Write a New Special Function]
|
|
|
|
[section:special_tut_impl Implementation]
|
|
|
|
In this section, we'll provide a "recipe" for adding a new special function to this library to make life easier for
|
|
future authors wishing to contribute. We'll assume the function returns a single floating-point result, and takes
|
|
two floating-point arguments. For the sake of exposition we'll give the function the name [~my_special].
|
|
|
|
Normally, the implementation of such a function is split into two layers - a public user layer, and an internal
|
|
implementation layer that does the actual work.
|
|
The implementation layer is declared inside a `detail` namespace and has a simple signature:
|
|
|
|
namespace boost { namespace math { namespace detail {
|
|
|
|
template <class T, class Policy>
|
|
T my_special_imp(const T& a, const T&b, const Policy& pol)
|
|
{
|
|
/* Implementation goes here */
|
|
}
|
|
|
|
}}} // namespaces
|
|
|
|
We'll come back to what can go inside the implementation later, but first lets look at the user layer.
|
|
This consists of two overloads of the function, with and without a __Policy argument:
|
|
|
|
namespace boost{ namespace math{
|
|
|
|
template <class T, class U>
|
|
typename tools::promote_args<T, U>::type my_special(const T& a, const U& b);
|
|
|
|
template <class T, class U, class Policy>
|
|
typename tools::promote_args<T, U>::type my_special(const T& a, const U& b, const Policy& pol);
|
|
|
|
}} // namespaces
|
|
|
|
Note how each argument has a different template type - this allows for mixed type arguments - the return
|
|
type is computed from a traits class and is the "common type" of all the arguments after any integer
|
|
arguments have been promoted to type `double`.
|
|
|
|
The implementation of the non-policy overload is trivial:
|
|
|
|
namespace boost{ namespace math{
|
|
|
|
template <class T, class U>
|
|
inline typename tools::promote_args<T, U>::type my_special(const T& a, const U& b)
|
|
{
|
|
// Simply forward with a default policy:
|
|
return my_special(a, b, policies::policy<>();
|
|
}
|
|
|
|
}} // namespaces
|
|
|
|
The implementation of the other overload is somewhat more complex, as there's some meta-programming to do,
|
|
but from a runtime perspective is still a one-line forwarding function. Here it is with comments explaining
|
|
what each line does:
|
|
|
|
namespace boost{ namespace math{
|
|
|
|
template <class T, class U, class Policy>
|
|
inline typename tools::promote_args<T, U>::type my_special(const T& a, const U& b, const Policy& pol)
|
|
{
|
|
//
|
|
// We've found some standard library functions to misbehave if any FPU exception flags
|
|
// are set prior to their call, this code will clear those flags, then reset them
|
|
// on exit:
|
|
//
|
|
BOOST_FPU_EXCEPTION_GUARD
|
|
//
|
|
// The type of the result - the common type of T and U after
|
|
// any integer types have been promoted to double:
|
|
//
|
|
typedef typename tools::promote_args<T, U>::type result_type;
|
|
//
|
|
// The type used for the calculation. This may be a wider type than
|
|
// the result in order to ensure full precision:
|
|
//
|
|
typedef typename policies::evaluation<result_type, Policy>::type value_type;
|
|
//
|
|
// The type of the policy to forward to the actual implementation.
|
|
// We disable promotion of float and double as that's [possibly]
|
|
// happened already in the line above. Also reset to the default
|
|
// any policies we don't use (reduces code bloat if we're called
|
|
// multiple times with differing policies we don't actually use).
|
|
// Also normalise the type, again to reduce code bloat in case we're
|
|
// called multiple times with functionally identical policies that happen
|
|
// to be different types.
|
|
//
|
|
typedef typename policies::normalise<
|
|
Policy,
|
|
policies::promote_float<false>,
|
|
policies::promote_double<false>,
|
|
policies::discrete_quantile<>,
|
|
policies::assert_undefined<> >::type forwarding_policy;
|
|
//
|
|
// Whew. Now we can make the actual call to the implementation.
|
|
// Arguments are explicitly cast to the evaluation type, and the result
|
|
// passed through checked_narrowing_cast which handles things like overflow
|
|
// according to the policy passed:
|
|
//
|
|
return policies::checked_narrowing_cast<result_type, forwarding_policy>(
|
|
detail::my_special_imp(
|
|
static_cast<value_type>(a),
|
|
static_cast<value_type>(x),
|
|
forwarding_policy()),
|
|
"boost::math::my_special<%1%>(%1%, %1%)");
|
|
}
|
|
|
|
}} // namespaces
|
|
|
|
We're now almost there, we just need to flesh out the details of the implementation layer:
|
|
|
|
namespace boost { namespace math { namespace detail {
|
|
|
|
template <class T, class Policy>
|
|
T my_special_imp(const T& a, const T&b, const Policy& pol)
|
|
{
|
|
/* Implementation goes here */
|
|
}
|
|
|
|
}}} // namespaces
|
|
|
|
The following guidelines indicate what (other than basic arithmetic) can go in the implementation:
|
|
|
|
* Error conditions (for example bad arguments) should be handled by calling one of the
|
|
[link math_toolkit.error_handling.finding_more_information policy based error handlers].
|
|
* Calls to standard library functions should be made unqualified (this allows argument
|
|
dependent lookup to find standard library functions for user-defined floating point
|
|
types such as those from __multiprecision). In addition, the macro `BOOST_MATH_STD_USING`
|
|
should appear at the start of the function (note no semi-colon afterwards!) so that
|
|
all the math functions in `namespace std` are visible in the current scope.
|
|
* Calls to other special functions should be made as fully qualified calls, and include the
|
|
policy parameter as the last argument, for example `boost::math::tgamma(a, pol)`.
|
|
* Where possible, evaluation of series, continued fractions, polynomials, or root
|
|
finding should use one of the [link math_toolkit.internals_overview boiler-plate functions]. In any case, after
|
|
any iterative method, you should verify that the number of iterations did not exceed the
|
|
maximum specified in the __Policy type, and if it did terminate as a result of exceeding the
|
|
maximum, then the appropriate error handler should be called (see existing code for examples).
|
|
* Numeric constants such as [pi] etc should be obtained via a call to the [link math_toolkit.constants appropriate function],
|
|
for example: `constants::pi<T>()`.
|
|
* Where tables of coefficients are used (for example for rational approximations), care should be taken
|
|
to ensure these are initialized at program startup to ensure thread safety when using user-defined number types.
|
|
See for example the use of `erf_initializer` in [@../../include/boost/math/special_functions/erf.hpp erf.hpp].
|
|
|
|
Here are some other useful internal functions:
|
|
|
|
[table
|
|
[[function][Meaning]]
|
|
[[`policies::digits<T, Policy>()`][Returns number of binary digits in T (possible overridden by the policy).]]
|
|
[[`policies::get_max_series_iterations<Policy>()`][Maximum number of iterations for series evaluation.]]
|
|
[[`policies::get_max_root_iterations<Policy>()`][Maximum number of iterations for root finding.]]
|
|
[[`polices::get_epsilon<T, Policy>()`][Epsilon for type T, possibly overridden by the Policy.]]
|
|
[[`tools::digits<T>()`][Returns the number of binary digits in T.]]
|
|
[[`tools::max_value<T>()`][Equivalent to `std::numeric_limits<T>::max()`]]
|
|
[[`tools::min_value<T>()`][Equivalent to `std::numeric_limits<T>::min()`]]
|
|
[[`tools::log_max_value<T>()`][Equivalent to the natural logarithm of `std::numeric_limits<T>::max()`]]
|
|
[[`tools::log_min_value<T>()`][Equivalent to the natural logarithm of `std::numeric_limits<T>::min()`]]
|
|
[[`tools::epsilon<T>()`][Equivalent to `std::numeric_limits<T>::epsilon()`.]]
|
|
[[`tools::root_epsilon<T>()`][Equivalent to the square root of `std::numeric_limits<T>::epsilon()`.]]
|
|
[[`tools::forth_root_epsilon<T>()`][Equivalent to the forth root of `std::numeric_limits<T>::epsilon()`.]]
|
|
]
|
|
|
|
[endsect]
|
|
|
|
[section:special_tut_test Testing]
|
|
|
|
We work under the assumption that untested code doesn't work, so some tests for your new special function are in order,
|
|
we'll divide these up in to 3 main categories:
|
|
|
|
[h4 Spot Tests]
|
|
|
|
Spot tests consist of checking that the expected exception is generated when the inputs are in error (or
|
|
otherwise generate undefined values), and checking any special values. We can check for expected exceptions
|
|
with `BOOST_CHECK_THROW`, so for example if it's a domain error for the last parameter to be outside the range
|
|
`[0,1]` then we might have:
|
|
|
|
BOOST_CHECK_THROW(my_special(0, -0.1), std::domain_error);
|
|
BOOST_CHECK_THROW(my_special(0, 1.1), std::domain_error);
|
|
|
|
When the function has known exact values (typically integer values) we can use `BOOST_CHECK_EQUAL`:
|
|
|
|
BOOST_CHECK_EQUAL(my_special(1.0, 0.0), 0);
|
|
BOOST_CHECK_EQUAL(my_special(1.0, 1.0), 1);
|
|
|
|
When the function has known values which are not exact (from a floating point perspective) then we can use
|
|
`BOOST_CHECK_CLOSE_FRACTION`:
|
|
|
|
// Assumes 4 epsilon is as close as we can get to a true value of 2Pi:
|
|
BOOST_CHECK_CLOSE_FRACTION(my_special(0.5, 0.5), 2 * constants::pi<double>(), std::numeric_limits<double>::epsilon() * 4);
|
|
|
|
[h4 Independent Test Values]
|
|
|
|
If the function is implemented by some other known good source (for example Mathematica or it's online versions
|
|
[@http://functions.wolfram.com functions.wolfram.com] or [@http://www.wolframalpha.com www.wolframalpha.com]
|
|
then it's a good idea to sanity check our implementation by having at least one independendly generated value
|
|
for each code branch our implementation may take. To slot these in nicely with our testing framework it's best to
|
|
tabulate these like this:
|
|
|
|
// function values calculated on http://functions.wolfram.com/
|
|
static const boost::array<boost::array<T, 3>, 10> my_special_data = {{
|
|
{{ SC_(0), SC_(0), SC_(1) }},
|
|
{{ SC_(0), SC_(1), SC_(1.26606587775200833559824462521471753760767031135496220680814) }},
|
|
/* More values here... */
|
|
}};
|
|
|
|
We'll see how to use this table and the meaning of the `SC_` macro later. One important point
|
|
is to make sure that the input values have exact binary representations: so choose values such as
|
|
1.5, 1.25, 1.125 etc. This ensures that if `my_special` is unusually sensitive in one area, that
|
|
we don't get apparently large errors just because the inputs are 0.5 ulp in error.
|
|
|
|
[h4 Random Test Values]
|
|
|
|
We can generate a large number of test values to check both for future regressions, and for
|
|
accumulated rounding or cancellation error in our implementation. Ideally we would use an
|
|
independent implementation for this (for example my_special may be defined in directly terms
|
|
of other special functions but not implemented that way for performance or accuracy reasons).
|
|
Alternatively we may use our own implementation directly, but with any special cases (asymptotic
|
|
expansions etc) disabled. We have a set of [link math_toolkit.internals.test_data tools]
|
|
to generate test data directly, here's a typical example:
|
|
|
|
[import ../../example/special_data.cpp]
|
|
[special_data_example]
|
|
|
|
Typically several sets of data will be generated this way, including random values in some "normal"
|
|
range, extreme values (very large or very small), and values close to any "interesting" behaviour
|
|
of the function (singularities etc).
|
|
|
|
[h4 The Test File Header]
|
|
|
|
We split the actual test file into 2 distinct parts: a header that contains the testing code
|
|
as a series of function templates, and the actual .cpp test driver that decides which types
|
|
are tested, and sets the "expected" error rates for those types. It's done this way because:
|
|
|
|
* We want to test with both built in floating point types, and with multiprecision types.
|
|
However, both compile and runtimes with the latter can be too long for the folks who run
|
|
the tests to realistically cope with, so it makes sense to split the test into (at least)
|
|
2 parts.
|
|
* The definition of the SC_ macro used in our tables of data may differ depending on what type
|
|
we're testing (see below). Again this is largely a matter of managing compile times as large tables
|
|
of user-defined-types can take a crazy amount of time to compile with some compilers.
|
|
|
|
The test header contains 2 functions:
|
|
|
|
template <class Real, class T>
|
|
void do_test(const T& data, const char* type_name, const char* test_name);
|
|
|
|
template <class T>
|
|
void test(T, const char* type_name);
|
|
|
|
Before implementing those, we'll include the headers we'll need, and provide a default
|
|
definition for the SC_ macro:
|
|
|
|
// A couple of Boost.Test headers in case we need any BOOST_CHECK_* macros:
|
|
#include <boost/test/unit_test.hpp>
|
|
#include <boost/test/floating_point_comparison.hpp>
|
|
// Our function to test:
|
|
#include <boost/math/special_functions/my_special.hpp>
|
|
// We need boost::array for our test data, plus a few headers from
|
|
// libs/math/test that contain our testing machinary:
|
|
#include <boost/array.hpp>
|
|
#include "functor.hpp"
|
|
#include "handle_test_result.hpp"
|
|
#include "table_type.hpp"
|
|
|
|
#ifndef SC_
|
|
#define SC_(x) static_cast<typename table_type<T>::type>(BOOST_JOIN(x, L))
|
|
#endif
|
|
|
|
The easiest function to implement is the "test" function which is what we'll be calling
|
|
from the test-driver program. It simply includes the files containing the tabular
|
|
test data and calls `do_test` function for each table, along with a description of what's
|
|
being tested:
|
|
|
|
template <class T>
|
|
void test(T, const char* type_name)
|
|
{
|
|
//
|
|
// The actual test data is rather verbose, so it's in a separate file
|
|
//
|
|
// The contents are as follows, each row of data contains
|
|
// three items, input value a, input value b and my_special(a, b):
|
|
//
|
|
# include "my_special_1.ipp"
|
|
|
|
do_test<T>(my_special_1, name, "MySpecial Function: Mathematica Values");
|
|
|
|
# include "my_special_2.ipp"
|
|
|
|
do_test<T>(my_special_2, name, "MySpecial Function: Random Values");
|
|
|
|
# include "my_special_3.ipp"
|
|
|
|
do_test<T>(my_special_3, name, "MySpecial Function: Very Small Values");
|
|
}
|
|
|
|
The function `do_test` takes each table of data and calculates values for each row
|
|
of data, along with statistics for max and mean error etc, most of this is handled
|
|
by some boilerplate code:
|
|
|
|
template <class Real, class T>
|
|
void do_test(const T& data, const char* type_name, const char* test_name)
|
|
{
|
|
// Get the type of each row and each element in the rows:
|
|
typedef typename T::value_type row_type;
|
|
typedef Real value_type;
|
|
|
|
// Get a pointer to our function, we have to use a workaround here
|
|
// as some compilers require the template types to be explicitly
|
|
// specified, while others don't much like it if it is!
|
|
typedef value_type (*pg)(value_type, value_type);
|
|
#if defined(BOOST_MATH_NO_DEDUCED_FUNCTION_POINTERS)
|
|
pg funcp = boost::math::my_special<value_type, value_type>;
|
|
#else
|
|
pg funcp = boost::math::my_special;
|
|
#endif
|
|
|
|
// Somewhere to hold our results:
|
|
boost::math::tools::test_result<value_type> result;
|
|
// And some pretty printing:
|
|
std::cout << "Testing " << test_name << " with type " << type_name
|
|
<< "\n~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~\n";
|
|
|
|
//
|
|
// Test my_special against data:
|
|
//
|
|
result = boost::math::tools::test_hetero<Real>(
|
|
/* First argument is the table */
|
|
data,
|
|
/* Next comes our function pointer, plus the indexes of it's arguments in the table */
|
|
bind_func<Real>(funcp, 0, 1),
|
|
/* Then the index of the result in the table - potentially we can test several
|
|
related functions this way, each having the same input arguments, and different
|
|
output values in different indexes in the table */
|
|
extract_result<Real>(2));
|
|
//
|
|
// Finish off with some boilerplate to check the results were within the expected errors,
|
|
// and pretty print the results:
|
|
//
|
|
handle_test_result(result, data[result.worst()], result.worst(), type_name, "boost::math::my_special", test_name);
|
|
}
|
|
|
|
Now we just need to write the test driver program, at it's most basic it looks something like this:
|
|
|
|
#include <boost/math/special_functions/math_fwd.hpp>
|
|
#include <boost/math/tools/test.hpp>
|
|
#include <boost/math/tools/stats.hpp>
|
|
#include <boost/type_traits.hpp>
|
|
#include <boost/array.hpp>
|
|
#include "functor.hpp"
|
|
|
|
#include "handle_test_result.hpp"
|
|
#include "test_my_special.hpp"
|
|
|
|
BOOST_AUTO_TEST_CASE( test_main )
|
|
{
|
|
//
|
|
// Test each floating point type, plus real_concept.
|
|
// We specify the name of each type by hand as typeid(T).name()
|
|
// often gives an unreadable mangled name.
|
|
//
|
|
test(0.1F, "float");
|
|
test(0.1, "double");
|
|
//
|
|
// Testing of long double and real_concept is protected
|
|
// by some logic to disable these for unsupported
|
|
// or problem compilers.
|
|
//
|
|
#ifndef BOOST_MATH_NO_LONG_DOUBLE_MATH_FUNCTIONS
|
|
test(0.1L, "long double");
|
|
#ifndef BOOST_MATH_NO_REAL_CONCEPT_TESTS
|
|
#if !BOOST_WORKAROUND(__BORLANDC__, BOOST_TESTED_AT(0x582))
|
|
test(boost::math::concepts::real_concept(0.1), "real_concept");
|
|
#endif
|
|
#endif
|
|
#else
|
|
std::cout << "<note>The long double tests have been disabled on this platform "
|
|
"either because the long double overloads of the usual math functions are "
|
|
"not available at all, or because they are too inaccurate for these tests "
|
|
"to pass.</note>" << std::cout;
|
|
#endif
|
|
}
|
|
|
|
That's almost all there is too it - except that if the above program is run it's very likely that
|
|
all the tests will fail as the default maximum allowable error is 1 epsilon. So we'll
|
|
define a function (don't forget to call it from the start of the `test_main` above) to
|
|
up the limits to something sensible, based both on the function we're calling and on
|
|
the particular tests plus the platform and compiler:
|
|
|
|
void expected_results()
|
|
{
|
|
//
|
|
// Define the max and mean errors expected for
|
|
// various compilers and platforms.
|
|
//
|
|
const char* largest_type;
|
|
#ifndef BOOST_MATH_NO_LONG_DOUBLE_MATH_FUNCTIONS
|
|
if(boost::math::policies::digits<double, boost::math::policies::policy<> >() == boost::math::policies::digits<long double, boost::math::policies::policy<> >())
|
|
{
|
|
largest_type = "(long\\s+)?double|real_concept";
|
|
}
|
|
else
|
|
{
|
|
largest_type = "long double|real_concept";
|
|
}
|
|
#else
|
|
largest_type = "(long\\s+)?double";
|
|
#endif
|
|
//
|
|
// We call add_expected_result for each error rate we wish to adjust, these tell
|
|
// handle_test_result what level of error is acceptable. We can have as many calls
|
|
// to add_expected_result as we need, each one establishes a rule for acceptable error
|
|
// with rules set first given preference.
|
|
//
|
|
add_expected_result(
|
|
/* First argument is a regular expression to match against the name of the compiler
|
|
set in BOOST_COMPILER */
|
|
".*",
|
|
/* Second argument is a regular expression to match against the name of the
|
|
C++ standard library as set in BOOST_STDLIB */
|
|
".*",
|
|
/* Third argument is a regular expression to match against the name of the
|
|
platform as set in BOOST_PLATFORM */
|
|
".*",
|
|
/* Forth argument is the name of the type being tested, normally we will
|
|
only need to up the acceptable error rate for the widest floating
|
|
point type being tested */
|
|
largest_real,
|
|
/* Fifth argument is a regular expression to match against
|
|
the name of the group of data being tested */
|
|
"MySpecial Function:.*Small.*",
|
|
/* Sixth argument is a regular expression to match against the name
|
|
of the function being tested */
|
|
"boost::math::my_special",
|
|
/* Seventh argument is the maximum allowable error expressed in units
|
|
of machine epsilon passed as a long integer value */
|
|
50,
|
|
/* Eighth argument is the maximum allowable mean error expressed in units
|
|
of machine epsilon passed as a long integer value */
|
|
20);
|
|
}
|
|
|
|
[h4 Testing Multiprecision Types]
|
|
|
|
Testing of multiprecision types is handled by the test drivers in libs/multiprecision/test/math,
|
|
please refer to these for examples. Note that these tests are run only occationally as they take
|
|
a lot of CPU cycles to build and run.
|
|
|
|
[h4 Improving Compile Times]
|
|
|
|
As noted above, these test programs can take a while to build as we're instantiating a lot of templates
|
|
for several different types, and our test runners are already stretched to the limit, and probably
|
|
using outdated "spare" hardware. There are two things we can do to speed things up:
|
|
|
|
* Use a precompiled header.
|
|
* Use separate compilation of our special function templates.
|
|
|
|
We can make these changes by changing the list of includes from:
|
|
|
|
#include <boost/math/special_functions/math_fwd.hpp>
|
|
#include <boost/math/tools/test.hpp>
|
|
#include <boost/math/tools/stats.hpp>
|
|
#include <boost/type_traits.hpp>
|
|
#include <boost/array.hpp>
|
|
#include "functor.hpp"
|
|
|
|
#include "handle_test_result.hpp"
|
|
|
|
To just:
|
|
|
|
#include <pch_light.hpp>
|
|
|
|
And changing
|
|
|
|
#include <boost/math/special_functions/my_special.hpp>
|
|
|
|
To:
|
|
|
|
#include <boost/math/special_functions/math_fwd.hpp>
|
|
|
|
The Jamfile target that builds the test program will need the targets
|
|
|
|
test_instances//test_instances pch_light
|
|
|
|
adding to it's list of source dependencies (see the Jamfile for examples).
|
|
|
|
Finally the project in libs/math/test/test_instances will need modifying
|
|
to instantiate function `my_special`.
|
|
|
|
These changes should be made last, when `my_special` is stable and the code is in Trunk.
|
|
|
|
[h4 Concept Checks]
|
|
|
|
Our concept checks verify that your function's implementation makes no assumptions that aren't
|
|
required by our [link math_toolkit.real_concepts Real number conceptual requirements]. They also
|
|
check for various common bugs and programming traps that we've fallen into over time. To
|
|
add your function to these tests, edit libs/math/test/compile_test/instantiate.hpp to add
|
|
calls to your function: there are 7 calls to each function, each with a different purpose.
|
|
Search for something like "ibeta" or "gamm_p" and follow their examples.
|
|
|
|
[endsect]
|
|
|
|
[endsect]
|
|
|
|
[/
|
|
Copyright 2013 John Maddock.
|
|
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).
|
|
]
|