On May 11 @fermatslibrary posted a gif file, https://twitter.com/fermatslibrary/status/862659602776805379, illustrating the motion of eight cocircular points. The Fermat's Library followers found it so fascinating that the tweet picked up more than 1000 likes and 800 retweets. Soon after I saw the gif I created a similar Python Plotly animation although the tweet did not mention how it was generated. @plotlygraphs tweeted a link to my Jupyter notebook presenting the animation code.
How I succeeded to reproduce it so fast? Here I explain the secret:
At the first sight you can think that the gif displays an illusory rectiliniar motion of the eight points, but it is a real one. I noticed that the moving points lie on a rolling circle along another circle, and I knew that a fixed point on a rolling circle describes a curve called hypocycloid. In the particular case when the ratio of the two radii is 2 the hypocycloid degenerates to a diameter in the base (fixed) circle.
In this Jupyter notebook I deduce the parametric equations of a hypoycyloid, animate its construction and explain why when $R/r=2$ any point on the rolling circle runs a diameter in the base circle.
In [1]:
from IPython.display import Image
Image(filename='generate-hypocycloid.png')
Out[1]:
We refer to the figure in the above cell to explain how we get the parameterization of the hypocycloid generated by a fixed point of a circle of center $O'_0$ and radius r, rolling without slipping along the circle of center O and radius $R>r$.
Suppose that initially the hypocycloid generating point, $P$, is located at $(R,0)$. After the small circle was rolling along the greater circle a length corresponding to an angle of measure, $t$, it reaches the point $P'$ on the circle $C(O'_t, r)$.
Rolling without slipping means that the length the arc $\stackrel{\frown}{PQ}$ of the greater circle equals the length of the arc $\stackrel{\frown}{P'Q}$ on the smaller one, i.e $Rt=r\omega$, where $\omega$ is the measure of the non-oriented angle $\widehat{P'O'_tQ}$ (i.e. we consider $\omega>0$) . Thus $\omega=(R/r)t$
The center $O'_t$ has the coordinates $x=(R-r)\cos(t), (R-r)\sin(t)$. The clockwise parameterization of the circle $C(O'_t,r)$ with respect to the coordinate system $x'O'_ty'$ is as follows:
$$\begin{array}{llr} x'(\tau)&=&r\cos(\tau)\\ y'(\tau)&=&-r\sin(\tau), \end{array}$$$\tau\in[0,2\pi]$.
Hence the point $P'$ on the hypocycloid has the coordinates: $x'=r\cos(\omega-t), y'=-r\sin(\omega-t)$, and with respect to $xOy$, the coordinates:
$x=(R-r)\cos(t)+r\cos(\omega-t), y=(R-r)\sin(t)-r\sin(\omega-t)$.
Replacing $\omega=(R/r)t$ we get the parameterization of the hypocycloid generated by the initial point $P$:
$$\begin{array}{lll} x(t)&=&(R-r)\cos(t)+r\cos(t(R-r)/r)\\ y(t)&=&(R-r)\sin(t)-r\sin(t(R-r)/r), \quad t\in[0,2\pi] \end{array}$$If $R/r=2$ the parametric equations of the corresponding hypocycloid are:
$$\begin{array}{lll} x(t)&=&2r\cos(t)\\ y(t)&=&0 \end{array}$$i.e. the moving point $P$ runs the diameter $y=0$, from the position $(R=2r, 0)$ to $(-R,0)$ when $t\in[0,\pi]$, and back to $(R,0)$, for $t\in[\pi, 2\pi]$.
What about the trajectory of any other point, $A$, on the rolling circle that at $t=0$ has the angular coordinate $\varphi$ with respect to the center $O'_0$?
We show that it is also a diameter in the base circle, referring to the figure in the next cell that is a particularization of the above figure to the case $R=2r$.
In [2]:
Image(filename='hypocycloid-2r.png')
Out[2]:
The arbitrary point $A$ on the rolling circle has, for t=0, the coordinates: $x=r+r\cos(\varphi), y=r\sin(\varphi)$.
The angle $\widehat{QO'_tP'}=\omega$ is in this case $2t$, and $\widehat{B'O'_tP'}=t$. Since $\widehat{A'O'_tP'}=\varphi$, we get that the position of the fixed point on the smaller circle, after rolling along an arc of length $r(2t-\varphi)$, is $A'(x(t)=r\cos(t)+r\cos(t-\varphi), y(t)=r\sin(t)-r\sin(t-\varphi))$, with $\varphi$ constant, and $t$ variable in the interval $[\varphi, 2\pi+\varphi]$.
Let us show that $y(t)/x(t)=$constant for all $t$, i.e. the generating point of the hypocycloid lies on a segment of line (diameter in the base circle):
$$\displaystyle\frac{y(t)}{x(t)}=\frac{r\sin(t)-r\sin(t-\varphi)}{r\cos(t)+r\cos(t-\varphi)}=\left\{\begin{array}{ll}\tan(\varphi/2)& \mbox{if}\:\: t=\varphi/2\\ \displaystyle\frac{2\cos(t-\varphi/2)\sin(\varphi/2)}{2\cos(t-\varphi/2)\cos(\varphi/2)}=\tan(\varphi/2)& \mbox{if}\:\: t\neq\varphi/2 \end{array}\right.$$Hence the @fermatslibrary animation, illustrated by a Python Plotly code in my Jupyter notebook, displays the motion of the eight points placed on the rolling circle of radius $r=R/2$, along the corresponding diameters in the base circle.
In [3]:
import numpy as np
from numpy import pi, cos, sin
import copy
In [4]:
import plotly.plotly as py
from plotly.grid_objs import Grid, Column
import time
Set the layout of the plot:
In [5]:
axis=dict(showline=False,
zeroline=False,
showgrid=False,
showticklabels=False,
range=[-1.1,1.1],
autorange=False,
title=''
)
layout=dict(title='',
font=dict(family='Balto'),
autosize=False,
width=600,
height=600,
showlegend=False,
xaxis=dict(axis),
yaxis=dict(axis),
hovermode='closest',
shapes=[],
updatemenus=[dict(type='buttons',
showactive=False,
y=1,
x=1.2,
xanchor='right',
yanchor='top',
pad=dict(l=10),
buttons=[dict(label='Play',
method='animate',
args=[None, dict(frame=dict(duration=90, redraw=False),
transition=dict(duration=0),
fromcurrent=True,
mode='immediate'
)]
)]
)]
)
Define the base circle:
In [6]:
layout['shapes'].append(dict(type= 'circle',
layer= 'below',
xref= 'x',
yref='y',
fillcolor= 'rgba(245,245,245, 0.95)',
x0= -1.005,
y0= -1.005,
x1= 1.005,
y1= 1.005,
line= dict(color= 'rgb(40,40,40)', width=2
)
)
)
In [7]:
def circle(C, rad):
#C=center, rad=radius
theta=np.linspace(0,1,100)
return C[0]+rad*cos(2*pi*theta), C[1]-rad*sin(2*pi*theta)
Prepare data for animation to be uploaded to Plotly cloud:
In [8]:
def set_my_columns(R=1.0, ratio=3):
#R=the radius of base circle
#ratio=R/r, where r=is the radius of the rolling circle
r=R/float(ratio)
xrol, yrol=circle([R-r, 0], 0)
my_columns=[Column(xrol, 'xrol'), Column(yrol, 'yrol')]
my_columns.append(Column([R-r, R], 'xrad'))
my_columns.append(Column([0,0], 'yrad'))
my_columns.append(Column([R], 'xstart'))
my_columns.append(Column([0], 'ystart'))
a=R-r
b=(R-r)/float(r)
frames=[]
t=np.linspace(0,1,50)
xpts=[]
ypts=[]
for k in range(t.shape[0]):
X,Y=circle([a*cos(2*pi*t[k]), a*sin(2*pi*t[k])], r)
my_columns.append(Column(X, 'xrcirc{}'.format(k+1)))
my_columns.append(Column(Y, 'yrcirc{}'.format(k+1)))
#The generator point has the coordinates(xp,yp)
xp=a*cos(2*pi*t[k])+r*cos(2*pi*b*t[k])
yp=a*sin(2*pi*t[k])-r*sin(2*pi*b*t[k])
xpts.append(xp)
ypts.append(yp)
my_columns.append(Column([a*cos(2*pi*t[k]), xp], 'xrad{}'.format(k+1)))
my_columns.append(Column([a*sin(2*pi*t[k]), yp], 'yrad{}'.format(k+1)))
my_columns.append(Column(copy.deepcopy(xpts), 'xpt{}'.format(k+1)))
my_columns.append(Column(copy.deepcopy(ypts), 'ypt{}'.format(k+1)))
return t, Grid(my_columns)
In [9]:
def set_data(grid):
return [dict(xsrc=grid.get_column_reference('xrol'),#rolling circle
ysrc= grid.get_column_reference('yrol'),
mode='lines',
line=dict(width=2, color='blue'),
name='',
),
dict(xsrc=grid.get_column_reference('xrad'),#radius in the rolling circle
ysrc= grid.get_column_reference('yrad'),
mode='markers+lines',
line=dict(width=1.5, color='blue'),
marker=dict(size=4, color='blue'),
name=''),
dict(xsrc=grid.get_column_reference('xstart'),#starting point on the hypocycloid
ysrc= grid.get_column_reference('ystart'),
mode='marker+lines',
line=dict(width=2, color='red', shape='spline'),
name='')
]
Set data for each animation frame:
In [10]:
def set_frames(t, grid):
return [dict(data=[dict(xsrc=grid.get_column_reference('xrcirc{}'.format(k+1)),#update rolling circ position
ysrc=grid.get_column_reference('yrcirc{}'.format(k+1))
),
dict(xsrc=grid.get_column_reference('xrad{}'.format(k+1)),#update the radius
ysrc=grid.get_column_reference('yrad{}'.format(k+1))#of generating point
),
dict(xsrc=grid.get_column_reference('xpt{}'.format(k+1)),#update hypocycloid arc
ysrc=grid.get_column_reference('ypt{}'.format(k+1))
)
],
traces=[0,1,2]) for k in range(t.shape[0])
]
Animate the generation of a hypocycloid with 3 cusps(i.e. $R/r=3$):
In [30]:
py.sign_in('empet', 'my_api_key')#access my Plotly account
t, grid=set_my_columns(R=1, ratio=3)
py.grid_ops.upload(grid, 'animdata-hypo3'+str(time.time()), auto_open=False)#upload data to Plotly cloud
Out[30]:
In [31]:
data1=set_data(grid)
frames1=set_frames(t, grid)
title='Hypocycloid with '+str(3)+' cusps, '+'<br>generated by a fixed point of a circle rolling inside another circle; R/r=3'
layout.update(title=title)
fig1=dict(data=data1, layout=layout, frames=frames1)
py.icreate_animations(fig1, filename='anim-hypocycl3'+str(time.time()))
Out[31]:
Hypocycloid with four cusps (astroid):
In [32]:
t, grid=set_my_columns(R=1, ratio=4)
py.grid_ops.upload(grid, 'animdata-hypo4'+str(time.time()), auto_open=False)#upload data to Plotly cloud
Out[32]:
In [33]:
data2=set_data(grid)
frames2=set_frames(t, grid)
title2='Hypocycloid with '+str(4)+' cusps, '+'<br>generated by a fixed point of a circle rolling inside another circle; R/r=4'
layout.update(title=title2)
fig2=dict(data=data2, layout=layout, frames=frames2)
py.icreate_animations(fig2, filename='anim-hypocycl4'+str(time.time()))
Out[33]:
Degenerate hypocycloid (R/r=2):
In [11]:
t, grid=set_my_columns(R=1, ratio=2)
py.grid_ops.upload(grid, 'animdata-hypo2'+str(time.time()), auto_open=False)#upload data to Plotly cloud
Out[11]:
In [12]:
data3=set_data(grid)
frames3=set_frames(t, grid)
title3='Degenerate Hypocycloid; R/r=2'
layout.update(title=title3)
fig3=dict(data=data3, layout=layout, frames=frames3)
py.icreate_animations(fig3, filename='anim-hypocycl2'+str(time.time()))
Out[12]:
In [14]:
from IPython.core.display import HTML
def css_styling():
styles = open("./custom.css", "r").read()
return HTML(styles)
css_styling()
Out[14]: