Calculating Pi with Numerical Integration in Python and C++


This article explains how to approximate Pi using the definite integral tex_76be5892a554905e12ae35ef06fa8411 Calculating Pi with Numerical Integration in Python and C++. It introduces the mathematics behind the identity, explains the trapezoidal and midpoint integration methods, and provides single-threaded and multithreaded implementations in both Python and C++. It also demonstrates how the same calculation can be divided across five distributed computing nodes, with a discussion of numerical accuracy, performance, parallelism, and practical limitations.

integral-math-pi Calculating Pi with Numerical Integration in Python and C++

The Definite Integral Computes the Pi

Pi, written as tex_1a571cd93f36c4348b397731bd2338bc Calculating Pi with Numerical Integration in Python and C++, appears throughout mathematics, physics, engineering, statistics, and computer science. Although programming languages normally provide a built-in value of Pi, calculating it ourselves is a useful way to learn about calculus, numerical integration, multithreading, and distributed computing.

In this article, we will calculate Pi using the definite integral:

tex_3f534db327af211fd2cc2f0cd10b106a Calculating Pi with Numerical Integration in Python and C++

We will cover:

  • Why the integral equals Pi
  • How numerical integration approximates an integral
  • The trapezoidal rule
  • The midpoint rule
  • Single-threaded Python
  • Multithreaded Python
  • Single-threaded C++
  • Multithreaded C++
  • How the same algorithm can be distributed across five nodes

Why Does This Integral Equal Pi?

Consider the integral:

tex_2196ac8abfc18f1174202800c855a542 Calculating Pi with Numerical Integration in Python and C++

The key observation is that the derivative of the inverse tangent function is:

tex_3fd3549da5e94cadd2dfdfbe001cf0f8 Calculating Pi with Numerical Integration in Python and C++

Therefore:

tex_2917c1e2b9abf711dba87807b813067e Calculating Pi with Numerical Integration in Python and C++

Applying the limits from zero to one gives:

tex_7ff79e7cca2f916a40b9a1053bcba8b5 Calculating Pi with Numerical Integration in Python and C++

This means:

tex_b1041bd6b4631866c3f3e21265241446 Calculating Pi with Numerical Integration in Python and C++

We know that:

tex_eb1195772c3c710f3da673e3bfc0a833 Calculating Pi with Numerical Integration in Python and C++

and:

tex_aaa6f1caf12477ee4c85133b3216a84f Calculating Pi with Numerical Integration in Python and C++

Therefore:

tex_b5e4ffd91441cf60a9317f7ba56e693f Calculating Pi with Numerical Integration in Python and C++

So:

tex_7ffc911ef4a982fdb53daf13d6f5a7bb Calculating Pi with Numerical Integration in Python and C++

This gives us a convenient way to calculate Pi numerically.

How Numerical Integration Works

A computer does not need to solve the integral symbolically. Instead, it can divide the interval into many small pieces and approximate the area under the curve.

Suppose we want to calculate:

tex_b63d41a1c16f3a1bd136c6773d328857 Calculating Pi with Numerical Integration in Python and C++

Divide the interval tex_468127d96274141856229c1626fefa41 Calculating Pi with Numerical Integration in Python and C++ into tex_1feb73aa300ebd9ba23674fb7a10f8ac Calculating Pi with Numerical Integration in Python and C++ equal subintervals.

The width of each interval is:

tex_1308967c44e54f32d1c440bb29dfcfe8 Calculating Pi with Numerical Integration in Python and C++

The points dividing the interval are:

tex_bb8b82b008e5d06ca0008b3af41a1e3c Calculating Pi with Numerical Integration in Python and C++

where:

tex_a99700732065b783efd91a683602fff1 Calculating Pi with Numerical Integration in Python and C++

There are several ways to approximate the area of each small section. Two common methods are:

  • The trapezoidal rule
  • The midpoint rule

The Trapezoidal Rule

