Introduction
In the early days of computing, generating random numbers was a significant computational challenge. In a landmark 1951 paper, mathematician John von Neumann detailed various “cooking recipes” for producing and using random numbers on machines like the ENIAC. While much of the paper focuses on generating uniform random digits, he also described ingenious methods for generating numbers from more complex, non-uniform probability distributions.
One of the most fundamental needs in scientific simulation—from modeling radioactive decay to calculating particle free-paths in physics—is sampling from an exponential distribution with probability density function:
$$f(x) = e^{-x} \quad \text{for } x \ge 0$$
Today’s standard approach is elegant and direct, but it requires computing a natural logarithm—a computationally expensive operation on early hardware. To sidestep this limitation, von Neumann described a fascinating alternative that uses only basic comparisons, resembling what he called “a well known game of chance Twenty-One, or Black Jack.”
In this post, we’ll explore both methods: the modern inverse transform approach and von Neumann’s ingenious comparison-based algorithm. We’ll implement them in Python, verify their correctness, and compare their performance—empirically testing the trade-offs von Neumann identified nearly 75 years ago.
Method 1: The Standard Approach (Inverse Transform Sampling)
The most common method for sampling from a given distribution is inverse transform sampling. This method relies on a fundamental principle: if you have a uniform random variable $U$ on the interval (0, 1), you can transform it into a random variable $X$ with any desired cumulative distribution function (CDF) $F(x)$ by applying:
$$X = F^{-1}(U)$$
For the exponential distribution, the CDF is $F(x) = 1 - e^{-x}$. To find the inverse, we set $U = 1 - e^{-X}$ and solve for $X$:
$$ \begin{align} e^{-X} &= 1 - U \ -X &= \ln(1 - U) \ X &= -\ln(1 - U) \end{align} $$
Here’s a useful simplification: since $U$ is uniformly distributed on (0, 1), the quantity $(1 - U)$ is also uniformly distributed on (0, 1). Therefore, we can use the simpler formula:
$$X = -\ln(U)$$
This gives us an efficient method for generating exponentially distributed numbers, provided the logarithm function is computationally accessible.
Python Implementation
Here’s a straightforward implementation using NumPy:
import numpy as np
def exponential_inverse_transform(n_samples=1):
"""
Generate samples from an exponential distribution using inverse transform sampling.
Args:
n_samples (int): Number of samples to generate.
Returns:
np.ndarray: Array of exponentially distributed samples.
"""
# Generate uniform random numbers
U = np.random.rand(n_samples)
# Apply the inverse transform
X = -np.log(U)
return X
# Generate 100,000 samples for testing
n_samples = 100000
inverse_samples = exponential_inverse_transform(n_samples)
Method 2: Von Neumann’s Ingenious Trick (Rejection Sampling)
Von Neumann proposed a clever alternative that avoids transcendental functions entirely. His procedure, which he noted “resembles a well known game of chance Twenty-One, or Black Jack,” generates sequences of uniform random numbers and accepts or rejects them based on simple comparison rules.
The algorithm works as follows to generate a single exponential sample $X$:
Initialize: Start with an integer offset
k = 0
, which will form the integer part of the final result.Generate a trial sequence:
- Generate uniform random numbers $Y_1, Y_2, Y_3, \ldots$ from (0, 1)
- Find the smallest integer
n
such that the sequence is no longer strictly decreasing - That is, find
n
where $Y_1 > Y_2 > \cdots > Y_n$ but $Y_n \leq Y_{n+1}$
Accept or reject:
- If
n
is odd: Accept the trial. Return $X = Y_1 + k$ and terminate. - If
n
is even: Reject the trial. Incrementk
by 1 and start a new trial.
- If
This process is guaranteed to terminate and produces samples that follow the exponential distribution exactly. As von Neumann elegantly put it, the machine has “in effect computed a logarithm by performing only discriminations on the relative magnitude of numbers.”
Python Implementation
This implementation requires more careful state management due to the nested trial structure:
import numpy as np
def exponential_von_neumann(n_samples=1):
"""
Generate samples from an exponential distribution using von Neumann's
comparison-based rejection sampling method.
Args:
n_samples (int): Number of samples to generate.
Returns:
tuple[np.ndarray, float]: Array of samples and average uniform draws per sample.
"""
samples = []
total_uniform_draws = 0
for _ in range(n_samples):
k = 0 # Integer offset
while True: # Trial loop
# Generate decreasing sequence
y_prev = np.random.rand()
total_uniform_draws += 1
y1 = y_prev # Store first value
n = 1
# Find length of decreasing sequence
while True:
y_curr = np.random.rand()
total_uniform_draws += 1
if y_prev <= y_curr:
break # Sequence no longer decreasing
y_prev = y_curr
n += 1
# Accept if n is odd, reject if even
if n % 2 == 1: # Accept
samples.append(y1 + k)
break
else: # Reject
k += 1
avg_draws = total_uniform_draws / n_samples
return np.array(samples), avg_draws
# Generate samples using von Neumann's method
von_neumann_samples, avg_draws = exponential_von_neumann(n_samples)
print(f"Von Neumann method used {avg_draws:.2f} uniform draws per sample on average.")
Von Neumann method used 5.91 uniform draws per sample on average.
The algorithm requires approximately 5.9 uniform draws per exponential sample, which aligns perfectly with von Neumann’s theoretical prediction of about 6 draws per sample.
Verification and Comparison
The critical test: do both methods actually produce the same distribution? And how do their performance characteristics compare?
Visual Verification
Let’s plot histograms of samples from both methods alongside the theoretical probability density function $f(x) = e^{-x}$:
import matplotlib.pyplot as plt
import seaborn as sns
# Configure plot aesthetics
sns.set_style("whitegrid")
plt.figure(figsize=(12, 7))
# Plot histograms for both methods
plt.hist(inverse_samples, bins=50, density=True, alpha=0.7,
label='Inverse Transform', color='skyblue')
plt.hist(von_neumann_samples, bins=50, density=True, alpha=0.7,
label="Von Neumann's Method", color='lightcoral')
# Overlay theoretical PDF
x = np.linspace(0, 8, 400)
pdf = np.exp(-x)
plt.plot(x, pdf, 'r-', linewidth=2, label='Theoretical PDF ($e^{-x}$)')
plt.title('Exponential Sampling Methods vs. Theoretical Distribution', fontsize=16)
plt.xlabel('x', fontsize=12)
plt.ylabel('Density', fontsize=12)
plt.legend()
plt.xlim(0, 8)
plt.tight_layout()
plt.show()

