UP | HOME

MRKISS Library Example
The Three Body Problem

Author: Mitch Richling
Updated: 2025-08-03 10:03:28
Generated: 2025-08-03 10:10:06

Copyright © 2025 Mitch Richling. All rights reserved.

Table of Contents

1. Introduction

This is one of my favorite IVP example problems.

This is a dimensionless, restricted three gravitational body problem. The Earth is stationary at the origin. The Moon orbits the Earth at a radius of \(1\) and is at \((1,0)\) at \(t=0\). The mass ratio is \(\frac{100}{8145}\). The position, \((x_1,x_2)\), and velocity, \((v_1,v_2)\), are governed by the following differential equation:

\[\begin{align*} \frac{\mathrm{d}v_1}{\mathrm{d}t} & = v_1 \\ \frac{\mathrm{d}v_2}{\mathrm{d}t} & = v_2 \\ \frac{\mathrm{d}x_1}{\mathrm{d}t} & = 2 v_2 + x_1 - \frac{\mu (x_1 + \mu -1)}{D_1} - \frac{(1 - \mu) (x_1 + \mu)}{D_2} \\ \frac{\mathrm{d}x_2}{\mathrm{d}t} & = -2 v_1 + x_2 - \frac{\mu x_2}{D_1} - \frac{(1 - \mu) x_2}{D_2} \end{align*}\]

Where \(D_1\) and \(D_2\) are defined as:

\[\begin{align*} D_1 & = \sqrt{\left(x_2^2 + (x_1 + \mu - 1)^2\right)^3} \\ D_2 & = \sqrt{\left(x_2^2 + (x_1 + \mu)\right)^3} \end{align*}\]

We solve the equations with the following initial values:

\[\begin{align*} (x_1,x_2) &= \left(\frac{497}{500}, 0\right) \\ (v_1,v_2) & = (0, -2.0015851063790825224) \end{align*}\]

Under these conditions the satellite will return to it's initial position after completing a three lobed orbit at \(t=17.06521656015796\).

The code for this example is found in ~examples/three_body.f90. Additionally the code may be found at the end of this document in the section Full Code Listing.

This example makes several MRKISS high level solver calls each illustrating a different feature or technique. In the sections that follow we examine each of those calls in detail.

2. Classical Solution

In this section we produce the classical image of the solution.

three_body.png

The solid pink curve is generated from 4000 points on the orbit generated with a fixed step size solver. This is quite expensive computationally taking about 16 milliseconds on my computer. Still, it produces a very nice curve.

! BEGIN: steps_fixed_stab_wt
call system_clock(c_beg)
call steps_fixed_stab_wt(status, istats, solution, eq, t_iv, y_iv, param, a, b1, c, t_end_o=t_end)
call system_clock(c_end)
print '(a)',       "Fixed t_delta run: "
print '(a,f10.3)', "                  Milliseconds: ", 1000*(c_end-c_beg)/DBLE(c_rate)
print '(a,i10)',   "                        Status: ", status
print '(a,i10)',   "               Solution Points: ", istats(1)
print '(a,i10)',   "          Total one_step calls: ", istats(2)
call print_solution(status, solution, filename_o="tree_body_steps_fixed_stab_wt.csv", end_o=istats(1))
! END: steps_fixed_stab_wt

The red dots on the solid pink curve are solution points generated using a classical, adaptive Runge-Kutta scheme.

The algorithms involved in the adaptive solution are far more complex than in the fixed step solution above. Yet the solver calls in these two code snippets are remarkably similar. IMO, the real value in an adaptive solver is the reduction in computational needs by making the step sizes as large as possible without sacrificing accuracy. As you can see the step size is much smaller near the singularities. The fixed run above takes 16 milliseconds while the adaptive run takes less than one millisecond.

More sophisticated libraries offer the ability to interpolate between solution points at pretty low cost while maintaining the accuracy of the overall method. For generative art and visualization we can use Hermite interpolation at an even lower cost. Normally interpolation is achieved outside the ODE solver and directly inside a visualization software platform – we simply provide the points and derivatives.

Notice in the snippet above we only report istats(1) and istats(2) because these are the only two values in istats updated by steps_fixed_stab_wt(). In the code below we add istats(4) because steps_adapt_etab_wt() updates this value.

! BEGIN: steps_adapt_etab_wt-std
call system_clock(c_beg)
call steps_adapt_etab_wt(status, istats, solution, eq, t_iv, y_iv, param, a, b1, b2, c, p1, p2, &
                         t_delta_max_o=t_delta*100, t_delta_ini_o=t_delta*20, error_tol_abs_o=[1.0e-9_rk], &
                         error_tol_rel_o=[1.0e-6_rk], t_max_o=t_end, t_end_o=t_end);
call system_clock(c_end)
print '(a)',       "Adaptive run: "
print '(a,f10.3)', "                  Milliseconds: ", 1000*(c_end-c_beg)/DBLE(c_rate)
print '(a,i10)',   "                        Status: ", status
print '(a,i10)',   "               Solution Points: ", istats(1)
print '(a,i10)',   "          Total one_step calls: ", istats(2)
print '(a,i10)',   "   y-err Adjust one_step calls: ", istats(4)
call print_solution(status, solution, filename_o="three_body_steps_adapt_etab_wt-std.csv", end_o=istats(1))
! END: steps_adapt_etab_wt-std

3. Interpolation

Most visualization tools directly support Hermite spline interpolation between points, and will happily use use the derivative information in the solution to connect the widely separated red dots in the adaptive solution with smooth curves. So it is rare that we wish to do interpolation simply to connect dots. More common is the need to "line up" two solutions on the same \(t\) values for some other reason. MRKISS can do both Hermite and linear interpolation:

three_body_interp_adapt_path.png

three_body_lin_interp_adapt_path.png

Note that Hermite interpolation provides an O(3) solution to the IVP. Linear interpolation is O(1). So both are inappropriate if high accuracy solutions are required. Here we can see the errors:

three_body_interp_adapt_error.png

The code for the above images:

