Alexandria 2.31.0
SDC-CH common library for the Euclid project
|
The MathUtils module provides and API for mathematical functions, operations on these functions and methods for converting data to functions. This page starts with a description of the basic interfaces and classes of this module and how to use them and finishes with guidelines on how to extend the module for new types of functions.
The MathUtils module defines a tree of interfaces which describe mathematical functions.
The core of the MathUtils module is the Function interface. This interface represents a mathematical function, which converts a value from domain X to a value of domain Y. The conversion is done by using the parenthesis operator:
Any code using the MathUtils module should use variables of reference or pointer to Function type instead of concrete implementation types, and rely only on the methods provided by the Function interface. This way the function used can be replaced easily with the minimum amount of modifications.
Note that because Function is an abstract class, copying a Function reference or pointer is not possible via the copy constructors and operators. For this reason the Function interface provides the Function::clone() method, which creates a copy of the function and returns a pointer to it:
The returned pointer points to an object of the correct sub-type. All function types are providing this functionality.
The Integrable is an interface extension of the Function interface, which indicates that the function integration can be done in a fast analytical way. The functions which inherit this interface are implementing the Integrable::integrate() method, which performs this calculation.
The Differentiable is an interface extension of the Function interface, which indicates that the derivative and the indefinite integral of the function exist can be represented by anther function. The functions which inherit this interface are implementing the Differentiable::derivative() and Differentiable::indefiniteIntegral() methods.
Note that the Differentiable functions are always Integrable, because their integration can be calculated based on the indefinite integral. The MathUtils module does take care of this optimization.
The current version of the MathUtils module provides the following concrete implementations of the Function interface.
The Polynomial class represents a polynomial of a single variable and arbitrary degree. It can be constructed by a vector containing its coefficients, where the order in the vector is the degree of the coefficient. For example:
Note that the Polynomial function implements the Differentiable interface (so also the Integrable interface).
The Piecewise class is a function defined by a set of knots, which divide the X domain in ranges, and a set of sub-functions (one for each range) which define its value. Outside of the knots, the piecewise function always returns zero. For example, the function:
\[ f(x) = \begin{cases} 0, & \text{if } x < 0 \\ x, & \text{if } 0 < x < 5 \\ 5+3x^2, & \text{if } 5 < x < 7 \\ 0, & \text{if } x > 7 \end{cases} \]
can be represented with the following code:
Note that the Piecewise function is implementing the Integrable interface by redirecting the calculation to its sub-functions, so its performance depends on the performance of the sub-functions.
The MathUtils module provides tools for handling Function objects which can be used with the following include statement:
The MathUtils module provides support for integration of functions via the integrate() method:
The integrate() method will take advantage of the functions which are implementing the Integrable interface during its calculation.
For functions that do not implement the Integrable interface, an optional third argument can be used to specify the numerical integration method to use, which can be any class implementing the NumericalIntegrationScheme interface. Note that the current version of the MathUtils module does not provide a default.
For Functions that do not provide the Differentiable API, the MathUtils module provides an implementation of numerical differentiation via finite differences.
The MathUtils module provides support for multiplication of functions via the multiply() method, which returns a Function instance representing the multiplication result:
The type of the returned Function depends on the types of the functions being multiplied (for more information on this refer the Specifying multiplication methods section bellow).
The MathUtils module provides interpolation functionality, which can be enabled by including the file:
The currently supported types of interpolation are linear and cubic spline:
The result of the interpolation is a Piecewise function with knots the knots of the interpolation. The linear interpolation contains as sub-functions linear polynomials and the spline interpolation third degree polynomials. Note that the result functions can be used as any other function, so integration is possible by using the integrate() method.
Extending the MathUtils module means adding new Function implementations. These implementations can be added in the MathUtils module itself, or might be application specific and exist in a different module. In both cases the steps for implementing new Function types are the same.
The first step is to create a new class which implements the Function interface, or one of its sub-interfaces (Integrable and Differentiable). As an example, the following code implements a function representing the constant function:
As shown in the code above, the Constant class inherits from the Integrable interface. This is because the integration of the constant function can be easily performed with just a multiplication. The implementations of the clone() function and the parenthesis operator are also self explanatory. The first returns a new instance of the Constant (constructed by using the constructor) and the second returns always the same value.
The Constant function example is a good example of a function that has an analytical multiplication with many other functions. When multiplied with a polynomial, for example, the result is a new polynomial with all its coefficients multiplied with the constant value.
MathUtils allows for definitions of such efficient function multiplications, so further manipulation of the results will take advantage of the the result type (for example for integration). This requires the following steps.
First, the function which performs the multiplication between the two functions must be written:
The above example returns a new Polynomial, the coefficients of which are all multiplied with the constant value. It is important to notice the signature of the function. It takes two arguments which are constant Function references and it returns a unique_ptr to a Function object.
The second step is to allow MathUtils module to know when to use the newly defined function. The MathUtils module does this search by using two maps defined in the MathUtils/function/multiplication.h file, the multiplySpecificSpecificMap and the multiplySpecificGenericMap. The first of these maps has as keys pairs of Function concrete types and as values the functions which can perform the multiplication of these Functions. This can be explained better with an example:
Note that if the multiplication of a function type with itself makes sense, it should also be added in the multiplySpecificSpecificMap. For example multiplying two Constant functions results to a new Constant function:
There are some cases that the multiplication of a custom Function type with any other type must be handled. A good example is the Piecewise function, which must delegate the multiplication to each of its sub-functions, independently of the type of the function it is multiplied with. This can be achieved by using the second map, multiplySpecificGenericMap, which has as keys a single Function type.
Note that the multiplySpecificSpecificMap is always searched first. If the pair of types is found there, any entries in the multiplySpecificGenericMap will be ignored (note that the order of the types does not matter). If a Function type is not found in any of the two maps, then the multiply() method returns a build-in wrapper type which performs the multiplication on the fly.
It is important to mention that the above examples call the insert() method of the map. This is necessary if the new Function definitions are not part of the MathUtils itself (because the maps are already initialized in MathUtils). These insertions must be performed before the new function types are used as parameters of the multiply() method.
In the case the new Function types are defined inside the MathUtils module as part of extending the module itself, the map entries can be directly added in the map initialization in the file multiplication.cpp, which is the recommended way.