The formula involving tex_1918380ed1434f6e19e6e69ae7fdf8f9 Calculating Pi with Numerical Integration in Python and C++, tex_e28bdff1200e38e14a3e444d2ece9c7f Calculating Pi with Numerical Integration in Python and C++, and division by two is the trapezoidal rule.

For one interval from tex_8e0efa9733b95d0da5b2169aa06dcf81 Calculating Pi with Numerical Integration in Python and C++ to tex_b35c00be16b2c40da318b1d92c2b1837 Calculating Pi with Numerical Integration in Python and C++, the function is approximated by a straight line. The resulting shape is a trapezoid.

Its area is:

tex_f41166df091c0d169b0696a7f6af9fa6 Calculating Pi with Numerical Integration in Python and C++

Adding the areas of all trapezoids gives:

tex_86f756c45b8689c7da8b068f002b3c53 Calculating Pi with Numerical Integration in Python and C++

An equivalent and often easier-to-implement form is:

tex_64bd5080d50a76d3b2efc119bfe3649c Calculating Pi with Numerical Integration in Python and C++

The two endpoints receive half weight:

tex_5eed05468fe92714cd36ed631785ab65 Calculating Pi with Numerical Integration in Python and C++

All interior points receive full weight:

tex_452f5b2ffe29d56db969ac3be4092d4b Calculating Pi with Numerical Integration in Python and C++

For our Pi integral:

tex_99bff1a5625c8921b807380a26a87419 Calculating Pi with Numerical Integration in Python and C++

and:

tex_5e3ba3c34a3079609603f6781294a710 Calculating Pi with Numerical Integration in Python and C++

Therefore:

tex_640a61ca3f8872affde92d620fa909de Calculating Pi with Numerical Integration in Python and C++

The trapezoidal approximation becomes:

tex_d040a712ba5eb2044728db0dae1d79a9 Calculating Pi with Numerical Integration in Python and C++

Since:

tex_edaadbc8e348403cc0dbfcc6e4e0630d Calculating Pi with Numerical Integration in Python and C++

and:

tex_42d10bbc07bf2053178f359b21960643 Calculating Pi with Numerical Integration in Python and C++

we can also write:

tex_99c0727f4229d884ea42edb039185ce0 Calculating Pi with Numerical Integration in Python and C++

A Small Trapezoidal Example

Suppose we use four intervals:

tex_84c2dd7b296eccfd44c62124befd9053 Calculating Pi with Numerical Integration in Python and C++

The interval width is:

tex_9ffd65ec9c5a5dccb5f602a7db195961 Calculating Pi with Numerical Integration in Python and C++

The five boundary points are:

tex_d9da1383e0ed669e5548bf8e4340843e Calculating Pi with Numerical Integration in Python and C++

The function values are approximately:

tex_970fbd72b9bc6da1272ef939121f9fc0 Calculating Pi with Numerical Integration in Python and C++

tex_d3455711558c665487cb4b022bf0f877 Calculating Pi with Numerical Integration in Python and C++

tex_70c4d6eb464cb2105f6ae2543be8fbcb Calculating Pi with Numerical Integration in Python and C++

tex_cde6c6f63b3c1ee8e6ceb19dfc4bfd5d Calculating Pi with Numerical Integration in Python and C++

tex_d62fc959c95215d9c7089b25aa9a4162 Calculating Pi with Numerical Integration in Python and C++

Applying the trapezoidal formula:

tex_9254a5be295fdf1715e73195ca033de2 Calculating Pi with Numerical Integration in Python and C++

Therefore:

tex_fc34c1d6e74f9026e55e3f318d13f514 Calculating Pi with Numerical Integration in Python and C++

The actual value is:

tex_2f49e1b3747ae5d02e51f3db97fe591d Calculating Pi with Numerical Integration in Python and C++

Using only four intervals already gives a reasonable approximation. Increasing tex_1feb73aa300ebd9ba23674fb7a10f8ac Calculating Pi with Numerical Integration in Python and C++ improves the result.

The Midpoint Rule

The implementations later in this article primarily use the midpoint rule.

Instead of evaluating the function at the boundaries of each interval, the midpoint rule evaluates it at the centre.