! BEGIN: steps_adapt_int
call system_clock(c_beg)
isolution = 0
call seq(status, isolution(1,:), from_o=0.0_rk, to_o=t_end);            ! Create new t values
call interpolate_solution(status, isolution, solution, end_o=istats(1)) ! Preform the interpolation
call system_clock(c_end)
print '(a)',       "Adaptive hermite interpolation run: "
print '(a,f10.3)', "                  Milliseconds: ", 1000*(c_end-c_beg)/DBLE(c_rate)
print '(a,i10)',   "                        Status: ", status
call print_solution(status, isolution, filename_o="three_body_steps_adapt_std_interpolated.csv", sol_no_dy_o=1)

call system_clock(c_beg)
isolution = 0
call seq(status, isolution(1,:), from_o=0.0_rk, to_o=t_end);
! Note we must provide y_dim_o because solution really contains dy.  
call interpolate_solution(status, isolution, solution, end_o=istats(1), y_dim_o=deq_dim, sol_no_dy_o=1)
call system_clock(c_end)
print '(a)',       "Adaptive linear interpolation run: "
print '(a,f10.3)', "                  Milliseconds: ", 1000*(c_end-c_beg)/DBLE(c_rate)
print '(a,i10)',   "                        Status: ", status
call print_solution(status, isolution, filename_o="three_body_steps_adapt_std_interpolated_lin.csv", sol_no_dy_o=1)
! END: steps_adapt_int

4. Fixed Steps & Method Order

The pink curve in our first image was the result of 4000 fixed steps with a 9th order Runge-Kutta method. Why did we pick a 9th order solver? Because it allowed us to use a step size small enough to be ascetically pleasing but large enough to not produce too many points for our simple plotting program.

From the perspective of generative art and visualization, fixed time step solutions are important because they preserve "time" in animations. We can achieve fixed steps via interpolation; however, it is frequently more convenient to simply use a fixed step size solver. Method order for generative art and visualization is often thought of as a tool to allow for ascetic step sizes instead of a way to achieve a solution accuracy requirement.

In the following image we see the unsatisfactory result of using a 5th order solver with the same step size:

three_body-dp.png

The code for the above solution is identical except for a change in Runge-Kutta method arguments:

! BEGIN: steps_fixed_stab_wt-dp
call system_clock(c_beg)
call steps_fixed_stab_wt(status, istats, solution, eq, t_iv, y_iv, param, dpa, dpb, dpc, t_end_o=t_end)
call system_clock(c_end)
print '(a)',       "Fixed t_delta run: "
print '(a,f10.3)', "                  Milliseconds: ", 1000*(c_end-c_beg)/DBLE(c_rate)
print '(a,i10)',   "                        Status: ", status
print '(a,i10)',   "               Solution Points: ", istats(1)
print '(a,i10)',   "          Total one_step calls: ", istats(2)
call print_solution(status, solution, filename_o="steps_fixed_stab_wt-dp.csv", end_o=istats(1))
! END: steps_fixed_stab_wt-dp

5. Adaptive Solution With More Steps For A Nice Plot

three_body_ylen.png

If we wanted more points in the adaptive solution we could use t_delta_max_o. This will get us more points, but it's not necessarily what we want for a nice graph. What we really want for a nice graph is a fixed maximum distance between plotted points which a fixed \(\Delta{t}\) will not necessarily deliver. That said we still want the adaptive algorithm to produce points closer together when accuracy requires it. One way to achieve that is with the step processing capability of steps_adapt_etab_wt() via the stepp_o argument. Only the first two components of the solution are plotted (the position of the satellite). What we want the stepp_o subroutine to do is shrink \(\Delta{t}\) if the distance between the first two components of the solution are too far away from the first two points of the previous solution. The following subroutine will do the trick:

! BEGIN: steps_adapt_etab_wt-fix-delta-stepp
! Example subroutine to adjust t_delta in an atempt to keep y_delta under a maximum value.
! It is sloppy because we assume t_delta is linearly proportional to y_delta_len
subroutine sp_sloppy_y_delta_len_max(status, end_run, sdf_flags, new_t_delta, pnt_idx, solution, t_delta, y_delta)
  integer(kind=ik), intent(out) :: status, end_run
  real(kind=rk),    intent(out) :: new_t_delta
  integer(kind=ik), intent(out) :: sdf_flags
  integer(kind=ik), intent(in)  :: pnt_idx
  real(kind=rk),    intent(in)  :: solution(:,:), t_delta, y_delta(:)
  real(kind=rk),      parameter :: y_delta_len_max = 0.1_rk
  integer,            parameter :: y_delta_len_idxs(2) = [1, 2]
  real(kind=rk)                 :: y_delta_len
  status    = 0_ik
  end_run   = 0_ik
  sdf_flags = 0_ik
  y_delta_len = norm2(y_delta(y_delta_len_idxs))
  if ( y_delta_len > y_delta_len_max) then
     new_t_delta = t_delta * y_delta_len_max / y_delta_len
  else
     new_t_delta = -1.0_rk
  end if
end subroutine sp_sloppy_y_delta_len_max
! END: steps_adapt_etab_wt-fix-delta-stepp

This isn't a perfect solution as we make the assumpiont that the length of the difference in y-space is proportional to \(\Delta{t}\), but it works pretty well in practice. A more robust solution can be achieved by adding an sdf_o function and isolating a \(\Delta{t}\) that produces a precisely separated solution. We touch on this topic later when we consider the steps_condy_stab_*t() solvers.

We "wire up" the above subroutine into steps_adapt_etab_wt() via the stepp_o argument. Also make note of the addition of istats(5) to our output report. This value is the number of steps recomputed because stepp_o provided a new t_delta value.

! BEGIN: steps_adapt_etab_wt-fix-delta-steps
call system_clock(c_beg)
call steps_adapt_etab_wt(status, istats, solution, eq, t_iv, y_iv, param, a, b1, b2, c, p1, p2, &
                         t_delta_max_o=t_delta*100, t_delta_ini_o=t_delta*20, error_tol_abs_o=[1.0e-9_rk], &
                         error_tol_rel_o=[1.0e-6_rk], t_max_o=t_end, t_end_o=t_end, &
                         stepp_o=sp_sloppy_y_delta_len_max);
