1[section:special_tut Tutorial: How to Write a New Special Function] 2 3[section:special_tut_impl Implementation] 4 5In this section, we'll provide a "recipe" for adding a new special function to this library to make life easier for 6future authors wishing to contribute. We'll assume the function returns a single floating-point result, and takes 7two floating-point arguments. For the sake of exposition we'll give the function the name [~my_special]. 8 9Normally, the implementation of such a function is split into two layers - a public user layer, and an internal 10implementation layer that does the actual work. 11The implementation layer is declared inside a `detail` namespace and has a simple signature: 12 13 namespace boost { namespace math { namespace detail { 14 15 template <class T, class Policy> 16 T my_special_imp(const T& a, const T&b, const Policy& pol) 17 { 18 /* Implementation goes here */ 19 } 20 21 }}} // namespaces 22 23We'll come back to what can go inside the implementation later, but first lets look at the user layer. 24This consists of two overloads of the function, with and without a __Policy argument: 25 26 namespace boost{ namespace math{ 27 28 template <class T, class U> 29 typename tools::promote_args<T, U>::type my_special(const T& a, const U& b); 30 31 template <class T, class U, class Policy> 32 typename tools::promote_args<T, U>::type my_special(const T& a, const U& b, const Policy& pol); 33 34 }} // namespaces 35 36Note how each argument has a different template type - this allows for mixed type arguments - the return 37type is computed from a traits class and is the "common type" of all the arguments after any integer 38arguments have been promoted to type `double`. 39 40The implementation of the non-policy overload is trivial: 41 42 namespace boost{ namespace math{ 43 44 template <class T, class U> 45 inline typename tools::promote_args<T, U>::type my_special(const T& a, const U& b) 46 { 47 // Simply forward with a default policy: 48 return my_special(a, b, policies::policy<>(); 49 } 50 51 }} // namespaces 52 53The implementation of the other overload is somewhat more complex, as there's some meta-programming to do, 54but from a runtime perspective is still a one-line forwarding function. Here it is with comments explaining 55what each line does: 56 57 namespace boost{ namespace math{ 58 59 template <class T, class U, class Policy> 60 inline typename tools::promote_args<T, U>::type my_special(const T& a, const U& b, const Policy& pol) 61 { 62 // 63 // We've found some standard library functions to misbehave if any FPU exception flags 64 // are set prior to their call, this code will clear those flags, then reset them 65 // on exit: 66 // 67 BOOST_FPU_EXCEPTION_GUARD 68 // 69 // The type of the result - the common type of T and U after 70 // any integer types have been promoted to double: 71 // 72 typedef typename tools::promote_args<T, U>::type result_type; 73 // 74 // The type used for the calculation. This may be a wider type than 75 // the result in order to ensure full precision: 76 // 77 typedef typename policies::evaluation<result_type, Policy>::type value_type; 78 // 79 // The type of the policy to forward to the actual implementation. 80 // We disable promotion of float and double as that's [possibly] 81 // happened already in the line above. Also reset to the default 82 // any policies we don't use (reduces code bloat if we're called 83 // multiple times with differing policies we don't actually use). 84 // Also normalise the type, again to reduce code bloat in case we're 85 // called multiple times with functionally identical policies that happen 86 // to be different types. 87 // 88 typedef typename policies::normalise< 89 Policy, 90 policies::promote_float<false>, 91 policies::promote_double<false>, 92 policies::discrete_quantile<>, 93 policies::assert_undefined<> >::type forwarding_policy; 94 // 95 // Whew. Now we can make the actual call to the implementation. 96 // Arguments are explicitly cast to the evaluation type, and the result 97 // passed through checked_narrowing_cast which handles things like overflow 98 // according to the policy passed: 99 // 100 return policies::checked_narrowing_cast<result_type, forwarding_policy>( 101 detail::my_special_imp( 102 static_cast<value_type>(a), 103 static_cast<value_type>(x), 104 forwarding_policy()), 105 "boost::math::my_special<%1%>(%1%, %1%)"); 106 } 107 108 }} // namespaces 109 110We're now almost there, we just need to flesh out the details of the implementation layer: 111 112 namespace boost { namespace math { namespace detail { 113 114 template <class T, class Policy> 115 T my_special_imp(const T& a, const T&b, const Policy& pol) 116 { 117 /* Implementation goes here */ 118 } 119 120 }}} // namespaces 121 122The following guidelines indicate what (other than basic arithmetic) can go in the implementation: 123 124* Error conditions (for example bad arguments) should be handled by calling one of the 125[link math_toolkit.error_handling.finding_more_information policy based error handlers]. 126* Calls to standard library functions should be made unqualified (this allows argument 127dependent lookup to find standard library functions for user-defined floating point 128types such as those from __multiprecision). In addition, the macro `BOOST_MATH_STD_USING` 129should appear at the start of the function (note no semi-colon afterwards!) so that 130all the math functions in `namespace std` are visible in the current scope. 131* Calls to other special functions should be made as fully qualified calls, and include the 132policy parameter as the last argument, for example `boost::math::tgamma(a, pol)`. 133* Where possible, evaluation of series, continued fractions, polynomials, or root 134finding should use one of the [link math_toolkit.internals_overview boiler-plate functions]. In any case, after 135any iterative method, you should verify that the number of iterations did not exceed the 136maximum specified in the __Policy type, and if it did terminate as a result of exceeding the 137maximum, then the appropriate error handler should be called (see existing code for examples). 138* Numeric constants such as [pi] etc should be obtained via a call to the [link math_toolkit.constants appropriate function], 139for example: `constants::pi<T>()`. 140* Where tables of coefficients are used (for example for rational approximations), care should be taken 141to ensure these are initialized at program startup to ensure thread safety when using user-defined number types. 142See for example the use of `erf_initializer` in [@../../include/boost/math/special_functions/erf.hpp erf.hpp]. 143 144Here are some other useful internal functions: 145 146[table 147[[function][Meaning]] 148[[`policies::digits<T, Policy>()`][Returns number of binary digits in T (possible overridden by the policy).]] 149[[`policies::get_max_series_iterations<Policy>()`][Maximum number of iterations for series evaluation.]] 150[[`policies::get_max_root_iterations<Policy>()`][Maximum number of iterations for root finding.]] 151[[`polices::get_epsilon<T, Policy>()`][Epsilon for type T, possibly overridden by the Policy.]] 152[[`tools::digits<T>()`][Returns the number of binary digits in T.]] 153[[`tools::max_value<T>()`][Equivalent to `std::numeric_limits<T>::max()`]] 154[[`tools::min_value<T>()`][Equivalent to `std::numeric_limits<T>::min()`]] 155[[`tools::log_max_value<T>()`][Equivalent to the natural logarithm of `std::numeric_limits<T>::max()`]] 156[[`tools::log_min_value<T>()`][Equivalent to the natural logarithm of `std::numeric_limits<T>::min()`]] 157[[`tools::epsilon<T>()`][Equivalent to `std::numeric_limits<T>::epsilon()`.]] 158[[`tools::root_epsilon<T>()`][Equivalent to the square root of `std::numeric_limits<T>::epsilon()`.]] 159[[`tools::forth_root_epsilon<T>()`][Equivalent to the forth root of `std::numeric_limits<T>::epsilon()`.]] 160] 161 162[endsect] 163 164[section:special_tut_test Testing] 165 166We work under the assumption that untested code doesn't work, so some tests for your new special function are in order, 167we'll divide these up in to 3 main categories: 168 169[h4 Spot Tests] 170 171Spot tests consist of checking that the expected exception is generated when the inputs are in error (or 172otherwise generate undefined values), and checking any special values. We can check for expected exceptions 173with `BOOST_CHECK_THROW`, so for example if it's a domain error for the last parameter to be outside the range 174`[0,1]` then we might have: 175 176 BOOST_CHECK_THROW(my_special(0, -0.1), std::domain_error); 177 BOOST_CHECK_THROW(my_special(0, 1.1), std::domain_error); 178 179When the function has known exact values (typically integer values) we can use `BOOST_CHECK_EQUAL`: 180 181 BOOST_CHECK_EQUAL(my_special(1.0, 0.0), 0); 182 BOOST_CHECK_EQUAL(my_special(1.0, 1.0), 1); 183 184When the function has known values which are not exact (from a floating point perspective) then we can use 185`BOOST_CHECK_CLOSE_FRACTION`: 186 187 // Assumes 4 epsilon is as close as we can get to a true value of 2Pi: 188 BOOST_CHECK_CLOSE_FRACTION(my_special(0.5, 0.5), 2 * constants::pi<double>(), std::numeric_limits<double>::epsilon() * 4); 189 190[h4 Independent Test Values] 191 192If the function is implemented by some other known good source (for example Mathematica or it's online versions 193[@http://functions.wolfram.com functions.wolfram.com] or [@http://www.wolframalpha.com www.wolframalpha.com] 194then it's a good idea to sanity check our implementation by having at least one independently generated value 195for each code branch our implementation may take. To slot these in nicely with our testing framework it's best to 196tabulate these like this: 197 198 // function values calculated on http://functions.wolfram.com/ 199 static const boost::array<boost::array<T, 3>, 10> my_special_data = {{ 200 {{ SC_(0), SC_(0), SC_(1) }}, 201 {{ SC_(0), SC_(1), SC_(1.26606587775200833559824462521471753760767031135496220680814) }}, 202 /* More values here... */ 203 }}; 204 205We'll see how to use this table and the meaning of the `SC_` macro later. One important point 206is to make sure that the input values have exact binary representations: so choose values such as 2071.5, 1.25, 1.125 etc. This ensures that if `my_special` is unusually sensitive in one area, that 208we don't get apparently large errors just because the inputs are 0.5 ulp in error. 209 210[h4 Random Test Values] 211 212We can generate a large number of test values to check both for future regressions, and for 213accumulated rounding or cancellation error in our implementation. Ideally we would use an 214independent implementation for this (for example my_special may be defined in directly terms 215of other special functions but not implemented that way for performance or accuracy reasons). 216Alternatively we may use our own implementation directly, but with any special cases (asymptotic 217expansions etc) disabled. We have a set of [link math_toolkit.internals.test_data tools] 218to generate test data directly, here's a typical example: 219 220[import ../../example/special_data.cpp] 221[special_data_example] 222 223Typically several sets of data will be generated this way, including random values in some "normal" 224range, extreme values (very large or very small), and values close to any "interesting" behaviour 225of the function (singularities etc). 226 227[h4 The Test File Header] 228 229We split the actual test file into 2 distinct parts: a header that contains the testing code 230as a series of function templates, and the actual .cpp test driver that decides which types 231are tested, and sets the "expected" error rates for those types. It's done this way because: 232 233* We want to test with both built in floating point types, and with multiprecision types. 234However, both compile and runtimes with the latter can be too long for the folks who run 235the tests to realistically cope with, so it makes sense to split the test into (at least) 2362 parts. 237* The definition of the SC_ macro used in our tables of data may differ depending on what type 238we're testing (see below). Again this is largely a matter of managing compile times as large tables 239of user-defined-types can take a crazy amount of time to compile with some compilers. 240 241The test header contains 2 functions: 242 243 template <class Real, class T> 244 void do_test(const T& data, const char* type_name, const char* test_name); 245 246 template <class T> 247 void test(T, const char* type_name); 248 249Before implementing those, we'll include the headers we'll need, and provide a default 250definition for the SC_ macro: 251 252 // A couple of Boost.Test headers in case we need any BOOST_CHECK_* macros: 253 #include <boost/test/unit_test.hpp> 254 #include <boost/test/tools/floating_point_comparison.hpp> 255 // Our function to test: 256 #include <boost/math/special_functions/my_special.hpp> 257 // We need boost::array for our test data, plus a few headers from 258 // libs/math/test that contain our testing machinery: 259 #include <boost/array.hpp> 260 #include "functor.hpp" 261 #include "handle_test_result.hpp" 262 #include "table_type.hpp" 263 264 #ifndef SC_ 265 #define SC_(x) static_cast<typename table_type<T>::type>(BOOST_JOIN(x, L)) 266 #endif 267 268The easiest function to implement is the "test" function which is what we'll be calling 269from the test-driver program. It simply includes the files containing the tabular 270test data and calls `do_test` function for each table, along with a description of what's 271being tested: 272 273 template <class T> 274 void test(T, const char* type_name) 275 { 276 // 277 // The actual test data is rather verbose, so it's in a separate file 278 // 279 // The contents are as follows, each row of data contains 280 // three items, input value a, input value b and my_special(a, b): 281 // 282 # include "my_special_1.ipp" 283 284 do_test<T>(my_special_1, name, "MySpecial Function: Mathematica Values"); 285 286 # include "my_special_2.ipp" 287 288 do_test<T>(my_special_2, name, "MySpecial Function: Random Values"); 289 290 # include "my_special_3.ipp" 291 292 do_test<T>(my_special_3, name, "MySpecial Function: Very Small Values"); 293 } 294 295The function `do_test` takes each table of data and calculates values for each row 296of data, along with statistics for max and mean error etc, most of this is handled 297by some boilerplate code: 298 299 template <class Real, class T> 300 void do_test(const T& data, const char* type_name, const char* test_name) 301 { 302 // Get the type of each row and each element in the rows: 303 typedef typename T::value_type row_type; 304 typedef Real value_type; 305 306 // Get a pointer to our function, we have to use a workaround here 307 // as some compilers require the template types to be explicitly 308 // specified, while others don't much like it if it is! 309 typedef value_type (*pg)(value_type, value_type); 310 #if defined(BOOST_MATH_NO_DEDUCED_FUNCTION_POINTERS) 311 pg funcp = boost::math::my_special<value_type, value_type>; 312 #else 313 pg funcp = boost::math::my_special; 314 #endif 315 316 // Somewhere to hold our results: 317 boost::math::tools::test_result<value_type> result; 318 // And some pretty printing: 319 std::cout << "Testing " << test_name << " with type " << type_name 320 << "\n~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~\n"; 321 322 // 323 // Test my_special against data: 324 // 325 result = boost::math::tools::test_hetero<Real>( 326 /* First argument is the table */ 327 data, 328 /* Next comes our function pointer, plus the indexes of it's arguments in the table */ 329 bind_func<Real>(funcp, 0, 1), 330 /* Then the index of the result in the table - potentially we can test several 331 related functions this way, each having the same input arguments, and different 332 output values in different indexes in the table */ 333 extract_result<Real>(2)); 334 // 335 // Finish off with some boilerplate to check the results were within the expected errors, 336 // and pretty print the results: 337 // 338 handle_test_result(result, data[result.worst()], result.worst(), type_name, "boost::math::my_special", test_name); 339 } 340 341Now we just need to write the test driver program, at it's most basic it looks something like this: 342 343 #include <boost/math/special_functions/math_fwd.hpp> 344 #include <boost/math/tools/test.hpp> 345 #include <boost/math/tools/stats.hpp> 346 #include <boost/type_traits.hpp> 347 #include <boost/array.hpp> 348 #include "functor.hpp" 349 350 #include "handle_test_result.hpp" 351 #include "test_my_special.hpp" 352 353 BOOST_AUTO_TEST_CASE( test_main ) 354 { 355 // 356 // Test each floating point type, plus real_concept. 357 // We specify the name of each type by hand as typeid(T).name() 358 // often gives an unreadable mangled name. 359 // 360 test(0.1F, "float"); 361 test(0.1, "double"); 362 // 363 // Testing of long double and real_concept is protected 364 // by some logic to disable these for unsupported 365 // or problem compilers. 366 // 367 #ifndef BOOST_MATH_NO_LONG_DOUBLE_MATH_FUNCTIONS 368 test(0.1L, "long double"); 369 #ifndef BOOST_MATH_NO_REAL_CONCEPT_TESTS 370 #if !BOOST_WORKAROUND(__BORLANDC__, BOOST_TESTED_AT(0x582)) 371 test(boost::math::concepts::real_concept(0.1), "real_concept"); 372 #endif 373 #endif 374 #else 375 std::cout << "<note>The long double tests have been disabled on this platform " 376 "either because the long double overloads of the usual math functions are " 377 "not available at all, or because they are too inaccurate for these tests " 378 "to pass.</note>" << std::cout; 379 #endif 380 } 381 382That's almost all there is too it - except that if the above program is run it's very likely that 383all the tests will fail as the default maximum allowable error is 1 epsilon. So we'll 384define a function (don't forget to call it from the start of the `test_main` above) to 385up the limits to something sensible, based both on the function we're calling and on 386the particular tests plus the platform and compiler: 387 388 void expected_results() 389 { 390 // 391 // Define the max and mean errors expected for 392 // various compilers and platforms. 393 // 394 const char* largest_type; 395 #ifndef BOOST_MATH_NO_LONG_DOUBLE_MATH_FUNCTIONS 396 if(boost::math::policies::digits<double, boost::math::policies::policy<> >() == boost::math::policies::digits<long double, boost::math::policies::policy<> >()) 397 { 398 largest_type = "(long\\s+)?double|real_concept"; 399 } 400 else 401 { 402 largest_type = "long double|real_concept"; 403 } 404 #else 405 largest_type = "(long\\s+)?double"; 406 #endif 407 // 408 // We call add_expected_result for each error rate we wish to adjust, these tell 409 // handle_test_result what level of error is acceptable. We can have as many calls 410 // to add_expected_result as we need, each one establishes a rule for acceptable error 411 // with rules set first given preference. 412 // 413 add_expected_result( 414 /* First argument is a regular expression to match against the name of the compiler 415 set in BOOST_COMPILER */ 416 ".*", 417 /* Second argument is a regular expression to match against the name of the 418 C++ standard library as set in BOOST_STDLIB */ 419 ".*", 420 /* Third argument is a regular expression to match against the name of the 421 platform as set in BOOST_PLATFORM */ 422 ".*", 423 /* Forth argument is the name of the type being tested, normally we will 424 only need to up the acceptable error rate for the widest floating 425 point type being tested */ 426 largest_real, 427 /* Fifth argument is a regular expression to match against 428 the name of the group of data being tested */ 429 "MySpecial Function:.*Small.*", 430 /* Sixth argument is a regular expression to match against the name 431 of the function being tested */ 432 "boost::math::my_special", 433 /* Seventh argument is the maximum allowable error expressed in units 434 of machine epsilon passed as a long integer value */ 435 50, 436 /* Eighth argument is the maximum allowable mean error expressed in units 437 of machine epsilon passed as a long integer value */ 438 20); 439 } 440 441[h4 Testing Multiprecision Types] 442 443Testing of multiprecision types is handled by the test drivers in libs/multiprecision/test/math, 444please refer to these for examples. Note that these tests are run only occasionally as they take 445a lot of CPU cycles to build and run. 446 447[h4 Improving Compile Times] 448 449As noted above, these test programs can take a while to build as we're instantiating a lot of templates 450for several different types, and our test runners are already stretched to the limit, and probably 451using outdated "spare" hardware. There are two things we can do to speed things up: 452 453* Use a precompiled header. 454* Use separate compilation of our special function templates. 455 456We can make these changes by changing the list of includes from: 457 458 #include <boost/math/special_functions/math_fwd.hpp> 459 #include <boost/math/tools/test.hpp> 460 #include <boost/math/tools/stats.hpp> 461 #include <boost/type_traits.hpp> 462 #include <boost/array.hpp> 463 #include "functor.hpp" 464 465 #include "handle_test_result.hpp" 466 467To just: 468 469 #include <pch_light.hpp> 470 471And changing 472 473 #include <boost/math/special_functions/my_special.hpp> 474 475To: 476 477 #include <boost/math/special_functions/math_fwd.hpp> 478 479The Jamfile target that builds the test program will need the targets 480 481 test_instances//test_instances pch_light 482 483adding to it's list of source dependencies (see the Jamfile for examples). 484 485Finally the project in libs/math/test/test_instances will need modifying 486to instantiate function `my_special`. 487 488These changes should be made last, when `my_special` is stable and the code is in Trunk. 489 490[h4 Concept Checks] 491 492Our concept checks verify that your function's implementation makes no assumptions that aren't 493required by our [link math_toolkit.real_concepts Real number conceptual requirements]. They also 494check for various common bugs and programming traps that we've fallen into over time. To 495add your function to these tests, edit libs/math/test/compile_test/instantiate.hpp to add 496calls to your function: there are 7 calls to each function, each with a different purpose. 497Search for something like "ibeta" or "gamm_p" and follow their examples. 498 499[endsect] 500 501[endsect] 502 503[/ 504 Copyright 2013 John Maddock. 505 Distributed under the Boost Software License, Version 1.0. 506 (See accompanying file LICENSE_1_0.txt or copy at 507 http://www.boost.org/LICENSE_1_0.txt). 508] 509