4

Not Only java.lang.Math: What About Apache.commons.Math?

 1 year ago
source link: https://devm.io/java/java-apache-commons-math
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

Experimenting with an open-source library of mathematical functions and utilities

Not Only java.lang.Math: What About Apache.commons.Math?

09. Aug 2022


Novice programmers are sometimes too hesitant to explore unfamiliar tools, even if they are commonly accepted solutions to their problems. However, you may also discover that some lesser-known tools are better suited to your tasks. There could be several reasons for this, the most obvious one being that these tools may be more effective and faster to implement. In this article, we will look at some parts of the Apache Commons Math library that can be useful for mathematical calculations.

What is Apache.Commons?

Apache Commons is a large and serious project dedicated to developing cool Java libraries. Or “a large set of small Java utilities” for various purposes. Some Apache Commons utilities underpin several well-known projects such as Tomcat, Hibernate, etc. As the name suggests, Apache Commons Math is a library for a wide range of mathematical operations. Let us see what sections it consists of.

Based on the functionality provided, Commons Math is divided into sixteen sub-packages.

As you can see, this is an excellent set for a scientist, with functions for any area of mathematics — numerical analysis, mathematical analysis, complex analysis, algebra, linear algebra, geometry, differential equations, and so on — included. Of course, a detailed review of this library requires several dozen articles. In this article, we will look at a few useful units and methods for those studying programming and hoping to work in knowledge-intensive industries. The article focuses on some Math standard library analogues, as well as classes for working with complex numbers and fractions.

Since Apache.Commons is an open-source project, you can fully examine the code of any class or method in the library and freely use and modify it in your projects.

org.apache.commons.math4.util

This set of utilities includes implementations of functions familiar from the Math package, as well as several others. First and foremost, I would like to consider the class FastMath, which contains methods that can be used instead of the standard Math and StrictMath classes for large-scale computation. For developers of the Math package, there are methods here that Multiply two numbers, detecting overflows.

static int multiplyExact(int a, int b)

Multiply two numbers, detecting overflows.

static long multiplyExact(long a, long b)

Multiply two numbers, detecting overflows.

You can also find some inverse trigonometric functions that are not included in the Math package here. For example, hyperbolic arc functions.

  • asinh(double)
  • acosh(double)
  • atanh(double)

We can, of course, write such methods ourselves, guided by known mathematical formulas or numerical methods. Here is the simplest version without checks:

public static double asinh(double x){

   return Math.log(x + Math.sqrt(Math.pow(x, 2) + 1));

}

If you look into the open-source Apache Commons FastMath class (for example, from IntelliJ IDEA IDE, Ctrl (Cmd) + Left click), you will see the following picture:

public static double asinh(double a) {
   boolean negative = false;
   if (a < 0.0D) {
       negative = true;
       a = -a;
   }

   double absAsinh;
   if (a > 0.167D) {
       absAsinh = log(sqrt(a * a + 1.0D) + a);
   } else {
       double a2 = a * a;
       if (a > 0.097D) {
           absAsinh = a * (1.0D - a2 * (0.3333333333333333D - a2 * (0.2D - a2 * (0.14285714285714285D - a2 * (0.1111111111111111D - a2 * (0.09090909090909091D - a2 * (0.07692307692307693D - a2 * (0.06666666666666667D - a2 * 0.058823529411764705D * 0.9375D) * 0.9285714285714286D) * 0.9166666666666666D) * 0.9D) * 0.875D) * 0.8333333333333334D) * 0.75D) * 0.5D);
       } else if (a > 0.036D) {
           absAsinh = a * (1.0D - a2 * (0.3333333333333333D - a2 * (0.2D - a2 * (0.14285714285714285D - a2 * (0.1111111111111111D - a2 * (0.09090909090909091D - a2 * 0.07692307692307693D * 0.9166666666666666D) * 0.9D) * 0.875D) * 0.8333333333333334D) * 0.75D) * 0.5D);
       } else if (a > 0.0036D) {
           absAsinh = a * (1.0D - a2 * (0.3333333333333333D - a2 * (0.2D - a2 * (0.14285714285714285D - a2 * 0.1111111111111111D * 0.875D) * 0.8333333333333334D) * 0.75D) * 0.5D);
       } else {
           absAsinh = a * (1.0D - a2 * (0.3333333333333333D - a2 * 0.2D * 0.75D) * 0.5D);
       }
   }
   return negative ? -absAsinh : absAsinh;
}

