CS303E Homework 7

Instructor: Dr. Bill Young
Due Date: Wednesday, March 5, 2025 at 11:59pm

Copyright © William D. Young. All rights reserved.

Practice with Classes: Calculate PI

In geometry the ratio of the circumference of a circle to its diameter is known as π. There are various ways to compute π. For example, it can be estimated from an infinite series of the form:

   π / 4 = 1 - (1/3) + (1/5) - (1/7) + (1/9) - (1/11) + ...
There is another novel approach to calculate π. Imagine that you have a dart board that is 2 units square. (Suppose that a were 2 in the figure above.) The square inscribes a circle of radius 1. The center of the circle coincides with the center of the square. Now imagine that you throw darts at that dart board randomly. If the throws are truly random, the ratio of the number of darts that fall within the circle to the total number of darts thrown is the same as the ratio of the area of the circle to the area of the square dart board. The area of a circle with unit radius is just π square units. The area of the dart board is 4 square units. Thus, the ratio of the area of the circle to the area of the square is π / 4. From that it's easy to estimate π; just multiply by 4.

That's the computation you'll be doing in this assignment, which has two main parts.

Part 1:

In the first part, you'll define and test two classes: Circle and Square. Below are template for these two classes. Just fill in the bodies of the methods. Put these and your code from Part 2 into a file CalculatePi.py.

Hint: a point (a, b) is inside a Circle if the distance from (a, b) to the circle center is less than the radius. You can calculate the distance between points (x1, y1) and (x2, y2) with the distance function, which is provided below. I'm leaving it to you to figure out how to decide whether a point is inside a square.

import math
import random

def distance( x1, y1, x2, y2 ):
    return math.sqrt( (x1 - x2)**2 + (y1 - y2)**2 )

class Circle:
    """Defines a circle object.  It's centered at (x, y) and has radius  rad."""

    def __init__(self, rad, x, y):
       """Define the Circle object."""
    
    def __str__(self):
       """Return a string describing the object."""

    def getRadius(self):
       """Return the radius."""

    def getCenterX(self):
       """Return the x component of the center point."""

    def getCenterY(self):
       """Return the y component of the center point."""

    def getArea(self):
       """Return the area of the circle."""


    def pointInCircle(self, a, b):
        """Return a boolean indicating whether point (a, b)
        is inside the circle."""

class Square:
    """Construct a square with given side length, aligned with the axes
    and top left corner at point (x, y)."""

    def __init__(self, side, x, y):
       """Define the Square object."""
    
    def __str__(self):
       """Return a string describing the object."""

    def getSide(self):
       """Return the side length of the Square."""

    def getULX(self):
       """Return the x component of the upper left corner."""
    
    def getULY(self):
       """Return the y component of the upper left corner."""
    
    def getArea(self):
       """Return the area of the square."""

    def pointInSquare(self, a, b):
        """Return a boolean indicating whether point (a, b)
        is inside the square."""
Be sure to test your classes before you proceed to Part 2. Below is some behavior you might see in testing these classes:
>>> from CalculatePi import *
>>> c = Circle( 1, 3, 9 )                   # define a Circle object
>>> print(c)
Circle of radius 1 centered at point (3, 9)
>>> c.getRadius()
1
>>> c.getCenterX()
3
>>> c.getCenterY()
9
>>> c.pointInCircle( 1, 2 )
False
>>> c.pointInCircle( 3.2, 8.4 )
True
>>> c.getArea()                             # notice the area
3.141592653589793
>>> s = Square( 2, -2, -4 )                 # define a square
>>> print(s)
Square of side 2 with upper left point at (-2, -4)
>>> s.getSide()
2
>>> s.getULX()
-2
>>> s.getULY()
-4
>>> s.getArea()
4
>>> s.pointInSquare( 0, 0 )
False
>>> s.pointInSquare( 0, -4 )
False
>>> s.pointInSquare( -1, -5 )
True
>>> 

Part 2:

In this second part, you'll define a Circle of radius 1 centered at the origin (0, 0) and a square of side 2 with upper left point at (-1, 1). Notice that this means that the circle in inscribed in the square as shown in the figure above. (Actually, we won't need the square for this computation, but go ahead and define it.)

