Setup¶

In [1]:
from math import acos as invCos
from math import cos as cos
from math import pi as pi
from math import sin as sin
from math import radians as MR
from math import sqrt as mS
from matplotlib.animation import FuncAnimation as FA
from matplotlib import colormaps as cm
import matplotlib.colors as mCm
from matplotlib.patches import Polygon as mpP
import matplotlib.pyplot as plt
import numpy as np
import pandas as pd
import seaborn as sns

Constants & Functions¶

In [12]:
def getViridis(n):
    """
    Get a viridis color sequence.

    Parameters:
    n - Number of colors.
    
    Returns:
    List of n hex codes as strings.
    """
    
    return([mCm.to_hex(cm['viridis'].resampled(n).colors[v]) for v in range(n)])





def getEndpoints(p):
    """
    Get the list of 101 endpoints for the first 100 segments.

    Parameters:
    p - 𝝋, from the problem.
    
    Returns:
    E - List of 29 endpoints.
    M - List of 28 midpoints.
    """
    
    def getMidpoint(p1, p2) :
        """
        Get the midpoint from two points.

        Parameters:
        p1, p2 - Two points (x,y).

        Returns:
        (x, y) - midpoint.
        """
        
        return((p1[0] + p2[0]) / 2,
               (p1[1] + p2[1]) / 2)
    
    #Initialize the two endpoints.
    E = [(0, 0), (1, 0)]

    #Initialize the midpoint.
    M = [(0.5, 0)]
    
    #Current angle is 0°.
    A = 0
    
    #Have to add 99 more endpoints.
    for i in range(2, 101) :
        
        #Turn counterclockwise.
        A += p
        
        #Previous endpoint.
        x = E[-1][0]
        y = E[-1][1]
        
        #Append new endpoint.
        E.append((x + i*cos(MR(A)), 
                 (y + i*sin(MR(A)))))
        
        #Append new midpoint.         
        M.append(getMidpoint(E[-2], E[-1]))
                 
    return(E, M)             
                
                 
#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 drawArc(center, theta0, theta1):
    
    """
    Draw an arc for 𝝋.

    Parameters:
    centers : [(x0, y0)]
    theta0 :[t00, *t01]
    theta1 :[t10, *t11]
    c : Petal color.
    o : Option. 
        If it is 'half', it is a half petal.
        If it is 'full', it is a full petal. If it is full, the optional * values above are used.

    Returns:
    None.
    However, patches are drawn.
    """
    
    #Center.
    x0, y0 = center
    #Create a list of angles between theta1 and theta2.
    a0 = np.linspace(theta0, theta1, 30)
    #Compute the (x, y) points on the arc
    points = [(x0, y0)] + [(x0 + 0.15*np.cos(MR(t)), y0 + 0.15*np.sin(MR(t))) for t in a0]
    
    return(mpP(xy = points,
               edgecolor = 'k',
               facecolor = 'none',
               lw = 1.5,
               zorder = 1))        
        
        
        
                 
                 