call system_clock(c_end)
print '(a)',       "Adaptive run w max y_delta length: "
print '(a,f10.3)', "                  Milliseconds: ", 1000*(c_end-c_beg)/DBLE(c_rate)
print '(a,i10)',   "                        Status: ", status
print '(a,i10)',   "               Solution Points: ", istats(1)
print '(a,i10)',   "          Total one_step calls: ", istats(2)
print '(a,i10)',   "   y-err Adjust one_step calls: ", istats(4)
print '(a,i10)',   "  stepp t_delta one_step calls: ", istats(5)
call print_solution(status, solution, filename_o="three_body_steps_adapt_etab_wt-fix-delta-steps.csv", end_o=istats(1))
! END: steps_adapt_etab_wt-fix-delta-steps

6. Truly Fixed Steps in y-space

We can achieve truly fixed step sizes in \(\mathbf{y}\mathrm{-space}\) with the steps_condy_stab_*t() solvers. In the image below we see the difference between fixed steps in \(t\mathrm{-space}\) vs \(\mathbf{y}\mathrm{-space}\) – remember the are only using the position components of the \(\mathbf{y}\) vector (the first two components) and not the velocity components (the last two components).

three_body_fixed_pos.png

Below are the velocity components plotted in the same manner as the position components. Notice the wildly differing distances between the solution points.

three_body_fixed_vel.png

In the code below we set y_delta_len_idxs_o to [1, 2] in order to have steps_condy_stab_wt() only use the first two components of the solution vector in it's length computation. This will produce steps that are 0.0034 long with an accuracy of 1.0e-5. Also note the addition of istats(3), istats(7) and istats(8) to our output report.

  ! BEGIN: steps_condy_stab_wt
call system_clock(c_beg)
call steps_condy_stab_wt(status, istats, solution, eq, t_iv, y_iv, param, a, b1, c, 0.0034_rk, .01_rk, &
                         y_delta_len_idxs_o=[1,2], y_sol_len_max_o=path_length, y_delta_len_tol_o=1.0e-5_rk)
call system_clock(c_end)
print '(a)',       "Fixed y_delta run: "
print '(a,f10.3)', "                  Milliseconds: ", 1000*(c_end-c_beg)/DBLE(c_rate)
print '(a,i10)',   "                        Status: ", status
print '(a,i10)',   "               Solution Points: ", istats(1)
print '(a,i10)',   "          Total one_step calls: ", istats(2)
print '(a,i10)',   "   y-len Adjust one_step calls: ", istats(3)
print '(a,i10)',   "              bisection limits: ", istats(7)
print '(a,i10)',   "           bad bisection start: ", istats(8)
call print_solution(status, solution, filename_o="three_body_steps_condy_stab_wt.csv", end_o=istats(1))
! END: steps_condy_stab_wt

We can also achieve a sloppy constant length \(\mathbf{y}\mathrm{-space}\) much like we did previously with steps_adapt_etab_wt() but with steps_sloppy_condy_stab_wt().

! BEGIN: steps_sloppy_condy_stab_wt
call system_clock(c_beg)
call steps_sloppy_condy_stab_wt(status, istats, solution, eq, t_iv, y_iv, param, a, b1, c, 0.0034_rk, .01_rk, &
                                y_delta_len_idxs_o=[1,2], y_sol_len_max_o=path_length)
call system_clock(c_end)
print '(a)',       "Sloppy Fixed y_delta run: "
print '(a,f10.3)', "                  Milliseconds: ", 1000*(c_end-c_beg)/DBLE(c_rate)
print '(a,i10)',   "                        Status: ", status
print '(a,i10)',   "               Solution Points: ", istats(1)
print '(a,i10)',   "          Total one_step calls: ", istats(2)
print '(a,i10)',   "   y-len Adjust one_step calls: ", istats(3)
call print_solution(status, solution, filename_o="steps_sloppy_condy_stab_wt.csv", end_o=istats(1))
! END: steps_sloppy_condy_stab_wt

7. Knowing When To Stop

Sometimes you don't know beforehand when you want the solver to stop. This is another place where stepp_o can help by providing a way to tell the solver when it's time to stop. For this example we simply tell the solver to stop when we get past a particular value of \(t\). Of course we could have done this with the t_max_o argument. The next section will explore a more realistic example, but it is complicated by the addition of an SDF function. In this example we keep it simple, and just use the

three_body_maxt.png

The idea is to use a subroutine for stepp_o that will tell steps_adapt_etab_wt() to quit when we hit a maximum value for \(t\). The following code will do the trick:

! BEGIN: steps_adapt_etab_wt-pho-t-max-stepp
! Example subroutine replicateing the functionality of t_max_o in steps_adapt_etab_wt().
subroutine sp_max_t(status, end_run, sdf_flags, new_t_delta, pnt_idx, solution, t_delta, y_delta)
  integer(kind=ik), intent(out) :: status
  integer(kind=ik), intent(out) :: end_run
  real(kind=rk),    intent(out) :: new_t_delta
  integer(kind=ik), intent(out) :: sdf_flags
  integer(kind=ik), intent(in)  :: pnt_idx
  real(kind=rk),    intent(in)  :: solution(:,:), t_delta, y_delta(:)
  real(kind=rk),    parameter   :: t_max = 6.2_rk
  status    = 0_ik
  sdf_flags = 0_ik
  new_t_delta = -1.0_rk
  if ( solution(1, pnt_idx-1) + t_delta > t_max) then
     end_run = 1_ik
  else
     end_run = 0_ik
  end if
end subroutine sp_max_t
! END: steps_adapt_etab_wt-pho-t-max-stepp

We wire up this subroutine to steps_adapt_etab_wt() via the stepp_o argument like so:

! BEGIN: steps_adapt_etab_wt-pho-t-max
call system_clock(c_beg)
call steps_adapt_etab_wt(status, istats, solution, eq, t_iv, y_iv, param, a, b1, b2, c, p1, p2, &
                         t_delta_max_o=t_delta*100, t_delta_ini_o=t_delta*20, error_tol_abs_o=[1.0e-9_rk], &
                         error_tol_rel_o=[1.0e-6_rk], t_max_o=t_end, t_end_o=t_end, &
                         stepp_o=sp_max_t);
