EmrysFilter 0.1.0
State estimation utilities in C++23 using Eigen
Loading...
Searching...
No Matches
Math.h File Reference

Mathematical constants and scalar functions that work in every scalar type the library accepts, where the standard library and Eigen serve only some of them. More...

#include <cmath>
#include <concepts>
#include <numbers>
#include <type_traits>
#include <EmrysFilter/Core/Macros.h>
#include <unsupported/Eigen/AutoDiff>
#include <unsupported/Eigen/SpecialFunctions>

Go to the source code of this file.

Concepts

concept  Emrys::RealScalar
 A type usable as the scalar of a distribution: a real number type such as float, double, or an automatic-differentiation scalar.

Functions

template<typename Scalar>
Scalar Emrys::pi ()
template<typename Scalar>
Scalar Emrys::log2Pi ()
template<typename Scalar>
Scalar Emrys::logGamma (std::type_identity_t< Scalar > const &x)
template<typename Scalar>
Scalar Emrys::logOnePlus (std::type_identity_t< Scalar > const &x)
template<typename Scalar>
Scalar Emrys::floorOf (std::type_identity_t< Scalar > const &x)
template<typename T>
bool Emrys::isFinite (T const &x)
template<typename T>
bool Emrys::isNaN (T const &x)
template<typename Derived>
bool Emrys::allFinite (Eigen::DenseBase< Derived > const &x)

Detailed Description

Mathematical constants and scalar functions that work in every scalar type the library accepts, where the standard library and Eigen serve only some of them.

Library code calls these rather than their std:: or Eigen::numext:: counterparts, and they are public so that a user's function evaluated in such a scalar, such as a measurement under methods::linearized(), can too: pi(), log2Pi(), logGamma(), logOnePlus(), isFinite(), isNaN(), and allFinite().

Why this header exists. Every distribution, model, and filter takes a Scalar, and RealScalar admits any real number type, not only float, double, and long double. Two kinds of custom scalar matter:

  • Automatic differentiation. methods::linearized(), for a function that does not return its Jacobian, evaluates the user's function in Eigen's AutoDiffScalar (Transform.h), so a function that builds or evaluates a library density inside it, such as a robust measurement's log-likelihood, runs the library in that scalar. Differentiating a filter's log-likelihood with respect to its noise parameters, to tune or learn them, would run the whole library in it; that is a goal this header serves, not something the tests yet show.
  • Multiprecision. A type such as MPFR's mpreal gives a reference to check the double-precision results against.

In such a scalar the obvious code fails, mostly at compile time, sometimes silently:

  • std::numbers::pi_v<Scalar> is ill-formed for anything but a floating-point type ([math.constants]).
  • EIGEN_PI is a macro (the project uses none) and a long double literal, so Scalar(EIGEN_PI) compiles for a multiprecision scalar but rounds π to 64 bits, or to 53 on MSVC, where long double is double. Every normalizer then carries that error with nothing to show for it.
  • Eigen::numext::lgamma has implementations for float and double only; anything else, long double included, is a static assertion.
  • AutoDiffScalar has no lgamma, log1p, floor, isfinite, or isnan, so the numext versions, std versions found through a using std::f; line, and Eigen's own DenseBase::allFinite() fail to compile for it.
  • Arithmetic on AutoDiffScalar values yields expression types (AutoDiffScalar<CwiseBinaryOp<...>>). A numext function deduces its type from such an argument and then cannot convert its result back, so numext::log(a * b) fails where numext::log(Scalar(a * b)) compiles.

What each function does. Each dispatches at compile time to the standard function for a floating-point scalar, to the chain rule on the value and the derivatives for an AutoDiffScalar, and otherwise to Eigen's or the scalar's own function, which a multiprecision type provides. Those returning a scalar take it as an explicit template argument (logGamma<Scalar>(x)), not a deduced one, so that an autodiff expression converts to the plain scalar first. For double they compile to the standard call.

Adding one. When library code needs another function that the standard library, Eigen, or AutoDiffScalar lacks for some scalar, it goes here with the same three branches, a test of each branch in tests/Core/math.cpp, and, if a density uses it, a case in tests/Distributions/autodiff_scalars.cpp, which evaluates the densities in AutoDiffScalar and so catches the next gap. Functions that AutoDiffScalar and Eigen both support (log, exp, sqrt, atan2) go through Eigen::numext:: directly, with a plain Scalar argument.

Read Forwarding.h next.