Skip to content

Orbit Analysis

Binder

The analemma.orbit module calculates Keplerian orbits. In this notebook we will use it to explore the properties of Earth's orbit which, together with its rotation about an axis inclined relative to its orbit, result in The Equation of Time.

import math
import numpy as np
from numpy import sin, cos
import matplotlib.pyplot as plt
from analemma import orbit, geometry

Earth's orientation and orbit.

earth = orbit.PlanetParameters.earth()

# start time at perihelion, sample once per mean day
t_integers = np.arange(int(earth.N))
t = earth.T_d * t_integers

print(f"t has {len(t)} times from {t[0]} to {t[-1]} seconds")
t has 365 times from 0 to 31449600 seconds

So, the variable \(t\) is our timeline, sampling one year at (mean) daily intervals.

The hour angle formula derived in The Hour Angle has inputs:

  • \(\sigma(t)\) - the progress of Earth on its orbit
  • \(\psi(t)\) - the spin of the Earth
  • \(\alpha\) - the tilt of the Earth's axis

Start by modeling \(\sigma(t)\) and \(\psi(t)\) linearly to isolate the effect of \(\alpha\).

pi = np.pi

# psi increases linearly with time, one complete revolution after one siderial day,
# and an offset of rho
psi = np.mod(earth.rho + earth.om_sd*t, 2*pi)
# Increase sigma at constant rate of 2*pi per solar year to isolate effect of alpha.
# Orbit (phi) starts at perihelion which is earth.rho degrees past -e_1
# and sigma starts at +e_1 which is pi radians away from -e_1, hence pi + earth.rho.
sigma = np.mod(pi + earth.rho + earth.om_y*t, 2*pi)

# calc hour angle
mu = np.mod(geometry.hour_angle(earth.alpha, sigma, psi), 2*pi)
mu0 = np.mod(geometry.hour_angle(0, sigma, psi), 2*pi)
mu45 = np.mod(geometry.hour_angle(45 / 180 * pi, sigma, psi), 2*pi)

def s2d(seconds):
    "Convert seconds to days"
    return seconds / 24 / 3600

def r2d(radians):
    "Convert radians to degrees"
    return radians / pi * 180

fig, ax = plt.subplots()
ax.plot( s2d(t), r2d(pi - mu0), label=r"$\alpha = 0^\circ$" )
ax.plot( s2d(t), r2d(pi - mu), label=r"$\alpha = 23.5^\circ$ (Earth)" )
ax.plot( s2d(t), r2d(pi - mu45), label=r"$\alpha = 45^\circ$" )
ax.legend()
ax.grid()
ax.set_xlabel( "Number of days since perihelion" )
ax.set_ylabel( r"Solar hour angle $\mu$ (degrees)" )
ax.set_title( r"Effect of planetary axis tilt ($\alpha$) on hour angle" )
Text(0.5, 1.0, 'Effect of planetary axis tilt ($\\alpha$) on hour angle')

png

Orbital Time

Now to introduce Keplerian dynamics and change the model for \(\sigma(t)\). The orbit \((r(t), \phi(t))\) can be parameterized as \(r(s)\), \(\phi(s)\) and \(t(s)\) as derived in the appendix of my paper, just quoted here. Starting with \(t(s)\):

s_vector = np.linspace(-pi/earth.Om/2, pi/earth.Om/2, 365)
t_vector = orbit.orbital_time(s_vector)
t_vector_eccentric = orbit.orbital_time(s_vector, earth.clone_with_eccentricity(0.5))
t_vector_very_eccentric = orbit.orbital_time(s_vector, earth.clone_with_eccentricity(0.99))

fig, ax = plt.subplots()
ax.plot( s_vector, s2d(t_vector), label=f"e = {earth.e} (Earth)" )
ax.plot( s_vector, s2d(t_vector_eccentric), label=f"e = {0.5}" )
ax.plot( s_vector, s2d(t_vector_very_eccentric), label=f"e = {0.99}" )
ax.legend()
ax.grid()
ax.set_xlabel( 'Spinor time parameter' )
ax.set_ylabel( 'Absolute time t in days' )
ax.set_title("Orbital Time")
Text(0.5, 1.0, 'Orbital Time')

png

Invert the time relation

In order to calcuate \(r(t) = r(t(s))\) and \(\phi(t) = \phi(t(s))\), we need invert the functional form for \(t(s)\).

\(s(t)\) is calculated by orbit.spinor_time().

t_eval = 50 * 24*3600 # evaluate 50 days into orbit
s1 = orbit.spinor_time(t_eval)
assert math.isclose(t_eval, orbit.orbital_time(s1), rel_tol=1e-9) # round trip
e99 = 0.99 # very eccentric
s2 = orbit.spinor_time(t_eval, earth.clone_with_eccentricity(e99))
assert not math.isclose(s1, s2, abs_tol=1e-5) # different e means different s
assert math.isclose(t_eval, orbit.orbital_time(s2, earth.clone_with_eccentricity(e99)), rel_tol=1e-7) # round trip

