Setup¶

In [1]:
import math
from math import pi as pi
from math import acos as icos
from math import cos as cos
from math import sin as sin
from math import sqrt as mS
import matplotlib
from matplotlib.animation import FuncAnimation as FA
from matplotlib.patches import Circle as mpC
from matplotlib.patches import Polygon as mpP
from matplotlib.path import Path
import matplotlib.pyplot as plt
import numpy as np
import pandas as pd
from random import random as RR

Constants & Functions¶

In [2]:
blue = "#2C71B3FF"
red = "#C74440FF"

t = mS(3)
u = t/2
v = 2*t/3

hexagon = [(2, 0), (1, t), (-1, t),
           (-2, 0), (-1, -t), (1, -t),
           (2, 0)]



#Clear axes.
def clearAx(ax):
    
    """
    Clear axes.

    Parameters:
    ax - Current ax.
    
    Returns:
    ax - Updated ax.
    """    
    
    ax.spines["top"].set_visible(False)
    ax.spines["right"].set_visible(False)
    ax.spines["bottom"].set_visible(False)
    ax.spines["left"].set_visible(False)
    ax.set_xticks([])
    ax.set_yticks([])
    return(ax)




def getDistance(x1, y1, o) :
    
    """
    Returns distance from point to nearest Team Vertex or Team Center.
    
    Parameters:
    (x1, y1) - Point.
    o - For Fiddler :
            - "Centroid" is (0, 0)
            - "Vertex0" is (2, 0)
            - "Vertex1" is (1, √3)
        For EC :
            - (x, y) Center of Centroid or Vertex team.
            - "Center" is (0, 0)
         
    Returns:
    d - Euclidean distance.
    """
    
    #Default is Team Centroid.
    if o in ["Centroid", "Center"] :
        a = 0
        b = 0
    
    #Fiddler.
    elif o == "Vertex0" :
        a = 2
    elif o == "Vertex1" :
        a = 1
        b = t
    
    #Extra Credit.
    else :
        a = o[0]
        b = o[1]
    
    #Return distance.
    d = mS((x1-a)**2 + (y1-b)**2)
    return(d)

    
    
def getIndex(x, y, ov) :
    
    """
    Returns index for heatmap
    
    Parameters:
    (x, y) - Point.
    ov - one of
        "Vertex0" is (2, 0)
        "Vertex1" is (1, √3) for Fiddler.
        [(xc, yc), (xv, yv)] for Extra Credit.
     
    Returns:
    index - Proportion of radial arm.
    """
    
    #Fiddler.
    if ov in ["Vertex0", "Vertex1"] :
        #Centroid.
        dc = getDistance(x, y, (0, 0))
        if ov == "Vertex0" :
            dv = getDistance(x, y, (2, 0))
        if ov == "Vertex1" :
            dv = getDistance(x, y, (1, mS(3)))  
    
    #Extra Credit.
    else :
        dc = getDistance(x, y, ov[0])
        dv = getDistance(x, y, ov[1])
    
    #Index
    return((dc - dv) / (dc + dv))
    
    
    
def createDF() :    
    
    """
    Heatmap dataframe for Fiddler.
    
    Parameters:
    None.
     
    Returns:
    df - Columns X, Y, I for interior of hexagon.
    """
    
    lot = Path(hexagon)
    Xs = []
    Ys = []
    Is = []
    
    #Go 0 to 𝜋/2.
    for theta in [i/1000 for i in range(1572)]:
        #𝜋/6 to 𝜋/2 is closer to V1.
        ov = "Vertex1"

        #0 to 𝜋/6 is closer to V2.
        if theta < 0.524 :
            ov = "Vertex0"
            
        #Radius 0 to √3. 
        for r in [j/100 for j in range(174)] :
            x = r*cos(theta)
            y = r*sin(theta)

            index = getIndex(x, y, ov)
            Xs.extend([x, -x, -x, x])
            Ys.extend([y, y, -y, -y])
            Is.extend([index, index, index, index])

        #Radius √3 to 2, some points are interior and some exterior.
        for r in [j/100 for j in range(174, 201)]:
            x = r*cos(theta)
            y = r*sin(theta)

            #Interior test.
            if lot.contains_point((x, y)) :
                index = getIndex(x, y, ov)
                
                #Add corresponding value for all 4 quadrants.
                Xs.extend([x, -x, -x, x])
                Ys.extend([y, y, -y, -y])
                Is.extend([index, index, index, index])
                
    df = pd.DataFrame({'X' : Xs,
                       'Y' : Ys,
                       'I' : Is})
    return(df)





