In [ ]:
# Analyzing the cycloid graph from "The Brachistochrone" research paper
        
File description: This ipython notebook attempts to graph and analyze a 
                  simple cycloid curve, from a set of parametric polar 
                  equations derived via the "Euler-Lagrange method" in 
                  the paper "The Brachistochrone", which attempted to 
                  prove, guided by the principle of Fermat's Theorem of 
                  Least Time and building upon Newton-Galileo's solution to 
                  Bernoulli's problem in Acta Eruditorum: given two points 
                  A and B in the vertical plane, the curve traced out by a 
                  point acted on only by gravity, which starts at A and reaches 
                  B in the shortest time is a cycloidial path called a 
                  Brachistochrone. Specifically, the paper shows that the 
                  Brachistochrone curve, which is essentially an inverted 
                  cycloid graph drawn by any circle rolling along an even plane, 
                  can be graphed with parametric functions taking in polar parameters 
                  r and theta. This notebook uses python to graph the parametric
                  functions to further analyze their meaning, as an extension 
                  and continuation of past research.
In [19]:
# import numpy for math symbols and matplotlib for plotting data
import numpy as np
import matplotlib.pyplot as plt
In [20]:
# define range for theta [0, 8pi] 
theta = np.linspace(0, 8*np.pi, 1000)
In [21]:
# define a radius for the cycloid-drawing circle
r = 50
In [22]:
# define parametric functions x and y
x = r * (theta - np.sin(theta))
y = r * (1 - np.cos(theta))
In [38]:
# Get subplot, and select axis
fig, ax = plt.subplots()

# Set grids to true, and set origin of axes
ax.grid(True, which='both')
ax.axhline(y=0, color='k')
ax.axvline(x=0, color='k')
Out[38]:
<matplotlib.lines.Line2D at 0x1182d42e8>
In [42]:
# Give graph a title
plt.title('Parametric Functions of a Cycloid Curve')
Out[42]:
<matplotlib.lines.Line2D at 0x1186e8e48>
In [44]:
# define range for x-axis [0, 1300]
left, right = plt.xlim()
plt.xlim(right=1300)

# define range for y-axis [0, 200]
bottom, top = plt.ylim()
plt.ylim(top=200)

# plot the cycloid path in black
plt.plot(x, y, color='black')
Out[44]:
[<matplotlib.lines.Line2D at 0x1188461d0>]
In [ ]:
As we can see, the graph describes the rolling motion of a circle of 
radius 50 along an even plane, successfully drawing out multiple cycloid
curves within the range of [0, 4*pi].

Overall, this gives us a visual of a cycloid curve, but since a Brachistochrone
is an inverted cycloid, we must flip the graph by reflecting it upon 
the y-axis. To do this, we must change the range of the y-axis, and 
apply the reflective transformation to the y parameter of the 
parametric equation upon calling the plot function. 
In [46]:
# define range for y-axis [-200, 100]
bottom, top = plt.ylim()
plt.ylim(bottom=-100, top=100)

# plot the cycloid path in black
plt.plot(x, -y, color='black')
Out[46]:
[<matplotlib.lines.Line2D at 0x118afaf28>]
In [ ]:
Now, the graph correctly demonstrates the brachistochrone curve, the path
of shortest time between two points A and B on a level plane. 
In [ ]:
Let's say we want to determine if the brachistochrone curve is indeed
faster. To do this, we will test a range of paths drawn on a coordinate
axis starting from the origin. Just for simplicity's sake, we will use 
the coordinates of the edges of a circle graph, with radius 1 around the 
origin, and limit the range of coordinates to Quadrant IV (+, -). This is 
because intuitively, we are drawing paths which are acted on by gravity
in a positive direction and neglecting paths which simply allow an 
object to free fall once put on the cycloid path. The purpose of this 
experiment is ultimately to determine if a cycloid curve can best a 
straight line in terms of the time needed to travel down its length. 
The equations we will use are given by the research paper, which has 
already proven their validity. 
In [ ]:
For example, take a point (1, -1) and draw a path to it from the origin.
This is the straight line path we will be comparing our findings to:
In [102]:
# define coordinates
x = np.arange(0, 1, 0.01)
y = -x