call system_clock(c_end)
print '(a)',       "Adaptive run w max t: "
print '(a,f10.3)', "                  Milliseconds: ", 1000*(c_end-c_beg)/DBLE(c_rate)
print '(a,i10)',   "                        Status: ", status
print '(a,i10)',   "               Solution Points: ", istats(1)
print '(a,i10)',   "          Total one_step calls: ", istats(2)
print '(a,i10)',   "   y-err Adjust one_step calls: ", istats(4)
call print_solution(status, solution, filename_o="three_body_steps_adapt_etab_wt-pho-t-max.csv", end_o=istats(1))
! END: steps_adapt_etab_wt-pho-t-max

8. Satellite & Moon Orbit Intersection

three_body_moon.png

In the image above note the last adaptive point is precisely on the intersection of the satellite and moon orbit. We could easily stop with a stepp_o routine after we cross the moon orbit – much like we did in the previous section. If we did that we would have a final solution segment that straddled the orbit, but it is unlikely that the final end point would be precisely on the orbit. What we need here is a way to find a \(\Delta{t}\) for our last interval that leads to a solution that precisely hits the moon's orbit. We can do that by adding and sdf_o subroutine and having our stepp_o subroutine tell steps_adapt_etab_wt() when to use it.

Lets take a look at the stepp_o subroutine first. This routine first checks to see if the solution point is on the moon's orbit, and tells steps_adapt_etab_wt() to quit if it is. This is very unlikely to happen, but we check anyhow. Next it checks to see if the solution segment straddles the moons orbit – i.e. if the previous solution was on one side of the orbit while the current on is on the other. If this occurs the stepp_o tells steps_adapt_etab_wt() two things: 1) Solve for the final \(\Delta{t}\) with sdf_o, and 2) quit after this solution.

! BEGIN: steps_adapt_etab_wt-isct-stepp
! Example subroutine to find the first intersection of the satellite path and the moon's orbit.  It works in conjunction with
! sdf_cross_moon().
subroutine sp_cross_moon(status, end_run, sdf_flags, new_t_delta, pnt_idx, solution, t_delta, y_delta)
  integer(kind=ik), intent(out) :: status, end_run
  real(kind=rk),    intent(out) :: new_t_delta
  integer(kind=ik), intent(out) :: sdf_flags
  integer(kind=ik), intent(in)  :: pnt_idx
  real(kind=rk),    intent(in)  :: solution(:,:), t_delta, y_delta(:)
  real(kind=rk),    parameter   :: eps = 0.0001_rk
  real(kind=rk)                 :: lp_d, cp_d    
  status      = 0_ik
  sdf_flags   = 0_ik
  end_run     = 0_ik
  new_t_delta = -1.0_rk
  if (solution(1, pnt_idx-1) > 0.2_rk) then
     cp_d = norm2(solution(2:3, pnt_idx-1)+y_delta(1:2))
     if ( abs(cp_d-1.0_rk)  < eps) then
        end_run   = 1_ik
     else
        lp_d = norm2(solution(2:3, pnt_idx-1))
        if ((min(lp_d, cp_d) < 1.0_rk) .and. (max(lp_d, cp_d) > 1.0_rk)) then
           sdf_flags = 1_ik
           end_run   = 1_ik
        end if
     end if
  end if
end subroutine sp_cross_moon
! END: steps_adapt_etab_wt-isct-stepp

The magical SDF function is pretty simple in this case. The moon's orbit in this scaled problem is the unit circle, so we just have to subtract the norm of the solution's position from 1!

! BEGIN: steps_adapt_etab_wt-isct-sdf
! Example SDF subroutine to isolate a point on a solution segment that crosses the unit circle.
subroutine sdf_cross_moon(status, dist, sdf_flags, t, y)
  use mrkiss_config, only: rk, ik
  implicit none
  integer(kind=ik), intent(out) :: status
  real(kind=rk),    intent(out) :: dist
  integer(kind=ik), intent(in)  :: sdf_flags
  real(kind=rk),    intent(in)  :: t, y(:)
  status = 0_ik
  dist = 1.0_rk - norm2(y(1:2))
end subroutine sdf_cross_moon
! END: steps_adapt_etab_wt-isct-sdf

As usual we wire these two functions up to steps_adapt_etab_wt() via the stepp_o and sdf_o arguments. Notice the addition of istats(7) and istats(8) to the reporting.

! BEGIN: steps_adapt_etab_wt-isct
call system_clock(c_beg)
call steps_adapt_etab_wt(status, istats, solution, eq, t_iv, y_iv, param, a, b1, b2, c, p1, p2, &
                         t_delta_max_o=t_delta*100, t_delta_ini_o=t_delta*20, error_tol_abs_o=[1.0e-9_rk], &
                         error_tol_rel_o=[1.0e-6_rk], t_max_o=t_end, t_end_o=t_end, &
                         stepp_o=sp_cross_moon, sdf_o=sdf_cross_moon);
call system_clock(c_end)
print '(a)',       "Adaptive run w moon orbit hit: "
print '(a,f10.3)', "                  Milliseconds: ", 1000*(c_end-c_beg)/DBLE(c_rate)
print '(a,i10)',   "                        Status: ", status
print '(a,i10)',   "               Solution Points: ", istats(1)
print '(a,i10)',   "          Total one_step calls: ", istats(2)
print '(a,i10)',   "   y-err Adjust one_step calls: ", istats(4)
print '(a,i10)',   "              bisection limits: ", istats(7)
print '(a,i10)',   "           bad bisection start: ", istats(8)
call print_solution(status, solution, filename_o="three_body_steps_adapt_etab_wt-isct.csv", end_o=istats(1))
! END: steps_adapt_etab_wt-isct

9. Full Code Listing

9.1. Fortran Code