def createDF2(teams) :    
    
    """
    Heatmap dataframe for EC.
    
    Parameters:
    teams - [(xc, yc), (xv, yv)]
            Starting points for Team Centroid and Team Vertex.
     
    Returns:
    df - Columns X, Y, I for interior of circle.
    """
    
    Xs = []
    Ys = []
    Is = []
    for theta in [i/500 for i in range(3142)] :
        for r in [j/100 for j in range(201)] :
            x = r*cos(theta)
            y = r*sin(theta)

            index = getIndex(x, y, teams)
            Xs.append(x)
            Ys.append(y)
            Is.append(index)
                
    df = pd.DataFrame({'X' : Xs,
                       'Y' : Ys,
                       'I' : Is})
    return(df)




def getArea(teams, scale) :
    
    """
    Heatmap dataframe for EC.
    
    Parameters:
    teams - [(xc, yc), (xv, yv)]
            Starting points for Team Centroid and Team Vertex.
    scale - Domain is [-scale, scale] for shape.
     
    Returns:
    area - Area of larger region.  Will be at least 𝜋/2.
    """
        
    x1 = teams[0][0]
    y1 = teams[0][1]

    x2 = teams[1][0]
    y2 = teams[1][1]

    midy = (y2 + y1) / 2
    
    #Vertical line, m = ∞
    if x1 == x2 : 
        
        a = abs(midy)
        
    else :
        
        m = (y2 - y1) / (x2 - x1) 
        midx = (x2 + x1) / 2
        a = abs(midx + m*midy) / mS(1 + m**2)
    
    B = icos(a/scale)
    
    circle = pi
    sector = B
    triangle = sin(2*B) / 2
    Area = circle - sector + triangle
    
    return(Area)

Fiddler¶