# Get subplot, and select axis
fig, ax = plt.subplots()

# Set grids to true, and set origin of axes
ax.grid(True, which='both')
ax.axhline(y=0, color='k')
ax.axvline(x=0, color='k')

ax.plot(x, y, color="black")
Out[102]:
[<matplotlib.lines.Line2D at 0x11a645c88>]
In [ ]:
Then, using the parametric functions we derived from 
our research, we can solve for the unknown parameter r
and determine the theta graph to draw the cycloid. 
In [163]:
# Consider the parameteric functions
X: 1 = r(THETA - sin(THETA))
Y: -1 = r(1 - cos(THETA))
    
Y: r = -1/(1-cos(THETA))
    
# Hence, substituting r into X,
1 = -(THETA - sin(THETA)) / (1 - cos(THETA))

# Graphing this function:

# define function, set x to THETA to determine x-intercept
x = np.linspace(-5, 5, 100)
y = -((x - np.sin(x))/(1 - np.cos(x))) - 1

# Ignore divide by 0
np.seterr(divide='ignore', invalid='ignore')

# Get subplot, and select axis
fig, ax = plt.subplots()

# Set grids to true, and set origin of axes
ax.grid(True, which='both')
ax.axhline(y=0, color='k')
ax.axvline(x=0, color='k')

ax.plot(x, y, color="black")
Out[163]:
[<matplotlib.lines.Line2D at 0x124db43c8>]
In [ ]:
From this graph, it is clear that the x-intercept occurs at approximately
x = -2.4. Translating this to radians, THETA = -2.4 Rad. Now, we can solve
for the radius r.
In [ ]:
# Recall from before, 
Y: r = -1/(1-cos(THETA))
    
# Hence, 
r = -1/(1 - cos(-2.4))
r = -0.58 units approximately
In [ ]:
Now, we can graph the cycloid
In [125]:
# define range for theta [0, 8pi] 
theta = np.linspace(0, -2.4, 100)

r = -0.58

# using r = -0.58, define parametric functions x and y
x = r * (theta - np.sin(theta))
y = r * (1 - np.cos(theta))

# Ignore divide by 0
np.seterr(divide='ignore', invalid='ignore')

# Get subplot, and select axis
fig, ax = plt.subplots()

# Set grids to true, and set origin of axes
ax.grid(True, which='both')
ax.axhline(y=0, color='k')
ax.axvline(x=0, color='k')

ax.plot(x, y, color="black")
Out[125]:
[<matplotlib.lines.Line2D at 0x122e32240>]
In [ ]:
Let's compare the graphs side-by-side
In [126]:
# define coordinates
x = np.arange(0, 1, 0.01)
y = -x

# Get subplot, and select axis
fig, ax = plt.subplots()

# Set grids to true, and set origin of axes
ax.grid(True, which='both')
ax.axhline(y=0, color='k')
ax.axvline(x=0, color='k')

ax.plot(x, y, color="black")

# define range for theta [0, 8pi] 
theta = np.linspace(0, -2.4, 100)

r = -0.58

# using r = -0.58, define parametric functions x and y
x = r * (theta - np.sin(theta))
y = r * (1 - np.cos(theta))

# Ignore divide by 0
np.seterr(divide='ignore', invalid='ignore')

ax.plot(x, y, color="black")
Out[126]:
[<matplotlib.lines.Line2D at 0x12308ef60>]
In [ ]:
We now have an accurate visual depiction of the 
fastest path between points (0, 0) and point(1, -1). Not 
convinced? There is still more to analyze. Using our equation
for least time traveled along this Brachistochrone path, 
we can populate a table comparing the times taken by a 
straight path and a cycloid path.
In [ ]:
To calculate the time taken by the straight path, we must
use Galileo's equation, thanks to him, we can use the following 
equation to determine the time taken for an object at rest
on top of an inclined plane to reach the bottom. 

            t = square-root[d * (2*l) / (g*h)],