!-------------------------------------------------------------------------------------------------
program three_body
  use, intrinsic :: iso_fortran_env,                only: output_unit, error_unit
  use            :: mrkiss_config,                  only: rk, ik, t_delta_tiny
  use            :: mrkiss_solvers_wt,              only: steps_fixed_stab_wt, steps_condy_stab_wt, steps_adapt_etab_wt, steps_sloppy_condy_stab_wt
  use            :: mrkiss_utils,                   only: print_solution
  use            :: mrkiss_eerk_verner_9_8,         only: a, b1, b2, c, p1, p2
  use            :: mrkiss_eerk_dormand_prince_5_4, only: dpa=>a, dpb=>b1, dpc=>c

  implicit none

  integer,          parameter :: deq_dim       = 4
  integer,          parameter :: num_points    = 4000
  real(kind=rk),    parameter :: t_iv          = 0.0_rk
  real(kind=rk),    parameter :: t_end         = 17.06521656015796_rk
  real(kind=rk),    parameter :: path_length   = 10.7068_rk 
  real(kind=rk),    parameter :: y_iv(deq_dim) = [0.994_rk, 0.0_rk, 0.0_rk, -2.0015851063790825224_rk]
  real(kind=rk),    parameter :: param(1)      = [1.0_rk / 81.45_rk]
  real(kind=rk),    parameter :: t_delta       = 17.06521656015796d0 / (num_points - 1 )

  real(kind=rk)               :: solution(1+deq_dim, num_points)
  integer(kind=ik)            :: status, istats(16)
  integer                     :: c_beg, c_end, c_rate

  call system_clock(count_rate=c_rate)

  ! BEGIN: steps_fixed_stab_wt
  call system_clock(c_beg)
  call steps_fixed_stab_wt(status, istats, solution, eq, t_iv, y_iv, param, a, b1, c, t_end_o=t_end)
  call system_clock(c_end)
  print '(a)',       "Fixed t_delta run: "
  print '(a,f10.3)', "                  Milliseconds: ", 1000*(c_end-c_beg)/DBLE(c_rate)
  print '(a,i10)',   "                        Status: ", status
  print '(a,i10)',   "               Solution Points: ", istats(1)
  print '(a,i10)',   "          Total one_step calls: ", istats(2)
  call print_solution(status, solution, filename_o="tree_body_steps_fixed_stab_wt.csv", end_o=istats(1))
  ! END: steps_fixed_stab_wt


  ! BEGIN: steps_fixed_stab_wt-dp
  call system_clock(c_beg)
  call steps_fixed_stab_wt(status, istats, solution, eq, t_iv, y_iv, param, dpa, dpb, dpc, t_end_o=t_end)
  call system_clock(c_end)
  print '(a)',       "Fixed t_delta run: "
  print '(a,f10.3)', "                  Milliseconds: ", 1000*(c_end-c_beg)/DBLE(c_rate)
  print '(a,i10)',   "                        Status: ", status
  print '(a,i10)',   "               Solution Points: ", istats(1)
  print '(a,i10)',   "          Total one_step calls: ", istats(2)
  call print_solution(status, solution, filename_o="steps_fixed_stab_wt-dp.csv", end_o=istats(1))
  ! END: steps_fixed_stab_wt-dp

    ! BEGIN: steps_condy_stab_wt
  call system_clock(c_beg)
  call steps_condy_stab_wt(status, istats, solution, eq, t_iv, y_iv, param, a, b1, c, 0.0034_rk, .01_rk, &
                           y_delta_len_idxs_o=[1,2], y_sol_len_max_o=path_length, y_delta_len_tol_o=1.0e-5_rk)
  call system_clock(c_end)
  print '(a)',       "Fixed y_delta run: "
  print '(a,f10.3)', "                  Milliseconds: ", 1000*(c_end-c_beg)/DBLE(c_rate)
  print '(a,i10)',   "                        Status: ", status
  print '(a,i10)',   "               Solution Points: ", istats(1)
  print '(a,i10)',   "          Total one_step calls: ", istats(2)
  print '(a,i10)',   "   y-len Adjust one_step calls: ", istats(3)
  print '(a,i10)',   "              bisection limits: ", istats(7)
  print '(a,i10)',   "           bad bisection start: ", istats(8)
  call print_solution(status, solution, filename_o="three_body_steps_condy_stab_wt.csv", end_o=istats(1))
  ! END: steps_condy_stab_wt

  ! BEGIN: steps_sloppy_condy_stab_wt
  call system_clock(c_beg)
  call steps_sloppy_condy_stab_wt(status, istats, solution, eq, t_iv, y_iv, param, a, b1, c, 0.0034_rk, .01_rk, &
                                  y_delta_len_idxs_o=[1,2], y_sol_len_max_o=path_length)
  call system_clock(c_end)
  print '(a)',       "Sloppy Fixed y_delta run: "
  print '(a,f10.3)', "                  Milliseconds: ", 1000*(c_end-c_beg)/DBLE(c_rate)
  print '(a,i10)',   "                        Status: ", status
  print '(a,i10)',   "               Solution Points: ", istats(1)
  print '(a,i10)',   "          Total one_step calls: ", istats(2)
  print '(a,i10)',   "   y-len Adjust one_step calls: ", istats(3)
  call print_solution(status, solution, filename_o="steps_sloppy_condy_stab_wt.csv", end_o=istats(1))
  ! END: steps_sloppy_condy_stab_wt

  ! BEGIN: steps_adapt_etab_wt-std
  call system_clock(c_beg)
  call steps_adapt_etab_wt(status, istats, solution, eq, t_iv, y_iv, param, a, b1, b2, c, p1, p2, &
                           t_delta_max_o=t_delta*100, t_delta_ini_o=t_delta*20, error_tol_abs_o=[1.0e-9_rk], &
                           error_tol_rel_o=[1.0e-6_rk], t_max_o=t_end, t_end_o=t_end);
  call system_clock(c_end)
  print '(a)',       "Adaptive run: "
  print '(a,f10.3)', "                  Milliseconds: ", 1000*(c_end-c_beg)/DBLE(c_rate)
  print '(a,i10)',   "                        Status: ", status
  print '(a,i10)',   "               Solution Points: ", istats(1)
  print '(a,i10)',   "          Total one_step calls: ", istats(2)
  print '(a,i10)',   "   y-err Adjust one_step calls: ", istats(4)
  call print_solution(status, solution, filename_o="three_body_steps_adapt_etab_wt-std.csv", end_o=istats(1))
  ! END: steps_adapt_etab_wt-std

  ! BEGIN: steps_adapt_etab_wt-fix-delta-steps
  call system_clock(c_beg)
  call steps_adapt_etab_wt(status, istats, solution, eq, t_iv, y_iv, param, a, b1, b2, c, p1, p2, &
                           t_delta_max_o=t_delta*100, t_delta_ini_o=t_delta*20, error_tol_abs_o=[1.0e-9_rk], &
                           error_tol_rel_o=[1.0e-6_rk], t_max_o=t_end, t_end_o=t_end, &
                           stepp_o=sp_sloppy_y_delta_len_max);
  call system_clock(c_end)
  print '(a)',       "Adaptive run w max y_delta length: "
  print '(a,f10.3)', "                  Milliseconds: ", 1000*(c_end-c_beg)/DBLE(c_rate)
  print '(a,i10)',   "                        Status: ", status
  print '(a,i10)',   "               Solution Points: ", istats(1)
  print '(a,i10)',   "          Total one_step calls: ", istats(2)
  print '(a,i10)',   "   y-err Adjust one_step calls: ", istats(4)
  print '(a,i10)',   "  stepp t_delta one_step calls: ", istats(5)
  call print_solution(status, solution, filename_o="three_body_steps_adapt_etab_wt-fix-delta-steps.csv", end_o=istats(1))
  ! END: steps_adapt_etab_wt-fix-delta-steps

  ! BEGIN: steps_adapt_etab_wt-pho-t-max
  call system_clock(c_beg)
  call steps_adapt_etab_wt(status, istats, solution, eq, t_iv, y_iv, param, a, b1, b2, c, p1, p2, &
                           t_delta_max_o=t_delta*100, t_delta_ini_o=t_delta*20, error_tol_abs_o=[1.0e-9_rk], &
                           error_tol_rel_o=[1.0e-6_rk], t_max_o=t_end, t_end_o=t_end, &
                           stepp_o=sp_max_t);
  call system_clock(c_end)
  print '(a)',       "Adaptive run w max t: "
  print '(a,f10.3)', "                  Milliseconds: ", 1000*(c_end-c_beg)/DBLE(c_rate)
  print '(a,i10)',   "                        Status: ", status
  print '(a,i10)',   "               Solution Points: ", istats(1)
  print '(a,i10)',   "          Total one_step calls: ", istats(2)
  print '(a,i10)',   "   y-err Adjust one_step calls: ", istats(4)
  call print_solution(status, solution, filename_o="three_body_steps_adapt_etab_wt-pho-t-max.csv", end_o=istats(1))
  ! END: steps_adapt_etab_wt-pho-t-max

  ! BEGIN: steps_adapt_etab_wt-isct
  call system_clock(c_beg)
  call steps_adapt_etab_wt(status, istats, solution, eq, t_iv, y_iv, param, a, b1, b2, c, p1, p2, &
                           t_delta_max_o=t_delta*100, t_delta_ini_o=t_delta*20, error_tol_abs_o=[1.0e-9_rk], &
                           error_tol_rel_o=[1.0e-6_rk], t_max_o=t_end, t_end_o=t_end, &
                           stepp_o=sp_cross_moon, sdf_o=sdf_cross_moon);
  call system_clock(c_end)
  print '(a)',       "Adaptive run w moon orbit hit: "
  print '(a,f10.3)', "                  Milliseconds: ", 1000*(c_end-c_beg)/DBLE(c_rate)
  print '(a,i10)',   "                        Status: ", status
  print '(a,i10)',   "               Solution Points: ", istats(1)
  print '(a,i10)',   "          Total one_step calls: ", istats(2)
  print '(a,i10)',   "   y-err Adjust one_step calls: ", istats(4)
  print '(a,i10)',   "              bisection limits: ", istats(7)
  print '(a,i10)',   "           bad bisection start: ", istats(8)
  call print_solution(status, solution, filename_o="three_body_steps_adapt_etab_wt-isct.csv", end_o=istats(1))
  ! END: steps_adapt_etab_wt-isct