def plotHelper(n, p, w, option) :
    """
    Plot Anita.

    Parameters:
    n - Number of colors.
    p - 𝝋, from the problem.
    w - Scale the window.
    
    Returns:
    None - plot is output.
    """  
    
    if option != "video" :
        #fig = plt.figure(figsize = (12, 7))
        dummy = 0
    
    else :
        dummy = 0
    
    V = getViridis(n)
    ax = fig.add_subplot(xlim = (-7*w, 5*w),
                         ylim = (-4*w, 3*w))
    fig.subplots_adjust(left = 0.01, bottom = 0.07, right = 0.99, top = 0.93)                

    #Title.
    ax.set_title("Anita's Path, Turning at " + f"{p:.2f}" + "° ", fontsize = 24)

    clearAx(ax)

    E, M = getEndpoints(p)             

    x = list(np.array(E)[:, 0])
    y = list(np.array(E)[:, 1])

    ax.scatter(x = x,
               y = y,
               lw = 1.5,
               c = 'k')

    #Segments.
    for i in range(n):

        ax.plot(list(x[i:i+2]),
                list(y[i:i+2]),
                lw = 3,
                c = V[i],
                zorder = 2)
        
    if option == "image" :
        #Save file.
        #fig.savefig("2025.10.03_" + str(p) + "deg.png", bbox_inches = 'tight')
        dummy = 0
    
    elif option == "answer" :
        for j in range(1, 3):

            ax.plot(list(x[j:j+2]),
                    list(y[j:j+2]),
                    lw = 8,
                    c = V[j],
                    zorder = 2)
            
        ax.plot([x[3], M[3][0]],
                [y[3], M[3][1]],
                lw = 10,
                c = V[4],
                zorder = 2)
        
        ax.add_patch(drawArc([x[1], y[1]], 0, p))
        ax.add_patch(drawArc([x[2], y[2]], p, 2*p))
        ax.add_patch(drawArc([x[3], y[3]], 2*p, 3*p))        
        
        s = [('1"', (M[0][0]-0.05, M[0][1]-0.2)),
             ('2"', (M[1][0], M[1][1])),
             ('3"', (M[2][0]-0.32, M[2][1])),
             ('4"', (M[3][0], M[3][1]-0.25))]             
        
        t =  [('2"', ((x[3] + M[3][0]) / 2, (y[3] + M[3][1]) / 2-0.25), V[3], p*3),
              ('φ', (x[1]+0.1, y[1]+0.3), 'k', (90-p)/2),
              ('φ', (x[2]+0.27*cos(MR(3*p/2)), y[2]+0.27*sin(MR(3*p/2))), 'k', p + (90-p)/2),
              ('φ', (x[3]+0.27*cos(MR(5*p/2)), y[3]+0.27*sin(MR(5*p/2))), 'k', 2*p + (90-p)/2)]
        
        for i in range(4) :            
            plt.annotate(text = s[i][0],
                         xy = s[i][1],
                         c = V[i],
                         size = 30,
                         rotation = p*(i))
             
            plt.annotate(text = t[i][0],
                         xy = t[i][1],
                         c = t[i][2],
                         size = 30,
                         ha = 'center',
                         va = 'center',
                         rotation = t[i][3])


        #Save file.
        #fig.savefig("2025.10.03_" + str(round(p,2)) + "deg.png", bbox_inches = 'tight')
In [3]:
plotHelper(46, 15, 30, "image")
In [4]:
plotHelper(32, 45, 6, "image")
In [5]:
plotHelper(18, 60, 2.5, "image")
In [6]:
plotHelper(25, 72, 2, "image")
In [7]:
plotHelper(20, 90, 1.5, "image")
In [8]:
plotHelper(20, 120, 2/3, "image")
In [9]:
plotHelper(20, 135, 3/5, "image")
In [10]:
plotHelper(7, 180-180*invCos(3/4)/pi, 0.5, "answer")
In [11]:
plotHelper(100, 90.5, 10, "image")
In [13]:
sns.set()
fig = plt.figure(figsize = (8, 14/3))

def animate(i):
    if i < 45 :
        j = i
        n = 3
    elif i < 90 :
        j = i
        n = round(i/9) - 2
    elif i < 180 :
        j = 45 + i/2
        n = round(2*i/15) - 4
    elif i < 270 :
        j = 90 + i/4
        n = round(2*i/9) - 20
    elif i < 360 :
        j = 123.75 + i/8
        n = round(2*i/9) - 20
    elif i < 450 :
        j = 146.25 + i/16
        n = round(2*i/9) - 20
    else :
        j = 160.3125 + i/32
        n = round(i/9) + 30

    plt.clf()
    plotHelper(n, j, 2/3, "video")

#Run animation.
anim = FA(fig, animate, frames = 630, interval = 50)   

#Save animation.
anim.save('2025.10.03.mp4');
In [14]:
sns.set()
fig = plt.figure(figsize = (8, 14/3))

def animate(i):
    if i < 45 :
        j = i
        n = 3
    elif i < 90 :
        j = i
        n = round(i/9) - 2
    elif i < 180 :
        j = 45 + i/2
        n = round(2*i/15) - 4
    elif i < 270 :
        j = 90 + i/4
        n = round(2*i/9) - 20
    elif i < 360 :
        j = 123.75 + i/8
        n = round(2*i/9) - 20
    elif i < 450 :
        j = 146.25 + i/16
        n = round(2*i/9) - 20
    else :
        j = 160.3125 + i/32
        n = round(i/9) + 30

    plt.clf()
    plotHelper(min(7 + round(j/3), 100), j, 4, "video")

#Run animation.
anim = FA(fig, animate, frames = 630, interval = 50)   

#Save animation.
anim.save('2025.10.03_ZO.mp4');
Rohan Lewis¶

2025.10.06¶