Where t is the time in seconds, d is the length of the ramp,
g is gravitational acceleration, and l and h are the width
and height of the ramp, respectively.

From our own research, we have determined that the time taken
for an object to travel down a cycloid ramp is:
    
            t = square-root[r/g] * THETA,

Where r and theta are the parametric function parameters. 
In [ ]:
We will now use pandas, a dataframe library, to table all 
of our data comparisons. Although we must first calculate
the necessary parameters to determine the time taken, for 
this, we will use the following set of coordinates in 
Quadrant IV of the coordinate axis:
    
     {(x,y) | x^2 + y^2 = 1, x:[0, 1], y:[0, -1]}

This simplifies the data collection process. For straight 
path, we have set d = 1, l = x, h = -y. Thus, 
            
            t = square-root[(2*x) / (g*(-y))]
        
For the cycloid path, we have to calculate r and theta for 
each coordinate. To discretize the graph, we will use 
coordinates rounded to 4 decimal places
In [161]:
# begin by importing pandas and math
import math as math
import pandas as pd

# Gravitational acceleration (9.81m/s2)
g = 9.81

# Get set of test-coordinates
pi = np.pi

# function to return n (x,y) coordinates lining circumference
def PointsInCircum(x_or_y, r, n):
    if x_or_y == 1:
        return [round(math.cos(-pi/(2*n)*x)*r, 4) for x in range(1,n)]
    else:
        return [round(math.sin(-pi/(2*n)*x)*r, 4) for x in range(1,n)]

x_vals = PointsInCircum(1, 1, 50)
y_vals = PointsInCircum(0, 1, 50)

print("X: ")
print(x_vals)
print("Y: ")
print(y_vals)
X: 
[0.9995, 0.998, 0.9956, 0.9921, 0.9877, 0.9823, 0.9759, 0.9686, 0.9603, 0.9511, 0.9409, 0.9298, 0.9178, 0.9048, 0.891, 0.8763, 0.8607, 0.8443, 0.8271, 0.809, 0.7902, 0.7705, 0.7501, 0.729, 0.7071, 0.6845, 0.6613, 0.6374, 0.6129, 0.5878, 0.5621, 0.5358, 0.509, 0.4818, 0.454, 0.4258, 0.3971, 0.3681, 0.3387, 0.309, 0.279, 0.2487, 0.2181, 0.1874, 0.1564, 0.1253, 0.0941, 0.0628, 0.0314]
Y: 
[-0.0314, -0.0628, -0.0941, -0.1253, -0.1564, -0.1874, -0.2181, -0.2487, -0.279, -0.309, -0.3387, -0.3681, -0.3971, -0.4258, -0.454, -0.4818, -0.509, -0.5358, -0.5621, -0.5878, -0.6129, -0.6374, -0.6613, -0.6845, -0.7071, -0.729, -0.7501, -0.7705, -0.7902, -0.809, -0.8271, -0.8443, -0.8607, -0.8763, -0.891, -0.9048, -0.9178, -0.9298, -0.9409, -0.9511, -0.9603, -0.9686, -0.9759, -0.9823, -0.9877, -0.9921, -0.9956, -0.998, -0.9995]
In [316]:
# Determine the time values for straight graph
t_strt = [round(math.sqrt((2*x_vals[index])/(g*(-y_vals[index]))), 4) for index in range(0, 49)]
In [301]:
# Determine time values for cycloid graph: to solve for r, we can use the following:
y = r * (1 - cos(THETA))
r = y/(1 - cos(THETA))

# Array for storing theta values
t_theta = []

# calculate the y-coordinate of the graph
def calculate_y(x_cord, y_cord):
    return y_cord*((x - np.sin(x))/(1 - np.cos(x))) - x_cord

# find x-intercept of graph via approximation
myrange = np.arange(-100, 100, 0.01)

index = 0