The midpoint of interval tex_7ab5725b1f27806587b2bc40f9653e78 Calculating Pi with Numerical Integration in Python and C++ is:

tex_61450d58c0c6aed9a9572f492f901208 Calculating Pi with Numerical Integration in Python and C++

The area of that interval is approximated by a rectangle:

tex_71302dfa6855d776b48cca433c0051c1 Calculating Pi with Numerical Integration in Python and C++

Adding all rectangles gives:

tex_8db5070628dd4b3ffc2755c504cf1874 Calculating Pi with Numerical Integration in Python and C++

For the Pi integral, tex_95c78b93118d9e1aea57ca5413c7d18d Calculating Pi with Numerical Integration in Python and C++, tex_3d495795a937f3c66da4466786cd621a Calculating Pi with Numerical Integration in Python and C++, and tex_91288f8c31bc88349c6df84cc3f44145 Calculating Pi with Numerical Integration in Python and C++.

Therefore:

tex_1368dafc3beabf5da41e95286d70061a Calculating Pi with Numerical Integration in Python and C++

and:

tex_fa23f93e1632164bd4ed06d40fd5be55 Calculating Pi with Numerical Integration in Python and C++

Substituting tex_91288f8c31bc88349c6df84cc3f44145 Calculating Pi with Numerical Integration in Python and C++:

tex_36d61e97e5e3376660bde35d71a99724 Calculating Pi with Numerical Integration in Python and C++

The larger tex_1feb73aa300ebd9ba23674fb7a10f8ac Calculating Pi with Numerical Integration in Python and C++ becomes, the closer the estimate gets to Pi.

Trapezoidal Rule Versus Midpoint Rule

Method Sample positions Formula
Trapezoidal rule Interval boundaries tex_0bedc3fd0c8a73488fe99275d1c86661 Calculating Pi with Numerical Integration in Python and C++
Midpoint rule Centre of each interval tex_9fab18e20c965d97aa7589873c4c5bad Calculating Pi with Numerical Integration in Python and C++

For a smooth function, both methods normally have an error proportional to:

tex_27692c6b15278ff5b1684732de493c5f Calculating Pi with Numerical Integration in Python and C++

However, the midpoint rule is often more accurate than the trapezoidal rule when both use the same number of intervals.

Single-Threaded Python Using the Midpoint Rule

The following Python program uses 25 million intervals.

import math
import time


def calculate_pi(total_steps: int) -> float:
    if total_steps <= 0:
        raise ValueError("total_steps must be positive")

    step_width = 1.0 / total_steps
    total = 0.0

    for step in range(total_steps):
        midpoint = (step + 0.5) * step_width
        total += 4.0 / (1.0 + midpoint * midpoint)

    return total * step_width


def main() -> None:
    total_steps = 25_000_000

    started_at = time.perf_counter()
    pi_estimate = calculate_pi(total_steps)
    elapsed_seconds = time.perf_counter() - started_at

    absolute_error = abs(pi_estimate - math.pi)

    print(f"Estimated Pi:   {pi_estimate:.15f}")
    print(f"Reference Pi:   {math.pi:.15f}")
    print(f"Absolute error: {absolute_error:.3e}")
    print(f"Elapsed time:   {elapsed_seconds:.3f} seconds")


if __name__ == "__main__":
    main()

The core calculation is:

midpoint = (step + 0.5) * step_width
total += 4.0 / (1.0 + midpoint * midpoint)

The first line calculates the midpoint of the current interval:

tex_c8e31a04f7fea4fed738204f83e7817c Calculating Pi with Numerical Integration in Python and C++

The second line evaluates:

tex_c8e61ac299b4d3bb5c5964d2713dbdbe Calculating Pi with Numerical Integration in Python and C++

After all values have been added, the result is multiplied by the interval width:

return total * step_width

Mathematically, this calculates:

tex_6b7ed9153c0c7d9c64a580cd4b696fef Calculating Pi with Numerical Integration in Python and C++

Single-Threaded Python Using the Trapezoidal Rule

The trapezoidal version evaluates the function at the interval boundaries rather than at the midpoints.

import math
import time


def calculate_pi_trapezoidal(total_steps: int) -> float:
    if total_steps <= 0:
        raise ValueError("total_steps must be positive")

    h = 1.0 / total_steps

    def f(x: float) -> float:
        return 4.0 / (1.0 + x * x)

    total = 0.5 * (f(0.0) + f(1.0))

    for step in range(1, total_steps):
        x = step * h
        total += f(x)

    return total * h


def main() -> None:
    total_steps = 25_000_000

    started_at = time.perf_counter()
    pi_estimate = calculate_pi_trapezoidal(total_steps)
    elapsed_seconds = time.perf_counter() - started_at

    absolute_error = abs(pi_estimate - math.pi)

    print(f"Estimated Pi:   {pi_estimate:.15f}")
    print(f"Reference Pi:   {math.pi:.15f}")
    print(f"Absolute error: {absolute_error:.3e}")
    print(f"Elapsed time:   {elapsed_seconds:.3f} seconds")


if __name__ == "__main__":
    main()

The endpoint contribution is calculated by:

total = 0.5 * (f(0.0) + f(1.0))

This corresponds to:

tex_9c1dc1092f1a4801e7367643d6bf87e6 Calculating Pi with Numerical Integration in Python and C++

The loop calculates the interior sum:

for step in range(1, total_steps):
    x = step * h
    total += f(x)

This corresponds to:

tex_86b25eb4f78ad323a4aa3757c0346605 Calculating Pi with Numerical Integration in Python and C++

Finally, the total is multiplied by tex_0150379203a76348438a0deea651610f Calculating Pi with Numerical Integration in Python and C++:

return total * h

Multithreaded Python Using the Midpoint Rule

We can divide the 25 million intervals among five workers.

For worker tex_0c4924e077c358daddc0041e6e70ab7f Calculating Pi with Numerical Integration in Python and C++ out of tex_502d40d526ff202c1fe6b81ca693c2bc Calculating Pi with Numerical Integration in Python and C++ workers, the start index is:

tex_9a1645e5a1a92fa763931b78d7b9e13e Calculating Pi with Numerical Integration in Python and C++

The end index is:

tex_c2bd0fbe9f9930d55ee7d8bba08a63d7 Calculating Pi with Numerical Integration in Python and C++

For five workers and 25 million intervals:

Worker Start step End step Number of steps
0 0 5,000,000 5,000,000
1 5,000,000 10,000,000 5,000,000
2 10,000,000 15,000,000 5,000,000
3 15,000,000 20,000,000 5,000,000
4 20,000,000 25,000,000 5,000,000

Here is an implementation using ThreadPoolExecutor:

import math
import time
from concurrent.futures import ThreadPoolExecutor


def calculate_partial_sum(
    start_step: int,
    end_step: int,
    step_width: float,
) -> float:
    partial_sum = 0.0

    for step in range(start_step, end_step):
        midpoint = (step + 0.5) * step_width
        partial_sum += 4.0 / (1.0 + midpoint * midpoint)

    return partial_sum


def calculate_pi_multithreaded(
    total_steps: int,
    worker_count: int,
) -> float:
    if total_steps <= 0:
        raise ValueError("total_steps must be positive")

    if worker_count <= 0:
        raise ValueError("worker_count must be positive")

    step_width = 1.0 / total_steps
    ranges = []

    for worker_id in range(worker_count):
        start_step = total_steps * worker_id // worker_count
        end_step = total_steps * (worker_id + 1) // worker_count
        ranges.append((start_step, end_step))

    with ThreadPoolExecutor(max_workers=worker_count) as executor:
        futures = [
            executor.submit(
                calculate_partial_sum,
                start_step,
                end_step,
                step_width,
            )
            for start_step, end_step in ranges
        ]

        partial_sums = [
            future.result()
            for future in futures
        ]

    return sum(partial_sums) * step_width