contains

  subroutine eq(status, dydt, t, y, param)
    integer(kind=ik), intent(out) :: status
    real(kind=rk),    intent(out) :: dydt(:)
    real(kind=rk),    intent(in)  :: t
    real(kind=rk),    intent(in)  :: y(:)
    real(kind=rk),    intent(in)  :: param(:)
    ! Vars
    real(kind=rk) x1,x2,v1,v2,mu,s1,s2,s3,x22,s12,s32,bf1,bf2
    ! Compute dydt
    x1  = y(1)                   ! y(1)     = Position x coordinate
    x2  = y(2)                   ! y(2)     = Position y coordinate
    v1  = y(3)                   ! y(3)     = Velocity x coordinate
    v2  = y(4)                   ! y(3)     = Velocity y coordinate
    s1  = x1 + param(1) - 1.0_rk ! param(1) = mu
    s2  = 1.0_rk - param(1)
    s3  = x1 + param(1)
    x22 = x2**2
    s12 = s1**2
    s32 = s3**2
    bf1 = (x22 + s12)**(3.0_rk/2.0_rk)
    bf2 = (x22 + s32)**(3.0_rk/2.0_rk)
    if (abs(bf1) < 0.0e-15) then
       status = 1
       return
    end if
    if (abs(bf2) < 0.0e-15) then
       status = 2
       return
    end if
    dydt(1) = v1
    dydt(2) = v2
    dydt(3) =   2 * v2 + x1 - (param(1) * s1) / bf1 - (s2 * s3) / bf2
    dydt(4) =  -2 * v1 + x2 - (param(1) * x2) / bf1 - (s2 * x2) / bf2
    status = 0
  end subroutine eq

  ! BEGIN: steps_adapt_etab_wt-pho-t-max-stepp
  ! Example subroutine replicateing the functionality of t_max_o in steps_adapt_etab_wt().
  subroutine sp_max_t(status, end_run, sdf_flags, new_t_delta, pnt_idx, solution, t_delta, y_delta)
    integer(kind=ik), intent(out) :: status
    integer(kind=ik), intent(out) :: end_run
    real(kind=rk),    intent(out) :: new_t_delta
    integer(kind=ik), intent(out) :: sdf_flags
    integer(kind=ik), intent(in)  :: pnt_idx
    real(kind=rk),    intent(in)  :: solution(:,:), t_delta, y_delta(:)
    real(kind=rk),    parameter   :: t_max = 6.2_rk
    status    = 0_ik
    sdf_flags = 0_ik
    new_t_delta = -1.0_rk
    if ( solution(1, pnt_idx-1) + t_delta > t_max) then
       end_run = 1_ik
    else
       end_run = 0_ik
    end if
  end subroutine sp_max_t
  ! END: steps_adapt_etab_wt-pho-t-max-stepp

  ! BEGIN: steps_adapt_etab_wt-fix-delta-stepp
  ! Example subroutine to adjust t_delta in an atempt to keep y_delta under a maximum value.
  ! It is sloppy because we assume t_delta is linearly proportional to y_delta_len
  subroutine sp_sloppy_y_delta_len_max(status, end_run, sdf_flags, new_t_delta, pnt_idx, solution, t_delta, y_delta)
    integer(kind=ik), intent(out) :: status, end_run
    real(kind=rk),    intent(out) :: new_t_delta
    integer(kind=ik), intent(out) :: sdf_flags
    integer(kind=ik), intent(in)  :: pnt_idx
    real(kind=rk),    intent(in)  :: solution(:,:), t_delta, y_delta(:)
    real(kind=rk),      parameter :: y_delta_len_max = 0.1_rk
    integer,            parameter :: y_delta_len_idxs(2) = [1, 2]
    real(kind=rk)                 :: y_delta_len
    status    = 0_ik
    end_run   = 0_ik
    sdf_flags = 0_ik
    y_delta_len = norm2(y_delta(y_delta_len_idxs))
    if ( y_delta_len > y_delta_len_max) then
       new_t_delta = t_delta * y_delta_len_max / y_delta_len
    else
       new_t_delta = -1.0_rk
    end if
  end subroutine sp_sloppy_y_delta_len_max
  ! END: steps_adapt_etab_wt-fix-delta-stepp

  ! BEGIN: steps_adapt_etab_wt-isct-stepp
  ! Example subroutine to find the first intersection of the satellite path and the moon's orbit.  It works in conjunction with
  ! sdf_cross_moon().
  subroutine sp_cross_moon(status, end_run, sdf_flags, new_t_delta, pnt_idx, solution, t_delta, y_delta)
    integer(kind=ik), intent(out) :: status, end_run
    real(kind=rk),    intent(out) :: new_t_delta
    integer(kind=ik), intent(out) :: sdf_flags
    integer(kind=ik), intent(in)  :: pnt_idx
    real(kind=rk),    intent(in)  :: solution(:,:), t_delta, y_delta(:)
    real(kind=rk),    parameter   :: eps = 0.0001_rk
    real(kind=rk)                 :: lp_d, cp_d    
    status      = 0_ik
    sdf_flags   = 0_ik
    end_run     = 0_ik
    new_t_delta = -1.0_rk
    if (solution(1, pnt_idx-1) > 0.2_rk) then
       cp_d = norm2(solution(2:3, pnt_idx-1)+y_delta(1:2))
       if ( abs(cp_d-1.0_rk)  < eps) then
          end_run   = 1_ik
       else
          lp_d = norm2(solution(2:3, pnt_idx-1))
          if ((min(lp_d, cp_d) < 1.0_rk) .and. (max(lp_d, cp_d) > 1.0_rk)) then
             sdf_flags = 1_ik
             end_run   = 1_ik
          end if
       end if
    end if
  end subroutine sp_cross_moon
  ! END: steps_adapt_etab_wt-isct-stepp

  ! BEGIN: steps_adapt_etab_wt-isct-sdf
  ! Example SDF subroutine to isolate a point on a solution segment that crosses the unit circle.
  subroutine sdf_cross_moon(status, dist, sdf_flags, t, y)
    use mrkiss_config, only: rk, ik
    implicit none
    integer(kind=ik), intent(out) :: status
    real(kind=rk),    intent(out) :: dist
    integer(kind=ik), intent(in)  :: sdf_flags
    real(kind=rk),    intent(in)  :: t, y(:)
    status = 0_ik
    dist = 1.0_rk - norm2(y(1:2))
  end subroutine sdf_cross_moon
  ! END: steps_adapt_etab_wt-isct-sdf

