This is a subsection of my Kepler Ptolemy project, in the math section. This is a big part of what I worked on in that project, but I felt it was more fitting in the programming section. This will be a shorter description, but it still taught me some valuable things.
What inspired this project was simply a desire to see what the Kepler Ptolemy system could look like when we eventually solved the system of differential equations. Cris (Cristopher Moore, who was my mentor for the larger Kepler Ptolemy project) delegated it to me, as it’s relatively simple and I’m familiar enough with Python to do it easily (or so I thought). My initial approach was to choose some angle step from the equant, then step out along that line until the area swept out by the most recent step is equal to our initial area step (this ensures that angular momentum is conserved, as discussed in the main post):
final_angle=np.piangle_step=0.001*np.pidistance_step=0.0001r=np.array((2,0))new_r=r+(0,math.sin(angle_step)*mag(r))l=np.array((1,0))fig,ax=plt.subplots(1,1)ax.plot((0,l[0]),(0,l[1]),'o',color='grey')semi_perim=0.5*(mag(r-l)+mag(r-new_r)+mag(new_r-l))area_step=math.sqrt(semi_perim*(semi_perim-mag(r-l))*(semi_perim-mag(r-new_r))*(semi_perim-mag(new_r-l)))print(area_step)points=[r]foriinnp.arange(0,final_angle,angle_step):## update and record the position
r=step(r,l,angle_step,area_step,distance_step)points.append(r)points=np.array(points)ax.plot(points[:,0],points[:,1])plt.show()defstep(r,l,angle_step,area_step,distance_step):## uses the geometry of a single step to determine the length of the next radius
pol_r=[mag(r),math.acos(r[0]/mag(r))]ifr[1]<0:## adjust range of acos properly
pol_r[1]=2*np.pi-pol_r[1]calc_area=0pol_new_r=np.array([0,pol_r[1]+angle_step])len_base=mag(r-l)whilecalc_area<area_step:pol_new_r[0]+=distance_stepnew_r=polar_to_cart(pol_new_r)len_short_side=mag(new_r-r)len_long_side=mag(new_r-l)semi_perim=0.5*(len_base+len_short_side+len_long_side)calc_area=math.sqrt(semi_perim*(semi_perim-len_base)*(semi_perim-len_short_side)*(semi_perim-len_long_side))## Heron's formula
print(calc_area)returnnew_rdefmag(vec):## returns the magnitutde of a vector
returnmath.sqrt(vec[0]**2+vec[1]**2)defangle(vec):## returns the angle of a vector
ifmag(vec)==0:return0angle=math.acos(vec[0]/mag(vec))ifvec[1]<0:## adjust range of acos
angle=2*np.pi-anglereturnangledefpolar_to_cart(point):## converts a point from polar to cartesian coordinates
r,theta=pointx,y=r*math.cos(theta),r*math.sin(theta)returnnp.array((x,y))if__name__=='__main__':main()
This method ultimately turned out to be unfruitful: If one were to run the above code, it would produce a mess of lines radiating from the equant. This is due to errors from starting the trawl along the radial line at length zero. To solve this issue, I found studying the geometry of a single timestep dt to be helpful:
From this, it is apparent that there is an easily calculable point close to but not past the new position of the curve: where the two lines cross in the center of the diagram. Updating my step function in the above program to start at this new point, it began producing viable images:
However, I quickly realized something about this entire geometry: If I kept track of the velocity at all times (by subtracting the new position from the old one and dividing by the timestep), I could avoid any type of for loop in each individual timestep, which turned out to be very simple to implement. The final version of the program is below:
final_angle=2*np.piangle_step=0.001*np.pi## can be set to improve resolution; however, higher resolution causes EARLIER divergence
num_derivs=0## number of derivatives to take and graph. 0 for only avocado
## initial position and velocity. Current conditions are good for producing stable avocados
r=np.array((-1,0))v=np.array((0,-1))## only valid when r = -1, 0 and v = 0, -1
ellipse=False## display an ellipse of the given eccentricity on top of the avocado
e=0.5## eccentricity
### NON ADJUSTABLE THINGS ###
l=(2*e*mag(r)/(1-e),0)fig,ax=plt.subplots(1,1)ax.plot((0,l[0]),(0,l[1]),'o',color='grey')area_step=0.5*angle_step*mag(v)*mag(r-l)*math.sin(angle(v)-angle(r-l))### ELLIPSE ###
a=mag(l)/2+mag(r)ell_points=[]foriinnp.arange(0,final_angle,angle_step):## calculate an equivalent set of points to the avocado curve for the ellipse
ell_r=(a*(1-e**2))/(1-e*math.cos(i-angle(l)+np.pi))ell_points.append(polar_to_cart((ell_r,i+np.pi)))ell_points=np.array(ell_points)## give graph reasonable constraints to prevent asmptotes from dominating
ax.set_xlim(min(ell_points[:,0])-0.5,max(ell_points[:,0])+0.5)ax.set_ylim(min(ell_points[:,1])-0.5,max(ell_points[:,1])+0.5)ifellipse:ax.plot(ell_points[:,0],ell_points[:,1],color='orange')### AVOCADO ###
points=[r]foriinnp.arange(0,final_angle,angle_step):## update and record the position
r=step(r,l,angle_step,area_step)points.append(r)points=np.array(points)ax.plot(points[:,0],points[:,1])plt.show()### DERIVATIVES ###
ifnum_derivs:## do nothing if num_derivs is 0
num_derivs+=1derivs=[]foriinrange(len(points)):new_set=[]forjinrange(num_derivs):new_set.append(take_deriv(points,i,j,angle_step))derivs.append(new_set)derivs=np.array(derivs[:-1*num_derivs])## cut out edge derivs
fig,axs=plt.subplots(1,num_derivs)fori,axinenumerate(axs):ax.plot(derivs[:,i,0],derivs[:,i,1])plt.show()defstep(r,l,angle_step,area_step):## uses the geometry of a single step to determine the length of the next radius
radii_angle=angle(r-l)-angle(r)new_r=((mag(r)*math.sin(radii_angle)-(2*area_step/mag(r-l)))/math.sin(radii_angle-angle_step),angle(r)+angle_step)returnpolar_to_cart(new_r)defmag(vec):## returns the magnitutde of a vector
returnmath.sqrt(vec[0]**2+vec[1]**2)defangle(vec):## returns the angle of a vector
ifmag(vec)==0:return0angle=math.acos(vec[0]/mag(vec))ifvec[1]<0:## adjust range of acos
angle=2*np.pi-anglereturnangledefpolar_to_cart(point):## converts a point from polar to cartesian coordinates
r,theta=pointx,y=r*math.cos(theta),r*math.sin(theta)returnnp.array((x,y))defcomb(n,r):## n choose r
returnmath.factorial(n)/(math.factorial(r)*math.factorial(n-r))deftake_deriv(points,i,d,time_step):## approximate the dth derivative of points
deriv=0forjinrange(d+1):deriv+=comb(d,j)*(-1)**j*points[(i+j)%len(points)]deriv/=time_step**dreturnlist(deriv)if__name__=='__main__':main()
This version has several additional bells and whistles when compared to the first program, but the underlying idea is the same. This also allows the user to input the desired shape of curve as an “eccentricty,” much the same as an ellipse’s eccentricity. This also shows that the Kepler Ptolemy curves are indeed a one-parameter family, meaning every shape the closed form can take can be characterized by a single number.
This project, although summarized quickly in this post, took a lot of effort and debugging on my part. I learned some overarching programming concepts, such as looking for a more efficient representation of the problem and new debugging techniques, as well as more specific elements relevant to this problem, such as the one-parameter nature of the Kepler Ptolemy curves.
This project has spanned over a year and will culminate in the publication of an academic paper in a scientific journal. The subject matter is highly arcane ...
In my post on my Jacob’s ladder, I wrote mainly about the process of building it and how it works. I also mentioned briefly the way the power supply works, b...