def main() -> None:
    total_steps = 25_000_000
    worker_count = 5

    started_at = time.perf_counter()

    pi_estimate = calculate_pi_multithreaded(
        total_steps,
        worker_count,
    )

    elapsed_seconds = time.perf_counter() - started_at
    absolute_error = abs(pi_estimate - math.pi)

    print(f"Workers:        {worker_count}")
    print(f"Estimated Pi:   {pi_estimate:.15f}")
    print(f"Reference Pi:   {math.pi:.15f}")
    print(f"Absolute error: {absolute_error:.3e}")
    print(f"Elapsed time:   {elapsed_seconds:.3f} seconds")


if __name__ == "__main__":
    main()

Each worker calculates one partial sum:

tex_ff67df356a92c3436575d640ef6b7d0c Calculating Pi with Numerical Integration in Python and C++

The main thread combines the results:

tex_e6f6e6d801ed6facfa5e084918c36ff9 Calculating Pi with Numerical Integration in Python and C++

An Important Python Limitation

The multithreaded Python version demonstrates how to divide the calculation into separate ranges, but it may not execute faster than the single-threaded version.

Standard CPython has a Global Interpreter Lock, commonly called the GIL. For CPU-bound Python code, only one thread normally executes Python bytecode at a time.

Therefore, five Python threads do not necessarily use five CPU cores effectively.

The threaded version may be:

  • Approximately as fast as the single-threaded version
  • Slightly slower because of thread-management overhead
  • Useful for demonstrating work partitioning
  • Unsuitable for achieving true CPU parallelism in normal CPython

For true CPU parallelism in Python, better choices include:

  • ProcessPoolExecutor
  • The multiprocessing module
  • NumPy
  • Native extensions
  • MPI
  • Separate distributed processes

Single-Threaded C++ Using the Midpoint Rule

C++ is well suited to tight numerical loops because it compiles directly to native machine code.

#include <chrono>
#include <cmath>
#include <cstdint>
#include <iomanip>
#include <iostream>
#include <stdexcept>


double calculate_pi(std::uint64_t total_steps) {
    if (total_steps == 0) {
        throw std::invalid_argument(
            "total_steps must be positive"
        );
    }

    const double step_width =
        1.0 / static_cast<double>(total_steps);

    double total = 0.0;

    for (
        std::uint64_t step = 0;
        step < total_steps;
        ++step
    ) {
        const double midpoint =
            (static_cast<double>(step) + 0.5)
            * step_width;

        total += 4.0 / (1.0 + midpoint * midpoint);
    }

    return total * step_width;
}


int main() {
    constexpr std::uint64_t total_steps = 25'000'000;

    const auto started_at =
        std::chrono::steady_clock::now();

    const double pi_estimate =
        calculate_pi(total_steps);

    const auto finished_at =
        std::chrono::steady_clock::now();

    const double elapsed_seconds =
        std::chrono::duration<double>(
            finished_at - started_at
        ).count();

    const double reference_pi = std::acos(-1.0);

    const double absolute_error =
        std::abs(pi_estimate - reference_pi);

    std::cout
        << std::fixed
        << std::setprecision(15);

    std::cout
        << "Estimated Pi:   "
        << pi_estimate
        << '\n';

    std::cout
        << "Reference Pi:   "
        << reference_pi
        << '\n';

    std::cout
        << std::scientific
        << std::setprecision(3);

    std::cout
        << "Absolute error: "
        << absolute_error
        << '\n';

    std::cout
        << std::fixed
        << std::setprecision(3);

    std::cout
        << "Elapsed time:   "
        << elapsed_seconds
        << " seconds\n";

    return 0;
}

Compile it with optimisation enabled:

g++ -O3 -std=c++17 pi_single.cpp -o pi_single

Run it using:

./pi_single

The -O3 option enables aggressive compiler optimisation. Without optimisation, the program may run considerably more slowly.

Single-Threaded C++ Using the Trapezoidal Rule

The following C++ version implements the trapezoidal formula:

tex_cf5deca53a7e3794d3e47ed22e569637 Calculating Pi with Numerical Integration in Python and C++

#include <chrono>
#include <cmath>
#include <cstdint>
#include <iomanip>
#include <iostream>
#include <stdexcept>