# Iterate through values and find closest x-intercept
while index<49:
    
    for x in myrange:
        y = calculate_y(x_vals[index], y_vals[index])
        if 0.00<= y <=0.01:
            t_theta.append(round(x,3))
            break
            
    index = index + 1

print(t_theta)
[-60.17, -28.86, -16.6, -15.82, -10.2, -9.81, -9.13, -4.35, -4.22, -4.09, -3.96, -3.84, -3.72, -3.61, -3.49, -3.38, -3.27, -3.16, -3.05, -2.95, -2.84, -2.74, -2.63, -2.53, -2.43, -2.33, -2.23, -2.13, -2.03, -1.93, -1.83, -1.74, -1.64, -1.54, -1.45, -1.35, -1.26, -1.16, -1.06, -0.97, -0.87, -0.78, -0.69, -0.59, -0.5, -0.4, -0.31, -0.21, -0.12]
In [310]:
# once all the theta values are calculated, we must calculate the r values
t_rvals = []

index = 0
while index<49:
    rvals = -y_vals[index]/(1-np.cos(t_theta[index]))
    t_rvals.append(round(rvals,3))
    index = index + 1

print (t_rvals)
[0.017, 0.034, 0.058, 0.063, 0.091, 0.097, 0.111, 0.184, 0.189, 0.195, 0.201, 0.208, 0.216, 0.225, 0.234, 0.244, 0.256, 0.268, 0.282, 0.297, 0.314, 0.332, 0.353, 0.376, 0.402, 0.432, 0.465, 0.503, 0.548, 0.599, 0.658, 0.723, 0.805, 0.904, 1.013, 1.159, 1.322, 1.548, 1.841, 2.188, 2.704, 3.351, 4.266, 5.81, 8.068, 12.568, 20.887, 45.427, 138.986]
In [324]:
# Hence, the cycloid graph time values are:
t_cycl = []

g = 9.81

index = 0
while index<49:
    time = -math.sqrt(t_rvals[index]/g)*t_theta[index]
    t_cycl.append(round(time,4))
    index = index + 1

print (t_cycl)
[2.5048, 1.699, 1.2764, 1.2678, 0.9824, 0.9755, 0.9712, 0.5957, 0.5857, 0.5766, 0.5668, 0.5592, 0.552, 0.5467, 0.539, 0.5331, 0.5282, 0.5223, 0.5171, 0.5133, 0.5081, 0.5041, 0.4989, 0.4953, 0.4919, 0.4889, 0.4855, 0.4823, 0.4798, 0.4769, 0.4739, 0.4724, 0.4698, 0.4675, 0.4659, 0.464, 0.4625, 0.4608, 0.4592, 0.4581, 0.4568, 0.4559, 0.455, 0.4541, 0.4534, 0.4528, 0.4523, 0.4519, 0.4517]
In [325]:
# Comparing the data side-by-side,
print("Straight Path (s): ")
print(t_strt)
print()
print("Cycloid Path (s): ")
print(t_cycl)
Straight Path (s): 
[2.5475, 1.8, 1.4687, 1.2705, 1.1347, 1.0338, 0.9551, 0.8911, 0.8377, 0.7922, 0.7526, 0.7176, 0.6864, 0.6582, 0.6325, 0.6089, 0.5871, 0.5668, 0.5477, 0.5297, 0.5127, 0.4964, 0.4809, 0.466, 0.4515, 0.4375, 0.424, 0.4107, 0.3977, 0.3849, 0.3722, 0.3597, 0.3472, 0.3348, 0.3223, 0.3097, 0.297, 0.2841, 0.2709, 0.2574, 0.2434, 0.2288, 0.2135, 0.1972, 0.1797, 0.1605, 0.1388, 0.1133, 0.08]

