I was inspired by this SO post to investigate a good Java8 way to calculate Pi based on simulation.
I used a similar task to learn about parallel programming on both CUDA, and Intel Xeon Phi processors. Those systems are more geared for parallel programming, but I felt inclined to apply it to 'regular' Java anyway.
Wikipedia has an small section showing this, and includes the following graphic:
The following code performs the above simulation:
import java.util.Arrays;
import java.util.Random;
/**
* Approximate the value of Pi by using a Monte-Carlo simulation for the area of a circle of radius 1.
*
* @author rolf
*
*/
public class PiGuess {
private static final ThreadLocal<Random> LOCAL_RANDOM = new ThreadLocal<Random>() {
protected Random initialValue() {
return new Random();
};
};
private static final int CPU_COUNT = Runtime.getRuntime().availableProcessors();
/**
* Split a number of samples as evenly as possible over the number of available processors.
* @param samples the samples to split
* @return an array containing the number of samples to process on each processor.
*/
private static final long[] apportion(final long samples) {
int core = CPU_COUNT;
final long[] portions = new long[core];
long remaining = samples;
while (core > 0) {
final long part = (remaining - 1 + core) / core;
core--;
portions[core] = part;
remaining -= part;
}
return portions;
}
/**
* Calculate the approximate area of a circle (radius 1.0) based on a sample system on a single quadrant of the circle.
* A parallel mechanism is used to improve performance.
* @param samples the number of samples to take
* @return the area of the circle.
*/
public static final double sampleCircleArea(final long samples) {
/*
Monte-Carlo simulation for the area of a circle.
A circle of radius 1 just fits in a square of sides 2.
In one quadrant of the square (area 1 by 1) we have a quarter circle
If we put the center of the circle at origin 0,0, and then randomly sample points
in that quadrant, we can tell whether that point is in the circle if the ray from the
origin is shorter than the radius of the circle.
If the point is at (x,y), then the ray is the 'hypotenuse' (using Pythagoras).
We know the area of the quadrant, we can sample millions of points in the quadrant,
and we can calculate a ratio of the quadrant's area that is inside the circle.
This sampled area, multiplied by 4, gives the area of the circle.
*/
// how many samples to process in each thread.
long[] counts = apportion(samples);
// add up how many samples appear in the circle
long inside = Arrays.stream(counts).parallel().map(s -> samplePortion(s)).sum();
// convert the quadrant area back to the circle area.
return (4.0 * inside) / samples;
}
/**
* Internal sampling method that counts the number of input samples that are inside the circle too.
* @param samples the samples to calculate
* @return the count of samples that are inside the circle.
*/
private static final long samplePortion(final long samples) {
final Random rand = LOCAL_RANDOM.get();
long inside = 0;
for (int i = 0; i < samples; i++) {
if (isInside(rand)) {
inside++;
}
}
return inside;
}
/**
* The core test for each sample, does a random point in the quadrant lie inside the circle.
* @param rand the source for the random circle.
* @return true if the random sample is inside the circle.
*/
private static final boolean isInside(final Random rand) {
final double x = rand.nextDouble();
final double y = rand.nextDouble();
return x * x + y * y <= 1.0;
}
public static void main(String[] args) {
double[] calculations = new double[100];
for (int i = 0; i < calculations.length; i++) {
double calc = sampleCircleArea(100000);
calculations[i] = calc;
System.out.printf("Loop %d guesses Pi at %.5f%n", i, calc);
}
System.out.printf("Overall calculation is %.5f%n", Arrays.stream(calculations).average().getAsDouble());
}
}
When I run it with 1,000,000 samples, I end with the following output:
Loop 97 guesses Pi at 3.14128 Loop 98 guesses Pi at 3.14102 Loop 99 guesses Pi at 3.14142 Overall calculation is 3.14154
My questions of particular interest are:
- Can performance be improved
- Are Java 8 mechanisms used appropriately? Are there any simplifications I missed?
- Are there any other observations?