double f(double x) {
    return 4.0 / (1.0 + x * x);
}


double calculate_pi_trapezoidal(
    std::uint64_t total_steps
) {
    if (total_steps == 0) {
        throw std::invalid_argument(
            "total_steps must be positive"
        );
    }

    const double h =
        1.0 / static_cast<double>(total_steps);

    double total =
        0.5 * (f(0.0) + f(1.0));

    for (
        std::uint64_t step = 1;
        step < total_steps;
        ++step
    ) {
        const double x =
            static_cast<double>(step) * h;

        total += f(x);
    }

    return total * h;
}


int main() {
    constexpr std::uint64_t total_steps = 25'000'000;

    const auto started_at =
        std::chrono::steady_clock::now();

    const double pi_estimate =
        calculate_pi_trapezoidal(total_steps);

    const auto finished_at =
        std::chrono::steady_clock::now();

    const double elapsed_seconds =
        std::chrono::duration<double>(
            finished_at - started_at
        ).count();

    const double reference_pi = std::acos(-1.0);

    const double absolute_error =
        std::abs(pi_estimate - reference_pi);

    std::cout
        << std::fixed
        << std::setprecision(15);

    std::cout
        << "Estimated Pi:   "
        << pi_estimate
        << '\n';

    std::cout
        << "Reference Pi:   "
        << reference_pi
        << '\n';

    std::cout
        << std::scientific
        << std::setprecision(3);

    std::cout
        << "Absolute error: "
        << absolute_error
        << '\n';

    std::cout
        << std::fixed
        << std::setprecision(3);

    std::cout
        << "Elapsed time:   "
        << elapsed_seconds
        << " seconds\n";

    return 0;
}

Compile it using:

g++ -O3 -std=c++17 pi_trapezoidal.cpp -o pi_trapezoidal

Multithreaded C++ Using the Midpoint Rule

Unlike ordinary CPython threads, C++ threads can execute CPU-bound work simultaneously on multiple processor cores.

The following implementation divides the calculation among five threads.

#include <chrono>
#include <cmath>
#include <cstdint>
#include <iomanip>
#include <iostream>
#include <numeric>
#include <stdexcept>
#include <thread>
#include <vector>


double calculate_partial_sum(
    std::uint64_t start_step,
    std::uint64_t end_step,
    double step_width
) {
    double partial_sum = 0.0;

    for (
        std::uint64_t step = start_step;
        step < end_step;
        ++step
    ) {
        const double midpoint =
            (static_cast<double>(step) + 0.5)
            * step_width;

        partial_sum +=
            4.0 / (1.0 + midpoint * midpoint);
    }

    return partial_sum;
}


double calculate_pi_multithreaded(
    std::uint64_t total_steps,
    std::size_t worker_count
) {
    if (total_steps == 0) {
        throw std::invalid_argument(
            "total_steps must be positive"
        );
    }

    if (worker_count == 0) {
        throw std::invalid_argument(
            "worker_count must be positive"
        );
    }

    const double step_width =
        1.0 / static_cast<double>(total_steps);

    std::vector<std::thread> workers;

    std::vector<double> partial_sums(
        worker_count,
        0.0
    );

    workers.reserve(worker_count);

    for (
        std::size_t worker_id = 0;
        worker_id < worker_count;
        ++worker_id
    ) {
        const std::uint64_t start_step =
            total_steps
            * worker_id
            / worker_count;

        const std::uint64_t end_step =
            total_steps
            * (worker_id + 1)
            / worker_count;

        workers.emplace_back(
            [
                &,
                worker_id,
                start_step,
                end_step
            ]() {
                partial_sums[worker_id] =
                    calculate_partial_sum(
                        start_step,
                        end_step,
                        step_width
                    );
            }
        );
    }

    for (std::thread& worker : workers) {
        worker.join();
    }

    const double combined_sum =
        std::accumulate(
            partial_sums.begin(),
            partial_sums.end(),
            0.0
        );

    return combined_sum * step_width;
}