The code is not particularly appealing, and it will be difficult for a third-party developer to understand why these specific coefficients were chosen. In terms of quality, our own method requires some fine-tuning, such as checking the scope, etc. However, we will not do that now. Let us look at a better example.

import org.apache.commons.math3.util.FastMath;
public class MyMathTest {
   public static double asinh(double x){
       return Math.log(x + Math.sqrt(Math.pow(x, 2) + 1));
   }
   public static double asinh2(double x){
       return FastMath.log(x + FastMath.sqrt(FastMath.pow(x,2) +1));
   }

   public static void main(String[] args) {
       int x = 5;
       System.out.println("asins:");
       System.out.println(asinh(x));
       System.out.println(asinh2(x));
       System.out.println(FastMath.asinh(x));

   }
}

Here we created two custom methods for calculating the inverse hyperbolic sine using the Math and FastMath functions, as well as the implementation of the method in FastMath.

asins:
2.3124383412727525
2.3124383412727525
2.3124383412727525

As you can see, the result is the same, which is not surprising. But how can we check the effectiveness of these methods? With such simple examples, we will fail.

Let us compare the performance of two methods for calculating sines, for example. How can we do that? We can, for example, create a loop and add up the sum of randomized elements passed to the sine function. Why randomized? For the arguments to differ, it is possible that one function will work better on some and worse on others. For the sake of experiment purity, it is better to give the same arguments to the input, that is, set the Random seed explicitly (just remember that Random itself can work longer than sine). Let us make a small array of 64 elements, for example, and fill it with random elements, and then, in a loop, give this array to both sine methods. Here is simplified pseudocode of the main idea:

double[] capturedRandom = new double[64] ;
capturedRandom[...] = random() * 2*PI;
double sumAngle = 0.0, sumSin = 0.0;
for(int i = 0; i < 10000 ; ++i) {
  for(int j = 0; j < 64 ; ++j) {
    double angle = capturedRandom[j];
    sumAngle += angle;
    sumSin += Math.sin(angle)
  }
}

At the same time, it is important that the result of the loop work goes somewhere. If you simply call the sine function without adding or writing it anywhere in the loop, the optimizer will generally discard the code as well as the entire loop, and the program will exit with an incorrect result (it will seem that everything works instantly, even regardless of the number of iterations in the loop). It is also desirable to take several measurements in a row without hibernating the JVM. Let us write such a program.

import org.apache.commons.math3.util.FastMath;

public class MathTest {

   private static double[] randomAngles = fillRandomAngles(64);

   private static double[] fillRandomAngles(int count) {
       double[] angles = new double[count];
       for (int i = 0; i < count; ++i) {
           angles[i] = Math.random() * Math.PI * 2;
       }
       return angles;
   }

   private static double measureMathSin(String introMessage, double[] angles, int iterationsCount) {
       int totalIterations = angles.length * iterationsCount;
       double anglesSum = 0.0, sinSum = 0.0;
       long startTime = System.nanoTime();
       for (int i = 0; i < iterationsCount; ++i) {
           for (double angle : angles) {
               anglesSum += angle;
               sinSum += Math.sin(angle);
           }
       }
       long endTime = System.nanoTime();
       System.out.println(introMessage + " anglesSum = " + anglesSum + " sinSum = " + sinSum);
       return (double) (endTime - startTime) / totalIterations;
   }

   private static double measureFastMathSin(String introMessage, double[] angles, int iterationsCount) {
       int totalIterations = angles.length * iterationsCount;
       double anglesSum = 0.0, sinSum = 0.0;
       long startTime = System.nanoTime();
       for (int i = 0; i < iterationsCount; ++i) {
           for (double angle : angles) {
               anglesSum += angle;
               sinSum += FastMath.sin(angle);
           }
       }
       long endTime = System.nanoTime();
       System.out.println(introMessage + " anglesSum = " + anglesSum + " sinSum = " + sinSum);
       return (double) (endTime - startTime) / totalIterations;
   }