The visualization confirms that both methods accurately reproduce the target exponential distribution. The empirical histograms align beautifully with the theoretical curve, validating the correctness of both algorithms.
Performance Analysis
Mathematical elegance doesn’t always translate to computational efficiency. Von Neumann himself observed that on the ENIAC, it was actually “slightly quicker to use a truncated power series for log(1-T)” than to perform all the comparisons his method required.
Let’s benchmark both approaches in a modern Python environment:
import time
# Benchmark inverse transform method
start_time = time.time()
_ = exponential_inverse_transform(n_samples)
inverse_time = time.time() - start_time
# Benchmark von Neumann method
start_time = time.time()
_ = exponential_von_neumann(n_samples)
vn_time = time.time() - start_time
print(f"Inverse Transform: {inverse_time:.4f} seconds")
print(f"Von Neumann Method: {vn_time:.4f} seconds")
print(f"Speedup factor: {vn_time / inverse_time:.1f}x")
Inverse Transform: 0.0018 seconds
Von Neumann Method: 0.1860 seconds
Speedup factor: 103.3x
The results are striking. The vectorized NumPy implementation of inverse transform sampling, leveraging a highly optimized logarithm function, outperforms the Python-looped von Neumann implementation by more than two orders of magnitude. This confirms von Neumann’s prescient observation: the “theoretically elegant” method that avoids transcendental functions isn’t necessarily the practical winner.
Conclusion
This exploration offers a window into the ingenuity of early computational mathematics. Von Neumann’s comparison-based algorithm demonstrates remarkable mathematical creativity—showing how to “compute a logarithm” using only basic machine operations. Our implementation confirms that his 75-year-old algorithm works flawlessly, producing exponentially distributed samples with perfect accuracy.
However, the performance comparison validates von Neumann’s own pragmatic assessment. While his rejection sampling method is intellectually elegant and historically significant, the direct logarithmic approach proves far more efficient on both early and modern hardware. It’s a timeless reminder in scientific computing: theoretical beauty and computational practicality don’t always align.
The enduring value of von Neumann’s work lies not just in the specific algorithm, but in the fundamental insight that creative mathematical thinking can circumvent apparent computational limitations. Even when the direct approach proves superior, understanding alternative methods deepens our appreciation for the rich landscape of algorithmic possibilities.