end program three_body

9.2. R Code

The images were produced with R.

#------------------------------------------------------------------------------------------------------------------------------
adDat <- fread('three_body_steps_adapt_etab_wt-std.csv')
ftDat <- fread('tree_body_steps_fixed_stab_wt.csv')
fyDat <- fread('three_body_steps_condy_stab_wt.csv')
loDat <- fread('steps_fixed_stab_wt-dp.csv')
loDat <- fread('steps_fixed_stab_wt-dp.csv')
slDat <- fread('steps_sloppy_condy_stab_wt.csv')
a2Dat <- fread('three_body_steps_adapt_etab_wt-fix-delta-steps.csv')
a3Dat <- fread('three_body_steps_adapt_etab_wt-pho-t-max.csv')
a4Dat <- fread('three_body_steps_adapt_etab_wt-isct.csv')

erDat <- data.table(b=c('Earth'), x=c(0), y=c(0))
moDat <- data.table(x=cos(seq(0, 2*pi, 0.01)), y=sin(seq(0, 2*pi, 0.01)))
m0Dat <- data.table(x=1.0, y=0.0)

gp <- ggplot() + 
  geom_point(data=erDat, aes(x=x, y=y, col='Earth')) +
  geom_path(data=moDat, aes(x=x, y=y, col='Moon')) +
  geom_path(data=ftDat, aes(x=y1, y=y2, col='Fixed Steps'))  +
  geom_point(data=adDat, aes(x=y1, y=y2, col='Adaptive Steps')) +
  geom_point(data=m0Dat, aes(x=x, y=y, col='Moon')) +
  scale_colour_manual(values=c("Earth"="blue", "Moon"="grey", "Fixed Steps"="pink", "Adaptive Steps"="red")) +
  labs(title='Restricted Three Body Problem', x=expression(x[1]), y=expression(x[2]), col='', 
       subtitle='No stepp_o') +
  coord_fixed()
ggsave(filename='three_body.png', plot=gp, width=1024, height=800, units='px', dpi=150)