Cycloid Path (s): 
[2.5048, 1.699, 1.2764, 1.2678, 0.9824, 0.9755, 0.9712, 0.5957, 0.5857, 0.5766, 0.5668, 0.5592, 0.552, 0.5467, 0.539, 0.5331, 0.5282, 0.5223, 0.5171, 0.5133, 0.5081, 0.5041, 0.4989, 0.4953, 0.4919, 0.4889, 0.4855, 0.4823, 0.4798, 0.4769, 0.4739, 0.4724, 0.4698, 0.4675, 0.4659, 0.464, 0.4625, 0.4608, 0.4592, 0.4581, 0.4568, 0.4559, 0.455, 0.4541, 0.4534, 0.4528, 0.4523, 0.4519, 0.4517]
In [326]:
# Start by defining the index for tabling
indicies = []
index = 1
while index<50:
    indicies.append(index)
    index += 1

print(indicies)
[1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49]
In [329]:
# Create data for tabling
data = {'Point number': indicies,
        'Straight Path (s)': t_strt,
        'Cycloid Path (s)': t_cycl}

df = pd.DataFrame(data)
df
Out[329]:
Point number Straight Path (s) Cycloid Path (s)
0 1 2.5475 2.5048
1 2 1.8000 1.6990
2 3 1.4687 1.2764
3 4 1.2705 1.2678
4 5 1.1347 0.9824
5 6 1.0338 0.9755
6 7 0.9551 0.9712
7 8 0.8911 0.5957
8 9 0.8377 0.5857
9 10 0.7922 0.5766
10 11 0.7526 0.5668
11 12 0.7176 0.5592
12 13 0.6864 0.5520
13 14 0.6582 0.5467
14 15 0.6325 0.5390
15 16 0.6089 0.5331
16 17 0.5871 0.5282
17 18 0.5668 0.5223
18 19 0.5477 0.5171
19 20 0.5297 0.5133
20 21 0.5127 0.5081
21 22 0.4964 0.5041
22 23 0.4809 0.4989
23 24 0.4660 0.4953
24 25 0.4515 0.4919
25 26 0.4375 0.4889
26 27 0.4240 0.4855
27 28 0.4107 0.4823
28 29 0.3977 0.4798
29 30 0.3849 0.4769
30 31 0.3722 0.4739
31 32 0.3597 0.4724
32 33 0.3472 0.4698
33 34 0.3348 0.4675
34 35 0.3223 0.4659
35 36 0.3097 0.4640
36 37 0.2970 0.4625
37 38 0.2841 0.4608
38 39 0.2709 0.4592
39 40 0.2574 0.4581
40 41 0.2434 0.4568
41 42 0.2288 0.4559
42 43 0.2135 0.4550
43 44 0.1972 0.4541
44 45 0.1797 0.4534
45 46 0.1605 0.4528
46 47 0.1388 0.4523
47 48 0.1133 0.4519
48 49 0.0800 0.4517
In [ ]:
As we can see, for the first 20 data points, the brachistochrone path 
is indeed faster, but after a while, the data points plateau, and the 
straight path decreases in time marginally. This is likely because the 
path has become near perfectly vertical meaning there is no frictino
to prevent the full force of gravitational acceleration, whereas for the
cycloid curve there may yet exist a path. Let's graph this and get a 
visual feel for this trend!
In [346]:
# plot both paths in different colors
plt.figure(figsize=(8, 6), dpi=80)

plt.title("Time Difference of Straight and Cycloid Paths")
plt.xlabel("Point Number")
plt.ylabel("Time taken(s)")

# Set intervals for axes
plt.xticks(np.arange(0, 51, 3))
plt.yticks(np.arange(0.0, 3.0, 0.25))

x = df.iloc[:,0]
y = df.iloc[:,1]
plt.plot(x, y, color='blue')

x = df.iloc[:,0]
y = df.iloc[:,2]
plt.plot(x, y, color='red')
Out[346]:
[<matplotlib.lines.Line2D at 0x12768abe0>]
In [ ]:
And that concludes our analysis! It has been a very enlightening research
and analysis process being able to analyze real-time data with python
frameworks. We have not only concluded that cycloid paths are indeed faster
most of the time, but also had the opportunity to explore many different 
ways of visualizing data. Overall, this has been a very enjoyable research
project for me, something I was not able to finish back in high school,
and I hope to continue in the future!