   public static void main(String[] args) throws Exception {
       System.out.println("Run recurrent measurements of Math.sin:");
       for (int m = 0; m < 15; ++m) {
           double mathPerformanceRate = measureMathSin("lang math measurement(" + m + "):", randomAngles, 100_000);
           System.out.println("mathPerformanceRate = " + mathPerformanceRate);
           Thread.sleep(1000);
           double fastMathPerformanceRate = measureFastMathSin("fast math measurement(" + m + "):", randomAngles, 100_000);
           System.out.println("fastMathPerformanceRate = " + fastMathPerformanceRate);
           Thread.sleep(1000);
       }
   }
}

Here is the output:

"C:\Program Files\Java\jdk-13.0.2\bin\java.exe" tests.MathTest
Run recurrent measurements of Math.sin:
lang math measurement(0): anglesSum = 1.8562502857676055E7 sinSum = 361279.52945610177
mathPerformanceRate = 15.360328125
fast math measurement(0): anglesSum = 1.8562502857676055E7 sinSum = 361279.52945610177
fastMathPerformanceRate = 30.992515625
lang math measurement(1): anglesSum = 1.8562502857676055E7 sinSum = 361279.52945610177
mathPerformanceRate = 11.793078125
fast math measurement(1): anglesSum = 1.8562502857676055E7 sinSum = 361279.52945610177
fastMathPerformanceRate = 23.971203125
lang math measurement(2): anglesSum = 1.8562502857676055E7 sinSum = 361279.52945610177
mathPerformanceRate = 10.642640625
fast math measurement(2): anglesSum = 1.8562502857676055E7 sinSum = 361279.52945610177
fastMathPerformanceRate = 22.390828125
lang math measurement(3): anglesSum = 1.8562502857676055E7 sinSum = 361279.52945610177
mathPerformanceRate = 10.648453125
fast math measurement(3): anglesSum = 1.8562502857676055E7 sinSum = 361279.52945610177
fastMathPerformanceRate = 23.799546875
lang math measurement(4): anglesSum = 1.8562502857676055E7 sinSum = 361279.52945610177
mathPerformanceRate = 10.931828125
fast math measurement(4): anglesSum = 1.8562502857676055E7 sinSum = 361279.52945610177
fastMathPerformanceRate = 23.020265625
lang math measurement(5): anglesSum = 1.8562502857676055E7 sinSum = 361279.52945610177
mathPerformanceRate = 11.12209375
fast math measurement(5): anglesSum = 1.8562502857676055E7 sinSum = 361279.52945610177
fastMathPerformanceRate = 22.92071875
lang math measurement(6): anglesSum = 1.8562502857676055E7 sinSum = 361279.52945610177
mathPerformanceRate = 10.78703125
fast math measurement(6): anglesSum = 1.8562502857676055E7 sinSum = 361279.52945610177
fastMathPerformanceRate = 22.76290625
lang math measurement(7): anglesSum = 1.8562502857676055E7 sinSum = 361279.52945610177
mathPerformanceRate = 10.539203125
fast math measurement(7): anglesSum = 1.8562502857676055E7 sinSum = 361279.52945610177
fastMathPerformanceRate = 23.134984375
lang math measurement(8): anglesSum = 1.8562502857676055E7 sinSum = 361279.52945610177
mathPerformanceRate = 10.80353125
fast math measurement(8): anglesSum = 1.8562502857676055E7 sinSum = 361279.52945610177
fastMathPerformanceRate = 23.21146875
lang math measurement(9): anglesSum = 1.8562502857676055E7 sinSum = 361279.52945610177
mathPerformanceRate = 10.78996875
fast math measurement(9): anglesSum = 1.8562502857676055E7 sinSum = 361279.52945610177
fastMathPerformanceRate = 22.454765625
lang math measurement(10): anglesSum = 1.8562502857676055E7 sinSum = 361279.52945610177
mathPerformanceRate = 11.12425
fast math measurement(10): anglesSum = 1.8562502857676055E7 sinSum = 361279.52945610177
fastMathPerformanceRate = 22.80259375
lang math measurement(11): anglesSum = 1.8562502857676055E7 sinSum = 361279.52945610177
mathPerformanceRate = 10.803765625
fast math measurement(11): anglesSum = 1.8562502857676055E7 sinSum = 361279.52945610177
fastMathPerformanceRate = 22.57290625
lang math measurement(12): anglesSum = 1.8562502857676055E7 sinSum = 361279.52945610177
mathPerformanceRate = 10.763
fast math measurement(12): anglesSum = 1.8562502857676055E7 sinSum = 361279.52945610177
fastMathPerformanceRate = 22.430453125
lang math measurement(13): anglesSum = 1.8562502857676055E7 sinSum = 361279.52945610177
mathPerformanceRate = 10.684859375
fast math measurement(13): anglesSum = 1.8562502857676055E7 sinSum = 361279.52945610177
fastMathPerformanceRate = 22.68584375
lang math measurement(14): anglesSum = 1.8562502857676055E7 sinSum = 361279.52945610177
mathPerformanceRate = 11.641078125
fast math measurement(14): anglesSum = 1.8562502857676055E7 sinSum = 361279.52945610177
fastMathPerformanceRate = 22.545

Wow! “Native” math.sin() looks two times better than “fast.” Perhaps this is because “native” just calls the machine command, whereas “fast” does something in Java code. Other FastMath function comparisons produced approximately the same results.

Working with fractions

Working with simple fractions in programming can be difficult at times, but Apache Commons Math has the Fraction and BigFraction classes for this. Let us look at an example where we add fractions, take the modulus, and give the reciprocal of our fraction (reciprocal () method).

import org.apache.commons.math3.fraction.Fraction;

public class MyMathTest {


   public static void main(String[] args) {

//here we’ve got three fraction numbers  
       Fraction fraction1 = new Fraction(-0.25);
       Fraction fraction2 = new Fraction(1,4);
       Fraction fraction3 = new Fraction(3,4);
       System.out.println(fraction1.add(fraction2));
// subtract two fractions
       System.out.println(fraction1.subtract(fraction2)); 
 // absolute value
       System.out.println(fraction1.abs());               System.out.println(fraction2.reciprocal());  // reciprocal
       System.out.println(fraction3.reciprocal());

   }

}

The output is:

0
-1 / 2
1 / 4
4
4 / 3

As you can see, we can work with both decimal and ordinal fractions.

Complex numbers

Complex numbers open up a plethora of opportunities for working with scientific calculations. They are used in everything from classical physics to complex mathematical models associated with technological inventions, as well as in working with quantum mechanics and fractals.

The Complex Numbers library includes arithmetic functions, the complex conjugation function, and many more. A complex number can be used as an argument to a math function. A complex number is written as follows:

Complex complex1 = new Complex(1.0, 5.0);

The first argument (1) is the real part and the second (5) is imaginary. We have a complex number a + 5i. Here is a small example.

import org.apache.commons.math3.complex.Complex;

public class MyMathTest3 {

    public static void main(String[] args) {

        Complex complex1 = new Complex(1.0, 5.0);
        Complex complex2 = new Complex(3.5, 7);

        System.out.println("Adding 2 complex numbers: " + complex1.add(complex2));
        System.out.println("Dividing 2 complex numbers:" + complex1.divide(complex2));
        System.out.println("complex conjugate of a + 5i = " + complex1.conjugate());
        System.out.println("sin (a + 5i) = " + complex1.sin());

    }
}

The output is:

Adding 2 complex numbers: (4.5, 12.0)
Dividing 2 complex numbers:(0.6285714285714286, 0.17142857142857143)
complex conjugate of a + 5i = (1.0, -5.0)
sin (a + 5i) = (62.44551846769654, 40.0921657779984)

Remember that the complex conjugation of a number a + bi is the number a - bi.

Conclusion

We have only covered a few useful classes for students and scientists in this article. Of course, such a large library requires more in-depth research. However, all its code is open, and you can delve deeply into it to understand how it works. Working with fractions and complex numbers in Apache Commons Math is interesting. Tools for analysis, algebra, statistics, and probability are also noteworthy, but are beyond the scope of this article. However, based on my experiments, I concluded that using native mathematical functions rather than standard ones is preferable. I conducted several experiments like the one described in this article, and all of them gave the same result: FastMath is slower than “ordinary” Math.


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK