JAX London Blog

Not only java.lang.Math: what about Apache.commons.Math?

Aug 24, 2022

Novice programmers are sometimes too hesitant to explore unfamiliar tools. Especially if there are generally accepted solutions to its problems. However, you may find that some lesser-known tools are better suited for your tasks. There can be many reasons, the simplest one being that these tools may simply have a better implementation and be faster. In this article, we will look at some parts of the Apache Commons Math library that will be useful for mathematical calculations.

What is Apache.Commons?

Apache Commons is a fairly large and serious project focused on creating cool Java libraries. Or “a large set of small Java utilities” for various purposes. Some of the Apache Commons utilities underpin a number of very well-known projects such as Tomcat, Hibernate, and others.

Apache Commons Math, as you might guess from the name, is a library for a wide variety of mathematical operations. Let’s see what sections it consists of.

Commons Math is divided into sixteen subpackages, based on functionality provided.

As you can see, this is a great set for a scientist, and here you can find very useful functions for any area of ​​​​mathematics – numerical analysis, mathematical analysis, complex analysis, algebra, linear algebra, geometry, differential equations. Of course, a detailed review of this library requires several dozen articles. Here, we will look at just a few useful units and methods for those who study programming and want to work in knowledge-intensive industries. This article is more aboutsome analogues of the Math standard library, as well as classes for working with complex numbers and fractions.

By the way, Apache.Commons is an open source project. This means that you can fully study the code of any class or method of the library, freely use them for your projects, and also change the code within them.

org.apache.commons.math4.util 

In this set of utilities you will find implementations of functions familiar from the Math package, as well as a number of others. First of all, I would like to consider the class FastMath, in which you can find methods that can be used instead of the standard Math and StrictMath classes for large scale computation. 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.

Here, you can also find some inverse trigonometric functions that are not in the Math package. For example, hyperbolic arc functions:

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

Sure, we can write such methods ourselves, guided either by known mathematical formulas or by 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 certainly not the most beautiful, and it will be quite difficult for a third-party developer to figure out why these particular coefficients were chosen. As for good, our own method requires some refinement, checking the scope and so on. But now we will not do this. Let’s take 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’ve created two custom methods for calculating the hyperbolic inverse 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 exactly the same, and this is not surprising. But how to check the effectiveness of the methods? With such simple examples, we will not succeed.

Let’s try to compare the performance of, for example, two methods for calculating sines. How can I do that? For example, create a loop and add up the sum of randomized elements that are passed to the sin function. Why randomized? In order for the arguments to be different, it may happen that on some of the arguments one function will work better, the other worse. For the purity of the experiment, 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 sin). Let’s create a small array, for example, of 64 elements and fill it with random elements, and then, in a loop, give this array to both sin methods. Here is the pseudocode of the main idea without details:

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, because if you simply call the sine function in the loop, but do not add or write it anywhere, the optimizer can generally throw this code out as unnecessary, and the whole loop as unnecessary, and then it can exit 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's 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 2 times better than "fast". Perhaps this is because "native" just calls the machine command, and "fast" does something in Java code.Approximately the same results were shown by other comparisons of functions from the FastMath. 

Working with fractions 

Sometimes working with simple fractions in programming is not so easy, but Apache Commons Math has the Fraction and BigFraction class for this. Let's give an example in which we add fractions, take the modulus, and also 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 see, we can work with fractions in both decimal and ordinary form

Complex numbers 

Complex numbers open up very broad possibilities for working with scientific calculations. They are used literally everywhere - from classical physics to complex mathematical models associated with technical inventions, in working with quantum mechanics and fractals.

The Complex Numbers library has arithmetic functions, the complex conjugation function, and many others. You can use a complex number as an argument to a math function. A complex number is written as follows:

Complex complex1 = new Complex(1.0, 5.0);

Where the first argument (1) is the real part and the second (5) is imaginary. So here 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)

Let me remind you that the complex conjugation of a number a + bi is the number a - bi.

Conclusion

In this article, we have only gone through some useful classes for students and scientists. Of course, such a huge library requires more in-depth research. Fortunately, all of its code is open and you can dive into it very deeply, understanding how the code works. In fact, working with fractions and complex numbers in Apache Commons Math looks interesting. Also noteworthy are tools for analysis, algebra, statistics, and probability that go beyond the scope of this article. But the standard mathematical functions, judging by my experiments, it is better to use native ones. I ran several experiments like the one I showed in this article, and they all showed the same result: FastMath not as fast as “ordinary” Math.

Behind the Tracks

Software Architecture & Design
Software innovation & more
Microservices
Architecture structure & more
Agile & Communication
Methodologies & more
DevOps & Continuous Delivery
Delivery Pipelines, Testing & more
Big Data & Machine Learning
Saving, processing & more