Orbital Radius

Although not necessary to calculate the equation of time, for completeness let's look at the radial coordinate (from the orbit's focus) over time also:

e_venus = 0.0068
e_mars = 0.0934

ven = earth.clone_with_eccentricity(e_venus)
mar = earth.clone_with_eccentricity(e_mars)

t_days = t/3600/24

orb_rad_ratio_venus = orbit.orbital_radius(orbit.spinor_time(t, ven), ven)/earth.a
orb_rad_ratio_earth = orbit.orbital_radius(orbit.spinor_time(t))/earth.a
orb_rad_ratio_mars = orbit.orbital_radius(orbit.spinor_time(t, mar), mar)/earth.a

fig, ax = plt.subplots()
ax.plot( t_days, orb_rad_ratio_venus, label=f"e = {e_venus} (Venus)" )
ax.plot( t_days, orb_rad_ratio_earth, label=f"e = {earth.e} (Earth)" )
ax.plot( t_days, orb_rad_ratio_mars, label=f"e = {e_mars} (Mars)" )
ax.legend()
ax.grid()
ax.set_xlabel("Orbital time (days)")
ax.set_ylabel("Orbital radius $r$ / semi-major axis $a$ ($r/a$)")
ax.set_title("Orbital Radius")
Text(0.5, 1.0, 'Orbital Radius')

png

Orbital Angle

The angle of the orbit (again in radial coordinates with origin at the orbit's focus) varies non-linearly with time for an elliptical orbit, and this provides the second component of the Equation of Time.

e_circle = 0.0
e_mercury = 0.2056
e_halleys_comet = 0.9671

circ = earth.clone_with_eccentricity(e_circle)
merc = earth.clone_with_eccentricity(e_mercury)
hall = earth.clone_with_eccentricity(e_halleys_comet)

t_days = t_vector/3600/24
orb_angle_circle = orbit.orbital_angle(orbit.spinor_time(t, circ), circ)
orb_angle_earth = orbit.orbital_angle(orbit.spinor_time(t))
orb_angle_mercury = orbit.orbital_angle(orbit.spinor_time(t, merc), merc)
orb_angle_halleys_comet = orbit.orbital_angle(orbit.spinor_time(t, hall), hall)

fig, ax = plt.subplots()
ax.plot( t_days, r2d(orb_angle_circle), "+", label=f"e = {e_circle} (Perfect Circle)" )
ax.plot( t_days, r2d(orb_angle_earth), label=f"e = {earth.e} (Earth)" )
ax.plot( t_days, r2d(orb_angle_mercury), label=f"e = {e_mercury} (Mercury)" )
ax.plot( t_days, r2d(orb_angle_halleys_comet), label=f"e = {e_halleys_comet} (Halley's Comet)" )
ax.legend()
ax.grid()
ax.set_xlabel("Orbital time (days)")
ax.set_ylabel("Orbital angle")
ax.set_title("Orbital Angle")
Text(0.5, 1.0, 'Orbital Angle')

png

Orbital Path

Again, not necessary for the Equation of Time, but for completeness, let's look at the Earth's orbital path:

def orbit_path(s, planet = earth):
    return (orbit.orbital_radius(orbit.spinor_time(t, planet), planet)*cos(orbit.orbital_angle(orbit.spinor_time(t, planet), planet)) / planet.a,
            orbit.orbital_radius(orbit.spinor_time(t, planet), planet)*sin(orbit.orbital_angle(orbit.spinor_time(t, planet), planet)) / planet.a) 

path_earth = orbit_path(orbit.spinor_time(t))
circ = earth.clone_with_eccentricity(e_circle)
path_circle = orbit_path(orbit.spinor_time(t, circ), circ)

fig, ax = plt.subplots()
ax.plot(*path_circle, "+", label="Perfect Circle")
ax.plot(*path_earth, label="Earth")
ax.grid()
ax.set_aspect("equal", "box")
ax.legend()
ax.set_xlabel("x/a")
ax.set_ylabel("y/a")
ax.set_title("Orbital Path (distance in units of semi-major axis, $a$)")
Text(0.5, 1.0, 'Orbital Path (distance in units of semi-major axis, $a$)')

png

Effect of Orbital Eccentricity

We can isolate the effect of a Keplerian orbit by setting the Earth's axis tilt angle \(\alpha\) to zero:

# now account for orbit in rate of increase of sigma
sigma = np.mod(pi + earth.rho + orbit.orbital_angle(orbit.spinor_time(t)), 2*pi)

mu = np.mod(geometry.hour_angle(0.0, sigma, psi), 2*pi)

fig, ax = plt.subplots()
ax.plot(s2d(t), r2d(mu))
ax.grid()
ax.set_xlabel("Days since perihelion")
ax.set_ylabel(r"Hour angle ($\mu$)")
_ = ax.set_title("Effect of orbital eccentricity ($e$) on hour angle")

png

For the mathematics of the analemma, see the notebooks starting with Setup and Definitions and culminating in The Analemma.