int main() {
    constexpr std::uint64_t total_steps = 25'000'000;
    constexpr std::size_t worker_count = 5;

    const auto started_at =
        std::chrono::steady_clock::now();

    const double pi_estimate =
        calculate_pi_multithreaded(
            total_steps,
            worker_count
        );

    const auto finished_at =
        std::chrono::steady_clock::now();

    const double elapsed_seconds =
        std::chrono::duration<double>(
            finished_at - started_at
        ).count();

    const double reference_pi = std::acos(-1.0);

    const double absolute_error =
        std::abs(pi_estimate - reference_pi);

    std::cout
        << "Workers:        "
        << worker_count
        << '\n';

    std::cout
        << std::fixed
        << std::setprecision(15);

    std::cout
        << "Estimated Pi:   "
        << pi_estimate
        << '\n';

    std::cout
        << "Reference Pi:   "
        << reference_pi
        << '\n';

    std::cout
        << std::scientific
        << std::setprecision(3);

    std::cout
        << "Absolute error: "
        << absolute_error
        << '\n';

    std::cout
        << std::fixed
        << std::setprecision(3);

    std::cout
        << "Elapsed time:   "
        << elapsed_seconds
        << " seconds\n";

    return 0;
}

Compile it with thread support and optimisation:

g++ -O3 -std=c++17 -pthread pi_multithreaded.cpp -o pi_multithreaded

Then run it:

./pi_multithreaded

Why Each Thread Uses a Local Sum

Inside each thread, the program calculates into a local variable:

double partial_sum = 0.0;

It does not update one shared global total during every iteration.

If every thread repeatedly updated the same variable, the program would need a mutex or atomic operation. That would introduce synchronisation and contention.

Instead, the program follows a map-and-reduce pattern:

  1. Assign one range of intervals to each worker.
  2. Let each worker calculate independently.
  3. Store one partial result per worker.
  4. Wait for all threads to complete.
  5. Add the partial results together.

Mathematically:

tex_037f2536e9b540830713eccff23a32c4 Calculating Pi with Numerical Integration in Python and C++

The final approximation is:

tex_406e2ace76c2cb9ba92037a7ac60a9a6 Calculating Pi with Numerical Integration in Python and C++

Extending the Algorithm to Five Distributed Nodes

Multithreading and distributed computing use the same mathematical partitioning, but they are different execution models.

Threads normally:

  • Run on one machine
  • Share the same memory
  • Belong to the same process
  • Communicate through shared variables

Distributed workers normally:

  • Run as separate processes
  • May run on separate machines
  • Do not share ordinary process memory
  • Communicate using files, sockets, MPI, RPC, or another distributed runtime

In a five-node job, every process receives a rank:

tex_f3551a3df72e77be2092140ba6e8e0f9 Calculating Pi with Numerical Integration in Python and C++

The total number of processes is:

tex_067aafc68622c97ff0cebcc155b663da Calculating Pi with Numerical Integration in Python and C++

Each rank calculates its own range using:

tex_9a1645e5a1a92fa763931b78d7b9e13e Calculating Pi with Numerical Integration in Python and C++

and:

tex_c2bd0fbe9f9930d55ee7d8bba08a63d7 Calculating Pi with Numerical Integration in Python and C++

In Python, the range assignment can be written as:

import os


rank = int(os.environ["RANK"])
world_size = int(os.environ["WORLD_SIZE"])

start_step = total_steps * rank // world_size
end_step = total_steps * (rank + 1) // world_size

Each rank calculates only its assigned section:

partial_sum = 0.0

for step in range(start_step, end_step):
    midpoint = (step + 0.5) * step_width

    partial_sum += 4.0 / (
        1.0 + midpoint * midpoint
    )

For five ranks, the final result is:

tex_e6f6e6d801ed6facfa5e084918c36ff9 Calculating Pi with Numerical Integration in Python and C++

In the Singularity example, every rank writes one result file:

rank-0.json
rank-1.json
rank-2.json
rank-3.json
rank-4.json

