High precision calculations

Yade supports high and arbitrary precision Real type for performing calculations (see [Kozicki2022] for details). All tests and checks pass but still the current support is in testing phase. The backend library is boost multiprecision along with corresponding boost math toolkit.

The supported types are following:

type

bits

decimal places [1]

notes

float

32

6

hardware accelerated (not useful, it is only for testing purposes)

double

64

15

hardware accelerated

long double

80

18

hardware accelerated

boost float128

128

33

depending on processor type it may be hardware accelerated, wrapped by boost

boost mpfr

Nbit

Nbit/(log(2)/log(10))

uses external mpfr library, wrapped by boost

boost cpp_bin_float

Nbit

Nbit/(log(2)/log(10))

uses boost only, but is slower

The last two types are arbitrary precision, and their number of bits Nbit or decimal places is specified as argument during compilation.

Note

See file Real.hpp for details. All Real types pass the real type concept test from boost concepts. The support for Eigen and CGAL is done with numerical traits.

Installation

The precompiled Yade Daily packages for Ubuntu 22.04 and Debian Bookworm, Trixie are provided for high precision types long double, float128 and mpfr150. To use high precision on other linux distributions Yade has to be compiled and installed from source code by following the regular installation instructions. With extra following caveats:

  1. Following packages are required to be installed: python3-mpmath libmpfr-dev libmpfrc++-dev libmpc-dev (the mpfr and mpc related packages are necessary only to use boost::multiprecision::mpfr type). These packages are already listed in the default requirements.

  2. A g++ compiler version 9.2.1 or higher is required. It shall be noted that upgrading only the compiler on an existing linux installation (an older one, in which packages for different versions of gcc were not introduced) is difficult and it is not recommended. A simpler solution is to upgrade entire linux installation.

  3. During cmake invocation specify:

    1. either number of bits as REAL_PRECISION_BITS=……,

    2. or number of requested decimal places as REAL_DECIMAL_PLACES=……, but not both

    3. optionally to use MPFR specify ENABLE_MPFR=ON (is OFF by default).

    4. optionally decide about using quadruple, octuple or higher precisions with -DENABLE_MULTI_REAL_HP=ON (default). This feature is independent of selecting the precision of Real type (in point 1. or 2. above) and works even when Real is chosen as double (i.e. no special choice is made: the default settings).

    The arbitrary precision (mpfr or cpp_bin_float) types are used only when more than 128 bits or more than 39 decimal places are requested. In such case if ENABLE_MPFR=OFF then the slower cpp_bin_float type is used. The difference in decimal places between 39 and 33 stems from the fact that 15 bits are used for exponent. Note: a fast quad-double (debian package libqd-dev) implementation with 62 decimal places is in the works with boost multiprecision team.

Supported modules

During compilation several Yade modules can be enabled or disabled by passing an ENABLE_* command line argument to cmake. The following table lists which modules are currently working with high precision (those marked with “maybe” were not tested):

ENABLE_* module name

HP support

cmake default setting

notes

ENABLE_GUI

yes

ON

native support [2]

ENABLE_CGAL

yes

ON

native support [2]

ENABLE_VTK

yes

ON

supported [3]

ENABLE_OPENMP

partial

ON

partial support [4]

ENABLE_MPI

maybe

OFF

not tested [5]

ENABLE_GTS

yes

ON

supported [6]

ENABLE_GL2PS

yes

ON

supported [6]

ENABLE_LINSOLV

no

OFF

not supported [7]

ENABLE_PARTIALSAT

no

OFF

not supported [7]

ENABLE_PFVFLOW

no

OFF

not supported [7]

ENABLE_TWOPHASEFLOW

no

OFF

not supported [7]

ENABLE_THERMAL

no

OFF

not supported [7]

ENABLE_LBMFLOW

yes

ON

supported [6]

ENABLE_SPH

maybe

OFF

not tested [8]

ENABLE_LIQMIGRATION

maybe

OFF

not tested [8]

ENABLE_MASK_ARBITRARY

maybe

OFF

not tested [8]

ENABLE_PROFILING

maybe

OFF

not tested [8]

ENABLE_POTENTIAL_BLOCKS

no

OFF

not supported [9]

ENABLE_POTENTIAL_PARTICLES

yes

ON

supported [10]

ENABLE_DEFORM

maybe

OFF

not tested [8]

ENABLE_OAR

maybe

OFF

not tested [8]

ENABLE_FEMLIKE

yes

ON

supported [6]

ENABLE_ASAN

yes

OFF

supported [6]

ENABLE_MPFR

yes

OFF

native support [2]

ENABLE_LS_DEM

no

ON

not supported [11]

The unsupported modules are automatically disabled during a high precision cmake stage.

Footnotes

Double, quadruple, octuple and higher precisions

Sometimes a critical section of the calculations in C++ would work better if it was performed in the higher precision to guarantee that it will produce the correct result in the default precision. A simple example is solving a system of linear equations (basically inverting a matrix) where some coefficients are very close to zero. Another example of alleviating such problem is the Kahan summation algorithm.

If requirements are satisfied, Yade supports higher precision multipliers in such a way that RealHP<1> is the Real type described above, and every higher number is a multiplier of the Real precision. RealHP<2> is double precision of RealHP<1>, RealHP<4> is quadruple precision and so on. The general formula for amount of decimal places is implemented in RealHP.hpp file and the number of decimal places used is simply a multiple N of decimal places in Real precision, it is used when native types are not available. The family of available native precision types is listed in the RealHPLadder type list.

All types listed in MathEigenTypes.hpp follow the same naming pattern: Vector3rHP<1> is the regular Vector3r and Vector3rHP<N> for any supported N uses the precision multiplier N. One could then use an Eigen algorithm for solving a system of linear equations with a higher N using MatrixXrHP<N> to obtain the result with higher precision. Then continuing calculations in default Real precision, after the critical section is done. The same naming convention is used for CGAL types, e.g. CGAL_AABB_treeHP<N> which are declared in file AliasCGAL.hpp.

Before we fully move to C++20 standard, one small restriction is in place: the precision multipliers actually supported are determined by these two defines in the RealHPConfig.hpp file:

  1. #define YADE_EIGENCGAL_HP (1)(2)(3)(4)(8)(10)(20) - the multipliers listed here will work in C++ for RealHP<N> in CGAL and Eigen. They are cheap in compilation time, but have to be listed here nonetheless. After we move code to C++20 this define will be removed and all multipliers will be supported via single template constraint. This inconvenience arises from the fact that both CGAL and Eigen libraries offer template specializations only for a specific type, not a generalized family of types. Thus this define is used to declare the required template specializations.

Hint

The highest precision available by default N= (20) corresponds to 300 decimal places when compiling Yade with the default settings, without changing REAL_DECIMAL_PLACES=…… cmake compilation option.

  1. #define YADE_MINIEIGEN_HP (1)(2) - the precision multipliers listed here are exported to python, they are expensive: each one makes compilation longer by 1 minute. Adding more can be useful only for debugging purposes. The double RealHP<2> type is by default listed here to allow exploring the higher precision types from python. Also please note that mpmath supports only one precision at a time. Having different mpmath variables with different precision is poorly supported, albeit mpmath authors promise to improve that in the future. Fortunately this is not a big problem for Yade users because the general goal here is to allow more precise calculations in the critical sections of C++ code, not in python. This problem is partially mitigated by changing mpmath precision each time when a C++python conversion occurs. So one should keep in mind that the variable mpmath.mp.dps always reflects the precision used by latest conversion performed, even if that conversion took place in GUI (not in the running script). Existing mpmath variables are not truncated to lower precision, their extra digits are simply ignored until mpmath.mp.dps is increased again, however the truncation might occur during assignment.

On some occasions it is useful to have an intuitive up-conversion between C++ types of different precisions, say for example to add RealHP<1> to RealHP<2> type. The file UpconversionOfBasicOperatorsHP.hpp serves this purpose. This header is not included by default, because more often than not, adding such two different types will be a mistake (efficiency–wise) and compiler will catch them and complain. After including this header this operation will become possible and the resultant type of such operation will be always the higher precision of the two types used. This file should be included only in .cpp files. If it was included in any .hpp file then it could pose problems with C++ type safety and will have unexpected consequences. An example usage of this header is in the following test routine.

Warning

Trying to use N unregistered in YADE_MINIEIGEN_HP for a Vector3rHP<N> type inside the YADE_CLASS_BASE_DOC_ATTRS_* macro to export it to python will not work. Only these N listed in YADE_MINIEIGEN_HP will work. However it is safe (and intended) to use these from YADE_EIGENCGAL_HP in the C++ calculations in critical sections of code, without exporting them to python.

Compatibility

Python

To declare python variables with Real and RealHP<N> precision use functions math.Real(…), math.Real1(…), math.Real2(…). Supported are precisions listed in YADE_MINIEIGEN_HP, but please note the mpmath-conversion-restrictions.

Python has native support for high precision types using mpmath package. Old Yade scripts that use supported modules can be immediately converted to high precision by switching to yade.minieigenHP. In order to do so, the following line:

from minieigen import *

has to be replaced with:

from yade.minieigenHP import *

Respectively import minieigen has to be replaced with import yade.minieigenHP as minieigen, the old name as minieigen being used only for the sake of backward compatibility. Then high precision (binary compatible) version of minieigen is used when non double type is used as Real.

The RealHP<N> higher precision vectors and matrices can be accessed in python by using the .HPn module scope. For example:

import yade.minieigenHP as mne
mne.HP2.Vector3(1,2,3) # produces Vector3 using RealHP<2> precision
mne.Vector3(1,2,3)     # without using HPn module scope it defaults to RealHP<1>

The respective math functions such as:

import yade.math as mth
mth.HP2.sqrt(2) # produces square root of 2 using RealHP<2> precision
mth.sqrt(2)     # without using HPn module scope it defaults to RealHP<1>

are supported as well and work by using the respective C++ function calls, which is usually faster than the mpmath functions.

Warning

There may be still some parts of python code that were not migrated to high precision and may not work well with mpmath module. See debugging section for details.

C++

Before introducing high precision it was assumed that Real is actually a POD double type. It was possible to use memset(…), memcpy(…) and similar functions on double. This was not a good approach and even some compiler #pragma commands were used to silence the compilation warnings. To make Real work with other types, this assumption had to be removed. A single memcpy(…) still remains in file openmp-accu.hpp and will have to be removed. In future development such raw memory access functions are to be avoided.

All remaining double were replaced with Real and any attempts to use double type in the code will fail in the gitlab-CI pipeline.

Mathematical functions of all high precision types are wrapped using file MathFunctions.hpp, these are the inline redirections to respective functions of the type that Yade is currently being compiled with. The code will not pass the pipeline checks if std:: is used. All functions that take Real argument should now call these functions in yade::math:: namespace. Functions which take only Real arguments may omit math:: specifier and use ADL instead. Examples:

  1. Call to std::min(a,b) is replaced with math::min(a,b), because a or b may be int (non Real) therefore math:: is necessary.

  2. Call to std::sqrt(a) can be replaced with either sqrt(a) or math::sqrt(a) thanks to ADL, because a is always Real.

If a new mathematical function is needed it has to be added in the following places:

  1. lib/high-precision/MathFunctions.hpp or lib/high-precision/MathComplexFunctions.hpp or lib/high-precision/MathSpecialFunctions.hpp, depending on function type.

  2. py/high-precision/_math.cpp, see math module for details.

  3. py/tests/testMath.py

  4. py/tests/testMathHelper.py

The tests for a new function are to be added in py/tests/testMath.py in one of these functions: oneArgMathCheck(…):, twoArgMathCheck(…):, threeArgMathCheck(…):. A table of approximate expected error tolerances in self.defaultTolerances is to be supplemented as well. To determine tolerances with better confidence it is recommended to temporarily increase number of tests in the test loop. To determine tolerances for currently implemented functions a range(1000000) in the loop was used.

Note

When passing arguments in C++ in function calls it is preferred to use const Real& rather than to make a copy of the argument as Real. The reason is following: in non high-precision regular case both the double type and the reference have 8 bytes. However float128 is 16 bytes large, while its reference is still only 8 bytes. So for regular precision, there is no difference. For all higher precision types it is beneficial to use const Real& as the function argument. Also for const Vector3r& arguments the speed gain is larger, even without high precision.

Using higher precisions in C++

As mentioned above RealHP<1> is the Real type and every higher number is a multiplier of the Real precision. RealHP<2> is twice the precision of RealHP<1>, RealHP<4> is quadruple precision and so on. In C++ you have access to these higher precision typedefs at all time, so it is possible to write some critical part of an algorithm in higher precision by declaring the respective variables to be of type RealHP<2> or RealHP<4> or higher.

String conversions

On the python side it is recommended to use math.Real(…) math.Real1(…), or math.toHP1(…) to declare python variables and math.radiansHP1(…) to convert angles to radians using full Pi precision.

On the C++ side it is recommended to use yade::math::toString(…) and yade::math::fromStringReal(…) conversion functions instead of boost::lexical_cast<std::string>(…). The toString and its high precision version toStringHP functions (in file RealIO.hpp) guarantee full precision during conversion. It is important to note that std::to_string does not guarantee this and boost::lexical_cast does not guarantee this either.

For higher precision types it is possible to control in runtime the precision of C++python during the RealHP<N> string conversion by changing the math.RealHPConfig.extraStringDigits10 static parameter. Each decimal digit needs \(\log_{10}(2)\approx3.3219\) bits. The std::numeric_limits<Real>::digits10 provides information about how many decimal digits are completely determined by binary representation, meaning that these digits are absolutely correct. However to convert back to binary more decimal digits are necessary because \(\log_{2}(10)\approx0.3010299\) decimal digits are used by each bit, and the last digit from std::numeric_limits<Real>::digits10 is not sufficient. In general 3 or more in extraStringDigits10 is enough to have an always working number round tripping. However if one wants to only extract results from python, without feeding them back in to continue calculations then a smaller value of extraStringDigits10 is recommended, like 0 or 1, to avoid a fake sense of having more precision, when it’s not there: these extra decimal digits are not correct in decimal sense. They are only there to have working number round tripping. See also a short discussion about this with boost developers. Also see file RealHPConfig.cpp for more details.

Note

The parameter extraStringDigits10 does not affect double conversions, because boost::python uses an internal converter for this particular type. It might be changed in the future if the need arises. E.g. using a class similar to ThinRealWrapper.

It is important to note that creating higher types such as RealHP<2> from string representation of RealHP<1> is ambiguous. Consider following example:

import yade.math as mth

mth.HP1.getDecomposedReal(1.23)['bits']
Out[2]: '10011101011100001010001111010111000010100011110101110'

mth.HP2.getDecomposedReal('1.23')['bits']  # passing the same arg in decimal format to HP2 produces nonzero bits after the first 53 bits of HP1
Out[3]: '10011101011100001010001111010111000010100011110101110000101000111101011100001010001111010111000010100011110101110'

mth.HP2.getDecomposedReal(mth.HP1.toHP2(1.23))['bits'] # it is possible to use yade.math.HPn.toHPm(…) conversion, which preserves binary representation
Out[4]: '10011101011100001010001111010111000010100011110101110000000000000000000000000000000000000000000000000000000000000'

Which of these two RealHP<2> binary representations is more desirable depends on what is needed:

  1. The best binary approximation of a 1.23 decimal.

  2. Reproducing the 53 binary bits of that number into a higher precision to continue the calculations on the same number which was previously in lower precision.

To achieve 1. simply pass the argument '1.23' as string. To achieve 2. use math.HPn.toHPm(…) or math.Realn(…) conversion, which maintains binary fidelity using a single static_cast<RealHP<m>>(…). Similar problem is discussed in mpmath and boost documentation.

The difference between toHPn and Realn is following: the functions HPn.toHPm create a \(m\times n\) matrix converting from RealHP<n> to RealHP<m>. When \(n<m\) then extra bits are set to zero (case 2 above, depending on what is required one might say that “precision loss occurs”). The functions math.Real(…), math.Real1(…), math.Real2(…) are aliases to the diagonal of this matrix (case 1 above, depending on what is required one might say that “no conversion loss occurs” when using them).

Hint

All RealHP<N> function arguments that are of type higher than double can also accept decimal strings. This allows to preserve precision above python default floating point precision.

Warning

On the contrary all the function arguments that are of type double can not accept decimal strings. To mitigate that one can use toHPn(…) converters with string arguments.

Hint

To make debugging of this problem easier the function math.toHP1(…) will raise RuntimeError if the argument is a python float (not a decimal string).

Warning