Then, we'll use this to estimate π by imagining that the square is the dartboard with inscribed circle as described above. We throw darts randomly at the board and calculate how many hit the circle, out of how many total throws. This ratio should approach π / 4. Notice that a dart hits the board if its x and y coordinates are both between -1 and 1. To see whether it hits the circle use the pointInCircle method.

To simulate the throwing of darts we will use a random number generator. In the random module is the function random.random() which returns a random float in the range 0.0 to 1.0. To generate a random value in range -1.0 to 1.0, you can do the following:

   r = (random.random() * 2) - 1
Think about why that works.

You should write two functions: estimatePi( c, n ) and main(). Function estimatePi( c, n ) does the following. It takes two parameters: a Circle c of radius 1 and centered at (0, 0) and a count n. Generate n points (x, y), where both x and y are in the range (-1.0..1.0). For each, see if the point is inside the Circle. Keep track of the number of hits (those in the circle). Return an estimate of π as (hits / n) * 4.

In the main() function, do the following: Define a Circle object of radius 1, centered at (0, 0) and a Square object of side 2 with upper left corner at (-1, 1). In a loop, run your estimatePi( c, n ) function on that Circle and with n set to powers of 10 between 100 and 10,000,000. You will print the results in a nice table as shown below:

n = 100        Calculated PI = 3.320000   Difference = +0.178407 
n = 1000       Calculated PI = 3.080000   Difference = -0.061593 
n = 10000      Calculated PI = 3.120400   Difference = -0.021193 
n = 100000     Calculated PI = 3.144720   Difference = +0.003127 
n = 1000000    Calculated PI = 3.142588   Difference = +0.000995 
n = 10000000   Calculated PI = 3.141796   Difference = +0.000204 
Notice that the Calculated PI value is the value you computed printed with 6 places of precision after the decimal point. The Difference field in the output is your calculated value of PI minus math.pi. Note that this might be positive or negative. You can make a value print with a sign by adding a "+" to your format, e.g., format(diff, "+0.6f").

Your output must be in the above format. The number of throws must be left justified. The calculated value of π and the difference must be expressed to six places of precision. There should be plus or minus sign on the difference. Notice that your values for estimated π and the difference from math.pi will differ from those shown above, and will differ each time you run the program. Ideally, the difference approaches 0 as n increases.

Turning In the Assignment:

The program should be in a file named CalculatePi.py. Submit the file via Canvas before the deadline shown at the top of this page. Submit it to the assignment hw7 under the assignments sections by uploading your Python file.

It must also contain a header with the following format:

# Assignment: HW7
# File: CalculatePi.py
# Student: 
# UT EID:
# Course Name: CS303E
# 
# Date Created:
# Description of Program: 

Programming tips

Magic methods: Classes are what make object-oriented (OO) languages like Python object-oriented. In Python every data item is an instance of some class, including numbers and strings. In early OO languages like Smalltalk this was much more explicit. For example, since 1 was an object, if you wanted to add 2 to 1, you had to call the add method on 1, like "1.add(2)". That gets tedious pretty fast!

But, in effect, that's still what's happening in Python. It's just that they hide it under a nicer syntax. For example, when you write "x + y" in Python, that's really a short way of calling: "x.__add__(y)", the __add__ method on the class of x. (Magic methods like __add__ and __str__ are explained in Slideset 7.)

>>> i1 = 10
>>> i2 = 20
>>> i1.__add__(i2)
30
>>> i1 + i2
30
Note that for some reason you can't call __add__ directly on integer literals; "10.__add__(20)" isn't allowed.

When you define a convenient notation for a more cumbersome underlying syntax, that's often called syntactic sugar. Luckily, the syntactic sugar means that you can use the familiar syntax of arithmetic almost all the time when you're doing arithmetic in Python. But Python also allows you to "overload" that familiar syntax in some very unfamiliar contexts. You've already seen that you can add two numbers; you can also "add" (concatenate) two strings. That's just because the __add__ method is defined in the string class to be concatenation.

If you wanted to define a way to "add" two circles or two squares using the syntax "d1 + d2" you could do it in Python and give it whatever meaning you wanted simply by defining "__add__" on the class. That wouldn't make much sense for this assignment. It might if, for example, you often needed the sum of the areas of several figures. You could even re-define the meaning of "+" on integers, but you'd almost certainly regret that!