Example: Planetary Orbit#
Let’s consider an example of integrating the orbit of Earth around the Sun (we’ll consider the mass of the Earth to be tiny compared to the Sun so we can neglect the Sun’s motion around the center of mass).
Kepler’s third law tells us:
where \(a\) is the semi-major axis and \(P\) is the period of the orbit. If we work in units of solar masses, astronomical units, and years, then this becomes:
which implies that \(G M_\star = 4 \pi^2\).
The evolution of the planet is given by
where \({\bf r} = (x, y)\) and \({\bf v} = (v_x, v_y)\) are vectors. This means that we have 4 equations overall to advance.
Note
We use \(\dot{y}\) as our notation for a time-derivative, \(dy/dt\),
Important
This is 4 first-order ordinary differential equations, so we need 4 initial conditions.
We want to evolve the orbit of Earth around the Sun using these ODEs.
Euler’s Method#
Using Euler’s method, as we just learned, the solution begins with a first-order accurate difference approximation to the derivative:
or
Implementation#
We’ll store the state of the solution at a single instance in time in a struct:
struct OrbitState {
double t;
double x;
double y;
double vx;
double vy;
};
and we’ll save the solution at each step in a vector:
std::vector<OrbitState> orbit_history{};
This corresponds to the “array-of-structs” storage scheme we discussed in our Vector of Structs section.
To make our code flexible, we’ll write the following functions:
A righthand side function, that takes an
OrbitStateand returns anOrbitState:OrbitState rhs(const OrbitState& state);
We can compute all of the ODE righthand sides from the input state and then return the derivatives using an
OrbitState(there, each component will actually be the derivative in time).As we’ve seen a few times now, we pass this in as a
constreference for performance. Theconstensures that it is read-only.A function to write out the history:
void write_history(const std::vector<OrbitState>& history);
We can adapt this as desired to write to the screen or a file (which we’ll see soon).
An integration function:
std::vector<OrbitState> integrate(const double a, const double tmax, const double dt);
This manages the time-stepping, implementing Euler’s method and storing the solution in our history vector each step. At the end of integration it will return the history.
An important parameter here is
dt—the size of our timestep. We’ll see that it has a large influence on the quality of our solution.
Implementation#
Here’s our implementation:
#include <iostream>
#include <iomanip>
#include <vector>
#include <cmath>
#include <numbers>
#include <format>
// G * Mass in AU, year, solar mass units
const double GM = 4.0 * std::numbers::pi * std::numbers::pi;
struct OrbitState {
double t;
double x;
double y;
double vx;
double vy;
};
OrbitState rhs(const OrbitState& state) {
OrbitState dodt{};
// dx/dt = vx; dy/dt = vy
dodt.x = state.vx;
dodt.y = state.vy;
// d(vx)/dt = - GMx/r**3; d(vy)/dt = - GMy/r**3
double r = std::sqrt(state.x * state.x + state.y * state.y);
dodt.vx = - GM * state.x / std::pow(r, 3);
dodt.vy = - GM * state.y / std::pow(r, 3);
return dodt;
}
void write_history(const std::vector<OrbitState>& history) {
for (const auto& o : history) {
std::cout
<< std::format("{:11.8f} {:11.8f} {:11.8f} {:11.8f} {:11.8f}\n",
o.t, o.x, o.y, o.vx, o.vy);
}
}
std::vector<OrbitState> integrate(const double a,
const double tmax,
const double dt_in) {
// how the history of the orbit
std::vector<OrbitState> orbit_history{};
// set initial conditions
OrbitState state{};
// assume circular orbit on the x-axis, counter-clockwise orbit
state.t = 0.0;
state.x = a;
state.y = 0.0;
state.vx = 0.0;
state.vy = std::sqrt(GM / a);
orbit_history.push_back(state);
double dt = dt_in;
// integration loop
while (state.t < tmax) {
// if the final dt step takes us past our stopping time
// (tmax), cut the timestep
if (state.t + dt > tmax) {
dt = tmax - state.t;
}
// get the derivatives
auto state_derivs = rhs(state);
// update the state
state.t += dt;
state.x += state_derivs.x * dt;
state.y += state_derivs.y * dt;
state.vx += state_derivs.vx * dt;
state.vy += state_derivs.vy * dt;
orbit_history.push_back(state);
}
return orbit_history;
}
int main() {
double tmax = 1.0;
double dt = 0.05;
double a = 1.0; // 1 AU
auto orbit_history = integrate(a, tmax, dt);
write_history(orbit_history);
}
Some notes:
We make
GMaconst, since it should not change.In our
integratefunction, we create anOrbitStatenamedstate. This will hold the current state at the start of each step / loop iteration, and be advanced to the new time at the end of the loop iteration.Note that when we do:
orbit_history.push_back(state);
We are making a copy of
stateand storing it in ourorbit_historyvector.We have a check to ensure that the last step does not take us past the maximum time we want to evolve for:
if (state.t + dt > tmax) { dt = tmax - state.t; }
Running#
This is setup for Earth (the semi-major axis is 1 AU), and to integrate for 1 year. After this time, the Earth should be right back where it started. Any difference in the final position from the initial position is integration error.
From our method, we expect that the error will decrease by a factor of 2 each time we cut the timestep in half.
We control is the timestep. Our default, dt = 0.05 corresponds to
1/20th of a year. The main question is—how small does our timestep
need to be to get an acceptable solution?
Plotting#
We can plot the solution using gnuplot.
First, when you run, redirect the output to a file:
./orbit_example > orbit.dat
Now run gnuplot:
gnuplot
Again, we’ll use plot and plot different columns against
one another:
set size ratio -1
set xlabel "x"
set ylabel "y"
plot 'orbit.dat' using 2:3 notitle w l
This will make something like:
Tip
We used a new gnuplot command here:
set size ratio -1
This ensures that the aspect ratio of the coordinates is preserved (so a circle will appear as a circle in our plot).
You can save the plot as:
set term png enhanced
set output "orbit.png"
replot
Look how bad this is!!!
We should have a nice circular orbit, but it goes way off a circle and out to much greater distances in the solar system.
The problem here is that our timestep is too large—truncation error is dominating.
try it…
Let’s see how the solution behaves as we cut the timestep.