Skip to content

Commit bcce276

Browse files
committed
Refactor Monte Carlo estimation functions and add doctest
- Refactored all functions to return values instead of printing output. - Added doctests for each fucntion. - Clarified docstrings. - Improved code style (typing hints, naming) to align with repo conventions.
1 parent 0a3a965 commit bcce276

File tree

1 file changed

+117
-69
lines changed

1 file changed

+117
-69
lines changed

maths/monte_carlo.py

+117-69
Original file line numberDiff line numberDiff line change
@@ -8,35 +8,53 @@
88
from statistics import mean
99

1010

11-
def pi_estimator(iterations: int):
11+
def is_in_unit_circle(x: float, y: float) -> bool:
1212
"""
13-
An implementation of the Monte Carlo method used to find pi.
13+
Checks whether the point (x, y) lies inside or on the boundary
14+
of a unit circle centered at the origin.
15+
16+
>>> is_in_unit_circle(0.0, 0.0)
17+
True
18+
>>> is_in_unit_circle(1.0, 0.0)
19+
True
20+
>>> is_in_unit_circle(1.0, 1.0)
21+
False
22+
"""
23+
return sqrt(x**2 + y**2) <= 1
24+
25+
26+
def pi_estimator(iterations: int) -> float:
27+
"""
28+
Estimate the value of pi using the Monte Carlo method.
29+
30+
The method simulates random points in the square and checks how
31+
many fall within the inscribed unit circle.
32+
33+
Intuition:
1434
1. Draw a 2x2 square centred at (0,0).
15-
2. Inscribe a circle within the square.
16-
3. For each iteration, place a dot anywhere in the square.
17-
a. Record the number of dots within the circle.
18-
4. After all the dots are placed, divide the dots in the circle by the total.
19-
5. Multiply this value by 4 to get your estimate of pi.
20-
6. Print the estimated and numpy value of pi
35+
2. Inscribe a unit circle within the square.
36+
3. For each iteration, place a dot randomly within the square.
37+
4. Divide the number of dots within the circle by the total number of dots.
38+
5. Multiply this ratio by 4 to estimate pi.
39+
40+
Args:
41+
iterations: Number of random points to generate.
42+
43+
Returns:
44+
Estimated value of pi.
45+
46+
>>> round(pi_estimator(100000), 2) in [3.13, 3.14, 3.15]
47+
True
2148
"""
2249

23-
# A local function to see if a dot lands in the circle.
24-
def is_in_circle(x: float, y: float) -> bool:
25-
distance_from_centre = sqrt((x**2) + (y**2))
26-
# Our circle has a radius of 1, so a distance
27-
# greater than 1 would land outside the circle.
28-
return distance_from_centre <= 1
50+
if iterations <= 0:
51+
raise ValueError("Number of iterations must be positive")
2952