I cannot stress this problem enough, please try running yade --check (or yade ./checkGravityRungeKuttaCashKarp54.py) in precision different than double after changing this line into g = -9.81. In this (particular and simple) case the getCurrentPos() function fails on the python side because low-precision g is multiplied by high-precision t.

Complex types

Complex numbers are supported as well. All standard C++ functions are available in lib/high-precision/MathComplexFunctions.hpp and also are exported to python in py/high-precision/_math.cpp. There is a cmake compilation option ENABLE_COMPLEX_MP which enables using better complex types from boost::multiprecision library for representing ComplexHP<N> family of types: complex128, mpc_complex, cpp_complex and complex_adaptor. It is ON by default whenever possible: for boost version >= 1.71. For older boost the ComplexHP<N> types are represented by std::complex<RealHP<N>> instead, which has larger numerical errors in some mathematical functions.

When using the ENABLE_COMPLEX_MP=ON (default) the previously mentioned lib/high-precision/UpconversionOfBasicOperatorsHP.hpp is not functional for complex types, it is a reported problem with the boost library.

When using MPFR type, the libmpc-dev package has to be installed (mentioned above).

Eigen and CGAL

Eigen and CGAL libraries have native high precision support.

VTK

Since VTK is only used to record results for later viewing in other software, such as paraview, the recording of all decimal places does not seem to be necessary (for now). Hence all recording commands in C++ convert Real type down to double using static_cast<double> command. This has been implemented via classes vtkPointsReal, vtkTransformReal and vtkDoubleArrayFromReal in file VTKCompatibility.hpp. Maybe VTK in the future will support non double types. If that will be needed, the interface can be updated there.

LAPACK

Lapack is an external library which only supports double type. Since it is not templatized it is not possible to use it with Real type. Current solution is to down-convert arguments to double upon calling linear equation solver (and other functions), then convert them back to Real. This temporary solution omits all benefits of high precision, so in the future Lapack is to be replaced with Eigen or other templatized libraries which support arbitrary floating point types.

Debugging

High precision is still in the experimental stages of implementation. Some errors may occur during use. Not all of these errors are caught by the checks and tests. Following examples may be instructive:

  1. Trying to use const references to Vector3r members - a type of problem with results in a segmentation fault during runtime.

  2. A part of python code does not cooperate with mpmath - the checks and tests do not cover all lines of the python code (yet), so more errors like this one are expected. The solution is to put the non compliant python functions into py/high-precision/math.py. Then replace original calls to this function with function in yade.math, e.g. numpy.linspace(…) is replaced with yade.math.linspace(…).

The most flexibility in debugging is with the long double type, because special files ThinRealWrapper.hpp, ThinComplexWrapper.hpp were written for that. They are implemented with boost::operators, using partially ordered field. Note that they do not provide operator++.

A couple of #defines were introduced in these two files to help debugging more difficult problems:

  1. YADE_IGNORE_IEEE_INFINITY_NAN - it can be used to detect all occurrences when NaN or Inf are used. Also it is recommended to use this define when compiling Yade with -Ofast flag, without -fno-associative-math -fno-finite-math-only -fsigned-zeros

  2. YADE_WRAPPER_THROW_ON_NAN_INF_REAL, YADE_WRAPPER_THROW_ON_NAN_INF_COMPLEX - can be useful for debugging when calculations go all wrong for unknown reason.

Also refer to address sanitizer section, as it is most useful for debugging in many cases.

Hint

If crash is inside a macro, for example YADE_CLASS_BASE_DOC_ATTRS_CTOR_PY, it is useful to know where inside this macro the problem happens. For this purpose it is possible to use g++ preprocessor to remove the macro and then compile the postprocessed code without the macro. Invoke the preprocessor with some variation of this command:

g++ -E -P core/Body.hpp -I ./ -I /usr/include/eigen3 -I /usr/include/python3.7m > /tmp/Body.hpp

Maybe use clang-format so that this file is more readable:

./scripts/clang-formatter.sh /tmp/Body.hpp

Be careful because such files tend to be large and clang-format is slow. So sometimes it is more useful to only use the last part of the file, where the macro was postprocessed. Then replace the macro in the original file in question, and then continue debugging. But this time it will be revealed where inside a macro the problem occurs.

Note

When asking questions about High Precision it is recommended to start the question title with [RealHP].