Defining a Circle Class#

When it works well, the idea of object-oriented programming (OOP) is for the code you write to be composable — that is, like Legos, you should be able to click together different things you’ve built to make something that is more than the sum of its parts.

To illustrate this concept, in this class you will define a Circle class in a manner that builds on the Point class you wrote in your previous assignment.

If creating Point or Circle objects feels esoteric or silly, don’t be fooled — being able to do geometric operations is key to a wide range of applications. Perhaps the most well known is geospatial analysis (often referred to as GIS, which is short for Geospatial Information Systems) — the science of understanding how things relate to one another spatially in the real world.

Point and Circle objects are two of the primitives GIS analysts use on a daily basis. Points can represent anything from an election polling place to the location of a source of airborne pollution or a ship at sea. Circles (and tools for measuring overlaps between circles or circles and points) are used to identify people or animals within a given radius of a point, or an area that may be exposed to a certain intensity of radiation generated from a point. Indeed, in these exercises we’re basically recreating some of the primitives used in one of the most important software libraries in the Python GIS ecosystem (shapely).

Bringing In Your Point Class#

The first thing you will put at the top of your code for this assignment — which should be in a file called circle.py — will be the line:

from point import Point 

Provided your point.py file is in the same directory as your circle.py file, this line will import your Point class and make it accessible from within this circle.py file. You can refer to it as just Point (like p = Point()). Note that if we had just said import point instead of from point import Point, you would need to write p = point.Point() to create a point object.

Note we have now come… full circle (sorry, couldn’t resist) with that if __name__ == "__main__": if statement. As mentioned before, code that is in that if block is only ever executed when a file is executed directly. But at the time, what it meant to not execute a file directly may have been unclear. Now you know! When we run from point import Point, Python is running all the code in point.py that defines the class Point and its functionality so it becomes accessible inside of this circle.py program. But it won’t run the tests at the bottom of point.py because that if statement does not evaluate to True.

When you submit your assignment to gradescope, you will need to zip circle.py and point.py into a single .zip file and upload them together since the autograder needs both files run!

Circle Requirements#

For the Circle class, you will need:

  • A Docstring summarizing the class (a string you put right under the class definition).

  • An __init__ method: takes a Point c and a number r and assigns the center and radius respectively.

    • The default values should be a Point at (0, 0) and radius 1.

    • You should assert that the radius is greater than or equal to 0.

    • Because Point is a mutable type, so you do not want to actually make the default value Point(0, 0) as that will cause problems (if you don’t remember why, you might want to revisit the reading “Default Arguments Revisited” in Coursera PPF, Module 4 “Diving Deeper with Lists”, in subsection “Looking More into Lists.” Here’s a link that might work but hard to link into Coursera courses.). Instead, you use the “default value is None, check for None, make the value you want” idiom that you learned in that reading:

    
    def func(x=None):
        if x is None:
            x = Point(0,0)
    
    
  • A __str__ method: returns a string representation of the circle. For example, a circle with center (0, 0) with radius 1 would be: "((0, 0), 1)".

  • A move method: takes dx and dy and moves the center of the circle accordingly. Remember, you already wrote move for Point!

  • An intersection_area method: takes a parameter other_circle and calculates the intersection of self, and other_circle.

Calculating Intersection Area#

Remember that a question like “what is the formula for the intersection area of two circles?” indicates a lack of domain knowledge (not a lack of programming skill). In such situations, you should consult an authoritative source in that domain. For this problem, the following source derives the equation you need: https://mathworld.wolfram.com/Circle-CircleIntersection.html.

Specifically, you need equation (14) on that page. You can compute \(cos^{-1}(x)\) with math.acos(x) in Python.

Note that the formula described above is for the case where the circles partially overlap. You will need to determine if the circles do not overlap at all, partially overlap, or overlap completely. If they do not overlap at all, the intersection area is 0. If they partially overlap, use the formula above. If they overlap completely, the intersection area is the area of the smaller circle.

We recommend drawing some pictures to figure help you figure out the algorithm to decide which case two circles fall into.

If you are feeling really stuck on the math, some code for the intersection area of partially overlapping circles is folded below, but please only use it after you’ve tried to figure things out yourself!


p1 = (
    small_r
    * small_r
    * math.acos((d * d + small_r * small_r - big_r * big_r) / (2 * d * small_r))
)
p2 = (
    big_r
    * big_r
    * math.acos((d * d + big_r * big_r - small_r * small_r) / (2 * d * big_r))
)
p3 = 0.5 * math.sqrt(
    (-d + small_r + big_r)
    * (d + small_r - big_r)
    * (d - small_r + big_r)
    * (d + small_r + big_r)
)

intersection_area = p1 + p2 - p3

where small_r is the smaller radius and big_r is the larger radius and d is the distance between radii. Note this is ONLY the answer for partially overlapping circles.

Scaffolding#

Please copy-paste the following code into your circle.py file as a starting place.


import math

from point import Point


class Circle:
    # write a docstring here!
    def __init__(self, c=None, r=1):
        # WRITE ME
        pass

    def __str__(self):
        # WRITE ME
        return ""

    def __repr__(self):
        return "Circle" + str(self)

    def move(self, dx, dy):
        # WRITE ME
        # hint: you should be calling move(dx,dy)
        # on the Point that is the center of the circle
        pass

    def intersection_area(self, other_circle):
        # WRITE ME
        # first check if no overlap
        #   if no overlap, return 0
        # next check for total overlap
        #   if so, return area of smaller circle
        # otherwise, use Wolfram formula for partial overlap
        return 0

    pass


if __name__ == "__main__":
    c1 = Circle()
    # note that c2 just touches, but does not intersect c1
    c2 = Circle(Point(3, 4), 5)
    c3 = Circle(Point(), 2)
    c4 = Circle(Point(4, 5), 6)
    c5 = Circle(Point(-2, -2), 3)
    c6 = Circle(Point(3, -3), 2)
    circles = [c1, c2, c3, c4, c5, c6]
    for ca in circles:
        print("---")
        print(str(ca))
        print(repr(ca))
        for cb in circles:
            print("{:.8f}".format(ca.intersection_area(cb)))
            pass
        pass
    c1.move(-1, 0)
    c2.move(1, 1)
    c3.move(2, -4)
    for ca in circles:
        print("---")
        print(str(ca))
        print(repr(ca))
        for cb in circles:
            print("{:.8f}".format(ca.intersection_area(cb)))
            pass
        pass
    pass

Testing#

As within point.py, this file includes some basic tests at the end of circle.py. The expected output is folded below. As usual, you are encouraged to do more testing to become confident in your code.


---
((0, 0), 1)
Circle((0, 0), 1)
3.14159265
1.50406278
3.14159265
0.74638183
1.80090139
0.00000000
---
((3, 4), 5)
Circle((3, 4), 5)
1.50406278
78.53981634
5.74769069
76.19439808
0.21244868
0.00000000
---
((0, 0), 2)
Circle((0, 0), 2)
3.14159265
5.74769069
12.56637061
4.28297693
6.03868357
0.00000000
---
((4, 5), 6)
Circle((4, 5), 6)
0.74638183
76.19439808
4.28297693
113.09733553
0.00000000
0.00000000
---
((-2, -2), 3)
Circle((-2, -2), 3)
1.80090139
0.21244868
6.03868357
0.00000000
28.27433388
0.00000000
---
((3, -3), 2)
Circle((3, -3), 2)
0.00000000
0.00000000
0.00000000
0.00000000
0.00000000
12.56637061
---
((-1, 0), 1)
Circle((-1, 0), 1)
3.14159265
0.00000000
0.00000000
0.00000000
2.89596536
0.00000000
---
((4, 5), 5)
Circle((4, 5), 5)
0.00000000
78.53981634
0.00000000
78.53981634
0.00000000
0.00000000
---
((2, -4), 2)
Circle((2, -4), 2)
0.00000000
0.00000000
12.56637061
0.00000000
0.77720565
7.02968231
---
((4, 5), 6)
Circle((4, 5), 6)
0.00000000
78.53981634
0.00000000
113.09733553
0.00000000
0.00000000
---
((-2, -2), 3)
Circle((-2, -2), 3)
2.89596536
0.00000000
0.77720565
0.00000000
28.27433388
0.00000000
---
((3, -3), 2)
Circle((3, -3), 2)
0.00000000
0.00000000
7.02968231
0.00000000
0.00000000
12.56637061

Submitting to Coursera#

Because your circle.py file imports from your point.py file, you must submit these two files together.

Done? Want More?#

Here are some extensions you can try!