Milankovitch cycles#

Dynamic Earth and Planets Stratigraphy (Year 1) High-Temperature Geochemistry Climate Gravity, Magnetism, and Orbital Dynamics

Milankovitch cycles are the effects of changes in Earth’s movements on Earth’s climate over thousands of years.

In 1920s, Serbian geophysicist and astronomer Milutin Milanković hypothesised that eccentricity, obliquity (axial tilt) and precession cause cyclical variations in solar radiation reaching Earth and influence climate patterns.


(1) Eccentricity (\(e\)) - this parameter describes how circular an orbit is:

* $e=0$ - ciruclar orbit
* $0<e<1$ - elliptic orbit
* $e = 1$ - parabolic trajectory
* $e>1$ - hyperbolic trajectory

Most bodies in solar system have \(e \ll 1\). Pluto and Mercury have \(e=0.25\) and \(e=0.21\). For eliptical orbits, the eccentricity can be calculated with periapsis and apoapsis:


where \(r_p=a(1-e)\), \(r_a=a(1-e)\) and \(a\) is the semi-major axis:

For Earth, the eccentricity varies between \(0.0034\) and \(0.058\) and is almost circular. The variations take place in \(95,000\) and \(125,000\) year cycles with beat period of \(400,000\) years. They loosely combine into \(100,000\) year cycles. Earth’s eccentricity changes are caused primarily by perturbations from Saturn and Jupiter.

(2) Obliquity - obliquity is the angle at which the Earth is titled to the ecliptic plane.

Due to gravitational perturbations, primarily from Saturn and Jupiter, it varies between \(22.1^\circ\) and \(24.5^\circ\). Increased obliquity causes more pronounced seasonal differences in temperatures. On Earth, obliquity changes in \(41,000\) year cycles.

(3) Precession - precession refers to direction of rotation axis and looks like a spinning top toy.

Precession on Earth takes \(21,000\) year cycles. Axial precession makes contrasts in extremes between hemisphere. It also gradually changes when seasons start.

Plotting Milankovitch cycles in time#

Orbital forcing data can be downloaded from here based on Laskar et al. (2004) paper “A long term numerical solution for the insolation quantities of the Earth”.

The dataset coprises of:

  • time - in thousands of years from present

  • eccentricity - calculated with \(\frac{r_a-r_p}{r_a+r_p}\)

  • obliquity - angle in radians

  • perihelion - angle between equinox & perihelion in radians

  • insolation - at \(65^\circ N\) at summer solstices (\(W/m^2\))

  • global insolation - solar flux in \(W/m^2\)

import pandas as pd
import numpy as np
import matplotlib.pyplot as plt

data = pd.read_csv("data/milankovitch.csv")
time eccentricity obliquity perihelion insolation global_insolation
0 -20000 0.028373 0.413422 3.641942 511.372693 342.087719
1 -19999 0.027002 0.412171 3.927210 515.449123 342.074725
2 -19998 0.025691 0.410716 4.214007 517.235678 342.062901
3 -19997 0.024361 0.409094 4.498755 516.561670 342.051515
4 -19996 0.022902 0.407352 4.783758 513.516924 342.039710

The data does not contain precession. To display precession, we can use precession index calculated via:

\[\text{precession index} = e \sin{\varpi},\]

where \(\varpi\) is the perihelion given in the database.

data["precession_index"] = data.eccentricity*(np.sin(data.perihelion))

Apart from orbital forcing, we can also plot some isotope data to find whether orbital cycles are reflected in data.

In this example, we downloaded composite antarctic ice core data with \(CO_2\) record for past \(800,000\) years compiled by Bereiter et al. (2015). The data is available from NOAA and can be downloaded here.

antarctica_data = pd.read_csv('data/antarctica_co2_composite.csv')
age_gas_calBP co2_ppm co2_1s_ppm
0 -51.03 368.02 0.06
1 -48.00 361.78 0.37
2 -46.28 359.65 0.10
3 -44.41 357.11 0.16
4 -43.08 353.95 0.04

Now, having both datasets we can plot on the same graph precession index, obliquity, eccentricity, insolation and \(CO_2\) record from Antarctica:

fig, axes = plt.subplots(5,1, figsize=(7,7), sharex=True)

ax1 = axes[0]
ax2 = axes[1]
ax3 = axes[2]
ax4 = axes[3]
ax5 = axes[4]

ax1.plot(-data.time, data.precession_index, color="#4dd327ff")
ax2.plot(-data.time, np.rad2deg(data.obliquity), color="#00bfffff")
ax3.plot(-data.time, data.eccentricity, color="#ff981cff")
ax4.plot(-data.time, data.insolation, color="#6931dfff")
ax5.plot(antarctica_data.age_gas_calBP/1000., antarctica_data.co2_ppm, color="#eb3d4eff")

ax1.set_title(r"Precession index ($e\times\sin{\varpi}$)")
ax4.set_title("Mean daily insolation at 65$^\circ$N on summer solstice")
ax5.set_title("CO$_2$ (ppmv) from Bereiter et al. (2015)")

ax2.set_ylabel("Degrees ($^\circ$)")
ax5.set_ylabel("CO$_2$ (ppmv)")

for ax in axes:
    for i in range(8):
        ax.axvline(100+100*i, color='gray', dashes=(4,2), lw=1)

plt.xlim(0, 800)
plt.xlabel("kyr ago")

Looking at the graph, we can see that \(CO_2\) peaks align with peaks in eccentricity, at \(\sim 100,000\) years intervals. Additionally, higher changes in insolation correlate with higher precession index.

Interactive plot of the orbital cycles#

from plotly.subplots import make_subplots
import plotly.graph_objects as go

fig = make_subplots(rows=4, cols=1, shared_xaxes=True)

fig.add_trace(go.Scatter(x=data.time, y=data.precession_index,
                         name="Precession Index",
                         line=dict(color="limegreen", width=1.5)), row=1,col=1)

fig.add_trace(go.Scatter(x=data.time, y=data.eccentricity,
                         line=dict(color="cyan", width=1.5)), row=3,col=1)

fig.add_trace(go.Scatter(x=data.time, y=np.rad2deg(data.obliquity),
                         line=dict(color="orange", width=1.5)), row=2,col=1)

fig.add_trace(go.Scatter(x=data.time, y=data.insolation,
                         name="Mean daily insolation at 65N on summer solstice",
                         line=dict(color="orchid", width=1.5)), row=4,col=1)

                 xaxis4_title='Kiloyears from present',
                 title="Milankovitch forcing throughout geological history")

fig.update_yaxes(title="", row=1, col=1, fixedrange=True)
                 row=2, col=1, fixedrange=True)
                 row=3, col=1, fixedrange=True, range=[-0.1,0.1])
                 row=4, col=1, fixedrange=True)