In [3]:
def plotHelper(option, n, fig = None) :
    
    if n == 1 :
        x = 7
    else :
        x = 9
    
    #Video sets up itself.
    if option == "Fiddler" or n == 1 :
        fig = plt.figure(figsize = (x, 7))  
    
    
    ax = fig.add_subplot(xlim = (-2.1, 2.1),
                         ylim = (-2.1, 2.1))
    fig.subplots_adjust(left = 0,
                        bottom = 0,
                        right = 1,
                        top = 1)

     
    #Remove axes and ticks.
    ax = clearAx(ax)
    
    #FIDDLER.
    if option == "Fiddler" :
    
        #Hexagon.
        ax.add_patch(mpP(xy = hexagon,
                         edgecolor = 'k',
                         facecolor = 'None',
                         lw = 1))
        #Diagram.
        if n == 1 :
            #Team Vertex
            ax.scatter(np.array(hexagon).T[0], np.array(hexagon).T[1], c = red, lw = 10)
            #Team Centroid
            ax.scatter(0, 0, c = blue, lw = 10)

            #Trace
            ax.plot([0, 1, 0, -1, -0.5, 0, 0],
                    [0, t, v, t, u, 0, v],
                    ls = ":",
                    lw = 1,
                    c = "k")

            #Midpoints
            ax.scatter((0, -0.5), (v, u), c = 'k', lw = 10)
            
            text = [("C", (0.05, 0), 'k'),
                    ("Q", (0.1, 1), 'k'),
                    ("R", (-0.6, 0.6), 'k')]

            for tt in text :
                plt.annotate(text = tt[0],
                             xy = tt[1],
                             c = tt[2],
                             size = 18,
                             zorder = 4)
       
        #Heatmap.
        if n == 2 :
            
            df = createDF()

    #EXTRA CREDIT.        
    else :
        
        #Circle
        ax.add_patch(mpC((0, 0),
                         radius = 2,
                         edgecolor = 'k',
                         facecolor = 'None',
                         lw = 1))
        #Diagram.
        if n == 1 :
            
            #Team Vertex
            ax.scatter(0.7, 0.3, c = red, lw = 10)
            #Team Centroid
            ax.scatter(0.1, -1.5, c = blue, lw = 10)

            #Midpoint, Center, Boundary, Boundary Center.
            xd1 = 1.71
            xd2 = -1.99
            yd1 = -mS(4-xd1**2)
            yd2 = mS(4-xd2**2)
            
            ax.scatter([0.4, 0, xd1, xd2, (xd1+xd2)/2],
                       [-0.6, 0, yd1, yd2, (yd1+yd2)/2],
                       c = 'k', lw = 10)

            ax.plot([0, xd1, xd2, 0, (xd1+xd2)/2],
                    [0, yd1, yd2, 0, (yd1+yd2)/2],
                    ls = ":",
                    lw = 1,
                    c = "k")
            
            ax.plot([0.7, 0.1],
                    [0.3, -1.5],
                    ls = ":",
                    lw = 1,
                    c = "k")
            
            text = [("Team C", (0.15, -1.5), blue),
                    ("Team V", (0.75, 0.3), red),
                    ("M", (0.4, -0.8), 'k'),
                    ("A", (xd1+0.05, yd1), 'k'),
                    ("B", (xd2+0.05, yd2), 'k'),
                    ("M'", ((xd1+xd2)/2, (yd1+yd2)/2-0.2), 'k'),
                    ("C", (0.05, 0), 'k'),
                    ("a", (-0.05, -0.24), 'k'),
                    ("b", (-1.1, -0.28), 'k'),
                    ("β", (-0.15, -0.15), 'k')]
         
            for tt in text :
                plt.annotate(text = tt[0],
                             xy = tt[1],
                             c = tt[2],
                             size = 18,
                             zorder = 4)

            

            
        else :
            df = createDF2(n)    
        
    #Heatmaps!  Fiddler or EC!    
    if n != 1 :
        
        cmap = plt.cm.seismic
        heatmap = plt.scatter(x = df['X'],
                              y = df['Y'],
                              c = df['I'],
                              cmap = cmap,
                              marker = "s",
                              s = 2,
                              vmin = -1,
                              vmax = 1,
                              plotnonfinite = True)
        #Title setup.
        ax.set_title('Can You Shovel the Snow?', fontsize = 24)


        #Colorbar.
        cb = plt.colorbar(heatmap,
                          ax = ax,
                          ticks = [-1, 1])
        cb.set_label('Proximity to Shovel Team',
                     labelpad = -120,
                     fontsize = 20)
        cb.ax.tick_params(labelsize = 16)
        cb.ax.set_yticklabels(["  Team\nCentroid", " Team\nVertex"])
        
        #Bottom left larger area proportion annotation.
        if option == "EC" :
            centroid = getDistance(n[0][0], n[0][1], "Center")
            vertex = getDistance(n[1][0], n[1][1], "Center")
            
            if centroid > vertex :
                output = red
            else :
                output = blue
            
            area = getArea(n, 2) / pi

            text = [("Larger", (-2, -1.56), output),
                    ("Region :", (-2, -1.78), output),
                    (str(area)[0:6], (-2, -2), output)]
         
            for tt in text :
                plt.annotate(text = tt[0],
                             xy = tt[1],
                             c = tt[2],
                             size = 22,
                             zorder = 4)
    
        n = 2
    
    fig.savefig("2026.03.06" + option + str(n) + ".png", bbox_inches = "tight")

    
    
    

#Animate 
def shovelLot() :
    
    fig = plt.figure(figsize = (9, 7.5))
    fig.set_tight_layout(True)
    
    def animate(i):
        j = 6*pi*i/300
        k = j / (3*pi)
        plt.clf()
        plotHelper("EC", [(0, -2+i/75), (k*cos(j), k*sin(j))], fig)
    
    #Run animation.
    anim = FA(fig, animate,
              frames = 301,
              interval = 100)   

    #Save animation.
    anim.save('2026.03.06EC.mp4'); 
In [4]:
plotHelper("Fiddler", 1)
In [5]:
plotHelper("Fiddler", 2)
In [6]:
plotHelper("EC", 1)
In [7]:
shovelLot()
In [8]:
letsgo = 0

for i in range(100000000) :
    r = mS(RR())
    theta = 2*pi*RR()

    letsgo += getArea([(mS(RR()), 0),
                       (r*cos(theta), r*sin(theta))], 1)/100000000
letsgo / pi
Out[8]:
0.6397646315890875
Rohan Lewis¶

2026.03.09¶