gp <- ggplot() + 
  geom_point(data=erDat, aes(x=x, y=y, col='Earth')) +
  geom_path(data=moDat, aes(x=x, y=y, col='Moon')) +
  geom_path(data=ftDat, aes(x=y1, y=y2, col='High Order Fixed Steps'))  +
  geom_path(data=loDat, aes(x=y1, y=y2, col='Low Order Fixed Steps')) +
  geom_point(data=m0Dat, aes(x=x, y=y, col='Moon')) +
  scale_colour_manual(values=c("Earth"="blue", "Moon"="grey", "High Order Fixed Steps"="pink", "Low Order Fixed Steps"="red")) +
  labs(title='Restricted Three Body Problem', x=expression(x[1]), y=expression(x[2]), col='', 
       subtitle='High vs. Low Order Fixed Steps') +
  coord_fixed()
ggsave(filename='three_body-dp.png', plot=gp, width=1024, height=800, units='px', dpi=150)

gp <- ggplot() + 
  geom_point(data=erDat, aes(x=x, y=y, col='Earth')) +
  geom_path(data=moDat, aes(x=x, y=y, col='Moon')) +
  geom_path(data=ftDat, aes(x=y1, y=y2, col='Fixed Steps'))  +
  geom_point(data=a2Dat, aes(x=y1, y=y2, col='Adaptive Steps')) +
  geom_point(data=m0Dat, aes(x=x, y=y, col='Moon')) +
  scale_colour_manual(values=c("Earth"="blue", "Moon"="grey", "Fixed Steps"="pink", "Adaptive Steps"="red")) +
  labs(title='Restricted Three Body Problem', x=expression(x[1]), y=expression(x[2]), col='', 
       subtitle='y_delta_length via stepp_o') +
  coord_fixed()
ggsave(filename='three_body_ylen.png', plot=gp, width=1024, height=800, units='px', dpi=150)

gp <- ggplot() + 
  geom_point(data=erDat, aes(x=x, y=y, col='Earth')) +
  geom_path(data=moDat, aes(x=x, y=y, col='Moon')) +
  geom_path(data=ftDat, aes(x=y1, y=y2, col='Fixed Steps'))  +
  geom_point(data=a3Dat, aes(x=y1, y=y2, col='Adaptive Steps')) +
  geom_point(data=m0Dat, aes(x=x, y=y, col='Moon')) +
  scale_colour_manual(values=c("Earth"="blue", "Moon"="grey", "Fixed Steps"="pink", "Adaptive Steps"="red")) +
  labs(title='Restricted Three Body Problem', x=expression(x[1]), y=expression(x[2]), col='', 
       subtitle='max_t via stepp_o') +
  coord_fixed()
ggsave(filename='three_body_maxt.png', plot=gp, width=1024, height=800, units='px', dpi=150)

gp <- ggplot() + 
  geom_point(data=erDat, aes(x=x, y=y, col='Earth')) +
  geom_path(data=moDat, aes(x=x, y=y, col='Moon')) +
  geom_path(data=ftDat, aes(x=y1, y=y2, col='Fixed Steps'))  +
  geom_point(data=a4Dat, aes(x=y1, y=y2, col='Adaptive Steps')) +
  geom_point(data=m0Dat, aes(x=x, y=y, col='Moon')) +
  scale_colour_manual(values=c("Earth"="blue", "Moon"="grey", "Fixed Steps"="pink", "Adaptive Steps"="red")) +
  labs(title='Restricted Three Body Problem', x=expression(x[1]), y=expression(x[2]), col='', 
       subtitle='Moon orbit intersection') +
  coord_fixed()
ggsave(filename='three_body_moon.png', plot=gp, width=1024, height=800, units='px', dpi=150)

gp <- ggplot() + 
  geom_point(data=ftDat %>% filter(t<0.15), aes(x=y1, y=y2-0.01, col='Fixed Time Steps')) + 
  geom_path( data=ftDat %>% filter(t<0.15), aes(x=y1, y=y2-0.01, col='Fixed Time Steps')) +
  geom_point(data=slDat %>% filter(t<0.15), aes(x=y1, y=y2-0.02, col='Sloppy Fixed Time Steps')) + 
  geom_path( data=slDat %>% filter(t<0.15), aes(x=y1, y=y2-0.02, col='Sloppy Fixed Time Steps')) +
  geom_point(data=fyDat %>% filter(t<0.15), aes(x=y1, y=y2, col='Fixed Position Steps')) +
  geom_path( data=fyDat %>% filter(t<0.15), aes(x=y1, y=y2, col='Fixed Position Steps')) +
  labs(title='Restricted Three Body Problem', x=expression(x[1]), y=expression(x[2]), col='', 
       subtitle='Fixed Position Steps vs Fixed Time Steps (position)') +
  theme(axis.text.x=element_blank(),
        axis.text.y=element_blank(),
        legend.position = c(0.2, 0.7)) +
  coord_fixed()
ggsave(filename='three_body_fixed_pos.png', plot=gp, width=1024, height=600, units='px', dpi=150)

gp <- ggplot() + 
  geom_point(data=ftDat %>% filter(t<0.15), aes(x=y4-0.12, y=y3-0.15, col='Fixed Time Steps')) + 
  geom_path( data=ftDat %>% filter(t<0.15), aes(x=y4-0.12, y=y3-0.15, col='Fixed Time Steps')) +
  geom_point(data=slDat %>% filter(t<0.15), aes(x=y4-0.12, y=y3-0.22, col='Sldat Fixed Time Steps')) + 
  geom_path( data=slDat %>% filter(t<0.15), aes(x=y4-0.12, y=y3-0.22, col='Sldat Fixed Time Steps')) +
  geom_point(data=fyDat %>% filter(t<0.15), aes(x=y4, y=y3, col='Fixed Position Steps')) +
  geom_path( data=fyDat %>% filter(t<0.15), aes(x=y4, y=y3, col='Fixed Position Steps')) +
  labs(title='Restricted Three Body Problem', x=expression(v[1]), y=expression(v[2]), col='', 
       subtitle='Fixed Position Steps vs Fixed Time Steps (velocity)') +
  theme(axis.text.x=element_blank(),
        axis.text.y=element_blank(),
        legend.position = c(0.7, 0.7)) +
  coord_fixed()
ggsave(filename='three_body_fixed_vel.png', plot=gp, width=1024, height=600, units='px', dpi=150)