Rank zero waits for all five files, reads their partial sums, adds them together, and produces the final Pi estimate.

This implements three distributed operations:

  • Barrier: wait until all workers finish
  • Gather: collect the partial results
  • Reduce: add the partial sums together

The file-based approach is simple and easy to understand, but it assumes that all nodes can access the same shared output directory.

For a production distributed application, MPI or another collective-communication framework would normally be more robust.

Accuracy of the Numerical Methods

For sufficiently smooth functions, both the trapezoidal rule and midpoint rule have an error proportional to approximately:

tex_27692c6b15278ff5b1684732de493c5f Calculating Pi with Numerical Integration in Python and C++

This means that doubling the number of intervals may reduce the numerical integration error by approximately a factor of four.

However, increasing tex_1feb73aa300ebd9ba23674fb7a10f8ac Calculating Pi with Numerical Integration in Python and C++ indefinitely does not guarantee unlimited accuracy.

Computers store floating-point numbers with finite precision. When millions or billions of values are added, rounding errors can accumulate.

Parallel versions may also produce slightly different final digits from single-threaded versions because floating-point addition is not perfectly associative:

tex_877f052891680ae910f78df6419a8ae3 Calculating Pi with Numerical Integration in Python and C++

The values are mathematically equivalent, but changing the order of floating-point additions can change the final rounded bits.

Performance Considerations

The multithreaded C++ implementation should normally outperform the single-threaded version when:

  • The machine has multiple CPU cores
  • The calculation is sufficiently large
  • The number of worker threads is reasonable
  • Compiler optimisation is enabled

Five threads do not guarantee a five-times speed improvement.

Performance depends on:

  • CPU core count
  • Processor frequency
  • Thermal throttling
  • Operating-system thread scheduling
  • Cache behaviour
  • Thread startup overhead
  • Other workloads running on the machine

For Python, the GIL is the main restriction. Adding more Python threads does not normally accelerate a pure Python CPU loop.

For distributed computing, node allocation, process startup, storage access, and result aggregation all add overhead.

Calculating Pi with 25 million intervals is a useful demonstration, but it is too small and simple to justify reserving several expensive GPU-backed nodes in a real production workload.

Comparing the Implementations

Implementation Execution model True CPU parallelism Expected performance
Python single-threaded One Python thread No Simple, but relatively slow
Python multithreaded Several Python threads Usually no because of the GIL Often no faster than one thread
C++ single-threaded One native thread No Usually much faster than Python
C++ multithreaded Several native threads Yes Usually the fastest local version
Five-node distributed Separate worker processes Yes Scalable, but has communication and startup overhead

Conclusion

The identity:

tex_76be5892a554905e12ae35ef06fa8411 Calculating Pi with Numerical Integration in Python and C++

provides a simple demonstration of how calculus can be converted into a computational problem.

The trapezoidal rule approximates the integral using straight lines between adjacent boundary points:

tex_0c15670df687966b00a9b61b8c6b63a3 Calculating Pi with Numerical Integration in Python and C++

The midpoint rule approximates the integral using the centre of each interval:

tex_11bc33eeb4ddc8a770b60530fc2aebd8 Calculating Pi with Numerical Integration in Python and C++

The same mathematical calculation can be executed using:

  • A single loop on one CPU core
  • Several threads on one machine
  • Several processes using multiple CPU cores
  • Several distributed workers running on separate nodes

The mathematical algorithm remains largely unchanged. What changes is how the integration range is divided, where the partial sums are calculated, and how the partial results are combined.

This is the central pattern behind many parallel algorithms:

tex_09010bc3ad2537657341e3e1a106fd3b Calculating Pi with Numerical Integration in Python and C++

Although calculating Pi is a small example, the same map-and-reduce structure appears in scientific simulation, machine learning, data processing, rendering, financial modelling, and large-scale distributed systems.

Math

–EOF (The Ultimate Computing & Technology Blog) —

7414 words
Last Post: Apple Office in Cambridge

The Permanent URL is: Calculating Pi with Numerical Integration in Python and C++ (AMP Version)

Leave a Reply