30-
# The proportion of guesses that landed in the circle
31-
proportion = mean(
32-
int(is_in_circle(uniform(-1.0, 1.0), uniform(-1.0, 1.0)))
53+
inside_circle = sum(
54+
is_in_unit_circle(uniform(-1.0, 1.0), uniform(-1.0, 1.0))
3355
for _ in range(iterations)
3456
)
35-
# The ratio of the area for circle to square is pi/4.
36-
pi_estimate = proportion * 4
37-
print(f"The estimated value of pi is {pi_estimate}")
38-
print(f"The numpy value of pi is {pi}")
39-
print(f"The total error is {abs(pi - pi_estimate)}")
57+
return (inside_circle / iterations) * 4
4058

4159

4260
def area_under_curve_estimator(
@@ -46,83 +64,113 @@ def area_under_curve_estimator(
4664
max_value: float = 1.0,
4765
) -> float:
4866
"""
49-
An implementation of the Monte Carlo method to find area under
50-
a single variable non-negative real-valued continuous function,
51-
say f(x), where x lies within a continuous bounded interval,
52-
say [min_value, max_value], where min_value and max_value are
53-
finite numbers
54-
1. Let x be a uniformly distributed random variable between min_value to
55-
max_value
56-
2. Expected value of f(x) =
57-
(integrate f(x) from min_value to max_value)/(max_value - min_value)
58-
3. Finding expected value of f(x):
59-
a. Repeatedly draw x from uniform distribution
60-
b. Evaluate f(x) at each of the drawn x values
61-
c. Expected value = average of the function evaluations
62-
4. Estimated value of integral = Expected value * (max_value - min_value)
63-
5. Returns estimated value
67+
Estimate the definite integral of a continuous, non-negative function f(x)
68+
over a bounded interval [min_value, max_value] using the Monte Carlo method.
69+
70+
Intuition:
71+
1. Randomly sample x values uniformly from [min_value, max_value].
72+
2. Compute the average of f(x) at the sample points.
73+
This estimates the expected value of f(x).
74+
3. Multiply this average by the interval width to estimate the integral.
75+
76+
Args:
77+
iterations: Number of random samples to draw.
78+
function_to_integrate: A function f(x) to integrate over [min_value, max_value].
79+
min_value: Lower bound of the interval.
80+
max value: Upper bound of the interval.
81+
82+
Returns:
83+
Estimated area under the curve (integral of f from min_value to max_value).
84+
85+
Raises:
86+
ValueError: If iterations <= 0 or min_value >= max_value.
87+
88+
Example:
89+
>>> def f(x): return x
90+
>>> round(area_under_curve_estimator(100000, f), 2) in [0.49, 0.5, 0.51]
91+
True
6492
"""
93+
if iterations <= 0:
94+
raise ValueError("Number of iterations must be positive")
95+
if min_value >= max_value:
96+
raise ValueError("min_value must be less than max_value")
6597

66-
return mean(
98+
samples = (
6799
function_to_integrate(uniform(min_value, max_value)) for _ in range(iterations)
68-
) * (max_value - min_value)
100+
)
101+
return mean(samples) * (max_value - min_value)
69102

70103

71104
def area_under_line_estimator_check(
72105
iterations: int, min_value: float = 0.0, max_value: float = 1.0
73-
) -> None:
106+
) -> tuple[float, float, float]:
74107
"""
75-
Checks estimation error for area_under_curve_estimator function
76-
for f(x) = x where x lies within min_value to max_value
77-
1. Calls "area_under_curve_estimator" function
78-
2. Compares with the expected value
79-
3. Prints estimated, expected and error value
108+
Demonstrate the Monte Carlo integration method by estimating
109+
the area under the line y = x over [min_value, max_value].
110+
111+
The exact integral of f(x) = x over [a, b] is:
112+
(b**2 - a**2) / 2
113+
114+
This helps illustrate that the Monte Carlo method produces
115+
a reasonable estimate for a simple, well-understood function.
116+
117+
Args:
118+
iterations: Number of samples to use in estimation.
119+
min_value: Lower bound of the integration interval.
120+
max_value: Upper bound of the integration interval.
121+
122+
Returns:
123+
A tuple of (estimated_value, expected_value, absolute_error).
124+
125+
>>> result = area_under_line_estimator_check(100000)
126+
>>> round(result[0], 2) in [0.49, 0.5, 0.51]
127+
True
80128
"""
81129

82130
def identity_function(x: float) -> float:
83-
"""
84-
Represents identity function
85-
>>> [function_to_integrate(x) for x in [-2.0, -1.0, 0.0, 1.0, 2.0]]
86-
[-2.0, -1.0, 0.0, 1.0, 2.0]
87-
"""
88131
return x
89132

90133
estimated_value = area_under_curve_estimator(
91134
iterations, identity_function, min_value, max_value
92135
)
93-
expected_value = (max_value * max_value - min_value * min_value) / 2
136+
expected_value = (max_value**2 - min_value**2) / 2
137+
error = abs(estimated_value - expected_value)
138+
return estimated_value, expected_value, error
94139

95-
print("******************")
96-
print(f"Estimating area under y=x where x varies from {min_value} to {max_value}")
97-
print(f"Estimated value is {estimated_value}")
98-
print(f"Expected value is {expected_value}")
99-
print(f"Total error is {abs(estimated_value - expected_value)}")
100-
print("******************")
101140

102-
103-
def pi_estimator_using_area_under_curve(iterations: int) -> None:
141+
def pi_estimator_using_area_under_curve(iterations: int) -> tuple[float, float, float]:
104142
"""
105-
Area under curve y = sqrt(4 - x^2) where x lies in 0 to 2 is equal to pi
143+
Estimate the value of pi using Monte Carlo integration.
144+
145+
The area under the curve y = sqrt(4 - x**2) from x = 0 to 2 is equal to pi.
146+
This function uses that fact to estimate pi.
147+
148+
Args:
149+
iterations: Number of random samples to use for estimation.
150+
151+
Returns:
152+
A tuple of (estimated_value, expected_value, absolute_error).
153+
154+
>>> result = pi_estimator_using_area_under_curve(100000)
155+
>>> round(result[0], 2) in [3.13, 3.14, 3.15]
156+
True
106157
"""
107158

108-
def function_to_integrate(x: float) -> float:
159+
def semicircle_function(x: float) -> float:
109160
"""
110161
Represents semi-circle with radius 2
111-
>>> [function_to_integrate(x) for x in [-2.0, 0.0, 2.0]]
162+
>>> [semicircle_function(x) for x in [-2.0, 0.0, 2.0]]
112163
[0.0, 2.0, 0.0]
113164
"""
114165
return sqrt(4.0 - x * x)
115166

116167
estimated_value = area_under_curve_estimator(
117-
iterations, function_to_integrate, 0.0, 2.0
168+
iterations, semicircle_function, 0.0, 2.0
118169
)
170+
expected_value = pi
171+
error = abs(estimated_value - expected_value)
119172

120-
print("******************")
121-
print("Estimating pi using area_under_curve_estimator")
122-
print(f"Estimated value is {estimated_value}")
123-
print(f"Expected value is {pi}")
124-
print(f"Total error is {abs(estimated_value - pi)}")
125-
print("******************")
173+
return estimated_value, expected_value, error
126174

127175

128176
if __name__ == "__main__":

0 commit comments

Comments
 (0)